webpack DefinePlugin解析
DefinePlugin是webpack的一个官方内置插件,它允许在 编译时 将你代码中的变量替换为其他值或表达式。这在需要根据开发模式与生产模式进行不同的操作时,非常有用。例如,如果想在开发构建中进行日志记录,而不在生产构建中进行,就可以定义一个全局常量去判断是否记录日志。这就是 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),
});
demo
console.log(PRODUCTION,VERSION,BROWSER_SUPPORTS_HTML5,TWO,typeof window,process.env.NODE_ENV);
源码入口
parser是一个hookMap,它就相当于一个管理hook的Map结构。
apply(compiler) {
const definitions = this.definitions;
compiler.hooks.compilation.tap(
"DefinePlugin",
(compilation, { normalModuleFactory }) => {
//...
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);
//...
})
}
parser的call时机在哪?完全就在于NormalModuleFactory.createParser时机
所以这个钩子的语义就是parser创建时的初始化钩子。
createParser(type, parserOptions = {}) {
parserOptions = mergeGlobalOptions(
this._globalParserOptions,
type,
parserOptions
);
const parser = this.hooks.createParser.for(type).call(parserOptions);
if (!parser) {
throw new Error(`No parser registered for ${type}`);
}
this.hooks.parser.for(type).call(parser, parserOptions);
return parser;
}
好,现在让我们看看具体初始化了什么逻辑。
首先现在program上定义一个钩子,在遍历JavaScript AST前(该时机由program定义位置所知),注册buildInfo.valueDependencies=new Map();
并定义
buildInfo.valueDependencies.set(VALUE_DEP_MAIN, mainValue);
const handler = parser => {
const mainValue = compilation.valueCacheVersions.get(VALUE_DEP_MAIN);
//mainValue是在DefinePlugin最初初始化时定义到compilation.valueCacheVersions上的
parser.hooks.program.tap("DefinePlugin", () => {
const { buildInfo } = parser.state.module;
if (!buildInfo.valueDependencies)
buildInfo.valueDependencies = new Map();
buildInfo.valueDependencies.set(VALUE_DEP_MAIN, mainValue);
});
//....
walkDefinitions(definitions, "");
}
然后开始遍历Definitions(这是用户提供的配置项,比如 PRODUCTION: JSON.stringify(true),)
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);
});
};
applyDefine
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 => {
/**
* this is needed in case there is a recursion in the DefinePlugin
* to prevent an endless recursion
* e.g.: new DefinePlugin({
* "a": "b",
* "b": "a"
* });
*/
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 => {
addValueDependency(originalKey);
const strCode = toCode(
code,
parser,
compilation.valueCacheVersions,
originalKey,
runtimeTemplate,
!parser.isAsiPosition(expr.range[0])
);
if (/__webpack_require__\s*(!?.)/.test(strCode)) {
return toConstantDependency(parser, strCode, [
RuntimeGlobals.require
])(expr);
} else if (/__webpack_require__/.test(strCode)) {
return toConstantDependency(parser, strCode, [
RuntimeGlobals.requireScope
])(expr);
} else {
return toConstantDependency(parser, strCode)(expr);
}
});
}
parser.hooks.evaluateTypeof.for(key).tap("DefinePlugin", expr => {
/**
* this is needed in case there is a recursion in the DefinePlugin
* to prevent an endless recursion
* e.g.: new DefinePlugin({
* "typeof a": "typeof b",
* "typeof b": "typeof a"
* });
*/
if (recurseTypeof) return;
recurseTypeof = true;
addValueDependency(originalKey);
const codeCode = toCode(
code,
parser,
compilation.valueCacheVersions,
originalKey,
runtimeTemplate,
null
);
const typeofCode = isTypeof
? codeCode
: "typeof (" + codeCode + ")";
const res = parser.evaluate(typeofCode);
recurseTypeof = false;
res.setRange(expr.range);
return res;
});
parser.hooks.typeof.for(key).tap("DefinePlugin", expr => {
addValueDependency(originalKey);
const codeCode = toCode(
code,
parser,
compilation.valueCacheVersions,
originalKey,
runtimeTemplate,
null
);
const typeofCode = isTypeof
? codeCode
: "typeof (" + codeCode + ")";
const res = parser.evaluate(typeofCode);
if (!res.isString()) return;
return toConstantDependency(
parser,
JSON.stringify(res.string)
).bind(parser)(expr);
});
};
hooks.expression
在applyDefine中定义的hooks.expression定义了对表达式的替换处理。
当代码解析到语句【key】时,便会触发如下钩子逻辑,不过先别急,我们先搞清楚expression钩子在何处会被触发。
parser.hooks.expression.for(key).tap("DefinePlugin", expr => {
//...
}
触发时机
单单指demo中的情况
比如PRODUCTION会被acron解析为Identifier
而在parse阶段中,会有这么一句
if (this.hooks.program.call(ast, comments) === undefined) {
//...其他解析语句
this.walkStatements(ast.body);
}
//然后会走到这
walkIdentifier(expression) {
this.callHooksForName(this.hooks.expression, expression.name, expression);
}
//最后
const hook = hookMap.get(name);//获取hook
if (hook !== undefined) {
const result = hook.call(...args); //执行hook
if (result !== undefined) return result;
}
具体逻辑
parser.hooks.expression.for(key).tap("DefinePlugin", expr =>{
addValueDependency(originalKey);
const strCode = toCode(
code,
parser,
compilation.valueCacheVersions,
originalKey,
runtimeTemplate,
!parser.isAsiPosition(expr.range[0])
);
if (/__webpack_require__\s*(!?.)/.test(strCode)) {
return toConstantDependency(parser, strCode, [
RuntimeGlobals.require
])(expr);
} else if (/__webpack_require__/.test(strCode)) {
return toConstantDependency(parser, strCode, [
RuntimeGlobals.requireScope
])(expr);
} else {
return toConstantDependency(parser, strCode)(expr);
}
});
}
addValueDependency
//这里会影响needBuild的逻辑,是控制是否构建模块的
const addValueDependency = key => {
const { buildInfo } = parser.state.module;
//这里就可以理解为设置key,value
buildInfo.valueDependencies.set(
VALUE_DEP_PREFIX + key,
compilation.valueCacheVersions.get(VALUE_DEP_PREFIX + key)
);
};
要搞懂addValueDependency到底做了什么,首先得理解
- compilation.valueCacheVersions这个map结构做了什么?
- buildInfo.valueDependencies这里的依赖收集起来有什么用?
toCode获取strCode
toConstantDependency
设置ConstDependency静态转换依赖。
exports.toConstantDependency = (parser, value, runtimeRequirements) => {
return function constDependency(expr) {
const dep = new ConstDependency(value, expr.range, runtimeRequirements);
dep.loc = expr.loc;
parser.state.module.addPresentationalDependency(dep);
return true;
};
};
ConstDependency是如何替换源码的?
出处在seal阶段
if (module.presentationalDependencies !== undefined) {
for (const dependency of module.presentationalDependencies) {
this.sourceDependency(
module,
dependency,
initFragments,
source,
generateContext
);
}
}
sourceDenpendency,会获取依赖上的执行模板
const constructor = /** @type {new (...args: any[]) => Dependency} */ (
dependency.constructor
);
//template可以理解为代码生成模板
const template = generateContext.dependencyTemplates.get(constructor);
///....
template.apply(dependency, source, templateContext);//然后执行
而ConstPendency的执行模板直接替换了源码中的某个片段内容
const dep = /** @type {ConstDependency} */ (dependency);
if (dep.runtimeRequirements) {
for (const req of dep.runtimeRequirements) {
templateContext.runtimeRequirements.add(req);
}
}
if (typeof dep.range === "number") {
source.insert(dep.range, dep.expression);
return;
}
source.replace(dep.range[0], dep.range[1] - 1, dep.expression);
hooks.canRename
在applyDefine中,也有hooks.canRename的调用:
parser.hooks.canRename.for(key).tap("DefinePlugin", () => {
addValueDependency(key);
return true;
});
在这里其实就是允许key可以被重命名,并借机收集key作为依赖,但这个重命名的工作不是替换,并不在definePlugin内做,有点点奇怪。
详细:
该hook会在js ast遍历时多处被call
- walkAssignmentExpression 赋值表达式
- _walkIIFE CallExpression 函数调用表达式发现是IIFE时
- walkVariableDeclaration 声明语句
canRename有什么用?其实是配合rename使用的钩子
当其返回true,rename钩子才能起作用。如下:
walkVariableDeclaration(statement) {
for (const declarator of statement.declarations) {
switch (declarator.type) {
case "VariableDeclarator": {
const renameIdentifier =
declarator.init && this.getRenameIdentifier(declarator.init);
if (renameIdentifier && declarator.id.type === "Identifier") {
const hook = this.hooks.canRename.get(renameIdentifier);
if (hook !== undefined && hook.call(declarator.init)) {
// renaming with "var a = b;"
const hook = this.hooks.rename.get(renameIdentifier);
if (hook === undefined || !hook.call(declarator.init)) {
this.setVariable(declarator.id.name, renameIdentifier);
}
break;
}
}
//...
}
}
}
}
官方也有所介绍这个钩子
hooks.typeof
parser.hooks.typeof.for(key).tap("DefinePlugin", expr => {
addValueDependency(originalKey);
const codeCode = toCode(
code,
parser,
compilation.valueCacheVersions,
originalKey,
runtimeTemplate,
null
);
const typeofCode = isTypeof
? codeCode
: "typeof (" + codeCode + ")";
const res = parser.evaluate(typeofCode);
if (!res.isString()) return;
return toConstantDependency(
parser,
JSON.stringify(res.string)
).bind(parser)(expr);
});
先执行typeof再用toConstantDependency替换。
转载自:https://juejin.cn/post/7170131779972497422