本文由團隊成員 咕嚕 撰寫,已受權塗鴉大前端獨家使用,包括但不限於編輯、標註原創等權益。javascript
Tapable 相似 node 中的重用到的 events 庫,其本質就是一個 發佈訂閱模式 。前端
var EventEmitter = require('events')
var ee = new EventEmitter()
ee.on('message', function (text) {
console.log(text)
})
ee.emit('message', 'hello world')
複製代碼
基於 tapbale 實例化:java
const { SyncHook } = require("tapable");
// 1. 建立鉤子實例
const sleep = new SyncHook();
// 2. 調用訂閱接口註冊回調
sleep.tap("test", () => {
console.log("callback A");
});
// 3. 調用發佈接口觸發回調
sleep.call();
// 4. 在 webpack compiler 對象上的訂閱,通常在 webpack 裏都掛載在 hooks 命名空間下
compiler.hooks.someHook.tap('MyPlugin', (params) => {
/* ... */
});
複製代碼
基於 node EventEmitter 實例化:node
const EventEmitter = require('events');
// 1. 建立鉤子實例
const sleep = new EventEmitter();
// 2. 調用訂閱接口註冊回調
sleep.on('test', () => {
console.log("callback A");
});
// 3. 調用發佈接口觸發回調
sleep.emit('test');
複製代碼
// 1. 建立鉤子實例時
class Compiler {
constructor() {
this.hooks = {
compilation: new SyncHook(["compilation", "params"]),
};
}
/* ... */
}
// 2. 調用發佈接口觸發回調時
newCompilation(params) {
const compilation = this.createCompilation();
compilation.name = this.name;
compilation.records = this.records;
this.hooks.thisCompilation.call(compilation, params);
this.hooks.compilation.call(compilation, params);
return compilation;
}
複製代碼
// 異步鉤子
compiler.hooks.beforeCompile.tapAsync(
'MyPlugin',
(params, callback) => {
console.log('Asynchronously tapping the run hook.');
callback();
}
);
// 異步 promise 鉤子
compiler.hooks.beforeCompile.tapPromise('MyPlugin', (params) => {
return new Promise((resolve) => setTimeout(resolve, 1000)).then(() => {
console.log('Asynchronously tapping the run hook with a delay.');
});
});
複製代碼
另外在 Webpack 中基於 Tapable 實現的類,都是 強耦合 的,以下面 Webpack 官方提供的 demo 所示,在 beforeCompile 階段,其參數是可被修改的,參考官方文檔的描述 This hook can be used to add/modify the compilation parametersreact
而 Webpack 也正是基於這種 強耦合 的方式,下面的 Compiler 和 Compilation 實例在特定時機觸發鉤子時會附帶上足夠的上下文信息,使得 Plugin 可以訂閱而且基於當前上下文信息和業務邏輯進而產生反作用(修改上下文狀態),從而影響後續的編譯流程。webpack
// 強耦合的基於 Tapable 類實現 compiler 實例
compiler.hooks.beforeCompile.tapAsync('MyPlugin', (params, callback) => {
params['MyPlugin - data'] = 'important stuff my plugin will use later';
callback();
});
複製代碼
源碼詳見 JavascriptParser - hooksgit
this.hooks = Object.freeze({
/** @type {HookMap<SyncBailHook<[CallExpressionNode, BasicEvaluatedExpression | undefined], BasicEvaluatedExpression | undefined | null>>} */
evaluateCallExpressionMember: new HookMap(
() => new SyncBailHook(["expression", "param"])
),
...otherHooks,
});
複製代碼
在使用 HookMap 的狀況下:github
// JavascriptParser.js
const property =
expr.callee.property.type === "Literal"
? `${expr.callee.property.value}`
: expr.callee.property.name;
const hook = this.hooks.evaluateCallExpressionMember.get(property);
if (hook !== undefined) {
return hook.call(expr, param);
}
// index.js
const a = expression.myFunc();
// MyPlugin.js
parser.hooks.evaluateCallExpressionMember
.for('myFunc')
.tap('MyPlugin', (expression, param) => {
/* ... */
return expressionResult;
});
複製代碼
在沒有 HookMap 的狀況下:web
// JavascriptParser.js
const property =
expr.callee.property.type === "Literal"
? `${expr.callee.property.value}`
: expr.callee.property.name;
return this.hooks[`evaluateCallExpressionMember${property.toFirstUpperCase()}`].call();
// index.js
const a = expression.myFunc();
// MyPlugin.js
parser.hooks.evaluateCallExpressionMemberMyFunc
.tap('MyPlugin', (expression, param) => {
/* ... */
return expressionResult;
});
複製代碼
Compiler 即編譯管理器,它記錄了完整的 Webpack 環境及配置信息,負責編譯,在 Webpack 從啓動到結束,compiler 只會生成一次。貫穿了 Webpack 打包的整個生命週期。在 compiler 對象上能夠拿到 當前 Webpack 配置信息,具體能夠查看下面 compiler.options 裏的一些數據。express
compiler 內部使用了 Tapable 類去實現插件的發佈和訂閱,能夠看下面一個最簡的例子。
const { AsyncSeriesHook, SyncHook } = require('tapable');
// 建立類
class Compiler {
constructor() {
this.hooks = {
run: new AsyncSeriesHook(['compiler']), // 異步鉤子
compile: new SyncHook(['params']), // 同步鉤子
};
}
run() {
// 執行異步鉤子
this.hooks.run.callAsync(this, (err) => {
this.compile(onCompiled);
});
}
compile() {
// 執行同步鉤子 並傳參
this.hooks.compile.call(params);
}
}
module.exports = Compiler;
複製代碼
const Compiler = require('./Compiler');
class MyPlugin {
apply(compiler) {
// 接受 compiler參數
compiler.hooks.run.tap('MyPlugin', () => console.log('開始編譯...'));
compiler.hooks.complier.tapAsync('MyPlugin', (name, age) => {
setTimeout(() => {
console.log('編譯中...');
}, 1000);
});
}
}
// 這裏相似於 webpack.config.js 的 plugins 配置
// 向 plugins 屬性傳入 new 實例
const myPlugin = new MyPlugin();
const options = {
plugins: [myPlugin],
};
const compiler = new Compiler(options);
compiler.run();
複製代碼
完整的實現可參考 Webpack v5.38.1 Compiler.js 源碼
compiler 的大部分 hooks 詳細描述文檔可參考 Webpack 官網提供的 Compiler Hooks
Compilation 表明了一次資源版本構建。當運行 webpack 時,每當檢測到一個文件變化,就會建立一個新的 compilation,從而生成一組新的編譯資源。一個 Compilation 對象表現了當前的模塊資源、編譯生成資源、變化的文件、以及被跟蹤依賴的狀態信息,簡單來說就是把本次打包編譯的內容存到內存裏。Compilation 對象也提供了插件須要自定義功能的回調,以供插件作自定義處理時選擇使用拓展。
Compilation 類也繼承自 Tapable 類並提供了一些生命週期鉤子,compilation 實例的具體結構可參考下圖。
compilation 的大部分 hooks 詳細描述文檔可參考 Webpack 官網提供的 Compilation Hooks
NormalModuleFactory 模塊被 Compiler 編譯用於生成各種模塊。從入口點開始,NormalModuleFactory 會分解每一個請求,解析文件內容以查找進一步的請求,而後經過分解全部請求以及解析新的文件來爬取所有文件。在最後階段,每一個依賴項都會成爲一個模塊 (Module) 實例。
NormalModuleFactory 類擴展了 Tapable 並提供瞭如下的生命週期鉤子,NormalModuleFactory 實例可參考下圖。
normalModuleFactory hooks 詳細描述文檔可參考 Webpack 官網提供的 NormalModuleFactory Hooks
parser 實例在 webpack 中被用於解析各種模塊,parser 是另外一個 webpack 中的繼承自 Tapable 的類,並基於此提供了一系列的鉤子函數方便插件開發者在解析模塊的過程當中進行一些自定義的操做,而 JavascriptParser 則是在 webpack 中用到相對來講最多的一個解析器,具體使用方式及其實例結構可參考下方;
compiler.hooks.normalModuleFactory.tap('MyPlugin', (factory) => {
factory.hooks.parser
.for('javascript/auto')
.tap('MyPlugin', (parser, options) => {
parser.hooks.someHook.tap(/* ... */);
});
});
複製代碼
JavscriptParser hooks 詳細描述文檔可參考 Webpack 官網提供的 Javascript Hooks
看到上面這些若是對 babel 或者 ast 有過簡單的瞭解是否是有一絲眼熟?
實例化 compiler 對象
graph TD O1[node api]-->A O2[webpack-cli]-->A O3[webpack-dev-server]-->A A["webpack(options, callback)"] -->B{"Array.isArray(options)"} B -->|是|D["createMultiCompiler(options)"] B -->|否|E["createCompiler(options)"] D -->F{callback?} E -->F F -->|是|F1{options.watch?} F1 -->|是|W["compiler.watch()"] --> Return F1 -->|否|Run["compiler.run()"] F -->|否|Run Run-->Return["return compiler"]
建立 compiler 時作了什麼
graph TD A["createCompiler(rawOptions)"]-->B["getNormalizedWebpackOptions(rawOptions)"] B-->|歸一化 options|C["applyWebpackOptionsBaseDefaults(options)"] C-->|給 options 寫入 context 默認值|D["new Compiler(options.context)"] D-->|實例化 compiler 對象|E["new NodeEnvironmentPlugin().apply(compiler)"] E-->F{"Array.isArray(options.plugin)"} F-->|是|F1["遍歷 plugin.apply(compiler)"] -->|應用插件|G F-->|否|G G["applyWebpackOptionsDefaults(options)"]-->|給 options 寫入默認值|Evt1 subgraph compiler.hooks Evt1["compiler.hooks.environment.call()"]-->Evt2 Evt2["compiler.hooks.afterEnvironment.call()"] I["compiler.hooks.initialize.call()"] end Evt2-->H["new WebpackOptionsApply().process(options, compiler)"]-->|基於 options 配置應用內置插件|I I-->R[return compiler]
compiler 編譯時作了什麼
graph TD A["compiler.run(callback)"]-->B["getNormalizedWebpackOptions(rawOptions)"]-->Evt1 subgraph compiler.hooks Evt1["compiler.hooks.beforeRun.callAsync(compiler)"] Evt2["compiler.hooks.run.callAsync(compiler)"] Evt3["compiler.hooks.beforeCompile.callAsync(params)"] Evt4["compiler.hooks.compile.call(compiler)"] Evt5["compiler.hooks.make.callAsync(compilation)"] Evt6["compiler.hooks.finishMake.callAsync(compilation)"] Evt7["compiler.hooks.afterCompile.callAsync(compilation)"] Evt8["compiler.hooks.done.callAsync(stats)"] EvtNMF["compiler.hooks.normalModuleFactory.call(normalModuleFactory)"] EvtCMF["compiler.hooks.contextModuleFactory.call(contextModuleFactory)"] end Evt1-->Evt2 Evt2-->E["compiler.readRecords()"] E-->|參考 options.recordsInputPath,這一步不重要|F F["compiler.compile(onCompiled)"]-->H["compile.newCompilationParams()"] H.->H1["params.normalModuleFactory = new NormalModuleFactory()"]-->EvtNMF H1.->H2["params.contextModuleFactory = new ContextModuleFactory()"]-->EvtCMF-->|params|Evt3 Evt3-->Evt4 Evt4-->I["compiler.newCompilation(params)"] I-->J["new Compilation(compiler)"]-->EvtC1 subgraph compilation.hooks EvtC1["compilation.hooks.thisCompilation.call(compilation, params)"] EvtC1-->EvtC2["compilation.hooks.compilation.call(compilation, params)"] end EvtC2-->EntryEvt1 EvtC2-->Evt5 Evt5-->EntryEvt2-->EntryCD EntryCD["EntryPlugin.createDependency(entry, options)"]-->|dep|addEntry addEntry["compilation.addEntry(context, dep, options)"]-->Evt6 Evt6-->L["compilation.finish()"] L-->M["compilation.seal()"] M-->Evt7 Evt7-->N["compiler.emitAssets()"] N-->O["compiler.emitRecords()"] O-->|參考options.recordsOutputPath,這一步不重要|P["new Stats(compilation)"]-->|stats|Evt8 subgraph EntryPlugin EntryEvt1["compiler.hooks.compilation.tap"] EntryEvt2["compiler.hooks.make.tapAsync"] end
compilation 在添加入口開始後作了什麼
graph TD addEntry["compilation.addEntry"] addEntryItem["compilation._addEntryItem"] addModuleTree["compilation.addModuleTree"] handleModuleCreation["compilation.handleModuleCreation"] factorizeModule["compilation.factorizeModule"] factorizeQueue["compilation.factorizeQueue.add"] ensureProcessing["compilation.factorizeQueue._ensureProcessing"] startProcessing["compilation.factorizeQueue._startProcessing"] processor["compilation.factorizeQueue._processor"] _factorizeModule["compilation._factorizeModule"] factoryCreate["NormalModuleFactory.create"] subgraph NormalModuleFactory beforeResolve["normalModuleFactory.hooks.beforeResolve.callAsync(resolveData)"] factorize["normalModuleFactory.hooks.factorize.callAsync(resolveData)"] resolve["normalModuleFactory.hooks.resolve.callAsync(resolveData)"] afterResolve["normalModuleFactory.hooks.afterResolve.callAsync(resolveData)"] createModule["normalModuleFactory.hooks.createModule.callAsync(createData, resolveData)"] end addModule["compilation.addModule"] buildModule["compilation.buildModule"] processModuleDependencies["compilation.processModuleDependencies"] addEntry-->addEntryItem-->addModuleTree -->handleModuleCreation-->factorizeModule -->factorizeQueue-->ensureProcessing--> startProcessing-->processor--> _factorizeModule-->factoryCreate--> beforeResolve-->factorize-->resolve-->afterResolve-->createModule--> addModule-->buildModule-->processModuleDependencies
# clone webpack 倉庫
$ git clone https://github.com/ShinyLeee/webpack.git
# 將 webpack link 到全局
$ cd webpack && npm link
# clone webpack-dev-server 倉庫
$ git clone https://github.com/ShinyLeee/webpack-dev-server.git
# 安裝依賴
$ cd webpack-dev-server & npm i
# 將 webpack-dev-server link 到全局
$ npm link
# 將 node_modules 裏的 webpack 和 webpack-dev-server 連接到剛剛 link 到全局本地倉庫
$ npm link webpack && npm link webpack-dev-server
# 安裝 ndb 方便調試
$ npm install -g ndb
# 運行 react-hmr demo
$ cd examples/cli/hmr && ndb webpack-dev-server
複製代碼
compilation.dependencyFactories、compilation.dependencyTemplates 中注入 HMR API(accept、decline)
webpack 總體構建流程基於 Tapable 事件流機制實現,很是靈活,可是喪失了可讀性,確實閱讀起來太...噁心,另一些方法和模塊都有 TS 的類型註釋是比較好閱讀的,可是註釋基本都只存在對參數的描述,對方法的描述幾乎沒有,這塊也很痛苦,不過內置的一些能力基本上全都是基於一些基類去實現的,只要理解了一塊其餘理解起來都仍是相對方便的,如 hooks 全都是基於 Tapable 類,以及一些 Module 類等。