一、开始
在用webpack做项目的过程中,可以在项目代码中使用process.env.NODE_ENV,根据开发环境和生产环境的不同做不同的逻辑。于是想到webpack是如何把process.env.NODE_ENV替换的呢?
对webpack打包原理不熟悉的同学,可以看一下这篇文章。
简单来说,webpack是利用了DeinePlugin来给源代码做变量替换,process.env.NODE_ENV是其中一个,webpack为此设置了默认值,另外在optimization配置中设置nodeEnv可以覆盖此默认值。
二、DeinePlugin
本次分析的webpack版本是v5.62.1。
在lib/DefinePlugin.js中定义了DefinePlugin,这个插件的使用方式如下:
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);
}
)
}
}上面只贴了关键逻辑。
DefinePlugin的apply方法中,在compiler.hooks.compilation里注册了名为DefinePlugin插件,其回调函数中定义了handler,然后在normalModuleFactory.hooks.parser的回调中使用了这个handler。
在handler中定义了walkDefinitions和applyDefine,然后调用了walkDefinitions,其判断definitions的key还是对象的话会进行递归替换,然后调用了applyDefine。applyDefine在parser.hooks.evaluateIdentifier/expression/evaluateTypeof等钩子中进行了code的替换。
整体的调用链和执行逻辑如下:

三、WebpackOptionsApply
在lib/webpack.js的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
}看下WebpackOptionsApply的process方法:
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_ENV为key的DefinePlugin。
其实这个optimization的nodeEnv是有默认值的,也就是说,默认情况下,我们在业务代码中写process.env.NODE_ENV就会被替换。
看下nodeEnv的默认值,在lib/config/defaults.js文件中:
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,则nodeEnv和mode的值相同,否则nodeEnv为false。
applyWebpackOptionsDefaults在createCompiler中被调用,也就是应用了默认值。
四、总结
本文从一个业务中的疑惑出发,简单介绍了DeinePlugin和WebpackOptionsApply的相关逻辑,可以看到变量替换还是嵌套在webpack的插件系统中,插件系统果然是webpack的灵魂。