很久不碰 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_ENVto a given string value.optimization.nodeEnvusesDefinePluginunless set tofalse.optimization.nodeEnvdefaults tomodeif 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 servecross-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 serveprocess.env.NODE_ENV 实际是 development ,12 个字符,末尾多一个空格。
set NODE_ENV=development&& npx webpack serveprocess.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