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è)鉤子的語義就是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)代碼解析到語句【key】時(shí),便會(huì)觸發(fā)如下鉤子邏輯,不過先別急,我們先搞清楚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) { //...其他解析語句 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這里的依賴收集起來有什么用?
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 聲明語句
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檢測(cè)用戶是否在線的6種方法總結(jié)
這篇文章主要為大家詳細(xì)介紹了JavaScript中實(shí)現(xiàn)檢測(cè)用戶是否在線的6種常用方法,文中的示例代碼講解詳細(xì),感興趣的可以跟隨小編一起學(xué)習(xí)一下2023-08-08- Node的優(yōu)勢(shì)我就不再亂吹捧了,它讓javascript統(tǒng)一web的前后臺(tái)成為了可能。但是對(duì)于新手來說,server端的JS代碼可能不像client端的代碼那么好調(diào)試,直觀。client端JS代碼的調(diào)試基本上經(jīng)歷了一個(gè)從“肉眼--alert()--firebug(或者其它的developer tools)”的一個(gè)過程。而對(duì)于server端的調(diào)試,可能新手仍然停留在使用“肉眼--console()”的階段。其實(shí),Node經(jīng)過了這么多年(雖然才短短幾年)的發(fā)展,也有了很多不錯(cuò)的第三方的調(diào)試工具。包括Node內(nèi)建的調(diào)試工具debugger、node-inspector等。2014-05-05
javascript開發(fā)中因空格引發(fā)的錯(cuò)誤
最近寫一個(gè)關(guān)于用JavaScript做圖片自動(dòng)切換問題發(fā)現(xiàn)一個(gè)非常奇特的問題,除了空格和換行外完全相同的代碼,在Firefox下卻有截然不同的運(yùn)行結(jié)果,今天記錄以提供他人留意及自我備查。2010-11-11javascript實(shí)現(xiàn)信息增刪改查的方法
這篇文章主要介紹了javascript實(shí)現(xiàn)信息增刪改查的方法,實(shí)例分析了javascript操作頁面元素實(shí)現(xiàn)針對(duì)頁面信息的增刪改查功能,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-07-07IE,firefox(火狐)瀏覽器無提示關(guān)閉窗口js實(shí)現(xiàn)代碼小結(jié)
在不是js打開的頁面上按window.close(),會(huì)有提示框,很煩,現(xiàn)在可以不用了,沒有提示框直接關(guān)閉窗口。下面腳本之家編輯特為大家整理了一些。2009-09-09