Vite中的dotenv

需求

Vite常用的命令有vite devvite build,项目会部署在开发环境和生产环境(可能还有别的环境,我们暂时不考虑,只考虑最简单的场景)。方便起见,我们后面都用阶段模式这两个概念来描述:

  • 阶段:开发阶段(vite dev) 和 构建阶段(vite build)
  • 模式:开发模式(development) 和 生产模式(production)

我们可能想实现这样的需求:

  1. 每个模式都有对应的配置文件,用来维护同样一组变量在不同模式下的值
  2. 通过在package.jsonscripts字段中声明不同命令,可以让不同阶段和模式起到排列组合使用的效果。这样的好处是,我们在开发的时候可能想使用生产模式的变量,不用改配置文件,直接运行相应的命令即可。
命令npm run devnpm run dev:prodnpm run build:devnpm run build:prod
开发阶段
构建阶段
开发模式
生产模式

实现

简单看了下Vite官网open in new window,发现它有一个 模式(mode) 的概念恰好和我们上面说得模式的概念一致。

  • mode值可以使用import.meta.env.MODE访问
  • 不同的vite命令,mode有各自的默认值:
    • vite dev:默认development
    • vite build:默认production
  • mode默认值可以通过传递 --mode 选项标志来覆盖命令使用的默认模式:
    • vite dev --mode production
    • vite build --mode development
  • Vite会使用dotenv自动加载项目根目录下的.env.${mode}下的环境变量

现在我们已经可以通过import.meta.env.MODE来区分不同的模式了,那么怎么区分不同的阶段呢?我注意到这两个变量:

  • import.meta.env.DEV: {boolean} 应用是否运行在开发环境 (永远与 import.meta.env.PROD相反)
  • import.meta.env.PROD: {boolean} 应用是否运行在生产环境。

我原本以为这两个变量就正好对应了开发阶段和构建阶段,但测试后发现它其实只和mode有关,与所处的阶段没有关系:

import.meta.env.DEV === (import.meta.env.MODE === 'development')
import.meta.env.PROD === (import.meta.env.MODE === 'production')

那有没有办法使得import.meta.env.DEVimport.meta.env.PROD符合我们的预期与阶段对应呢?我查看了Vite源码发现是可行的。

首先在Vite源码仓库open in new window页面按下键盘的.键,即可跳转到在线vscode编辑器查看仓库源码。我们区分大小写全局查找PROD,定位到import.meta.env.PROD定义在/packages/vite/src/node/config.ts文件下。

const resolved: ResolvedConfig = {
  // ...
  env: {
    ...userEnv,
    BASE_URL,
    MODE: mode,
    DEV: !isProduction,
    PROD: isProduction  // 453行
  },
  // ...
}

我们接着看453行的isProduction的定义:

const userEnv =
    inlineConfig.envFile !== false &&
    loadEnv(mode, envDir, resolveEnvPrefix(config)) // 348行

// Note it is possible for user to have a custom mode, e.g. `staging` where
// production-like behavior is expected. This is indicated by NODE_ENV=production
// loaded from `.staging.env` and set by us as VITE_USER_NODE_ENV
const isProduction = (process.env.VITE_USER_NODE_ENV || mode) === 'production'  // 353行
if (isProduction) {
  // in case default mode was not production and is overwritten
  process.env.NODE_ENV = 'production'
}

可以看到,这里和我刚才的推测import.meta.env.PROD === (import.meta.env.MODE === 'production')已经很接近了,这里的mode就是import.meta.env.MODE,我们来看看process.env.VITE_USER_NODE_ENV又是什么。

export function loadEnv(  // 967行
  mode: string,
  envDir: string,
  prefixes: string | string[] = 'VITE_'
): Record<string, string> {
  if (mode === 'local') {
    throw new Error(
      `"local" cannot be used as a mode name because it conflicts with ` +
        `the .local postfix for .env files.`
    )
  }
  prefixes = arraify(prefixes)
  const env: Record<string, string> = {}
  const envFiles = [
    /** mode local file */ `.env.${mode}.local`,
    /** mode file */ `.env.${mode}`,
    /** local file */ `.env.local`,
    /** default file */ `.env`
  ]

  // check if there are actual env variables starting with VITE_*
  // these are typically provided inline and should be prioritized
  for (const key in process.env) {
    if (
      prefixes.some((prefix) => key.startsWith(prefix)) &&
      env[key] === undefined
    ) {
      env[key] = process.env[key] as string
    }
  }
  /**
   * 上面的代码主要干了一件事,遍历`process.env`里的变量
   * 把以`VITE_`开头的变量存储到`env`对象中
   */

  /**
   * 下面的for循环主要做了这些事:
   * 1. 用dotenv模块解析所有的`.env.${mode}`文件
   * 2. 把第1步解析出的变量中,以`VITE_`开头的变量存储到`env`对象中
   * 3. 再第2步中额外判断,如果变量的key是NODE_ENV,就把process.env.VITE_USER_NODE_ENV设为NODE_ENV的值
   */
  for (const file of envFiles) {
    const path = lookupFile(envDir, [file], true)
    if (path) {
      const parsed = dotenv.parse(fs.readFileSync(path), {
        debug: !!process.env.DEBUG || undefined
      })

      // let environment variables use each other
      dotenvExpand({
        parsed,
        // prevent process.env mutation
        ignoreProcessEnv: true
      } as any)

      // only keys that start with prefix are exposed to client
      for (const [key, value] of Object.entries(parsed)) {
        if (
          prefixes.some((prefix) => key.startsWith(prefix)) &&
          env[key] === undefined
        ) {
          env[key] = value
        } else if (key === 'NODE_ENV') {
          // NODE_ENV override in .env file
          process.env.VITE_USER_NODE_ENV = value  // 1021行
        }
      }
    }
  }
  return env
}

可以看到代码中定义了loadEnv函数,代码内容较多,我在其中做了注释,这里不再赘述。我画了一张思维导图,应该能更清晰的描述这个过程。

那到这里,其实我们可以确定了,如果我们没有在.env.${mode}文件中设置过NODE_ENV变量,那import.meta.env.PROD的值就是import.meta.env.MODE === 'production'的返回值,与我们上面的猜想一致。

那如果我们在.env.${mode}文件中设置了NODE_ENV,则import.meta.env.PROD还是与模式绑定的:

# .env.production
NODE_ENV = production # 如果设置为production,不论vite dev:prod还是vite build:prod,import.meta.env.PROD的值都是ture。结果还是与mode绑定的

其实我们只需要在命令行设置VITE_USER_NODE_ENV环境变量即可:

VITE_USER_NODE_ENV=development vite dev --mode development
VITE_USER_NODE_ENV=development vite dev:prod --mode production
VITE_USER_NODE_ENV=production vite build:dev --mode development
VITE_USER_NODE_ENV=production vite build:prod --mode production

但是要在scripts中设置环境变量需要借助cross-env包:

npm install -D cross-env
{
  "scripts": {
    "dev": "cross-env VITE_USER_NODE_ENV=development vite --mode development",
    "dev:prod": "cross-env VITE_USER_NODE_ENV=development vite --mode production",
    "build:dev": "cross-env VITE_USER_NODE_ENV=production vite build --mode development",
    "build:prod": "cross-env VITE_USER_NODE_ENV=production vite build --mode production"
  }
}
Last Updated: