从 process.env.NODE_ENV 设置失败说起

很久不碰 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') over process: { 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'

如果有模块使用到了 processprocess.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 uses DefinePlugin unless set to false. optimization.nodeEnv defaults to mode if set, else falls back to 'production'.

可以看出 optimization.nodeEnv 为真值,是因为设置了 mode,如果 modeprocess.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 的 exportcross-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=developmentcross-env 只能解析出环境变量,缺少后续命令。 等 cross-env 执行成功后,执行后一条命令 npx webpack serve,由于环境变量没有设置成功,webpack.config.js 中的 process.env.NODE_ENVundefined

在 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。

相关链接