一次Vite define的深入研究
前言
Vite,作为一个现代前端开发环境,它的出现极大地提升了前端开发的效率和体验。Vite引领了前端构建工具的新趋势,不仅使得HMR(热模块替换)变得更加迅速,而且在构建速度、ES Modules支持等方面也表现出色。而在Vite中,有一个配置选项 define,我们将在这篇文章中深入探讨。
Vite和Rollup
值得注意的是,Vite在内部使用了Rollup作为其打包器。因此,Vite的define配置和Rollup的replace插件有类似的作用:它们都可以在源码中替换特定的字符串。然而,Vite对Rollup进行了优化,使得在开发模式下,Vite不需要使用Rollup,而在构建时才会使用Rollup进行打包。
因此,Vite的define在开发模式和生产模式下的行为是不同的。
Vite中的 define
在Vite中,define 选项主要用于在开发期间全局替换和内联环境变量或者其它变量。在上述代码示例中,我们将process.env定义为一个空对象,意味着在源码中的任何process.env引用都会被替换为一个空对象。
例子分析
先看一个Vite配置的例子:
我们测试将环境变量对象process.env写入Vite .
src/index入口文件
假设我有个入口文件,src/index.ts,代码如下,为了方便测试 process.env 功能,我只保留了关键代码
process.env.NODE_ENV !== "production" && console.log("test1");
process.env.NODE_ENV === "production" && console.log("test2");
例子1:纯Vite配置模式
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
define: {
'process.env': {}
}
})
网上有一篇教程,高赞的解决方法是:
我在Vite打包后的代码,关键节点加了两个log。作用:打印代码被替换前是什么样,代码被替换后是什么样。(代码路径,node_modules/vite/dist/node/chunks/dep-934dbc7c.js)
编辑切换为居中
添加图片注释,不超过 140 字(可选)
dev模式
pnpm run dev
不走这段代码,无log,符合预期。
build模式
pnpm run build
代码转换前Vite会进行变量替换(define就是干这个事的),最终 process.env.NODE_ENV被转换成了"production",符合预期。
Define
这里稍微补充下define的逻辑,
define替换分为两步,
-
replace,替换 "process.env" 为 {},你可以认为这阶段是纯文本处理。
-
transform,将ES6代码替换为 目标代码,通常是ES5,这里面会涉及语法解析,词法解析,就转换成AST的流程。
例子2.1:'process.env': {}
我们现在改下配置,将打包模式改成 umd格式。
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
define: {
'process.env': {}
},
build: {
lib: {
formats: ['umd'],
name: 'test',
entry: {
// 设置入口文件
entry: './src/index',
},
},
}
})
pnpm run build
控制台报错,被replace后的代码 {}.NODE_ENV !== "production"报错,错误原因是Rollup把{}当成了块状作用域处理。
说明在lib模式下,process.env 的处理 Vite 并没有处理好。anywhere,只是一个兼容问题,我们继续往下看。
例子2.2:'process.env': ({})
修改配置为
'process.env': `({})`
build成功了,替换成({})
,Rollup能当成JS的空对象解析,编译通过。
例子2.3:改成if语句
当我们修改用户源代码,Vite配置还是'process.env': {}
// process.env.NODE_ENV !== "production" && console.log("test1");
// process.env.NODE_ENV === "production" && console.log("test2");
if (process.env.NODE_ENV !== "production") {
console.log("test1");
} else {
console.log("test2");
}
打包结果,正常。在 if ()里的{}能被正确解析成对象。
浏览器解析
上面都是Rollup AST解析的处理,下面我们看看浏览器里的处理。
1、{}.a
2、({}).a
3、if ({}.a)
也是符合预期,Rollup块状作用域的解析规则和浏览器一致。
源码浅析
Vite版本 4.3.5,代码路径:/vite/packages/vite/src/node/plugins/define.ts
export function definePlugin(config: ResolvedConfig): Plugin {
const isBuild = config.command === 'build'
const isBuildLib = isBuild && config.build.lib
// ignore replace process.env in lib build
const processEnv: Record<string, string> = {}
const processNodeEnv: Record<string, string> = {}
if (!isBuildLib) {
const nodeEnv = process.env.NODE_ENV || config.mode
Object.assign(processEnv, {
'process.env.': `({}).`,
'global.process.env.': `({}).`,
'globalThis.process.env.': `({}).`,
})
Object.assign(processNodeEnv, {
'process.env.NODE_ENV': JSON.stringify(nodeEnv),
'global.process.env.NODE_ENV': JSON.stringify(nodeEnv),
'globalThis.process.env.NODE_ENV': JSON.stringify(nodeEnv),
__vite_process_env_NODE_ENV: JSON.stringify(nodeEnv),
})
}
// ... 省略
function generatePattern(
ssr: boolean,
): [Record<string, string | undefined>, RegExp | null] {
const replaceProcessEnv = !ssr || config.ssr?.target === 'webworker'
const replacements: Record<string, string> = {
...(replaceProcessEnv ? processNodeEnv : {}),
...getImportMetaKeys(ssr),
...userDefine,
...getImportMetaFallbackKeys(ssr),
...(replaceProcessEnv ? processEnv : {}),
}
if (isBuild && !replaceProcessEnv) {
replacements['__vite_process_env_NODE_ENV'] = 'process.env.NODE_ENV'
}
const replacementsKeys = Object.keys(replacements)
const pattern = replacementsKeys.length
? new RegExp(
// Mustn't be preceded by a char that can be part of an identifier
// or a '.' that isn't part of a spread operator
'(?<![\p{L}\p{N}_$]|(?<!\.\.)\.)(' +
replacementsKeys.map(escapeRegex).join('|') +
// Mustn't be followed by a char that can be part of an identifier
// or an assignment (but allow equality operators)
')(?:(?<=\.)|(?![\p{L}\p{N}_$]|\s*?=[^=]))',
'gu',
)
: null
return [replacements, pattern]
}
const defaultPattern = generatePattern(false)
const ssrPattern = generatePattern(true)
return {
name: 'vite:define',
transform(code, id, options) {
const ssr = options?.ssr === true
if (!ssr && !isBuild) {
// for dev we inject actual global defines in the vite client to
// avoid the transform cost.
return
}
// ... 省略
const s = new MagicString(code)
let hasReplaced = false
let match: RegExpExecArray | null
while ((match = pattern.exec(code))) {
hasReplaced = true
const start = match.index
const end = start + match[0].length
const replacement = '' + replacements[match[1]]
s.update(start, end, replacement)
}
if (!hasReplaced) {
return null
}
return transformStableResult(s, id, config)
},
}
}
我们看上面源代码,发现 Vite在 开发模式dev 和 生产模式build,有不同的分支逻辑处理。
- dev模式,vite在客户端注入实际的全局定义
- build模式,走Vite的replace,和rollup的transform
看注释就知道:// 对于开发模式,我们在vite客户端注入实际的全局定义,以避免转换成本。
export function definePlugin(config: ResolvedConfig): Plugin {
return {
name: 'vite:define',
transform(code, id, options) {
if (!ssr && !isBuild) {
// for dev we inject actual global defines in the vite client to
// avoid the transform cost.
return
}
}
}
}
}
如果是 lib 模式,则不会走这段兜底,代码会报错,其他模式没问题。
if (!isBuildLib) {
const nodeEnv = process.env.NODE_ENV || config.mode
Object.assign(processEnv, {
'process.env.': `({}).`,
'global.process.env.': `({}).`,
'globalThis.process.env.': `({}).`,
})
Object.assign(processNodeEnv, {
'process.env.NODE_ENV': JSON.stringify(nodeEnv),
'global.process.env.NODE_ENV': JSON.stringify(nodeEnv),
'globalThis.process.env.NODE_ENV': JSON.stringify(nodeEnv),
__vite_process_env_NODE_ENV: JSON.stringify(nodeEnv),
})
}
代码替换的核心逻辑就是这段恶心的正则,看不懂。
const pattern = replacementsKeys.length
? new RegExp(
// Mustn't be preceded by a char that can be part of an identifier
// or a '.' that isn't part of a spread operator
'(?<![\p{L}\p{N}_$]|(?<!\.\.)\.)(' +
replacementsKeys.map(escapeRegex).join('|') +
// Mustn't be followed by a char that can be part of an identifier
// or an assignment (but allow equality operators)
')(?:(?<=\.)|(?![\p{L}\p{N}_$]|\s*?=[^=]))',
'gu',
)
: null
return [replacements, pattern]
结论
虽然这个Feature很容易解决,但其中涉及到内容还挺有意思,包括,构建替换原理,块状作用域与对象解析规则,浏览器和构建工具解析规范等等。最终我推荐用官方推荐的方式,就不用有那么多问题,官方推荐用静态变量替换。
总的来说,Vite的define选项提供了一种灵活的方式,可以在源码中全局替换特定的字符串。虽然其在开发模式和生产模式下的行为有所不同,但其核心目的是一样的:提供一种机制,使得我们可以在构建时替换源代码。
转载自:https://juejin.cn/post/7240084575757877309