很久不碰 webpack,偶尔写个实验代码意外翻了车,本文记录下这次过程。
process.env.NODE_ENV 变量
在 Node.js 应用开发中,环境变量 NODE_ENV
常被用于表示当前的运行模式,这一约定被 Express 之类的 Web 框架发扬光大,可以参考 Set NODE_ENV to “production”。
NODE_ENV=production node app.js
除了 Node 框架,React、Vue 这些主要面向浏览器环境的框架代码中也大量使用 process.env.NODE_ENV
来优化生产环境构建,尽管浏览器中并没有 process
对象。
比如 React npm 包的入口代码:
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react.production.min.js');
} else {
module.exports = require('./cjs/react.development.js');
}
像 Vue 3 的代码已经开始转而使用 __DEV__
代替 process.env.NODE_ENV !== 'production'
判断。
webpack 的 DefinePlugin 插件
由于浏览器环境没有 process
对象,所以构建时需要打包器进行替换处理。或者说非 Node 框架中的 process.env.NODE_ENV
天生就是为了打包器而设计。
对于 webpack 来说,可以使用内置的 DefinePlugin
插件,将代码中的 process.env.NODE_ENV
替换为命令行中设置的环境变量。
在 new DefinePlugin()
中定义 process.env.NODE_ENV
变量,显然有三种写法:
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
});
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV)
}
});
new webpack.DefinePlugin({
process: {
env: {
NODE_ENV: JSON.stringify(process.env.NODE_ENV)
}
}
});
这三种写法有什么区别这里先按下不表,只说推荐哪种。
webpack 官方文档是这么写的,见 https://webpack.js.org/plugins/define-plugin/#:~:text=When%20defining%20values,to%20be%20defined.:
When defining values for process prefer
'process.env.NODE_ENV': JSON.stringify('production')
overprocess: { env: { NODE_ENV: JSON.stringify('production') } }
. Using the latter will overwrite the process object which can break compatibility with some modules that expect other values on the process object to be defined.
后者会覆盖 process
对象,可能破坏一些模块的兼容性,因此推荐写成 process.env.NODE_ENV
形式。
我在项目中进行了配置,运行后报错:
WARNING in DefinePlugin
Conflicting values for 'process.env.NODE_ENV'
明明只定义了一个 process.env.NODE_ENV
,居然会提示冲突?
查看 webpack 的 DefinePlugin.js
代码,见 https://github.com/webpack/webpack/blob/789e58514b5747f6474bc247e4e104ce22892a2c/lib/DefinePlugin.js#L563-L589:
const walkDefinitionsForValues = (definitions, prefix) => {
Object.keys(definitions).forEach(key => {
const code = definitions[key];
const version = toCacheVersion(code);
const name = VALUE_DEP_PREFIX + prefix + key;
mainHash.update('|' + prefix + key);
const oldVersion = compilation.valueCacheVersions.get(name);
if (oldVersion === undefined) {
compilation.valueCacheVersions.set(name, version);
} else if (oldVersion !== version) {
const warning = new WebpackError(
`DefinePlugin\nConflicting values for '${prefix + key}'`
);
warning.details = `'${oldVersion}' !== '${version}'`;
warning.hideStack = true;
compilation.warnings.push(warning);
}
if (
code &&
typeof code === 'object' &&
!(code instanceof RuntimeValue) &&
!(code instanceof RegExp)
) {
walkDefinitionsForValues(code, prefix + key + '.');
}
});
};
其中 walkDefinitionsForValues()
会递归遍历 new DefinePlugin()
传入的值,由此可见报 Conflicting values for 'process.env.NODE_ENV'
错误,的确是因为 compilation.valueCacheVersions
中已经存在旧值。
同时也可以看出,上面的提及的三种写法都是有效的,而第三种写法最终会创建三个变量:
process
:{ env: { NODE_ENV: 'production' } }
process.env
:{ NODE_ENV: 'production' }
process.env.NODE_ENV
:'production'
如果有模块使用到了
process
或process.env
,会产生意想不到的破坏效果。
重复的值从哪里来的?再翻 webpack 代码,见 WebpackOptionsApply.js
,其中有这么一段,见 https://github.com/webpack/webpack/blob/789e58514b5747f6474bc247e4e104ce22892a2c/lib/WebpackOptionsApply.js#L498-L503:
if (options.optimization.nodeEnv) {
const DefinePlugin = require('./DefinePlugin');
new DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(options.optimization.nodeEnv)
}).apply(compiler);
}
如果 optimization.nodeEnv
为真值,webpack 会主动增加一条 process.env.NODE_ENV
定义,值为 optimization.nodeEnv
。
搜索 webpack 文档,看看 optimization.nodeEnv
作用是什么,见 https://webpack.js.org/configuration/optimization/#optimizationnodeenv。
Tells webpack to set
process.env.NODE_ENV
to a given string value.optimization.nodeEnv
usesDefinePlugin
unless set tofalse
.optimization.nodeEnv
defaults tomode
if set, else falls back to'production'
.
可以看出 optimization.nodeEnv
为真值,是因为设置了 mode
,如果 mode
和 process.env.NODE_ENV
不一致就会提示设置的值有冲突。
对照 webpack.config.js
,显然是命令行设置的 process.env.NODE_ENV
环境变量没符合预期。
module.exports = {
mode: process.env.NODE_ENV === 'development' ? 'development' : 'production',
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
})
]
};
问题出在 package.json
中的 scripts
,用错了 cross-env
:
{
"scripts": {
"dev": "cross-env NODE_ENV=development && npx webpack serve", // 有问题
"build": "cross-env NODE_ENV=production && npx webpack" // 有问题
}
}
参考 cross-env
的使用说明 https://www.npmjs.com/package/cross-env#usage(顺便一提该库已经停止维护)。
cross-env NODE_ENV=development && npx webpack serve
应该去掉 &&
,改成:
cross-env NODE_ENV=development npx webpack serve
cross-env
并不是我以为的单纯设置环境变量,它既不是 Windows 的 set
也不是 Shell 的 export
。cross-env
会对命令行参数进行解析,然后调用 cross-spawn
执行后续命令。
cross-env NODE_ENV=development npx webpack serve
的效果类似于如下代码:
const { spawn } = require('cross-spawn');
spawn('npx', ['webpack', 'serve'], {
env: {
NODE_ENV: 'development'
}
});
所以多了 &&
的 cross-env NODE_ENV=development && npx webpack serve
才不会按预期执行。
前一条命令 cross-env NODE_ENV=development
,cross-env
只能解析出环境变量,缺少后续命令。
等 cross-env
执行成功后,执行后一条命令 npx webpack serve
,由于环境变量没有设置成功,webpack.config.js
中的 process.env.NODE_ENV
为 undefined
。
在 Windows 上可以这样验证 cross-env
:
cross-env NODE_ENV=development set NODE_ENV && npx webpack serve
在 Linux 或者 macOS 上执行:
cross-env NODE_ENV=development echo $NODE_ENV && npx webpack serve
最终 package.json
改成这样:
{
"scripts": {
"dev": "cross-env NODE_ENV=development npx webpack serve",
"build": "cross-env NODE_ENV=production npx webpack"
}
}
Bonus: Windows 的 set 命令
在 Windows 上,如果使用内置的 set
命令设置环境变量也有让人出乎意料的问题。
set NODE_ENV=development && npx webpack serve
process.env.NODE_ENV
实际是 development
,12 个字符,末尾多一个空格。
set NODE_ENV=development&& npx webpack serve
process.env.NODE_ENV
才是 development
,11 个字符。
所以之前写的 在 Windows 命令提示符和 Shell 中设置环境变量、运行命令,其实隐藏了 bug。
相关链接
- Set NODE_ENV to “production” - Express https://expressjs.com/en/advanced/best-practice-performance.html#set-node_env-to-production
- https://webpack.js.org/plugins/define-plugin/
- https://webpack.js.org/configuration/optimization/#optimizationnodeenv
- https://github.com/webpack/webpack/
- https://www.npmjs.com/package/cross-env