dotenv
前言
承接上一篇文章 初识dotenv:juejin.cn/post/737244…, 我们了解到dotenv是一种常用的环境变量管理工具,用于加载.env
文件中的键值对到环境变量中。在解析键值对时,dotenv会处理两种情况:存在DOTENV_KEY
和不存在DOTENV_KEY
。
在本文中,我们将深入探讨当.env
文件中不存在指定的DOTENV_KEY
时,dotenv的源码流程。通过对这一流程的深入剖析,我们可以更好地理解dotenv的工作原理,为我们在实际项目中的应用提供更加坚实的基础。
入口函数confing()
首先调用入口函数confing()
config() 入口函数中可以传入配置选项参数options
对象 ,用于指定不同的配置信息,可以包含以下属性:
-
path
:一个字符串或字符串数组,用于指定要加载的.env
文件的路径。如果是字符串数组,则会按顺序尝试加载多个文件,以最后一个有效的文件为准。 -
processEnv
:一个对象,用于指定要填充的目标环境变量对象。默认为process.env
。 -
debug
:一个布尔值,用于启用调试模式,会输出调试信息。默认为false
。 -
示例:
{
"path": "/path/to/.env",
"processEnv": {},
"debug": true
}
代码:
/**
* 根据提供的选项配置环境变量。
* 如果未设置 DOTENV_KEY,则回退到从原始的 .env 文件配置。
* 如果设置了 DOTENV_KEY 但相应的 .env.vault 文件不存在,则警告用户并回退到从原始的 .env 文件配置。
* 如果设置了 DOTENV_KEY 并且 .env.vault 文件存在,则从加密的 .env.vault 文件配置环境变量。
*
* @param {Object} options - 配置选项。
* @returns {Object} - 解析后的环境变量。
*/
function config(options) {
// 如果未设置 DOTENV_KEY,则回退到原始 dotenv 配置
if (_dotenvKey(options).length === 0) {
return DotenvModule.configDotenv(options);
}
const vaultPath = _vaultPath(options);
// 如果设置了 DOTENV_KEY 但 .env.vault 文件不存在
if (!vaultPath) {
_warn(`您设置了 DOTENV_KEY,但在 ${vaultPath} 处找不到 .env.vault 文件。您是否忘记构建它了?`);
return DotenvModule.configDotenv(options);
}
// 从加密的 .env.vault 文件配置环境变量
return DotenvModule._configVault(options);
}
confing()先调用
_dotenvKey()
函数来判断是否存在DOTENV_KEY,未存在则调用configDotenv()
函数加载和解析 .env 文件中的环境变量。
获取DOTENV_KEY函数_dotenvKey()
DOTENV_KEY 的格式是一个字符串,包含了加密环境变量文件的相关信息,格式如下:
dotenv://:密钥@域名/路径/文件名.vault?参数=值
以下是一个示例 DOTENV_KEY 的值:
dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=production
在这个示例中:
dotenv://
表示使用 dotenv 加密方案。:key_1234
是密钥。dotenvx.com
是域名。/vault/.env.vault
是文件路径。?environment=production
是附加的参数,用于指定环境。
代码:
/**
* 获取 DOTENV_KEY 的值
*
* 此函数根据不同的优先级返回 DOTENV_KEY 的值。
*
* @param {object} options - 配置选项对象,包含 DOTENV_KEY。
* @returns {string} - 返回 DOTENV_KEY 或者空字符串。
*
*/
function _dotenvKey(options) {
// 优先使用开发者直接设置的 options.DOTENV_KEY
if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) {
return options.DOTENV_KEY
}
// 其次使用 process.env 中的 DOTENV_KEY 环境变量
if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {
return process.env.DOTENV_KEY
}
// 最后返回空字符串
return ''
}
逻辑:
-
- 如果 options 存在且 options.DOTENV_KEY 存在且长度大于 0,则返回 options.DOTENV_KEY。
-
- 如果 process.env 中存在 DOTENV_KEY 环境变量且长度大于 0,则返回 process.env.DOTENV_KEY。
-
- 如果以上条件均不满足,则返回空字符串。
解析环境变量函数configDotenv()
此函数加载和解析 .env 文件中的环境变量,并将其添加到 process.env 中。
代码比较长,分步骤解析如下:
- 解析
.env
文件路径和设置默认编码:
-
process.cwd()
: 获取当前 Node.js 进程的工作目录,即运行 Node.js 应用程序时所处的目录。 -
path.resolve()
:path
模块是 Node.js 提供的用于处理文件路径的核心模块,resolve()
方法用于将路径或路径片段解析为绝对路径。这里将当前工作目录和.env
文件名作为参数传入,以获取.env
文件的绝对路径。 -
最终得到的
dotenvPath
是.env
文件的绝对路径,可以确保后续的文件读取操作能够准确地定位到该文件
// 解析 .env 文件路径
const dotenvPath = path.resolve(process.cwd(), '.env')
// 设置默认编码为 UTF-8
let encoding = 'utf8'
- 检查是否启用了调试模式:
通过检查options
对象中的debug
属性是否存在来确定是否启用了调试模式。
// 检查是否启用了调试模式
const debug = Boolean(options && options.debug)
- 检查是否提供了自定义编码:
-
如果提供了自定义编码,将其赋值给
encoding
变量。 -
如果未提供自定义编码并且启用了调试模式,则输出默认编码信息。
if (options && options.encoding) {
encoding = options.encoding
} else {
// 如果启用了调试模式,输出编码信息
if (debug) {
_debug('No encoding is specified. UTF-8 is used by default')
}
}
- 处理
.env
文件路径数组:
-
将要加载的
.env
文件的路径保存在optionPaths
数组中,如果未提供,则默认加载当前工作目录下的.env
文件。 -
如果提供了自定义文件路径,则将其解析为绝对路径并添加到
optionPaths
数组中。
// 设置要加载的 .env 文件路径数组,默认加载当前工作目录下的 .env 文件
let optionPaths = [dotenvPath] // 默认为 .env 文件路径
if (options && options.path) {
if (!Array.isArray(options.path)) {
// 如果提供的路径不是数组,将其转换为数组
optionPaths = [_resolveHome(options.path)]
} else {
optionPaths = [] // 重置默认值
// 遍历提供的路径数组
for (const filepath of options.path) {
// 将路径解析为绝对路径,并添加到 optionPaths 数组中
optionPaths.push(_resolveHome(filepath))
}
}
}
- 解析环境变量:
-
使用
fs.readFileSync()
方法读取并解析每个.env
文件的内容。 -
将解析后的环境变量合并到
parsedAll
对象中。 -
如果在解析过程中出现错误,则记录最后一个加载错误。
// 用于临时保存解析后的环境变量数据的对象
let parsedAll = {}
// 用于保存最后一个加载错误
let lastError
// 遍历所有 .env 文件路径,逐一加载并解析环境变量
for (const path of optionPaths) {
try {
// 读取并解析 .env 文件内容
const parsed = DotenvModule.parse(fs.readFileSync(path, { encoding }))
// 将解析结果合并到 parsedAll 对象中
DotenvModule.populate(parsedAll, parsed, options)
} catch (e) {
// 如果启用了调试模式,输出加载错误信息
if (debug) {
_debug(`Failed to load ${path} ${e.message}`)
}
// 记录最后一个加载错误
lastError = e
}
}
- 处理自定义环境变量:
-
如果提供了自定义环境变量对象,则将解析结果添加到该对象中。
-
否则,将解析结果添加到
process.env
中。
// 如果提供了自定义环境变量对象,则将解析结果添加到该对象中
let processEnv = process.env
if (options && options.processEnv != null) {
processEnv = options.processEnv
}
DotenvModule.populate(processEnv, parsedAll, options)
- 返回结果:
// 如果存在加载错误,则返回包含错误信息的对象,否则返回解析后的环境变量对象
if (lastError) {
return { parsed: parsedAll, error: lastError }
} else {
return { parsed: parsedAll }
}
在
configDotenv
函数中调用parse
解析文件内容并将其转换为键值对格式 ,调用populate
将解析结果添加到process.env
对象 中。
内容转换键值对函数parse()| 重点
/**
*解析环境变量文件内容并将其转换为键值对格式的函数。
参数:
- src: 待解析的环境变量文件内容,类型为字符串或缓冲区。
返回值:
- obj: 解析后的环境变量对象,键为环境变量的名称,值为环境变量的值。
*/
// 解析环境变量文件内容并将其转换为键值对格式的函数。
function parse(src) {
const obj = {} // 初始化一个空对象,用于存储解析后的环境变量
let lines = src.toString() // 将传入的文件内容转换为字符串形式
lines = lines.replace(/\r\n?/mg, '\n') // 将所有换行符统一转换为 \n
let match
// 使用正则表达式 LINE 对文件内容进行逐行匹配,以解析环境变量
while ((match = LINE.exec(lines)) != null) {
const key = match[1] // 获取环境变量的名称作为键
let value = (match[2] || '') // 获取环境变量的值,默认为空字符串
value = value.trim() // 去除值的首尾空格
const maybeQuote = value[0] // 检查值是否被引号包裹
// 如果值被双引号包裹,则去除双引号,并将 \n 和 \r 转换为相应的换行符
if (maybeQuote === '"') {
value = value.replace(/\\n/g, '\n')
value = value.replace(/\\r/g, '\r')
}
obj[key] = value // 将解析后的键值对存储到对象中
}
return obj // 返回解析后的环境变量对象
}
详细步骤:
-
初始化一个空对象 obj,用于存储解析后的环境变量。
-
将传入的文件内容 src 转换为字符串形式,存储在变量 lines 中。
-
将 lines 中的所有换行符统一转换为 \n。
-
使用正则表达式 LINE 对 lines 进行匹配,以逐行解析环境变量。
-
在循环中,对于每一行匹配结果 match:
-
提取环境变量的名称作为键 key。
-
提取环境变量的值作为值 value。
-
对值进行修剪操作,去除首尾空格。
-
如果值被双引号包裹,则去除双引号,并将 \n 和 \r 转换为相应的换行符。
-
将解析后的键值对存储到 obj 中。
- 循环结束后,返回解析后的环境变量对象 obj。
LINK正则表达式解析如下:
(样式有点花,就直接截图了笔记文档里的记录)
将环境变量加载到process.env对象函数populate()
-
参数解构和默认参数:
function populate(processEnv, parsed, options = {}) { const debug = Boolean(options && options.debug) const override = Boolean(options && options.override)
-
这个函数接受三个参数:
processEnv
,parsed
,和可选的options
对象。 -
options
对象具有两个可能的属性:debug
和override
。如果没有提供options
对象,将使用默认参数{}
。
-
参数类型检查:
if (typeof parsed !== 'object') { const err = new Error('OBJECT_REQUIRED: Please check the processEnv argument being passed to populate') err.code = 'OBJECT_REQUIRED' throw err }
此段代码确保 parsed
参数是一个对象,如果不是,将抛出一个带有错误代码的错误对象。
-
应用环境变量:
for (const key of Object.keys(parsed)) { if (Object.prototype.hasOwnProperty.call(processEnv, key)) { if (override === true) { processEnv[key] = parsed[key] } if (debug) { if (override === true) { _debug(`"${key}" is already defined and WAS overwritten`) } else { _debug(`"${key}" is already defined and was NOT overwritten`) } } } else { processEnv[key] = parsed[key] } }
-
这个循环遍历
parsed
对象的所有键,并检查每个键是否已存在于processEnv
中。 -
如果存在,根据
override
的值来确定是否覆盖已存在的环境变量。 -
如果启用了调试模式 (
debug
),则会输出相应的调试信息。 -
如果键不存在于
processEnv
中,则直接将其添加到processEnv
中。
部分代码详细说明:
const override = Boolean(options && options.override)
-
override
是一个布尔值参数,用于控制在将解析出的环境变量应用到process.env
中时,是否覆盖已存在的同名环境变量。 -
当
override
参数为true
时,如果已存在同名环境变量,则新解析的环境变量将覆盖原有的同名环境变量;当override
参数为false
时,如果已存在同名环境变量,则新解析的环境变量不会覆盖原有的同名环境变量,保留原值。
Object.prototype.hasOwnProperty.call(processEnv, key)
Object.prototype.hasOwnProperty:
-
hasOwnProperty
是 JavaScript 中所有对象继承自Object.prototype
的一个方法。这个方法返回一个布尔值,指示对象自身是否具有指定的属性。 -
例如,
obj.hasOwnProperty('key')
返回true
如果obj
直接拥有key
属性,而不是通过原型链继承的。
.call(processEnv, key) :
-
call
方法在这里用于调用hasOwnProperty
方法,并且将processEnv
作为this
上下文,key
作为参数。 -
这样做的目的是避免直接在对象上调用
hasOwnProperty
方法,因为对象可能会有一个名为hasOwnProperty
的自身属性,这会覆盖原型链上的方法。
最后
本文章代码含量较高,阅读可能比较吃力,当小伙伴在阅读复杂代码时,尝试复刻源码是一个很好的学习方法。
通过手动编写或重现代码,你会更深入地理解每个步骤和其中涉及的知识点。这种实践有助于加强你对代码逻辑和实现细节的理解,提升你的编程技能。
在尝试复刻源码时,可以尝试以下方法:
- 逐行分析:逐行分析代码,理解每一步的作用和目的。
- 手动编写:尝试手动编写类似的函数或代码片段,以便加深对每个步骤的理解。
- 调试和测试:通过调试和测试你的代码,验证其是否与原始代码的行为一致。
- 尝试不同的实现方式:在复刻源码的过程中,可以尝试使用不同的实现方式或算法来达到相同的目标,这有助于拓展你的思维和解决问题的能力。
敬请期待源码解析下文!