# 一、开始

在用webpack做项目的过程中,可以在项目代码中使用process.env.NODE_ENV,根据开发环境和生产环境的不同做不同的逻辑。于是想到webpack是如何把process.env.NODE_ENV替换的呢?

webpack打包原理不熟悉的同学,可以看一下这篇文章 (opens new window)

简单来说,webpack是利用了DeinePlugin来给源代码做变量替换,process.env.NODE_ENV是其中一个,webpack为此设置了默认值,另外在optimization配置中设置nodeEnv可以覆盖此默认值。

# 二、DeinePlugin (opens new window)

本次分析的webpack版本是v5.62.1

lib/DefinePlugin.js (opens new window)中定义了DefinePlugin (opens new window),这个插件的使用方式如下:

new webpack.DefinePlugin({
  PRODUCTION: JSON.stringify(true),
  VERSION: JSON.stringify('5fa3b9'),
  BROWSER_SUPPORTS_HTML5: true,
  TWO: '1+1',
  'typeof window': JSON.stringify('object'),
  'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
});

DefinePlugin的定义如下:

class DefinePlugin {
  constructor(definitions) {
    this.definitions = definitions;
  }

  apply(compiler) {
    const definitions = this.definitions;
    compiler.hooks.compilation.tap(
      "DefinePlugin",
      (compilation, { normalModuleFactory }) => {

        const handler = parser => { 
          const walkDefinitions = (definitions, prefix) => {
            Object.keys(definitions).forEach(key => {
              const code = definitions[key];
              if (
                code &&
                typeof code === "object" &&
                !(code instanceof RuntimeValue) &&
                !(code instanceof RegExp)
              ) {
                walkDefinitions(code, prefix + key + ".");
                applyObjectDefine(prefix + key, code);
                return;
              }
              applyDefineKey(prefix, key);
              applyDefine(prefix + key, code);
            });
          };

          const applyDefine = (key, code) => {
            const originalKey = key;
            const isTypeof = /^typeof\s+/.test(key);
            if (isTypeof) key = key.replace(/^typeof\s+/, "");
            let recurse = false;
            let recurseTypeof = false;
            if (!isTypeof) {
              parser.hooks.canRename.for(key).tap("DefinePlugin", () => {
                addValueDependency(originalKey);
                return true;
              });
              parser.hooks.evaluateIdentifier
                .for(key)
                .tap("DefinePlugin", expr => {
                  if (recurse) return;
                  addValueDependency(originalKey);
                  recurse = true;
                  const res = parser.evaluate(
                    toCode(
                      code,
                      parser,
                      compilation.valueCacheVersions,
                      key,
                      runtimeTemplate,
                      null
                    )
                  );
                  recurse = false;
                  res.setRange(expr.range);
                  return res;
                });
              parser.hooks.expression.for(key).tap("DefinePlugin", expr => {/**/});
            }
            parser.hooks.evaluateTypeof.for(key).tap("DefinePlugin", expr => {/**/});
            parser.hooks.typeof.for(key).tap("DefinePlugin", expr => {/**/});
          };

          walkDefinitions(definitions, "");
        }

        normalModuleFactory.hooks.parser
          .for("javascript/auto")
          .tap("DefinePlugin", handler);
        normalModuleFactory.hooks.parser
          .for("javascript/dynamic")
          .tap("DefinePlugin", handler);
        normalModuleFactory.hooks.parser
          .for("javascript/esm")
          .tap("DefinePlugin", handler);
      }
    )
  }
}

上面只贴了关键逻辑。

DefinePluginapply方法中,在compiler.hooks.compilation里注册了名为DefinePlugin插件,其回调函数中定义了handler,然后在normalModuleFactory.hooks.parser的回调中使用了这个handler

handler中定义了walkDefinitionsapplyDefine,然后调用了walkDefinitions,其判断definitionskey还是对象的话会进行递归替换,然后调用了applyDefineapplyDefineparser.hooks.evaluateIdentifier/expression/evaluateTypeof等钩子中进行了code的替换。

整体的调用链和执行逻辑如下:

# 三、WebpackOptionsApply

lib/webpack.js (opens new window)createCompiler中执行了new WebpackOptionsApply().process(),这个方法就是执行一些用户设置和默认设置。

const WebpackOptionsApply = require("./WebpackOptionsApply");
const { applyWebpackOptionsDefaults } = require("./config/defaults");

const createCompiler = rawOptions => {
  const options = getNormalizedWebpackOptions(rawOptions);
  const compiler = new Compiler(options.context, options);
  applyWebpackOptionsDefaults(options);
  // ...
  new WebpackOptionsApply().process(options, compiler);
  return compiler
}

看下WebpackOptionsApplyprocess方法:

class WebpackOptionsApply extends OptionsApply {
  process(options, compiler) {
    // ...
    if (options.optimization.nodeEnv) {
      const DefinePlugin = require("./DefinePlugin");
      new DefinePlugin({
        "process.env.NODE_ENV": JSON.stringify(options.optimization.nodeEnv)
      }).apply(compiler);
    }
  }
}

从上面可以看出,如果在options.optimization中设置了nodeEnv,则会新建一个process.env.NODE_ENVkeyDefinePlugin

其实这个optimizationnodeEnv是有默认值的,也就是说,默认情况下,我们在业务代码中写process.env.NODE_ENV就会被替换。

看下nodeEnv的默认值,在lib/config/defaults.js (opens new window)文件中:

const F = (obj, prop, factory) => {
  if (obj[prop] === undefined) {
    obj[prop] = factory();
  }
};

const applyOptimizationDefaults = (
  optimization,
  { production, development, records }
) => {
  F(optimization, "nodeEnv", () => {
    if (production) return "production";
    if (development) return "development";
    return false;
  });
}

const applyWebpackOptionsDefaults = options => {
  const development = mode === "development";
  const production = mode === "production" || !mode;
  // ...
  applyOptimizationDefaults(options.optimization, {
    development,
    production,
    records: !!(options.recordsInputPath || options.recordsOutputPath)
  });
}

exports.applyWebpackOptionsDefaults = applyWebpackOptionsDefaults;

从上面可以看出,nodeEnv的默认值和mode有关。mode如果是production或者development,则nodeEnvmode的值相同,否则nodeEnvfalse

applyWebpackOptionsDefaultscreateCompiler中被调用,也就是应用了默认值。

# 四、总结

本文从一个业务中的疑惑出发,简单介绍了DeinePluginWebpackOptionsApply的相关逻辑,可以看到变量替换还是嵌套在webpack的插件系统中,插件系统果然是webpack的灵魂。

# 五、相关资料

  1. 深入Webpack打包原理 (opens new window)
  2. DefinePlugin (opens new window)
  3. optimization.nodeEnv (opens new window)