# 一、开始
在用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);
}
)
}
}
上面只贴了关键逻辑。
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 (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
}
看下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 (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
,则nodeEnv
和mode
的值相同,否则nodeEnv
为false
。
applyWebpackOptionsDefaults
在createCompiler
中被调用,也就是应用了默认值。
# 四、总结
本文从一个业务中的疑惑出发,简单介绍了DeinePlugin
和WebpackOptionsApply
的相关逻辑,可以看到变量替换还是嵌套在webpack
的插件系统中,插件系统果然是webpack
的灵魂。