无论是vue还是react项目,项目根目录下一般都会有.env文件,保存不同环境的变量配置。而Dotenv就是一个零依赖模块,可将 .env文件中的环境变量加载至process.env中。那么Dotenv是如何做到这一点的?我打开了项目里所使用的Dotenv源码。
版本对比
以下的Dotenv源码阅读基于8.2.0的版本,目前最新的版本是16.3.1,核心文件mian.js中,代码量从100行扩展到了300行。我阅读了两个版本的代码,16.3.1的代码里主要添加了对环境变量进行加密的功能,核心方法config和parse的逻辑不变,但是添加了许多更细致的处理规范,能理解8.2.0的代码逻辑就足够明白Dotenv是如何运作的了。
Dotenv gitlub地址:https://github.com/motdotla/dotenv/tree/master
//16.3.1版本中main.js所导出的方法
module.exports.configDotenv = DotenvModule.configDotenv
module.exports._configVault = DotenvModule._configVault
module.exports._parseVault = DotenvModule._parseVault
module.exports.config = DotenvModule.config
module.exports.decrypt = DotenvModule.decrypt
module.exports.parse = DotenvModule.parse
module.exports.populate = DotenvModule.populate
//8.2.0版本中main.js所导出的方法
module.exports.config = config
module.exports.parse = parse
源码分析
其实在main.js中,作者已经备注了config和parse方法分别是做什么的。简单来说
1.parse方法负责将.env文件中KEY = VAL的内容转化成对象形式
2.config方法负责遍历该对象,将其填充到process.env上
parse方法
const NEWLINE = '\n'
const RE_INI_KEY_VAL = /^\s*([\w.-]+)\s*=\s*(.*)?\s*$/
const RE_NEWLINES = /\\n/g
const NEWLINES_MATCH = /\n|\r|\r\n/
function parse (src /*: string | Buffer */, options /*: ?DotenvParseOptions */) /*: DotenvParseOutput */ {
const debug = Boolean(options && options.debug)
const obj = {}
// 将.env变量通过换行符分隔
src.toString().split(NEWLINES_MATCH).forEach(function (line, idx) {
// 判断每一项是否符合KEY = VAL的标准
const keyValueArr = line.match(RE_INI_KEY_VAL)
if (keyValueArr != null) {
const key = keyValueArr[1]
// 下面几行代码主要是在判断等号后的val值是使用单引号还是双引号包裹
// 然后将引号包裹的值去除首位的空格后取出
let val = (keyValueArr[2] || '')
const end = val.length - 1
const isDoubleQuoted = val[0] === '"' && val[end] === '"'
const isSingleQuoted = val[0] === "'" && val[end] === "'"
if (isSingleQuoted || isDoubleQuoted) {
val = val.substring(1, end)
if (isDoubleQuoted) {
val = val.replace(RE_NEWLINES, NEWLINE)
}
} else {
val = val.trim()
}
// 将该项添加至对象中
obj[key] = val
} else if (debug) {
log(`did not match key and value when parsing line ${idx + 1}: ${line}`)
}
})
return obj
}
config方法
function config (options /*: ?DotenvConfigOptions */) /*: DotenvConfigOutput */ {
// 读取当前目录下的.env文件
let dotenvPath = path.resolve(process.cwd(), '.env')
let encoding /*: string */ = 'utf8'
let debug = false
if (options) {
// 如果设置了自定义环境变量的文件,那么优先使用该文件
if (options.path != null) {
dotenvPath = options.path
}
// 编码方式
if (options.encoding != null) {
encoding = options.encoding
}
// 是否debug
if (options.debug != null) {
debug = true
}
}
try {
// 调用fs.readFileSync读取.env文件,将读取的结果传入parse方法,得到对象
const parsed = parse(fs.readFileSync(dotenvPath, { encoding }), { debug })
// 遍历该对象
Object.keys(parsed).forEach(function (key) {
// 如果process.env不存在该值,那么将将值添加到process.env上
if (!Object.prototype.hasOwnProperty.call(process.env, key)) {
process.env[key] = parsed[key]
} else if (debug) {
log(`"${key}" is already defined in \`process.env\` and will not be overwritten`)
}
})
return { parsed }
} catch (e) {
return { error: e }
}
}