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** 中