很多前端项目里都能看到下面这样的条件判断:
if (process.env.NODE_ENV === 'development') {
console.log('当前是开发环境');
} else {
console.log('当前是生产环境');
}
这段代码在本地跑起来没有任何问题,可一旦打包发布到线上,浏览器里却没有 process 这个全局对象,更别说 process.env.NODE_ENV 了。实际上,真正让这段代码生效的并不是 Node.js 运行时,而是 Webpack 在构建阶段做的一件隐蔽工作:通过 DefinePlugin 把 process.env.NODE_ENV 这个成员表达式直接替换成了字符串字面量。理解这个替换过程,对排查构建问题、优化打包体积以及合理组织环境配置都很有帮助。

一、为什么浏览器里没有 process.env
Node.js 程序运行在服务端,process 是 Node.js 注入到全局作用域的一个核心模块对象,它上面挂载了 env、argv、cwd 等大量与操作系统进程相关的属性。而在浏览器环境中,JavaScript 运行在浏览器提供的沙箱里,全局对象是 window,并没有 process 这个变量。如果你直接在浏览器控制台执行 console.log(process),会抛出一个 ReferenceError: process is not defined。这就是为什么前端源码里直接写 process.env.NODE_ENV 如果不经过任何处理,放到线上必然报错。
Webpack 作为一个模块打包工具,它并不是简单地把源文件复制到输出目录,而是会解析模块依赖、处理各种 loader,并且支持在构建阶段对代码内容做静态替换。DefinePlugin 就是专门用来做这种源码级替换的插件。它可以在编译时扫描代码中出现的成员表达式或者标识符,然后把它们替换成指定的常量表达式。对于 process.env.NODE_ENV 这个典型的成员表达式,只需要在 Webpack 配置里把它映射到一个字符串字面量,就能让浏览器安全执行。
这里要特别注意替换发生的时间点:它发生在 Webpack 的构建阶段,也就是模块被解析和生成最终 bundle 的过程中。它不是运行时动态读取某个全局变量,而是在代码生成之前就把源码文本改了。换句话说,最终输出到 bundle 里的代码根本不包含 process.env 这样的对象访问。
二、DefinePlugin 的替换机制
DefinePlugin 的用法比较简单,在 webpack.config.js 中加入 plugins 配置即可。下面是一个基础示例:
const webpack = require('webpack');
module.exports = {
// 其他配置省略
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
})
]
};
注意键名必须写上完整的成员表达式,而且要加上引号。值部分使用了 JSON.stringify,这样最终替换到代码中的就是带双引号的字符串字面量 "production"。如果不加 JSON.stringify,直接写成 'production',那么替换结果会是 production 这个标识符,而不是字符串字面量,运行时就会去找一个叫 production 的变量,导致错误。JSON.stringify 的作用是给字符串加上双引号,保证它在代码里是一个合法的 JavaScript 字面量。
DefinePlugin 的替换并不是简单的文本查找替换,而是基于 Webpack 内部的 JavaScript 解析器,在抽象语法树(AST)层面识别出符合配置的成员表达式。这意味着只有与配置完全匹配的表达式才会被替换。比如你配置了 process.env.NODE_ENV,那么源码中出现的 process.env.NODE_ENV 会被替换,但 process.env.OTHER_VAR 不会被替换,process.env 本身也不会被替换。这种精确匹配机制可以避免误伤其他代码。
替换完成后,原来的条件判断就会变成下面这样:
if ("production" === 'development') {
console.log('当前是开发环境');
} else {
console.log('当前是生产环境');
}
可以看到,process.env.NODE_ENV 已经被字符串字面量替代,浏览器执行这段代码时不会再尝试访问 process 对象,自然就不会报错。同时,这样的静态字符串比较也为后续的压缩优化提供了基础。
三、mode 参数与 NODE_ENV 的默认注入
从 Webpack 4 开始,配置对象新增了一个 mode 选项,可选值有 none、development 和 production。很多人以为 mode 只是用来控制内置优化策略的开关,但实际上它还会触发一个隐式的 DefinePlugin 注入。当 mode 设置为 development 时,Webpack 会自动把 process.env.NODE_ENV 替换成 "development";当 mode 设置为 production 时,则替换成 "production"。这个行为是 Webpack 内部默认执行的,不需要手动添加 DefinePlugin。
具体来说,mode 会通过 Webpack 内部的 DefinePlugin 实例完成注入。Webpack 源码中会根据 mode 的值生成一个默认的 DefinePlugin 配置,键就是 process.env.NODE_ENV。因此,如果你的项目已经配置了 mode: 'production',但源码里仍然使用 process.env.NODE_ENV 来做环境判断,打包后这些判断依然会生效。只不过要注意,这个默认注入只会处理 process.env.NODE_ENV 这一项,如果你还需要注入其他自定义环境变量,比如 process.env.API_BASE_URL,就必须自己额外配置 DefinePlugin。
另外,如果你在 plugins 里手动添加了 DefinePlugin 并且也配置了 process.env.NODE_ENV,那么 Webpack 会按照插件数组的顺序依次执行替换。通常手动配置的 DefinePlugin 会覆盖 mode 默认注入的值吗?答案是:后执行的替换会覆盖前一次替换的结果。由于 plugins 数组中的插件会按照顺序依次处理代码,如果手动 DefinePlugin 在数组里比较靠后,那么它就可以覆盖 mode 自动注入的值。但为了避免混乱,建议不要在同一个项目里同时使用 mode 自动注入和手动注入同一个键,保持单一来源更清晰。
四、死代码消除与 tree shaking
DefinePlugin 完成替换之后,得到的代码里会出现类似 if ("production" === 'development') 这样的常量比较。这种代码在运行时必然有一个分支永远不会执行,但它为什么会被删除?这就要靠压缩工具来做了。Webpack 在生产模式下默认使用 TerserPlugin 进行代码压缩,Terser 会对 JavaScript 进行静态分析,识别出永远为 false 的条件,然后把对应的分支代码直接删除。
例如下面的源码:
if (process.env.NODE_ENV === 'development') {
console.log('开发模式调试信息');
debugger;
}
经过 DefinePlugin 替换,如果 mode 是 production,就会变成:
if ("production" === 'development') {
console.log('开发模式调试信息');
debugger;
}
Terser 看到 "production" === 'development' 这个条件永远为 false,就会把整个 if 代码块删掉,最终线上 bundle 里不会包含任何开发调试逻辑。这就是为什么开发环境打包的文件体积往往比生产环境大很多,因为生产环境不但做了压缩,还通过死代码消除把大量冗余分支去掉了。
同样的原理也适用于自定义环境变量。假如你在项目里写了大量基于 process.env.API_BASE_URL 的条件判断,并且用 DefinePlugin 把它替换成固定值,那么 Terser 就能把不匹配的分支全部删掉。反过来,如果你没有使用 DefinePlugin,而是试图在运行时定义一个全局变量来模拟 process.env,那么 Terser 无法在构建阶段判断条件的真假,这些代码就会被完整保留,甚至可能因为运行时没有 process 而报错。
死代码消除的另一个好处是配合 ES Module 的 tree shaking 能力。虽然 tree shaking 主要处理未使用的导出,但 DefinePlugin 和 Terser 的组合能把同一个模块内部不必要的分支裁剪掉,进一步缩小体积。尤其当项目依赖多个大型库时,库内部常常包含针对不同环境的代码路径,合理的环境变量注入可以帮助去除那些只在开发环境才需要的路径。
五、常见问题与最佳实践
一个很常见的现象是,开发者在自己配置 DefinePlugin 时忘记使用 JSON.stringify,导致 process.env.NODE_ENV 被替换成了一个未定义的标识符,最终运行时报错 NODE_ENV is not defined。这个错误通常发生在开发环境切换时,因为 mode 自动注入被手动的错误配置覆盖了。解决方法是检查 DefinePlugin 的值是否经过了 JSON.stringify 处理,或者直接使用 Webpack 内置的 mode 注入,不要重复配置同一个键。
另一个容易混淆的点是 process.env 和 import.meta.env 的区别。前者是 Node.js 生态下常用的环境变量访问方式,后者则是 Vite 等基于 ES Module 的构建工具提供的环境变量接口。两者在语义上有相似之处,但注入机制不一样。Webpack 项目里更推荐使用 process.env.NODE_ENV,因为它有成熟的 DefinePlugin 支持和 Terser 优化配合。
如果你需要注入多个自定义环境变量,建议把所有键值都集中在一个 DefinePlugin 实例中,并且都经过 JSON.stringify 处理。同时不要在源码里随意访问未配置的 process.env.xxx,因为未替换的表达式会原样进入 bundle,导致浏览器运行时尝试访问不存在的 process 而报错。如果确实需要动态读取环境变量,可以考虑通过构建工具生成一个 config.js 文件,把变量挂载到 window 对象上,但这种方式失去了静态优化的能力,需要权衡使用。
最后,推荐在 npm scripts 中使用 cross-env 来设置 Node.js 进程级的环境变量,配合 Webpack 的 mode 或 DefinePlugin 使用。这样可以跨平台地保证环境变量在构建脚本阶段就被正确读取。例如在 package.json 中写 cross-env NODE_ENV=production webpack --mode production,这样既可以通过 mode 自动注入 process.env.NODE_ENV,也可以让构建脚本内部的其他逻辑读取到同样的环境值。
理解 process.env.NODE_ENV 在 Webpack 构建阶段的注入原理,本质上就是理解源码替换与静态优化的过程。DefinePlugin 负责把环境变量变成字符串字面量,Terser 负责根据这些字面量删除永远不可达的分支,两者配合才让前端项目在生产环境里既能安全运行又能保持较小体积。掌握了这个链路,以后排查环境变量相关的问题就会更有方向。
Webpackprocess.env.NODE_ENVDefinePlugin修改时间:2026-10-03 12:47:08