likes
comments
collection
share

vite是如何解析用户配置的.env的

作者站长头像
站长
· 阅读数 89

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

vite是如何解析用户配置的.env的

它的执行文件在 bin/vite.js

再打开 vite/src 目录,我们可以看到 env.ts 文件,关于加载 env 相关的代码就在这个文件下。

vite是如何解析用户配置的.env的

我们打开 env.ts 文件, command + K + command + 0 折叠代码(vscode),可以看到两个函数声明 loadEnvresolveEnvPrefix ,从函数名称不难看出, loadEnv 应该就是加载 env 的函数, resolveEnvPrefix 应该是处理环境变量前缀的。

下面我们在 loadEnv 函数的第一行打个断点,然后在vite的根目录下运行 node bin/vite.js, 也可以用 node bin/vite.js devnode bin/vite.js serve 因为我们实际运行项目就是用 vite dev / vite serve / vite ,就是执行我们在开发环境执行的命令。

这样我们就可以进入断点了。

vite是如何解析用户配置的.env的

我们来看一下调用栈

vite是如何解析用户配置的.env的

执行了 vite 命令,先执行对应的 action,action里面调用了 server/index.tscreateServer

vite是如何解析用户配置的.env的

createServer 里调用了 node/config.tsresolveConfig

vite是如何解析用户配置的.env的

resolveConfig里调用了 node/env.ts 的 loadEnv 函数

2. 分析 loadEnv 函数

2.1 mode 不能为 local

默认情况下,vite 在开发环境的 modedevelopment ,生成环境是 production

有时候,这两个还不够用,所以就需要自己额外再指定。我们可以通过命令行 --mode=xxx 来自己指定 ,指定了mode之后,可以通过 .env.[mode] 文件读取指定 mode 下的环境变量。但 mode 不能为 local ,因为跟内置的 local 冲突了。

vite是如何解析用户配置的.env的

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 对象,对象上提供了一些列方法,其中就有 isFileisDirectory 。不过源码中指定了 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_ENVBROWSERBROWSER_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
评论
请登录