vite是如何解析用户配置的.env的
0. 前言
正文:
我们在实际项目开发的过程中,会用到一些环境变量,根据不同的环境,变量的值可能不一样,比如 测试环境 / 预发环境 / 线上环境,接口地址、资源前缀等等。在使用vite的时候,我们只要在项目中添加 .env
或者 .env.[mode]
就可以,详细使用可以参考 vite的官方文档。那么vite是如何解析这个env文件的呢?看一下vite关于解析 .env
文件的相关源码,探究其原理。
1.debug运行起来
vite
的源码在 packages/vite
目录下,我们通过项目的 CONTRIBUTING.md
可以了解到,在 vite
目录下,运行 pnpm run dev
,就可以进行方便的调试模式。运行完 pnpm run dev
,再查看 package.json
它的执行文件在 bin/vite.js
再打开 vite/src
目录,我们可以看到 env.ts
文件,关于加载 env 相关的代码就在这个文件下。
我们打开 env.ts
文件, command + K
+ command + 0
折叠代码(vscode),可以看到两个函数声明 loadEnv
和 resolveEnvPrefix
,从函数名称不难看出, loadEnv
应该就是加载 env 的函数, resolveEnvPrefix
应该是处理环境变量前缀的。
下面我们在 loadEnv
函数的第一行打个断点,然后在vite的根目录下运行 node bin/vite.js
, 也可以用 node bin/vite.js dev
或 node bin/vite.js serve
因为我们实际运行项目就是用 vite dev
/ vite serve
/ vite
,就是执行我们在开发环境执行的命令。
这样我们就可以进入断点了。
我们来看一下调用栈
执行了 vite 命令,先执行对应的 action,action里面调用了 server/index.ts
的 createServer
createServer 里调用了 node/config.ts
的 resolveConfig
resolveConfig里调用了 node/env.ts
的 loadEnv 函数
2. 分析 loadEnv 函数
2.1 mode 不能为 local
默认情况下,vite 在开发环境的 mode
是 development
,生成环境是 production
。
有时候,这两个还不够用,所以就需要自己额外再指定。我们可以通过命令行 --mode=xxx
来自己指定 ,指定了mode之后,可以通过 .env.[mode]
文件读取指定 mode
下的环境变量。但 mode
不能为 local
,因为跟内置的 local
冲突了。
2.1 arraify 函数
源码如下
// env.ts
prefixes = arraify(prefixes)
// utils.ts
export function arraify<T>(target: T | T[]): T[] {
return Array.isArray(target) ? target : [target]
}
arraify
函数还挺实用的,项目中也有可能会经常用到,就是对参数同时支持某一类型和以某一类型组成的数组,这里用来处理环境变量的前缀,统一处理成数组,这样方便后面的操作。
2.3 解析env文件
核心的逻辑就是下面这段代码
const envFiles = [
/** default file */ `.env`,
/** local file */ `.env.local`,
/** mode file */ `.env.${mode}`,
/** mode local file */ `.env.${mode}.local`,
]
// ...
// 核心逻辑
const parsed = Object.fromEntries(
envFiles.flatMap((file) => {
// 找到文件路径,envDir是可以通过配置项指定的,你可以用个文件件把它统一放在一起,默认就是根目录
const filePath = path.join(envDir, file)
// 如果不是文件,则忽略
if (!tryStatSync(filePath)?.isFile()) return []
// parse 得到一个key-value对象
return Object.entries(parse(fs.readFileSync(filePath)))
}),
)
// ...
export function tryStatSync(file: string): fs.Stats | undefined {
try {
return fs.statSync(file, { throwIfNoEntry: false })
} catch {
// Ignore errors
}
}
这里我们看用到的几个函数
Object.fromEntries 这是将键值对列表转换为对象。
Array.prototype.flatMap 这是通过将给定的回调用于数组的每个元素,然后将结果展平一级返回新的数组。可以理解为是先map(),后flat(1)的合集操作。
我们看下 tryStatSync
这个函数,作用是获取指定目录文件的信息,判断它是不是一个文件。用 fs.stateSync
方法,返回的是 fs.Stats
对象,对象上提供了一些列方法,其中就有 isFile
和 isDirectory
。不过源码中指定了 throwIfNoEntry
参数为false,表示指定目录的文件如果不存在不抛出错误,默认其实是 true
,会抛出错误的,那么 try catch
好像就没有什么必要了,不知道为什么要加,感觉是多余的。
最最核心的就是一句 parse(fs.readFileSync(filePath)
,读取文件内容,然后交给 dotenv 提供的 parse
方法,将文件解析成对象。因为涉及到多个文件的对象合并,所以这里作者采用的方法是先用 Object.entries
将对象变成键值对数组,然后展平一层,再将键值对数组转换成一个对象。
当然在这里我们也可以用 reduce的方式进行合并,我用reduce实现了一遍,也比较简单。
const parsed = envFiles.reduce((result, file) => {
const filePath = path.join(envDir, file)
if (!tryStatSync(filePath)?.isFile()) return result
return {...result, ...parse(fs.readFileSync(filePath)}
}, {})
2.4 处理特殊环境变量
源码如下:
if (parsed.NODE_ENV && process.env.VITE_USER_NODE_ENV === undefined) {
process.env.VITE_USER_NODE_ENV = parsed.NODE_ENV
}
// support BROWSER and BROWSER_ARGS env variables
if (parsed.BROWSER && process.env.BROWSER === undefined) {
process.env.BROWSER = parsed.BROWSER
}
if (parsed.BROWSER_ARGS && process.env.BROWSER_ARGS === undefined) {
process.env.BROWSER_ARGS = parsed.BROWSER_ARGS
}
// let environment variables use each other
// `expand` patched in patches/dotenv-expand@9.0.0.patch
expand({ parsed })
主要用到的是 dotenv-expand
这个包是在 dotenv
之上添加了变量扩展,也就是可以在环境变量文件 env 中使用变量,比如可以 USERNAME = ${USER}
。
在扩展之前处理了一些特殊的环境变量 NODE_ENV
、 BROWSER
、 BROWSER_ARGS
, NODE_ENV
是为了跟内置的 NODE_ENV
冲突,所以重命名成了 VITE_USER_NODE_ENV
,意味着我们在环境变量里设置 NODE_ENV
,需要用 process.env.VITE_USER_NODE_ENV
去读取,而不是 process.env.NODE_ENV
。其他两个是用在对浏览器的判断处理上的,搜索了一下源码,是在 openBrowser.js
中会用到,也就是打开浏览器用到的。
2.5 返回env对象
源码如下
for (const [key, value] of Object.entries(parsed)) {
if (prefixes.some((prefix) => key.startsWith(prefix))) {
env[key] = value
}
}
for (const key in process.env) {
if (prefixes.some((prefix) => key.startsWith(prefix))) {
env[key] = process.env[key] as string
}
}
return env
剩下的就是把解析出来的环境变量和内置的环境变量,筛选出以指定前缀的都收集在 env 对象上,然后返回出去。默认前缀是 VITE_
.
这就是整个 loadEnv 做的事情,最终返回的是从 env 环境变量文件中解析出来的环境变量和内置的环境变量。
这里vite要求,环境变量必须要有一个统一的前缀,且前缀不能为空。这么做的目的是为了防止意外将环境变量泄漏给客户端,所以需要必须指定一个前缀。
dotenv 本身提供了一个 config
函数,它的作用是读取 .env
文件,然后把所有的变量都挂载在 process.env
对象上。
require('dotenv').config()
console.log(process.env)
vite 除了 .env
文件还支持 .env.[mode]
、 .env.local
、 .env.[mode].local
文件,并对环境变量做了统一前缀的约束。
3.总结
这里只是对解析环境变量做了简单的源码分析,其实核心逻辑就是利用 dotenv
对指定的环境变量做解析,然后处理几个特殊的环境变量,最后返回出去。
vite 使用的是 cac 实现的终端命令,关于这一块的使用,其实也不难,后面可以自己使用这个来实现一个自己的终端命令。
转载自:https://juejin.cn/post/7255795059660455994