Skip to content

封装dotenv库实现类似Vite加载环境变量的行为

dotenv的作用

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

简单使用示例

安装依赖

sh
yarn add dotenv

编写 .env 文件

sh
# 数据库相关内容
DB_USER=test
DB_PWD=123456
DB_PORT=3306

test.js

js
const dotenv = require('dotenv')

const res = dotenv.config()

console.log(res);

console.log(process.env.DB_PORT);

运行结果

注意.env文件需在项目的根目录中(终端执行指令的目录)

图片

Vite中读取.env文件的规则

这里直接贴图原文档内容,不做过多赘述

图片

很详细的叙述了Vite加载额外环境变量的规则

循序渐进的实现

指定存放环境变量文件

  1. 首先通过process.cwd(),获取终端执行命令的路径
js
console.log(process.cwd())

图片

  1. dotenv.config的参数定义
ts
export interface DotenvConfigOptions {
  /**
   * You may specify a custom path if your file containing environment variables is located elsewhere.
   */
  path?: string;

  /**
   * You may specify the encoding of your file containing environment variables.
   */
  encoding?: string;

  /**
   * You may turn on logging to help debug why certain keys or values are not being set as you expect.
   */
  debug?: boolean;
}

可以看出通过path参数指定环境变量的路径

测试代码如下

js
const dotenv = require('dotenv')
const baseDir = process.cwd()

const res1 = dotenv.config({path:`${baseDir}/.lalala`})
const res2 = dotenv.config({path:`${baseDir}/.lalala2`})
const res3 = dotenv.config({path:`${baseDir}/.不存在`})

console.log(res1);
console.log(res2);
console.log(res3);
console.log(process.env.TEST_ENV);

输出内容分别如下

js
{ parsed: { TEST_ENV: 'test1' } }
{ parsed: { TEST_ENV: 'test2' } }
{
  error: 'xxx'
}
test1

可以看出当指定文件存在时,解析的内容会在结果的parsed属性中

不存在时,解析结果只包含错误信息error

当指定了path参数后,执行多个donEnv.config方法,只会自动加载第一个的解析结果到process.env中去

加载读取的环境变量

这个步骤简单,直接遍历parsed属性的内容,将其添加到process.env属性上即可

js
function load(parseEnvObj) {
  const { parsed } = parseEnvObj
  if (parsed && parsed instanceof Object) {
    Object.getOwnPropertyNames(parsed).forEach((k) => {
      process.env[k] = parsed[k]
    })
  }
}

测试

js
const baseDir = process.cwd()

const res1 = dotenv.config({ path: `${baseDir}/.lalala` })
const res2 = dotenv.config({ path: `${baseDir}/.lalala2` })
const res3 = dotenv.config({ path: `${baseDir}/.不存在` })

load(res1);
load(res2);
load(res3);
console.log(process.env.TEST_ENV); // test2

实现Vite的读取规则

js
function loadEnv() {
    const baseDir = process.cwd()
    // .env
    load(dotenv.config({ path: `${baseDir}/.env` }))
    // .env.local
    load(dotenv.config({ path: `${baseDir}/.env.local` }))
    // .env.[mode]
    load(dotenv.config({ path: `${baseDir}/.env.${process.env.NODE_ENV}` }))
    // .env.[mode].local
    load(dotenv.config({ path: `${baseDir}/.env.${process.env.NODE_ENV}.local` }))
}

简单封装成一个函数,直接调用loadEnv

mode的设置可以通过cross-env库设置,如下所示

package.json

json
{
    "scripts": {
        "dev": "cross-env NODE_ENV=development node ./tests/test.js",
    },
}

限制环境变量前缀

简单修改后的代码,添加了前缀的过滤

js
function load(parseEnvObj, prefix = '') {
    const { parsed } = parseEnvObj
    if (parsed && parsed instanceof Object) {
        Object.getOwnPropertyNames(parsed).forEach((k) => {
            if (k.indexOf(prefix) === 0) {
                process.env[k] = parsed[k]
            }else{
                process.env[k] = undefined
            }
        })
    }
}
function loadEnv(options = {}) {
    const { prefix = '' } = options
    const baseDir = process.cwd()
    // .env
    load(dotenv.config({ path: `${baseDir}/.env` }), prefix)
    // .env.local
    load(dotenv.config({ path: `${baseDir}/.env.local` }), prefix)
    // .env.[mode]
    load(dotenv.config({ path: `${baseDir}/.env.${process.env.NODE_ENV}` }), prefix)
    // .env.[mode].local
    load(dotenv.config({ path: `${baseDir}/.env.${process.env.NODE_ENV}.local` }), prefix)
}

测试

.env内容

sh
# 数据库相关内容
DB_USER=test
DB_PWD=123456
DB_PORT=3306

SUGAR_USER=sugar
js
loadEnv()
console.log(process.env.DB_USER); // test
console.log(process.env.SUGAR_USER); // sugar
js
loadEnv({prefix:'SUGAR'})
console.log(process.env.DB_USER); // undefined
console.log(process.env.SUGAR_USER); // sugar

最终版

js
// 读取配置的环境变量
const dotenv = require('dotenv')

function load(parseEnvObj, prefix = '') {
  const { parsed } = parseEnvObj
  if (parsed && parsed instanceof Object) {
      Object.getOwnPropertyNames(parsed).forEach((k) => {
          if (k.indexOf(prefix) === 0) {
              process.env[k] = parsed[k]
          }else{
              process.env[k] = undefined
          }
      })
  }
}
function loadEnv(options = {}) {
  const { prefix = '' } = options
  const baseDir = process.cwd()
  // .env
  load(dotenv.config({ path: `${baseDir}/.env` }), prefix)
  // .env.local
  load(dotenv.config({ path: `${baseDir}/.env.local` }), prefix)
  // .env.[mode]
  load(dotenv.config({ path: `${baseDir}/.env.${process.env.NODE_ENV}` }), prefix)
  // .env.[mode].local
  load(dotenv.config({ path: `${baseDir}/.env.${process.env.NODE_ENV}.local` }), prefix)
}

module.exports = loadEnv

使用

js
const loadEnv = require('modulePath')
loadEnv()
// or
loadEnv({prefix:'xx'})

TODO

下一次分享一下Vite中这部分的源码实现逻辑