浅析 vite 环境变量静态替换原理

3,002 阅读8分钟

本文正在参加「金石计划」

前言

环境变量的静态替换指:使用编译工具对前端项目构建打包过程中将代码中的特定字符串标记进行替换。

不论使用的前端框架是 react、vue、angular 还是 svelte,在构建打包时绝大多数都会进行环境变量的替换,因此弄懂环境变量静态替换原理尤其重要。

所以,本文以当下热门的构建工具 vite 为例,来简单跟踪分析其环境变量替换的过程和原理。

vite 简介

vite 是什么就不多说,简言之:它是当前十分热门的前端构建工具,同时支持 vue、react 等多种框架的构建打包。

官网上其特性介绍如下:

image.png

其和传统的构建工具最大的不同是:

使用原生 ESM 文件,无需打包

ESMES6 Module 的简称,本地开发环境启动时入口的 index.html 页面使用直接加载经过 vite 转换之后的 ES6 模块

webpack 系列构建工具在本地开发环境启动时,会将所有 JS 资源转换为 UMD 模块

这一不同点也导致了 vite 在开发环境与生产环境下环境变量“替换”的原理不一样,下文【疑问解释】部分会讲到。

环境变量替换简述

前端项目中环境变量替换的过程可以描述为:

项目中一般会有 .env .env.test .env.prod 这样的配置文件,在不同的打包环境下构建工具会解析不同的配置文件,这些配置文件最后会将源码中的相应标记替换成配置文件的值。

image.png

vite 环境变量的用法和 webpack 中不太一样,官网描述如下:

image.png

不管是 vite 自带的还是用户通过 .env 配置文件指定的环境变量都是通过 import.meta.env.XXX 暴露出来。

替换效果展示

这里使用命令 npm create vite@latest 创建一个基于 vite 的示例项目,观察环境变量的替换效果。

源代码使用环境变量 import.meta.env.VITE_SOME_KEY,通过 .env 文件配置其对应的值为字符串 123

image.png

image.png

开发环境

调试工具看到 import.meta.env 被替换成了一个对象,其中包含 .env 中定义的 VITE_SOME_KEY

image.png

image.png

同时,这段代码的打印逻辑也正确执行了。

image.png

生产环境

使用 npx vite build 构建项目,然后使用 npx vite preview 预览项目。

通过调试工具进行代码格式化,看到 import.meta.env.VITE_SOME_KEY 直接被替换成了字符串 123

image.png

毫无疑问,代码逻辑同样也正确执行了;但是,打包后代码中变量变量是直接被替换成了字符串,开发环境中却不是这样

这个区别点先不展开讲,下文再继续说。

动态取值

上面源码里环境变量的写法是一种静态写法,下面看下其态动态取值写法。

vite 官网强调生产环境下环境变量动态取值写法是无效的。

image.png

动态取值写法近似如下:

const key = 'VITE_SOME_KEY'
console.log(import.meta.env[key])

修改源码环境变量为动态取值写法

image.png

开发环境使用动态取值写法,是生效的

image.png

生产环境环境变量替换为如下:

image.png

代码逻辑执行正确,说明生产环境下动态取值也有效的

image.png

这里,动态取值与官方文档上不一致,至于为什么会生效后文再解答。

替换过程跟踪分析

下面重点跟踪分析 vite 构建打包过程中环境变量的替换原理,从打包命令 npx vite build 开始分析。

vite 解析配置文件路径

vite 打包命令第一步是解析相关配置文件。

读取vite.conig.js配置文件,若configFile或envFile设置为 false 则不解析环境变量。

根据构建模式 mode 标识解析项目根路径下的 env 配置文件路径。

按照以下顺序依次检查并解析配置文件。

image.png

例如:若执行默认的生产环境构建命令 npx vite build,则 modeproduction,解析配置文件的顺序为 .env => .env.local => .env.production => .env.production.local

若以上配置文件中存在同名的 key,则按照上述顺序后者覆盖前者

image.png

.env.production.local 配置文件的优先级最高,VITE_SOME_KEY 的值为 '123456'

image.png

dotenv 解析配置内容

vite 根据 mode 解析到的配置文件路径进行文件读取得到文件字符串,然后将其交给 dotenv 进行配置解析。

image.png

上面环境变量配置转换为下方的配置对象

image.png

这里将文本以换行符 \n 进行分割,同时 = 作为对象 keyvalue 的分隔符,value 默认为空字符串,使用 # 注释的行会被忽略。

dotenv-expand 扩展配置语法

dotenv 将文本 FOO=bar 转换为对象 { FOO: 'bar' },这是一种最简单的转换语法,doctenv-expand 则对这个转换语法进行了扩展。

PASSWORD="s1mpl3"
DB_PASS=$PASSWORD

这种写法的字面意思应该是

let DB_PASS = PASSWORD = "s1mpl3"

得到的对象应该是

{
    DB_PASS: 's1mpl3',
    PASSWORD: 's1mpl3',
}

但是经过前面步骤解析后 DB_PASS 的值是 '$PASSWORD',于是需要使用 dotenv-expand 进一步转换配置对象。

