dotenv如何实现env环境变量注入
我们常常看到,在使用 cli 的时候可以通过配置.xxx.env ,.env 之类的文件,将文件内的常量注入到 process.env 或是 process.xx 上,其实现原理其实简单,这里我们通过解析 dotenv 的源码来学习其中原理.
dotenv (opens new window) 从功能的角度出发,我们可以将功能分为以下方面:
- 查找并读取 .env 文件
- 解析 .env 文件内容,将其拆成键值对的对象形式
- 将拆分好的值赋到 process.env 上
- 最后返回解析后得到的对象
# 实现
先看.env 文件的写法:
// .env
REDIS_NAME=redis-EkmW
REDIS_HOST=127.0.0.1
REDIS_PORT=55000
REDIS_PASSWORD=123456
REDIS_CACHE_TIME = 3600
1
2
3
4
5
6
2
3
4
5
6
再看简单实现:
interface Src {
src: string; // parse函数的src为fs读取的.env文件的字符串内容, 为Buffer数据流,可解码为string
}
// 搜索字符串中满足条件xxx=yyy的数据
const LINE =
/(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm;
function parse(src: string) {
const obj = {};
// buffer 转为 string
let lines = src.toString();
let match;
// exec() 方法在一个指定字符串中执行一个搜索匹配。返回一个结果数组或 null, 例如:
/* match: ['REDIS_NAME=redis-EkmW',
'REDIS_NAME',
'redis-EkmW', ...]
*/
while ((match = LINE.exec(lines)) != null) {
// 拿到键
const key = match[1];
// 拿到值
let value = match[2] || "";
// 移除两端空格
value = value.trim();
// 判断值是否为 "xx" 的情况
const maybeQuote = value[0];
// 移除value最外层的'"`符号
value = value.replace(/^(['"`])([\s\S]*)\1$/gm, "$2");
// 如果双引号,则展开换行
if (maybeQuote === '"') {
value = value.replace(/\\n/g, "\n");
value = value.replace(/\\r/g, "\r");
}
obj[key] = value;
}
return obj;
}
function _resolveHome(envPath) {
return envPath[0] === "~"
? path.join(os.homedir(), envPath.slice(1))
: envPath;
}
interface Options {
path?: string; // .env文件的路径
encoding?: string; // 解码方式。默认为 utf-8
override?: boolean; // 默认为false,为true时.env文件的值覆盖其应用上设置的任何环境变量
}
function config(options: Options) {
let dotenvPath = path.resolve(process.cwd(), ".env");
let encoding = "utf8";
const override = Boolean(options && options.override);
if (options) {
// 自定义.env路径
if (options.path != null) {
dotenvPath = _resolveHome(options.path);
}
// 自定义解码
if (options.encoding != null) {
encoding = options.encoding;
}
}
try {
// Specifying an encoding returns a string instead of a buffer
// 指定编码将fs读出的buffer解码为字符串
const parsed = parse(fs.readFileSync(dotenvPath, { encoding }))
console.log(parsed)
Object.keys(parsed).forEach(function (key) {
// 为process.env配值
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`)
}
}
}
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
我们可以看到, 其根本就是用 fs.readFileSync 读取 .env 文件,并解析文件为键值对形式的对象,将最终结果对象遍历赋值到 process.env 上, 最终实现了功能。
# 小结
dotenv 把环境变量加载进 process.env 对于前端项目来说还不够,因为浏览器环境是访问不到 process 的,需要通过 webpack 的 DefinePlugin 在构建阶段把变量替换为对应的值。
new webpack.DefinePlugin({
PRODUCTION: JSON.stringify(true),
VERSION: JSON.stringify("5fa3b9"),
BROWSER_SUPPORTS_HTML5: true,
TWO: "1+1",
"typeof window": JSON.stringify("object"),
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
});
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
例如我们可以采用dotenv-webpack (opens new window), 其原理与 dotenv 类似,只是最终将处理后的值填入了 webpack.DefinePlugin 中去,最终以实现:
// file1.js
console.log(process.env.DB_HOST);
// '127.0.0.1'
// bundle.js
console.log("127.0.0.1");
1
2
3
4
5
6
2
3
4
5
6
这样的效果。
编辑 (opens new window)
上次更新: 2022/12/11, 20:19:48