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

3,011 阅读1分钟

1. dotenv介绍

Dotenv 是一个零依赖模块,可将 .env 文件中的环境变量加载到 process.env 中。可以使用dotenv-expand来扩展。 还有dotenv-cli推荐使用。

//dotenv的使用:
import * as dotenv from 'dotenv' 
dotenv.config()

//dotenv-cli配置不同的.env文件,不同环境使用不同的脚本命令。
"scripts": {
    "dev": "dotenv -e .local.env -e .env.dev react-app-rewired start",
    "build": "dotenv -e .env.prod react-app-rewired build",
    "dev:test": "dotenv -e .local.env -e .env.dev react-app-rewired start",
    "dev:prod": "dotenv -e .local.env -e .env.prod react-app-rewired start",
  },

2.dotenv代码

2.1 dotenv实现的主要流程

1.读取env文件
2.parse解析文件内容生成键值对的对象,返回解析的结果
3. 合并配置到process.env
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*(?:#.*)?(?:$|$)/mg

// Parser src into an Object
function parse (src) {
  const obj = {}

  // Convert buffer to string
  let lines = src.toString()

  // Convert line breaks to same format
  lines = lines.replace(/\r\n?/mg, '\n')

  let match
  while ((match = LINE.exec(lines)) != null) {
    /**
     * match的数据结构
        0:
        'NAME="xxxxx"'
        1:
        'NAME'
        2:
        '"xxxxx"'
    */
    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$/mg, '$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 _log (message) {
  console.log(`[dotenv][DEBUG] ${message}`)
}

function _resolveHome (envPath) {
    // os.homedir()方法是os模块的内置应用程序编程接口,用于获取当前用户的主目录路径。
  return envPath[0] === '~' ? path.join(os.homedir(), envPath.slice(1)) : envPath
}

// Populates process.env from .env file
function config (options) {
  let dotenvPath = path.resolve(process.cwd(), '.env')
  let encoding = 'utf8'
  const debug = Boolean(options && options.debug)
  const override = Boolean(options && options.override)

  if (options) {
    if (options.path != null) {
        // 如果有配置path则获取新的path
      dotenvPath = _resolveHome(options.path)
    }
    if (options.encoding != null) {
      encoding = options.encoding
    }
  }

  try {
    // Specifying an encoding returns a string instead of a buffer
    const parsed = DotenvModule.parse(fs.readFileSync(dotenvPath, { encoding }))

    Object.keys(parsed).forEach(function (key) {
      //判断key是否是process.env上的属性,如果不是则赋值,如果是,并且override === true,则覆盖原有的
      if (!Object.prototype.hasOwnProperty.call(process.env, key)) {
        process.env[key] = parsed[key]
      } else {
        if (override === true) {
          process.env[key] = parsed[key]
        }
        // debug模式下输入日志
        if (debug) {
          if (override === true) {
            _log(`"${key}" is already defined in \`process.env\` and WAS overwritten`)
          } else {
            _log(`"${key}" is already defined in \`process.env\` and was NOT overwritten`)
          }
        }
      }
    })

    return { parsed }
  } catch (e) {
    if (debug) {
      _log(`Failed to load ${dotenvPath} ${e.message}`)
    }

    return { error: e }
  }
}

const DotenvModule = {
  config,
  parse
}

module.exports.config = DotenvModule.config
module.exports.parse = DotenvModule.parse
module.exports = DotenvModule

2.2 parse方法返回的数据

image.png

2.3 正则的解析

image.png

小结

dotenv的实现简单流程就是读取.env文件,然后生成一个key/value对象,合并到process.env上。整个流程比较简单,跑几次代码就比较清楚了。正则这里比较难理解。