项目中 .env 文件原理是什么?

278 阅读2分钟

1 学习目标

学习并实现 dotenv 实现原理


2 环境准备

// 克隆项目
git clone https://github.com/motdotla/dotenv
// 打开项目并安装依赖
cd dotenv && npm i

3 dotenv 文件的作用

dotenv 是一个零依赖模块,通过读取 .env 中的环境变量加载到 process.env 上


4 dotenv 原理

项目中经常会使用 .env 作为环境变量

VUE_APP_BASEURL=/supervision/

从 env 中,大致可以分析出 dotenv 功能

1、读取 env 文件
2、解析 env 成键值对
3、绑定到 process.env 中


5 简单实现 dotenv

根据上面的功能分析,简单实现如下

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

const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm

// 这是 dotenv 源码中的 parse 函数
function parse(src) {
  const obj = {}
  // Convert buffer to string
  let lines = src.toString()
  // Convert line breaks to same format
  lines = lines.replace(/\r\n?/gm, '\n')
  let match
  while ((match = LINE.exec(lines)) != null) {
    const key = match[1]
    // Default undefined or null to empty string
    let value = match[2] || ''
    // Remove whitespace
    value = value.trim()
    // Check if double quoted
    const maybeQuote = value[0]
    // Remove surrounding quotes
    value = value.replace(/^(['"`])([\s\S]*)\1$/gm, '$2')
    // Expand newlines if double quoted
    if (maybeQuote === '"') {
      value = value.replace(/\\n/g, '\n')
      value = value.replace(/\\r/g, '\r')
    }
    // Add to object
    obj[key] = value
  }
  return obj
}

function main() {
  // 获取 .evn 文件路径 (process.cwd: 当前执行程序的路径)
  const envPath = path.resolve(process.cwd(), '.env')
  // 读取 .env 文件 encoding: "ascii" "utf-8" "base64"
  // 如果不定义 encoding 将返回缓存二进制文件
  const envFile = fs.readFileSync(envPath, { encoding: 'utf-8' })
  // 分割成对象
  const parsed = parse(envFile)
  Object.keys(parsed).forEach(function (key) {
    console.log(key in process.env)
    // 判断 key 是否已经在 process.env 上
    // hasOwnProperty 判断对象是否包含自定义属性而不是原型链上的属性
    // 防止 process 有可能存在使用 hasOwnProperty 属性名的属性,直接使用 Object 上的原型链的 hasOwnProperty
    if (!Object.prototype.hasOwnProperty.call(process.env, key)) {
      process.env[key] = parsed[key]
    } else {
      console.log(`"${key}" is already defined in \`process.env\` and was NOT overwritten`)
    }
  })
}

main()

6 完善 main 函数

上面的实现缺少了很多功能,比如

1、缺少自定义配置,包括 自定义路径、自定义编码方式、是否允许覆盖变量
2、增加容错处理
3、增加debug模式

const fs = require('fs')
const path = require('path')
const os = require('os')

const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm

// 这是 dotenv 源码中的 parse 函数
function parse(src) {
  const obj = {}
  // Convert buffer to string
  let lines = src.toString()
  // Convert line breaks to same format
  lines = lines.replace(/\r\n?/gm, '\n')
  let match
  while ((match = LINE.exec(lines)) != null) {
    const key = match[1]
    // Default undefined or null to empty string
    let value = match[2] || ''
    // Remove whitespace
    value = value.trim()
    // Check if double quoted
    const maybeQuote = value[0]
    // Remove surrounding quotes
    value = value.replace(/^(['"`])([\s\S]*)\1$/gm, '$2')
    // Expand newlines if double quoted
    if (maybeQuote === '"') {
      value = value.replace(/\\n/g, '\n')
      value = value.replace(/\\r/g, '\r')
    }
    // Add to object
    obj[key] = value
  }

  return obj
}

// 这是 dotenv 源码中的 _resolveHome 函数
function _resolveHome(envPath) {
  // os.homedir 获取当前用户的主目录路径
  return envPath[0] === '~' ? path.join(os.homedir(), envPath.slice(1)) : envPath
}

function main(options) {
  // 获取 .evn 文件路径 (process.cwd: 当前执行程序的路径)
  let envPath = path.resolve(process.cwd(), '.env')
  let encoding = 'utf-8'
  let debug = Boolean(options && options.debug)
  let override = Boolean(options && options.override)
  if (options && options.path) {
    envPath = _resolveHome(options.path)
  }
  if (options && options.encoding) {
    encoding = options.encoding
  }
  // 读取 .env 文件 encoding: "ascii" "utf-8" "base64"
  // 如果不定义 encoding 将返回缓存二进制文件
  const envFile = fs.readFileSync(envPath, { encoding })
  try {
    // "utf-8" 编码的格式 获取到 hhj=999。如果编码为 base64 将获取到 aHVhbmdoYW9qaWU9OTk5
    // 分割成对象 {hhj: 999}
    const parsed = parse(envFile)
    Object.keys(parsed).forEach(function (key) {
      // 判断 key 是否已经在 process.env 上
      // hasOwnProperty:判断对象是否包含自定义属性而不是原型链上的属性
      // 防止 process 有可能存在使用 hasOwnProperty 属性名的属性,直接使用 Object 上的原型链的 hasOwnProperty
      if (!Object.prototype.hasOwnProperty.call(process.env, key)) {
        process.env[key] = parsed[key]
      } else {
        if (override) {
          process.env[key] = parsed[key]
        }
        if (debug) {
          console.log(`"${key}" is already defined in \`process.env\` and was NOT overwritten`)
        }
      }
      return parsed
    })
  } catch (error) {
    return { error }
  }
}

main({
  // path: '~/study/soundCode/mySoundCode/dotenv/env/.env-pro'
  path: './env/.env-pro',
  encoding: 'utf-8',
  override: true,
  debug: true
})

上面的函数大体就是 dotenv 源码中的流程,具体可以在 dotenv/lib/main.js 中查看


7 总结

一句话总结 dotenv 库原理,读取 env 中的环境变量绑定到** process.env** 中