转换后的配置对象为

image.png

vite 过滤配置对象

vite 会遍历配置对象将不以 prefix 开头的 key 移除掉;prefix 默认为:'VITE_',可以通过 vite.config.js 中的 envPrefix 配置项进行修改。

过滤后的配置对象为

image.png

vite 追加环境变量

1. 追加 process.env 环境变量

检查当前 process.env 中是否有以 prefix 开头的环境变量配置,有则将其追加到上一步的配置对象中。

如果执行的构建命令是 VITE_SOME_KEY=456 npx vite build,则 process.env 上带有命令传入的环境变量 VITE_SOME_KEY ,其会被合并到配置对象上并覆盖已有的同名配置。

因此,命令行当中的传入的环境变量配置优先级最高

2. 追加内建环境变量

接着追加 vite 内建环境变量

image.png

追加之后的结果如下:

image.png

definePlugin 配置项生成

这里的 definePlugin 是环境变量静态替换的核心 rollup 插件,由 vite 插件包提供的,并非 @rollup/plugin-replace 插件;在 vite 中该插件的名称叫作:vite:define

image.png

image.png

image.png

definePlugin 构建文本替换正则

将不同类型的环境变量合并

image.png

image.png

合并后的环境变量对象

image.png

根据上述环境变量对象生成用于源码字符串替换的正则表达式

image.png

该正则表达式可视化的结果

image.png

definePlugin 进行文本替换

经过上述过程,环境变量静态替换相关的准备工作已完成,将生成的配置项传递给 rollup 来编译源码。

image.png

根据 rollup 的插件运行规则,执行到 definePlugin 插件的 transform 阶段,开始使用上述正则表达式匹配源码进行文本替换。

image.png

替换之后的源码如下:

image.png

到这里,APP.vue 源码的所有环境变量标记都已经被替换,其他源代码依次按照 rollup 的文件遍历处理规则也会被 definePlugin 插件进行替换。

main.js 中的静态取值写法被替换为如下:

image.png

magic-string 进行字符串替换

对源代码进行字符串替换是替换的细节,但还是单独讲一下。definePlugin 内部使用了 magic-string 这个非常底层的工具库,@rollup/plugin-replace 中也用到了。

通过上一步的正则表达式去匹配源码,找到环境变量标记的在字符串中的索引位置,然后使用进行替换。

使用 magic-string 可以简化对字符串替换的操作,关键是方便生成 sourcemap,它的使用示例如下:

import MagicString from 'magic-string';
import fs from 'fs'

const s = new MagicString('problems = 99');

s.overwrite(0, 8, 'answer');
s.toString(); // 'answer = 99'

s.overwrite(11, 13, '42'); // character indices always refer to the original string
s.toString(); // 'answer = 42'

s.prepend('var ').append(';'); // most methods are chainable
s.toString(); // 'var answer = 42;'

const map = s.generateMap({
  source: 'source.js',
  file: 'converted.js.map',
  includeContent: true
}); // generates a v3 sourcemap

fs.writeFileSync('converted.js', s.toString());
fs.writeFileSync('converted.js.map', map.toString());

疑问解释

下面解答上文中没有解释的几个问题。

为何生产环境下动态取值也有效的?

definePlugin 初始化配置得到的环境变量配置对象 replacements 可以看到,import.meta.env 对应的值是一个 JSON 字符串。

image.png

definePlugin 用正则表达式替换源码过程中会将动态取值写法进行替换。

let key = 'VITE_SOME_KEY'
import.meta.env[key]

被替换为:

let key = 'VITE_SOME_KEY'
{"VITE_SOME_KEY":"12345","BASE_URL":"/","MODE":"production","DEV":false,"PROD":true,"SSR":false}[key]

所以,生产环境下动态取值写法有效。

vite 开发环境与生产环境下环境变态替换是否有区别?

肯定是有区别的。

开发环境下我们编写的源码、第三方 npm 包都会被编译成原生 ESM 模块,入口 html 页面通过 <script type="module" src="xxx"></script> 来加载执行这些编译好的 ESM 模块。

这种方式加载的 ESM 模块内部的存在一个全局的 import.meta 对象,vite 在转换代码为 ESM 模块时,会在文件头部定义环境变量配置,如下:

image.png

image.png

开发环境下源码中的环境变量标记未被替换,而生产环境下源码中的环境变量标记会按照上文分析的过程进行替换。

总结

vite 在生产环境下环境变量的静态替换过程可以概括为下图:

image.png

关键流程总结为:通过 vite 的项目配置解析出环境变量配置,然后借助 rollup 对源码进行替换,处理字符串替换的核心 rollup 插件是 vite 自己编写的 vite:define 插件

对于 .env 文件内容的解析、字符串的替换 vite 没有重新造轮子,分别采用了 dotenv、doctenv-expandmagic-string 来实现。

出于篇幅的考虑,本文未对 vite 开发环境下环境变量的替换过程做详细跟踪分析,后续再专门出一篇文章进行讲解。

参考资料