每日一个npm包 —— dotenv

1,692 阅读3分钟

.env 的作用

💡 加载 .env 文件中的变量到 process.env

.env 文件是用来自定义配置的一个简单方法。我们可以将一些不能在代码中存储的 敏感信息 / 账号数据 从代码中剥离出来,作为环境变量存储在 .env 文件中。

.env 的使用方法

.env 文件通常不包含在版本控制内,因为它可能包含敏感的 API Key 或 密码。所有需要环境变量定义的项目都可以创建一个 .env.example 文件。项目合作开发者可以独立的复制 .env.example 并重命名为 .env,同时将其修改为正确的本地环境配置:API Key 或者其他必要的值。 在这个使用方法中 .env 文件应该添加到 .gitignore 文件中保证永远不会出现在 git 中。

  1. 在根目录下添加 .env 文件,eg:

    DB_HOST=127.0.0.1
    DB_NAME=timeseriesmonitor
    DB_PORT=5432
    DB_USER=tsm
    DB_UNSECURE=true
    
  2. 引入 dotenvnpm install dotenv

    const dotenv = require('dotenv');
    
    dotenv.config('./env');  // .env 中的环境变量被加载到 process.env 中
    
    console.log(process.env);
    
  3. 打印log如下:

    {
    
      ...
      DB_HOST: '127.0.0.1',
      DB_NAME: 'timeseriesmonitor',
      DB_PORT: '5432',
      DB_UNSECURE: 'true',
      DB_USER: 'tsm',
      ...
    }
    

.env 源码解读

核心代码逻辑在 lib/main.js 中,可以看到刚开始先初始化了几个正则表达式

正则表达式

const RE_INI_KEY_VAL = /^\s*([\w.-]+)\s*=\s*(.*)?\s*$/
const RE_NEWLINES = /\\n/g
const NEWLINES_MATCH = /\r\n|\n|\r/

我们可以通过 regexr.com 来交互式查看表达式各部分的含义:

  • const RE_INI_KEY_VAL = /^\s*([\w.-]+)\s*=\s*(.*)?\s*$/ 匹配 .env 文件中的环境变量,如 DB_HOST=127.0.0.1,注意到表达式中有两个部分被 () 包起来了,这是为了后续正则匹配的时候方便提取出匹配的字符串,即环境变量 keyvalue 的值

  • const RE_NEWLINES = /\\n/g 匹配 \n 字符串,\\:前面的 \ 将后面的 \ 转义掉了,所以这里匹配的是 \n 字符串,而不是换行符。

  • const NEWLINES_MATCH = /\r\n|\n|\r/ 匹配换行符:三种换行符是为了兼容各操作系统,不同操作系统换行符有所不同:\n: Unix系统,\r\n: Windows系统,\r: Mac系统

    • 回车 \r 本义是光标重新回到本行开头,r 的英文 return,控制字符为 CR,即 Carriage Return;
    • 换行 \n 本义是光标往下一行(不一定到下一行行首),n 的英文 newline,控制字符为 LF,即 Line Feed;

parse 函数

💡 将 .env 中的字符串转换成 Object

核心逻辑简化如下:

function parse(src) {
  const obj = {}
  
  // 用 NEWLINES_MATCH 分割每行表达式,再 forEach 依次处理
  src.split(NEWLINES_MATCH).forEach(function (line, idx) {
    // 用 RE_INI_KEY_VAL 匹配 'KEY=VAL' 中的 'KEY' 和 'VAL'
    const keyValueArr = line.match(RE_INI_KEY_VAL)
    // matched?
    if (keyValueArr != null) {
      const key = keyValueArr[1]
      const val = (keyValueArr[2] || '').trim()
      obj[key] = val
    }
  })

  return obj
}

其关键是用了 match 方法匹配 'KEY=VAL' 中的 'KEY''VAL'match 方法若匹配到了 line 中的键值对则会返回一个数组,这个数组的第一项是整个正则表达式所匹配的字符串,后面会接表达式中用 () 包围起来的正则表达式匹配的字符串。所以 key = keyValueArr[1]val = keyValueArr[2]

config 函数

💡 将 .env 中的环境变量加载到 process.env

核心逻辑简化如下:

function config(options) {
  let dotenvPath = path.resolve(process.cwd(), '.env')

  const parsed = parse(fs.readFileSync(dotenvPath, { encoding: 'utf8' }))

  Object.keys(parsed).forEach(function (key) {
    if (!Object.prototype.hasOwnProperty.call(process.env, key)) {
      process.env[key] = parsed[key]
    }
  })

  return { parsed }
}

config 函数分三步:

  1. 获取 .env 文件的路径并读取文件
  2. 将文件的字符串传入 parse 函数中解析成 Object
  3. 遍历 Object 将其加载到 process.env

注意代码中使用 Object.prototype.hasOwnProperty.call(process.env, key) 判断 key 是否已经存在于 process.env 中,若存在,则不进行覆盖。使用 Object.prototype.hasOwnProperty 是避免原型链查找,只判断 key 是否存在于 process.env 中,而不是其原型链上,这样做可以省去原型链查找的耗时。

总结

dotenv 源码非常简短只有 118 行,但其有 14.5k star(截止至本文写稿时间),源码简单易懂,建议自己动手看看。

参考资料