Webpack系列-第一篇基礎雜記
Webpack系列-第二篇插件機制雜記
Webpack系列-第三篇流程雜記node
webpack自己並不難,他所完成的各類複雜炫酷的功能都依賴於他的插件機制。或許咱們在平常的開發需求中並不須要本身動手寫一個插件,然而,瞭解其中的機制也是一種學習的方向,當插件出現問題時,咱們也可以本身來定位。webpack
Webpack的插件機制依賴於一個核心的庫, Tapable。
在深刻webpack的插件機制以前,須要對該核心庫有必定的瞭解。git
tapable 是一個相似於nodejs 的EventEmitter 的庫, 主要是控制鉤子函數的發佈與訂閱。固然,tapable提供的hook機制比較全面,分爲同步和異步兩個大類(異步中又區分異步並行和異步串行),而根據事件執行的終止條件的不一樣,由衍生出 Bail/Waterfall/Loop 類型。github
基本使用:web
const {
SyncHook
} = require('tapable')
// 建立一個同步 Hook,指定參數
const hook = new SyncHook(['arg1', 'arg2'])
// 註冊
hook.tap('a', function (arg1, arg2) {
console.log('a')
})
hook.tap('b', function (arg1, arg2) {
console.log('b')
})
hook.call(1, 2)
複製代碼
鉤子類型: 算法
BasicHook:執行每個,不關心函數的返回值,有SyncHook、AsyncParallelHook、AsyncSeriesHook。segmentfault
BailHook:順序執行 Hook,遇到第一個結果result!==undefined則返回,再也不繼續執行。有:SyncBailHook、AsyncSeriseBailHook, AsyncParallelBailHook。api
什麼樣的場景下會使用到 BailHook 呢?設想以下一個例子:假設咱們有一個模塊 M,若是它知足 A 或者 B 或者 C 三者任何一個條件,就將其打包爲一個單獨的。這裏的 A、B、C 不存在前後順序,那麼就可使用 AsyncParallelBailHook 來解決:數組
x.hooks.拆分模塊的Hook.tap('A', () => {
if (A 判斷條件知足) {
return true
}
})
x.hooks.拆分模塊的Hook.tap('B', () => {
if (B 判斷條件知足) {
return true
}
})
x.hooks.拆分模塊的Hook.tap('C', () => {
if (C 判斷條件知足) {
return true
}
})
複製代碼
若是 A 中返回爲 true,那麼就無須再去判斷 B 和 C。 可是當 A、B、C 的校驗,須要嚴格遵循前後順序時,就須要使用有順序的 SyncBailHook(A、B、C 是同步函數時使用) 或者 AsyncSeriseBailHook(A、B、C 是異步函數時使用)。promise
WaterfallHook:相似於 reduce,若是前一個 Hook 函數的結果 result !== undefined,則 result 會做爲後一個 Hook 函數的第一個參數。既然是順序執行,那麼就只有 Sync 和 AsyncSeries 類中提供這個Hook:SyncWaterfallHook,AsyncSeriesWaterfallHook 當一個數據,須要通過 A,B,C 三個階段的處理獲得最終結果,而且 A 中若是知足條件 a 就處理,不然不處理,B 和 C 一樣,那麼可使用以下
x.hooks.tap('A', (data) => {
if (知足 A 須要處理的條件) {
// 處理數據 data
return data
} else {
return
}
})
x.hooks.tap('B', (data) => {
if (知足B須要處理的條件) {
// 處理數據 data
return data
} else {
return
}
})
x.hooks.tap('C', (data) => {
if (知足 C 須要處理的條件) {
// 處理數據 data
return data
} else {
return
}
})
複製代碼
LoopHook:不停的循環執行 Hook,直到全部函數結果 result === undefined。一樣的,因爲對串行性有依賴,因此只有 SyncLoopHook 和 AsyncSeriseLoopHook (PS:暫時沒看到具體使用 Case)
Tapable 基本邏輯是,先經過類實例的 tap 方法註冊對應 Hook 的處理函數, 這裏直接分析sync同步鉤子的主要流程,其餘的異步鉤子和攔截器等就不贅述了。
const hook = new SyncHook(['arg1', 'arg2'])
複製代碼
從該句代碼, 做爲源碼分析的入口,
class SyncHook extends Hook {
// 錯誤處理,防止調用者調用異步鉤子
tapAsync() {
throw new Error("tapAsync is not supported on a SyncHook");
}
// 錯誤處理,防止調用者調用promise鉤子
tapPromise() {
throw new Error("tapPromise is not supported on a SyncHook");
}
// 核心實現
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
}
複製代碼
從類SyncHook看到, 他是繼承於一個基類Hook, 他的核心實現compile等會再講, 咱們先看看基類Hook
// 變量的初始化
constructor(args) {
if (!Array.isArray(args)) args = [];
this._args = args;
this.taps = [];
this.interceptors = [];
this.call = this._call;
this.promise = this._promise;
this.callAsync = this._callAsync;
this._x = undefined;
}
複製代碼
初始化完成後, 一般會註冊一個事件, 如:
// 註冊
hook.tap('a', function (arg1, arg2) {
console.log('a')
})
hook.tap('b', function (arg1, arg2) {
console.log('b')
})
複製代碼
很明顯, 這兩個語句都會調用基類中的tap方法:
tap(options, fn) {
// 參數處理
if (typeof options === "string") options = { name: options };
if (typeof options !== "object" || options === null)
throw new Error(
"Invalid arguments to tap(options: Object, fn: function)"
);
options = Object.assign({ type: "sync", fn: fn }, options);
if (typeof options.name !== "string" || options.name === "")
throw new Error("Missing name for tap");
// 執行攔截器的register函數, 比較簡單不分析
options = this._runRegisterInterceptors(options);
// 處理註冊事件
this._insert(options);
}
複製代碼
從上面的源碼分析, 能夠看到_insert方法是註冊階段的關鍵函數, 直接進入該方法內部
_insert(item) {
// 重置全部的 調用 方法
this._resetCompilation();
// 將註冊事件排序後放進taps數組
let before;
if (typeof item.before === "string") before = new Set([item.before]);
else if (Array.isArray(item.before)) {
before = new Set(item.before);
}
let stage = 0;
if (typeof item.stage === "number") stage = item.stage;
let i = this.taps.length;
while (i > 0) {
i--;
const x = this.taps[i];
this.taps[i + 1] = x;
const xStage = x.stage || 0;
if (before) {
if (before.has(x.name)) {
before.delete(x.name);
continue;
}
if (before.size > 0) {
continue;
}
}
if (xStage > stage) {
continue;
}
i++;
break;
}
this.taps[i] = item;
}
}
複製代碼
_insert主要是排序tap並放入到taps數組裏面, 排序的算法並非特別複雜,這裏就不贅述了, 到了這裏, 註冊階段就已經結束了, 繼續看觸發階段。
hook.call(1, 2) // 觸發函數
複製代碼
在基類hook中, 有一個初始化過程,
this.call = this._call;
Object.defineProperties(Hook.prototype, {
_call: {
value: createCompileDelegate("call", "sync"),
configurable: true,
writable: true
},
_promise: {
value: createCompileDelegate("promise", "promise"),
configurable: true,
writable: true
},
_callAsync: {
value: createCompileDelegate("callAsync", "async"),
configurable: true,
writable: true
}
});
複製代碼
咱們能夠看出_call是由createCompileDelegate生成的, 往下看
function createCompileDelegate(name, type) {
return function lazyCompileHook(...args) {
this[name] = this._createCall(type);
return this[name](...args);
};
}
複製代碼
createCompileDelegate返回一個名爲lazyCompileHook的函數,顧名思義,即懶編譯, 直到調用call的時候, 纔會編譯出正在的call函數。
createCompileDelegate也是調用的_createCall, 而_createCall調用了Compier函數
_createCall(type) {
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
});
}
compile(options) {
throw new Error("Abstract: should be overriden");
}
複製代碼
能夠看到compiler必須由子類重寫, 返回到syncHook的compile函數, 即咱們一開始說的核心方法
class SyncHookCodeFactory extends HookCodeFactory {
content({ onError, onResult, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
}
const factory = new SyncHookCodeFactory();
class SyncHook extends Hook {
...
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
}
複製代碼
關鍵就在於SyncHookCodeFactory和工廠類HookCodeFactory, 先看setup函數,
setup(instance, options) {
// 這裏的instance 是syncHook 實例, 其實就是把tap進來的鉤子數組給到鉤子的_x屬性裏.
instance._x = options.taps.map(t => t.fn);
}
複製代碼
而後是最關鍵的create函數, 能夠看到最後返回的fn,實際上是一個new Function動態生成的函數
create(options) {
// 初始化參數,保存options到本對象this.options,保存new Hook(["options"]) 傳入的參數到 this._args
this.init(options);
let fn;
// 動態構建鉤子,這裏是抽象層,分同步, 異步, promise
switch (this.options.type) {
// 先看同步
case "sync":
// 動態返回一個鉤子函數
fn = new Function(
// 生成函數的參數,no before no after 返回參數字符串 xxx,xxx 在
// 注意這裏this.args返回的是一個字符串,
// 在這個例子中是options
this.args(),
'"use strict";\n' +
this.header() +
this.content({
onError: err => `throw ${err};\n`,
onResult: result => `return ${result};\n`,
onDone: () => "",
rethrowIfPossible: true
})
);
break;
case "async":
fn = new Function(
this.args({
after: "_callback"
}),
'"use strict";\n' +
this.header() +
// 這個 content 調用的是子類類的 content 函數,
// 參數由子類傳,實際返回的是 this.callTapsSeries() 返回的類容
this.content({
onError: err => `_callback(${err});\n`,
onResult: result => `_callback(null, ${result});\n`,
onDone: () => "_callback();\n"
})
);
break;
case "promise":
let code = "";
code += '"use strict";\n';
code += "return new Promise((_resolve, _reject) => {\n";
code += "var _sync = true;\n";
code += this.header();
code += this.content({
onError: err => {
let code = "";
code += "if(_sync)\n";
code += `_resolve(Promise.resolve().then(() => { throw ${err}; }));\n`;
code += "else\n";
code += `_reject(${err});\n`;
return code;
},
onResult: result => `_resolve(${result});\n`,
onDone: () => "_resolve();\n"
});
code += "_sync = false;\n";
code += "});\n";
fn = new Function(this.args(), code);
break;
}
// 把剛纔init賦的值初始化爲undefined
// this.options = undefined;
// this._args = undefined;
this.deinit();
return fn;
}
複製代碼
最後生成的代碼大體以下, 參考文章
"use strict";
function (options) {
var _context;
var _x = this._x;
var _taps = this.taps;
var _interterceptors = this.interceptors;
// 咱們只有一個攔截器因此下面的只會生成一個
_interceptors[0].call(options);
var _tap0 = _taps[0];
_interceptors[0].tap(_tap0);
var _fn0 = _x[0];
_fn0(options);
var _tap1 = _taps[1];
_interceptors[1].tap(_tap1);
var _fn1 = _x[1];
_fn1(options);
var _tap2 = _taps[2];
_interceptors[2].tap(_tap2);
var _fn2 = _x[2];
_fn2(options);
var _tap3 = _taps[3];
_interceptors[3].tap(_tap3);
var _fn3 = _x[3];
_fn3(options);
}
複製代碼
ok, 以上就是Tapabled的機制, 然而本篇的主要對象實際上是基於tapable實現的compile和compilation對象。不過因爲他們都是基於tapable,因此介紹的篇幅相對短一點。
compiler 對象表明了完整的 webpack 環境配置。這個對象在啓動 webpack 時被一次性創建,並配置好全部可操做的設置,包括 options,loader 和 plugin。當在 webpack 環境中應用一個插件時,插件將收到此 compiler 對象的引用。可使用 compiler 來訪問 webpack 的主環境。
也就是說, compile是webpack的總體環境。
class Compiler extends Tapable {
constructor(context) {
super();
this.hooks = {
/** @type {SyncBailHook<Compilation>} */
shouldEmit: new SyncBailHook(["compilation"]),
/** @type {AsyncSeriesHook<Stats>} */
done: new AsyncSeriesHook(["stats"]),
/** @type {AsyncSeriesHook<>} */
additionalPass: new AsyncSeriesHook([]),
/** @type {AsyncSeriesHook<Compiler>} */
......
......
some code
};
......
......
some code
}
複製代碼
能夠看到, Compier繼承了Tapable, 而且在實例上綁定了一個hook對象, 使得Compier的實例compier能夠像這樣使用
compiler.hooks.compile.tapAsync(
'afterCompile',
(compilation, callback) => {
console.log('This is an example plugin!');
console.log('Here’s the `compilation` object which represents a single build of assets:', compilation);
// 使用 webpack 提供的 plugin API 操做構建結果
compilation.addModule(/* ... */);
callback();
}
);
複製代碼
compilation 對象表明了一次資源版本構建。當運行 webpack 開發環境中間件時,每當檢測到一個文件變化,就會建立一個新的 compilation,從而生成一組新的編譯資源。一個 compilation 對象表現了當前的模塊資源、編譯生成資源、變化的文件、以及被跟蹤依賴的狀態信息。compilation 對象也提供了不少關鍵時機的回調,以供插件作自定義處理時選擇使用。
class Compilation extends Tapable {
/**
* Creates an instance of Compilation.
* @param {Compiler} compiler the compiler which created the compilation
*/
constructor(compiler) {
super();
this.hooks = {
/** @type {SyncHook<Module>} */
buildModule: new SyncHook(["module"]),
/** @type {SyncHook<Module>} */
rebuildModule: new SyncHook(["module"]),
/** @type {SyncHook<Module, Error>} */
failedModule: new SyncHook(["module", "error"]),
/** @type {SyncHook<Module>} */
succeedModule: new SyncHook(["module"]),
/** @type {SyncHook<Dependency, string>} */
addEntry: new SyncHook(["entry", "name"]),
/** @type {SyncHook<Dependency, string, Error>} */
}
}
}
複製代碼
具體參考上面提到的compiler實現。
瞭解到tapable\compiler\compilation以後, 再來看插件的實現就再也不一頭霧水了
如下代碼源自官方文檔
class MyExampleWebpackPlugin {
// 定義 `apply` 方法
apply(compiler) {
// 指定要追加的事件鉤子函數
compiler.hooks.compile.tapAsync(
'afterCompile',
(compilation, callback) => {
console.log('This is an example plugin!');
console.log('Here’s the `compilation` object which represents a single build of assets:', compilation);
// 使用 webpack 提供的 plugin API 操做構建結果
compilation.addModule(/* ... */);
callback();
}
);
}
}
複製代碼
能夠看到其實就是在apply中傳入一個Compiler實例, 而後基於該實例註冊事件, compilation同理, 最後webpack會在各流程執行call方法。
事件鉤子 | 觸發時機 | 參數 | 類型 |
---|---|---|---|
entry-option | 初始化 option | - | SyncBailHook |
run | 開始編譯 | compiler | AsyncSeriesHook |
compile | 真正開始的編譯,在建立 compilation 對象以前 | compilation | SyncHook |
compilation | 生成好了 compilation 對象,能夠操做這個對象啦 | compilation | SyncHook |
make | 從 entry 開始遞歸分析依賴,準備對每一個模塊進行 build | compilation | AsyncParallelHook |
after-compile | 編譯 build 過程結束 | compilation | AsyncSeriesHook |
emit | 在將內存中 assets 內容寫到磁盤文件夾以前 | compilation | AsyncSeriesHook |
after-emit | 在將內存中 assets 內容寫到磁盤文件夾以後 | compilation | AsyncSeriesHook |
done | 完成全部的編譯過程 | stats | AsyncSeriesHook |
failed | 編譯失敗的時候 | error | SyncHook |
事件鉤子 | 觸發時機 | 參數 | 類型 |
---|---|---|---|
normal-module-loader | 普通模塊 loader,真正(一個接一個地)加載模塊圖(graph)中全部模塊的函數。 | loaderContext module | SyncHook |
seal | 編譯(compilation)中止接收新模塊時觸發。 | - | SyncHook |
optimize | 優化階段開始時觸發。 | - | SyncHook |
optimize-modules | 模塊的優化 | modules | SyncBailHook |
optimize-chunks | 優化 chunk | chunks | SyncBailHook |
additional-assets | 爲編譯(compilation)建立附加資源(asset)。 | - | AsyncSeriesHook |
optimize-chunk-assets | 優化全部 chunk 資源(asset)。 | chunks | AsyncSeriesHook |
optimize-assets | 優化存儲在 compilation.assets 中的全部資源(asset) | assets | AsyncSeriesHook |
插件機制並不複雜,webpack也不復雜,複雜的是插件自己..
另外, 本應該先寫流程的, 流程只能後面補上了。
不知足於只會使用系列: tapable
webpack系列之二Tapable
編寫一個插件
Compiler
Compilation
compiler和comnpilation鉤子
看清楚真正的 Webpack 插件