Skip to content

vue-cli 项目中常用的 .env 文件原理是什么?如何实现?

1. 前言

阅读本文,你将学到:

1. 学会 dotenv 原理和实现
2. 学会使用 fs模块 获取文件并解析
3. ...

2. dotenv 的作用

dotenv 是一个零依赖模块,可将 .env 文件中的环境变量加载到 process.env 中。它只能用于 node 环境下,因为只有 node 环境下才有全局变量 process.env。

如果需要使用变量,则配合扩展包 dot-expand 使用。使用变量的意思就是可以使用 $VAR 的形式使用 process.env 已有的变量,当然它不止这点功能。

在 vue-cli 和 create-react-app 中都有使用 dotenv 和 dotenv-expand。并且他们还使用 webpack.DefinePlugin 将 process.env 变量注入到客户端浏览器环境下的代码中,这样你的浏览器 js 代码也能使用 process.env 全局变量了。

js
webpackConfig
  .plugin('define')
    .use(require('webpack').DefinePlugin, [
      resolveClientEnv(options)
    ])

const prefixRE = /^VUE_APP_/

function resolveClientEnv (options, raw) {
  const env = {}
  Object.keys(process.env).forEach(key => {
    if (prefixRE.test(key) || key === 'NODE_ENV') {
      env[key] = process.env[key]
    }
  })
  env.BASE_URL = options.publicPath

  if (raw) {
    return env
  }

  for (const key in env) {
    env[key] = JSON.stringify(env[key])
  }
  return {
    'process.env': env 
  }
}

使用变量:

FOO=foo
BAR=bar

CONCAT=$FOO$BAR # 等效 CONCAT=foobar

3. .env 文件使用

我们项目中经常会用到 .env 文件写法:

NAME=卢克斯
AGE=18
BLOG=https://lukecheng233.com
ACTIVITY=run
WEIXIN=LuxCurl

单从这个文件来看,我们可以知道有如下功能需要实现:

  1. 读取 .env 文件
  2. 解析 .env 文件拆成键值对的对象形式
  3. 赋值到 process.env 上
  4. 最后返回解析后得到的对象

4. 简单实现

根据分析问题,我们最终可以简单把代码实现如下:

js
const fs = require('fs');
const path = require('path');

const parse = function parse(src){
    const obj = {};
    // 用换行符 分割
    // 比如
    /**
      NAME=卢克斯
      AGE=18
      BLOG=https://lukecheng233.com
      ACTIVITY=run
      WEIXIN=LuxCurl
    */
    src.toString().split('\n').forEach(function(line, index){
        // 用等号分割
        const keyValueArr = line.split('=');
        // NAME
        key = keyValueArr[0];
        // 卢克斯
        val = keyValueArr[1] || '';
        obj[key] = val;
    });
    // { NAME: '卢克斯', ... }
    return obj;
}

const config = function(){
    // 读取 node 执行的当前路径下的 .env 文件
    let dotenvPath = path.resolve(process.cwd(), '.env');
    // 按 utf-8 解析文件,得到对象
    // { NAME: '卢克斯', ... }
    const parsed = parse(fs.readFileSync(dotenvPath, 'utf-8'));

    // 键值对形式赋值到 process.env 变量上,原先存在的不赋值
    Object.keys(parsed).forEach(function(key){
        if(!Object.prototype.hasOwnProperty.call(process.env, key)){
            process.env[key] = parsed[key];
        }
    });

    // 返回对象
    return parsed;
};

console.log(config());
console.log(process.env);

// 导出 config parse 函数
module.exports.config = config;
module.exports.parse = parse;

5. 继续完善 config 函数

简版的 config 函数还缺失挺多功能,比如:

可由用户自定义路径
可由用户自定义解析编码规则
添加 debug 模式
完善报错输出,用户写的 env 文件自由度比较大,所以需要容错机制。

根据功能,我们很容易实现以下代码:

js
function resolveHome (envPath) {
    return envPath[0] === '~' ? path.join(os.homedir(), envPath.slice(1)) : envPath
}

const config = function(options){
    // 读取 node 执行的当前路径下的 .env 文件
    let dotenvPath = path.resolve(process.cwd(), '.env');
    // utf8
    let encoding = 'utf8';
    // debug 模式,输出提示等信息
    let debug = false;
    // 对象
    if (options) {
        if (options.path != null) {
            // 解析路径
            dotenvPath = resolveHome(options.path)
        }
        // 使用配置的编码方式
        if (options.encoding != null) {
            encoding = options.encoding
        }
        // 有配置就设置为 true
        if (options.debug != null) {
            debug = true
        }
    }

    try {
        // 按 utf-8 解析文件,得到对象
        // { NAME: '卢克斯', ... }
        // debug 传递给 parse 函数 便于
        const parsed = parse(fs.readFileSync(dotenvPath, { encoding }), { debug });

        // 键值对形式赋值到 process.env 变量上,原先存在的不赋值
        Object.keys(parsed).forEach(function(key){
            if(!Object.prototype.hasOwnProperty.call(process.env, key)){
                process.env[key] = parsed[key];
            } else if (debug) {
                console.log(`"${key}" is already defined in \`process.env\` and will not be overwritten`);
            }
        });

        // 返回对象
        return parsed;
    }
    catch (e) {
        return { error: e };
    }
};

dotenv 源码中,parse 函数主要是一些正则和单双引号、跨平台等细致处理。这里就暂时不阐述。

6. 总结

一句话总结 dotenv 库的原理。用 fs.readFileSync 读取 .env 文件,并解析文件为键值对形式的对象,将最终结果对象遍历赋值到 process.env 上。

Released under the MIT License.