webpack DefinePlugin源碼入口解析
正文
DefinePlugin是webpack的一個(gè)官方內(nèi)置插件,它允許在 編譯時(shí) 將你代碼中的變量替換為其他值或表達(dá)式。這在需要根據(jù)開發(fā)模式與生產(chǎn)模式進(jìn)行不同的操作時(shí),非常有用。例如,如果想在開發(fā)構(gòu)建中進(jìn)行日志記錄,而不在生產(chǎn)構(gòu)建中進(jìn)行,就可以定義一個(gè)全局常量去判斷是否記錄日志。這就是 DefinePlugin 的發(fā)光之處,設(shè)置好它,就可以忘掉開發(fā)環(huán)境和生產(chǎn)環(huán)境的構(gòu)建規(guī)則。
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是一個(gè)hookMap,它就相當(dāng)于一個(gè)管理hook的Map結(jié)構(gòu)。
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時(shí)機(jī)在哪?完全就在于NormalModuleFactory.createParser時(shí)機(jī)
所以這個(gè)鉤子的語(yǔ)義就是parser創(chuàng)建時(shí)的初始化鉤子。
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;
}
好,現(xiàn)在讓我們看看具體初始化了什么邏輯。
首先現(xiàn)在program上定義一個(gè)鉤子,在遍歷JavaScript AST前(該時(shí)機(jī)由program定義位置所知),注冊(cè)buildInfo.valueDependencies=new Map();
并定義
buildInfo.valueDependencies.set(VALUE_DEP_MAIN, mainValue);
const handler = parser => {
const mainValue = compilation.valueCacheVersions.get(VALUE_DEP_MAIN);
//mainValue是在DefinePlugin最初初始化時(shí)定義到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(這是用戶提供的配置項(xiàng),比如 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)
) {
//如果是對(duì)象就遞歸調(diào)用
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定義了對(duì)表達(dá)式的替換處理。
當(dāng)代碼解析到語(yǔ)句【key】時(shí),便會(huì)觸發(fā)如下鉤子邏輯,不過(guò)先別急,我們先搞清楚expression鉤子在何處會(huì)被觸發(fā)。
parser.hooks.expression.for(key).tap("DefinePlugin", expr => {
//...
}
觸發(fā)時(shí)機(jī)
單單指demo中的情況
比如PRODUCTION會(huì)被acron解析為Identifier
而在parse階段中,會(huì)有這么一句
if (this.hooks.program.call(ast, comments) === undefined) {
//...其他解析語(yǔ)句
this.walkStatements(ast.body);
}
//然后會(huì)走到這
walkIdentifier(expression) {
this.callHooksForName(this.hooks.expression, expression.name, expression);
}
//最后
const hook = hookMap.get(name);//獲取hook
if (hook !== undefined) {
const result = hook.call(...args); //執(zhí)行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
//這里會(huì)影響needBuild的邏輯,是控制是否構(gòu)建模塊的
const addValueDependency = key => {
const { buildInfo } = parser.state.module;
//這里就可以理解為設(shè)置key,value
buildInfo.valueDependencies.set(
VALUE_DEP_PREFIX + key,
compilation.valueCacheVersions.get(VALUE_DEP_PREFIX + key)
);
};
要搞懂a(chǎn)ddValueDependency到底做了什么,首先得理解
- compilation.valueCacheVersions這個(gè)map結(jié)構(gòu)做了什么?
- buildInfo.valueDependencies這里的依賴收集起來(lái)有什么用?
toCode獲取strCode
toConstantDependency
設(shè)置ConstDependency靜態(tài)轉(zhuǎn)換依賴。
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,會(huì)獲取依賴上的執(zhí)行模板
const constructor = /** @type {new (...args: any[]) => Dependency} */ (
dependency.constructor
);
//template可以理解為代碼生成模板
const template = generateContext.dependencyTemplates.get(constructor);
///....
template.apply(dependency, source, templateContext);//然后執(zhí)行
而ConstPendency的執(zhí)行模板直接替換了源碼中的某個(gè)片段內(nèi)容
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的調(diào)用:
parser.hooks.canRename.for(key).tap("DefinePlugin", () => {
addValueDependency(key);
return true;
});
在這里其實(shí)就是允許key可以被重命名,并借機(jī)收集key作為依賴,但這個(gè)重命名的工作不是替換,并不在definePlugin內(nèi)做,有點(diǎn)點(diǎn)奇怪。
詳細(xì):
該hook會(huì)在js ast遍歷時(shí)多處被call
- walkAssignmentExpression 賦值表達(dá)式
- _walkIIFE CallExpression 函數(shù)調(diào)用表達(dá)式發(fā)現(xiàn)是IIFE時(shí)
- walkVariableDeclaration 聲明語(yǔ)句
canRename有什么用?其實(shí)是配合rename使用的鉤子
當(dāng)其返回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;
}
}
//...
}
}
}
}
官方也有所介紹這個(gè)鉤子

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);
});
先執(zhí)行typeof再用toConstantDependency替換。。
以上就是webpack DefinePlugin源碼入口解析的詳細(xì)內(nèi)容,更多關(guān)于webpack DefinePlugin入口的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript詞法作用域與調(diào)用對(duì)象深入理解
關(guān)于 Javascript 的函數(shù)作用域、調(diào)用對(duì)象和閉包之間的關(guān)系很微妙,關(guān)于它們的文章已經(jīng)有很多,本文做了一些總結(jié),需要的朋友可以參考下2012-11-11
深入理解關(guān)于javascript中apply()和call()方法的區(qū)別
下面小編就為大家?guī)?lái)一篇深入理解關(guān)于javascript中apply()和call()方法的區(qū)別。小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-04-04
javascript 動(dòng)態(tài)腳本添加的簡(jiǎn)單方法
下面小編就為大家?guī)?lái)一篇javascript 動(dòng)態(tài)腳本添加的簡(jiǎn)單方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-10-10
js實(shí)現(xiàn)移動(dòng)端圖片滑塊驗(yàn)證功能
這篇文章主要為大家詳細(xì)介紹了js實(shí)現(xiàn)移動(dòng)端圖片滑塊驗(yàn)證功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-09-09
JsonServer安裝及啟動(dòng)過(guò)程圖解
這篇文章主要介紹了JsonServer安裝及啟動(dòng)過(guò)程圖解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-02-02
JavaScript實(shí)現(xiàn)獲取img的原始尺寸的方法詳解
在微信小程序開發(fā)時(shí),它的image標(biāo)簽有一個(gè)默認(rèn)高度,這樣你的圖片很可能出現(xiàn)被壓縮變形的情況,所以就需要獲取到圖片的原始尺寸對(duì)image的寬高設(shè)置,本文就來(lái)分享一下JavaScript實(shí)現(xiàn)獲取img的原始尺寸的方法吧2023-03-03
js history對(duì)象簡(jiǎn)單實(shí)現(xiàn)返回和前進(jìn)
返回和前進(jìn)大家應(yīng)該不陌生吧,瀏覽器上面的返回和前進(jìn)按鈕大家瀏覽網(wǎng)頁(yè)時(shí)都會(huì)應(yīng)到的,下面就為大家介紹下js中是如何實(shí)現(xiàn)所謂的返回和前進(jìn)2013-10-10

