- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第22期,链接:juejin.cn/post/708310…
在一些项目中存在.env文件,但是这个文件到底有什么作用呢?从名字上看一定是和环境相关的配置或者变量。有时一些项目是一套通用的代码,但是对于不同的公司和局点都是有一些定制化的要求,所以有一些配置文件是不一致的,区分这些不同环境就需要用到环境变量,设置不同的环境变量需要通过process.env来访问,那么这些变量是怎么存放到process.env中的。
为了解决上面的问题,那么就需要用到一个包---dotenv。
使用方式
.env文件是怎么样的,怎么使用的。 创建一个.env文件然后填写类似下面的内容(键值对的方式进行赋值) 这样就可以将需要的配置填写到文件中了
S3_BUCKET="YOURS3BUCKET"
SECRET_KEY="YOURSECRETKEYGOESHERE"
填写了对应的配置文件,那么要如何添加到环境变量中呢?使用如下的config函数就可以将.env中的变量添加到process.env中了。
require('dotenv').config()
console.log(process.env)
源码解读
读源码看package.json,可以知道入口文件为lib/main.js:
const fs = require('fs')
const path = require('path')
const os = require('os')
const packageJson = require('../package.json')
const version = packageJson.version
const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg
function parse (src) {}
function _log (message) {
console.log(`[dotenv@${version}][DEBUG] ${message}`)
}
function _resolveHome (envPath) {
return envPath[0] === '~' ? path.join(os.homedir(), envPath.slice(1)) : envPath
}
function config (options) {}
const DotenvModule = {
config,
parse
}
module.exports.config = DotenvModule.config
module.exports.parse = DotenvModule.parse
module.exports = DotenvModule
看了上面的代码其实主要就暴露了两个函数分别为config和parse,config函数是用来加载配置环境变量到process.env中的,parse函数是将.env文件读到内容 解析为对象格式返回。 那下面来分析一下这些函数的作用。
config
function config (options) {
// 读取 node 执行的当前路径下的 .env 文件
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) {
// 如果存在路径信息时,就对路径进行解析,保证读取对应路径下的.env配置文件
dotenvPath = _resolveHome(options.path)
}
if (options.encoding != null) {
encoding = options.encoding
}
}
try {
// 使用parse函数解析读取到的配置文件
const parsed = DotenvModule.parse(fs.readFileSync(dotenvPath, { encoding }))
// 将键值对赋值到process.env上
Object.keys(parsed).forEach(function (key) {
if (!Object.prototype.hasOwnProperty.call(process.env, key)) {
process.env[key] = parsed[key]
} else {
// 是否需要覆盖对应的属性
if (override === true) {
process.env[key] = parsed[key]
}
// 是否需要打印信息
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 }
}
}
其实代码很简单,就是先将用户的配置选项进行解析包括路径,打印与否,重写与否,编码格式。然后fs.readFileSync(dotenvPath, { encoding })读取对应的文件,并使用parse函数进行解析成键值对,使用循环将键值对赋值到process.env上
parse
function parse (src) {
const obj = {}
// 将文件的内容string化
let lines = src.toString()
// 将内容中的换行符进行替换
lines = lines.replace(/\r\n?/mg, '\n')
let match
// 检索解析内容是否匹配正则形如key=value,exec返回匹配则返回函数值
while ((match = LINE.exec(lines)) != null) {
const key = match[1]
let value = (match[2] || '')
// trim()删除两端空格
value = value.trim()
// 获取字符串的第一个值,主要是为了判断是否为'"'
const maybeQuote = value[0]
// 删除周围的"
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')
}
// 将值赋值到对象上,返回一个对象
obj[key] = value
}
return obj
}
parse主要就是进行解析文件返回键值对,其实很简单,主要是里面牵扯正则以及字符串的处理比较复杂,因为考虑的情况比较多,所以看起来较为复杂,如果不考虑对特殊情况的处理,应该会简单很多,这也就是我们和开源作者的区别