想研究一下Webpack運行原理,發現都提到了tapable,搞得我雲裏霧裏,那咱們就好好研究一番,這究竟是個啥庫。webpack
在Webpack官方文檔上,查看Webpack的聲明週期鉤子函數,能夠看到下圖的內容:git
能夠看到run函數是AsyncSeriesHook類型的鉤子函數,這個就是tapable提供的鉤子類型了。github
想理解Webpack的運行流程,先要了解這個鉤子的使用,近而瞭解Webpack在運行的過程當中,是如何調用各類插件的。web
依照慣例,咱們先搭建個最簡單的項目:npm
安裝必要的庫:json
npm install --save-dev wepback
npm install --save-dev webpack-cli
npm install --save-dev webpack-dev-server
npm install --save tapable
複製代碼
咱們在src下寫咱們的測試代碼,而後運行起來,看咱們的實驗結果。 webpack.config.js的配置以下:數組
module.exports = {
entry: {
index: __dirname + "/src/index.js",
},
output: {
path: __dirname + "/dist",//打包後的文件存放的地方
filename: "[name].js", //打包後輸出文件的文件名
chunkFilename: '[name].js',
},
mode: 'development',
devtool: false,
devServer: {
contentBase: "./dist",//本地服務器所加載的頁面所在的目錄
historyApiFallback: true,//不跳轉
inline: true//實時刷新
},
}
複製代碼
在package.json中配置好啓動腳本,使用npm run server便可查看運行結果:promise
"scripts": {
"start": "webpack",
"server": "webpack-dev-server --open"
},
複製代碼
tapable的github地址是:github.com/webpack/tap…瀏覽器
這裏給出的是tapable-1分支的地址,我看這個分支纔是Webpack如今使用的。安全
依照它readme.md中介紹,tapable暴露了不少的Hook類,能夠幫助咱們爲插件建立鉤子。
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
複製代碼
這麼多Hook,咱們一個一個來,先看看SyncHook怎麼使用,在index.js中寫下:
import { SyncHook } from 'tapable';
const hook = new SyncHook(); // 建立鉤子對象
hook.tap('logPlugin', () => console.log('被勾了')); // tap方法註冊鉤子回調
hook.call(); // call方法調用鉤子,打印出‘被勾了’三個字
複製代碼
使用npm run server,在瀏覽器中運行成功。也成功打印‘被勾了’。用起來 仍是很簡單的。
這就是經典的事件註冊和觸發機制啊。實際使用的時候,聲明事件和觸發事件的代碼一般在一個類中,註冊事件的代碼在另外一個類(咱們的插件)中。代碼以下:
// Car.js
import { SyncHook } from 'tapable';
export default class Car {
constructor() {
this.startHook = new SyncHook();
}
start() {
this.startHook.call();
}
}
複製代碼
// index.js
import Car from './Car';
const car = new Car();
car.startHook.tap('startPlugin', () => console.log('我係一下安全帶'));
car.start();
複製代碼
鉤子的使用基本就是這個意思,Car中只負責聲明和調用鉤子,真正的執行邏輯,再也不Car中,而是在註冊它的index.js之中,是在Car以外。這樣就作到了很好的解耦。
對於Car而言,經過這種註冊插件的方式,豐富本身的功能。
我但願這樣:
// index.js
import Car from './Car';
const car = new Car();
car.accelerateHook.tap('acceleratePlugin', (speed) => console.log(`加速到${speed}`));
car.accelerate(100); // 調用時,將100傳給插件回調的speed
複製代碼
能夠這樣寫Car類:
import { SyncHook } from 'tapable';
export default class Car {
constructor() {
this.startHook = new SyncHook();
this.accelerateHook = new SyncHook(["newSpeed"]); // 在聲明的時候,說明我這個Hook須要一個參數便可。
}
start() {
this.startHook.call();
}
accelerate(speed) {
this.accelerateHook.call(speed);
}
}
複製代碼
這樣就完成了帶參數的Hook,SyncHook參數是傳遞個數組,就是說也可讓咱們傳遞多個參數,如 new SyncHook(["arg1","arg2","arg3"])。這樣在call的時候也能夠傳遞三個參數,在回調函數,也能接收到call的三個參數。
咱們的Car類,就是一個Tapable類,事件的聲明和調用中心。
Hook的註冊/調用機制咱們大體瞭解了,SyncHook的工做很完美,可是tapable還提供了不少Hook,這些Hook又是解決什麼問題的呢?
緣由在於對於某一個事件,咱們能夠註冊屢次,以下:
const car = new Car();
car.hooks.brake.tap('brakePlugin1', () => console.log(`剎車1`));
car.hooks.brake.tap('brakePlugin2', () => console.log(`剎車2`));
car.hooks.brake.tap('brakePlugin3', () => console.log(`剎車3`));
car.brake(); // 會打印‘剎車1’‘剎車2’‘剎車3’
複製代碼
這裏咱們爲Car類添加了hooks.brake的鉤子,和一個brake方法。brake的鉤子被註冊了3次,咱們調用brake方式的時候,3個插件都接受到了事件。
咱們稍微重構了一下Car類,聽說這種寫法,更符合tapable使用的最佳實踐,其實就是將鉤子都放到一個hooks字段裏。Car代碼以下:
import { SyncHook, SyncBailHook } from 'tapable';
export default class Car {
constructor() {
this.hooks = {
start: new SyncHook(),
accelerate: new SyncHook(["newSpeed"]),
brake: new SyncBailHook(), // 這裏咱們要使用SyncBailHook鉤子啦
};
}
start() {
this.hooks.start.call();
}
accelerate(speed) {
this.hooks.accelerate.call(speed);
}
brake() {
this.hooks.brake.call();
}
}
複製代碼
咱們如今要知足這樣一個需求,無論你註冊多少插件,我只想被剎兩次,就不通知別的插件了。這時候就SyncBailHook就能夠,代碼以下:
import Car from './Car';
const car = new Car();
car.hooks.brake.tap('brakePlugin1', () => console.log(`剎車1`));
// 只需在不想繼續往下走的插件return非undefined便可。
car.hooks.brake.tap('brakePlugin2', () => { console.log(`剎車2`); return 1; });
car.hooks.brake.tap('brakePlugin3', () => console.log(`剎車3`));
car.brake(); // 只會打印‘剎車1’‘剎車2’
複製代碼
SyncBailHook就是根據每一步返回的值來決定要不要繼續往下走,若是return了一個非undefined的值 那就不會往下走,注意 若是什麼都不return 也至關於return了一個undefined。
由此推測,tabable提供各種鉤子,目的是處理這些外部插件的關係。
搞明白了第二個鉤子,接下來的鉤子就很好理解了,這裏直接給出SyncWaterfallHook的定義:它的每一步都依賴上一步的執行結果,也就是上一步return的值就是下一步的參數。
咱們改造一下accelerate鉤子爲SyncWaterfallHook:
import { SyncHook, SyncBailHook, SyncWaterfallHook } from 'tapable';
export default class Car {
constructor() {
this.hooks = {
start: new SyncHook(),
accelerate: new SyncWaterfallHook(["newSpeed"]), // 重點在這裏
brake: new SyncBailHook(),
};
}
start() {
this.hooks.start.call();
}
accelerate(speed) {
this.hooks.accelerate.call(speed);
}
brake() {
this.hooks.brake.call();
}
}
複製代碼
// index.js
import Car from './Car';
const car = new Car();
car.hooks.accelerate.tap('acceleratePlugin1', (speed) => { console.log(`加速到${speed}`); return speed + 100; });
car.hooks.accelerate.tap('acceleratePlugin2', (speed) => { console.log(`加速到${speed}`); return speed + 100; });
car.hooks.accelerate.tap('acceleratePlugin3', (speed) => { console.log(`加速到${speed}`); });
car.accelerate(50); // 打印‘加速到50’‘加速到150’‘加速到250’
複製代碼
SyncLoopHook是同步的循環鉤子,它的插件若是返回一個非undefined。就會一直執行這個插件的回調函數,直到它返回undefined。
咱們把start的鉤子改爲SyncLoopHook。
import { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook } from 'tapable';
export default class Car {
constructor() {
this.hooks = {
start: new SyncLoopHook(), // 重點看這裏
accelerate: new SyncWaterfallHook(["newSpeed"]),
brake: new SyncBailHook(),
};
}
start() {
this.hooks.start.call();
}
accelerate(speed) {
this.hooks.accelerate.call(speed);
}
brake() {
this.hooks.brake.call();
}
}
複製代碼
// index.js
import Car from './Car';
let index = 0;
const car = new Car();
car.hooks.start.tap('startPlugin1', () => {
console.log(`啓動`);
if (index < 5) {
index++;
return 1;
}
}); // 這回咱們獲得一輛破車,啓動6次纔會啓動成功。
car.hooks.start.tap('startPlugin2', () => {
console.log(`啓動成功`);
});
car.start(); // 打印‘啓動’6次,打印‘啓動成功’一次。
複製代碼
當插件的回調函數,存在異步的時候。就須要使用異步的鉤子了。
AsyncParallelHook處理異步並行執行的插件。
咱們在Car類中添加calculateRoutes,使用AsyncParallelHook。再寫一個calculateRoutes方法,調用callAsync方法時會觸發鉤子執行。這裏能夠傳遞一個回調,當全部插件都執行完畢的時候,被調用。
// Car.js
import {
...
AsyncParallelHook,
} from 'tapable';
export default class Car {
constructor() {
this.hooks = {
...
calculateRoutes: new AsyncParallelHook(),
};
}
...
calculateRoutes(callback) {
this.hooks.calculateRoutes.callAsync(callback);
}
}
複製代碼
// index.js
import Car from './Car';
const car = new Car();
car.hooks.calculateRoutes.tapAsync('calculateRoutesPlugin1', (callback) => {
setTimeout(() => {
console.log('計算路線1');
callback();
}, 1000);
});
car.hooks.calculateRoutes.tapAsync('calculateRoutesPlugin2', (callback) => {
setTimeout(() => {
console.log('計算路線2');
callback();
}, 2000);
});
car.calculateRoutes(() => { console.log('最終的回調'); }); // 會在1s的時候打印‘計算路線1’。2s的時候打印‘計算路線2’。緊接着打印‘最終的回調’
複製代碼
我以爲AsyncParallelHook的精髓就在於這個最終的回調。當全部的異步任務執行結束後,再最終的回調中執行接下來的代碼。能夠確保全部的插件的代碼都執行完畢後,再執行某些邏輯。若是不須要這個最終的回調來執行某些代碼,那使用SyncHook就好了啊,反正你又不關心插件中的代碼何時執行完畢。
除了使用tapAsync/callAsync的方式使用AsyncParallelHook。還可使用tapPromise/promise的方式。
代碼以下:
// Car.js
import {
SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook,
AsyncParallelHook,
} from 'tapable';
export default class Car {
constructor() {
this.hooks = {
...
calculateRoutes: new AsyncParallelHook(),
};
}
...
calculateRoutes() {
return this.hooks.calculateRoutes.promise();
}
}
複製代碼
// index.js
import Car from './Car';
const car = new Car();
car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin1', () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('計算路線1');
resolve();
}, 1000);
});
});
car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin2', () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('計算路線2');
resolve();
}, 2000);
});
});
car.calculateRoutes().then(() => { console.log('最終的回調'); });
複製代碼
只是用法上有區別,效果同tapAsync/callAsync同樣的。
這個我靠猜都知道它是怎麼回事了,插件都並行執行,有一個執行成功而且傳遞的值不是undefined,就調用最終的回調。
來驗證一下猜測:
// Car.js
import {
AsyncParallelBailHook,
} from 'tapable';
export default class Car {
constructor() {
this.hooks = {
drift: new AsyncParallelBailHook(),
};
}
drift(callback) {
this.hooks.drift.callAsync(callback);
}
}
複製代碼
// index.js
import Car from './Car';
const car = new Car();
car.hooks.drift.tapAsync('driftPlugin1', (callback) => {
setTimeout(() => {
console.log('計算路線1');
callback(1); // 這裏傳遞個1,不是undefined
}, 1000);
});
car.hooks.drift.tapAsync('driftPlugin2', (callback) => {
setTimeout(() => {
console.log('計算路線2');
callback(2); // 這裏傳遞個2,不是undefined
}, 2000);
});
car.drift((result) => { console.log('最終的回調', result); });
// 打印結果是,等1s打印'計算路線1' ,立刻打印‘最終的回調 1’,再到第2s,打印'計算路線2'
複製代碼
咱們來分析下打印結果,說明AsyncParallelBailHook在插件調用callback時,若是給callback傳參數,就會立馬調用最終的回調函數。但並不會阻止其餘插件繼續執行本身的異步,只不過最終的回調拿不到這些比較慢的插件的回調結果了。
一樣的AsyncParallelBailHook也有promise的調用方式,與AsyncParallelHook相似,我們就不實驗了。
說完了並行,那必定有串行。就是插件一個一個的按順序執行。
實驗代碼以下:
// Car.js
import {
AsyncSeriesHook,
} from 'tapable';
export default class Car {
constructor() {
this.hooks = {
calculateRoutes: new AsyncSeriesHook(),
};
}
calculateRoutes() {
return this.hooks.calculateRoutes.promise();
}
}
複製代碼
// index.js
import Car from './Car';
const car = new Car();
car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin1', () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('計算路線1');
resolve();
}, 1000);
});
});
car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin2', () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('計算路線2');
resolve();
}, 2000);
});
});
car.calculateRoutes().then(() => { console.log('最終的回調'); });
// 1s事後,打印計算路線1,再過2s(而不是到了第2s,而是到了第3s),打印計算路線2,再立馬打印最終的回調。
複製代碼
咱們這裏直接使用promise的格式,同樣執行。
串行執行,而且只要一個插件有返回值,立馬調用最終的回調,而且不會繼續執行後續的插件。
實驗代碼以下:
// Car.js
import {
AsyncSeriesBailHook,
} from 'tapable';
export default class Car {
constructor() {
this.hooks = {
calculateRoutes: new AsyncSeriesBailHook(),
};
}
calculateRoutes() {
return this.hooks.calculateRoutes.promise();
}
}
複製代碼
// index.js
import Car from './Car';
const car = new Car();
car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin1', () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('計算路線1');
resolve(1);
}, 1000);
});
});
car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin2', () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('計算路線2');
resolve(2);
}, 2000);
});
});
car.calculateRoutes().then(() => { console.log('最終的回調'); });
// 1s事後,打印計算路線1,立馬打印最終的回調,不會再執行計算路線2了。
複製代碼
串行執行,而且前一個插件的返回值,會做爲後一個插件的參數。
代碼以下:
// Car.js
import {
AsyncSeriesWaterfallHook,
} from 'tapable';
export default class Car {
constructor() {
this.hooks = {
calculateRoutes: new AsyncSeriesWaterfallHook(['home']), // 要標註一下,要傳參數啦
};
}
calculateRoutes() {
return this.hooks.calculateRoutes.promise();
}
}
複製代碼
// index.js
import Car from './Car';
const car = new Car();
car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin1', (result) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('計算路線1', result);
resolve(1);
}, 1000);
});
});
car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin2', (result) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('計算路線2', result);
resolve(2);
}, 2000);
});
});
car.calculateRoutes().then(() => { console.log('最終的回調'); });
// 1s事後,打印計算路線1 undefined,再過2s打印計算路線2 北京,而後立馬打印最終的回調。
複製代碼
打印結果如圖:
咱們將註冊插件的邏輯單獨封裝出來,以下:
export default class CalculateRoutesPlugin {
// 調用apply方法就能夠完成註冊
apply(car) {
car.hooks.calculateRoutes.tapPromise('calculateRoutesPlugin', (result) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('計算路線1', result);
resolve('北京');
}, 1000);
});
});
}
}
複製代碼
在index.js中調用:
// index.js
import Car from './Car';
import CalculateRoutesPlugin from './CalculateRoutesPlugin';
const car = new Car();
const calculateRoutesPlugin = new CalculateRoutesPlugin();
calculateRoutesPlugin.apply(car); // 此節重點邏輯
car.calculateRoutes().then(() => { console.log('最終的回調'); });
// 運行正常,會打印'計算路線1'
複製代碼
看到這裏,代碼和Webpack的使用方式就差很少了,car相似Webpack中的Compiler/Compilation。index.js比做是Webpack的運行類,使用咱們的Car(類比Compiler/Compilation),使用注入來的CalculateRoutesPlugin(類比Webpack的各類插件)。完成打包工做。
tapable的readme中沒有介紹一個類,就是Tapable,可是是可使用到的,以下
const {
Tapable
} = require("tapable");
export default class Car extends Tapable {
...
}
複製代碼
若是看tapable源碼的話,看不到這個類,可是切換到tapable-1分支,能夠看到。
在Webpack源碼中,Compiler和Compilation都和上面的Car同樣,繼承自Tapable。
那Tapable究竟幹了啥啊,看了一下它源碼,發現它啥也沒幹,就是一個標誌,表示我這個類是一個能夠註冊插件的類。
雖然沒有什麼加強的功能,可是此時的Car有了兩個限制。以下:
const car = new Car();
car.apply(); // 報錯 Tapable.apply is deprecated. Call apply on the plugin directly instead
car.plugin(); // 報錯 Tapable.plugin is deprecated. Use new API on `.hooks` instead
複製代碼
這兩個方法不讓用了,我理解是爲Webpack而作的限制,提醒插件做者升級本身的插件,使用最新的實踐。
上面咱們研究了鉤子們的使用,接下來作一些總結。首先來講鉤子的類型。
基本鉤子。註冊的插件順序執行。如SyncHook、AsyncParallelHook、AsyncSeriesHook。
瀑布流鉤子。前一個插件的返回值,是後一個插件的入參。如SyncWaterfallHook,AsyncSeriesWaterfallHook。
Bail鉤子。Bail鉤子是指一個插件返回非undefined的值,就不繼續執行後續的插件。我理解這裏Bail是取迅速離開的意思。如:SyncBailHook,AsyncSeriesBailHook
循環鉤子。循環調用插件,直到插件的返回值是undefined。如SyncLoopHook。
咱們還能夠爲鉤子添加攔截器。 一個插件從對鉤子註冊,到鉤子調用,再到插件響應。咱們均可以經過攔截器監聽到。
car.hooks.calculateRoutes.intercept({
call: (...args) => {
console.log(...args, 'intercept call');
}, // 插件被call時響應。
//
register: (tap) => {
console.log(tap, 'ntercept register');
return tap;
},// 插件用tap方法註冊時響應。
loop: (...args) => {
console.log(...args, 'intercept loop')
},// loop hook的插件被調用時響應。
tap: (tap) => {
console.log(tap, 'intercept tap')
} // hook的插件被調用時響應。
})
複製代碼
插件和攔截器均可以往裏面傳一個上下文對象的參數,該對象可用於向後續插件和攔截器傳遞任意值。
myCar.hooks.accelerate.intercept({
context: true, // 這裏配置啓用上下文對象
tap: (context, tapInfo) => {
if (context) { // 這裏就能夠拿到上下文對象
context.hasMuffler = true;
}
}
});
myCar.hooks.accelerate.tap({
name: "NoisePlugin",
context: true
}, (context, newSpeed) => {
// 這裏能夠拿到攔截器裏的上下文對象,而後咱們在插件裏利用它的值作相應操做。
if (context && context.hasMuffler) {
console.log("Silence...");
} else {
console.log("Vroom!");
}
});
複製代碼
tapable的簡單使用,就研究到這裏。它爲插件機制提供了很強大的支持,不但讓咱們對主體(Car)註冊各類插件,還能控制插件彼此的關係,控制自身相應的時機。
在Webpack中使用這樣的庫,再合適不過,Webpack是一個插件的集合,經過tapable,有效的將插件們組織起來,在合理的時機,合理的調用。