摘要:引入定義一個自己的插件。一個最基礎(chǔ)的的代碼是這樣的在構(gòu)造函數(shù)中獲取用戶給該插件傳入的配置會調(diào)用實例的方法給插件實例傳入對象導出在使用這個時,相關(guān)配置代碼如下和在開發(fā)時最常用的兩個對象就是和,它們是和之間的橋梁。
本文示例源代碼請戳github博客,建議大家動手敲敲代碼。
webpack本質(zhì)上是一種事件流的機制,它的工作流程就是將各個插件串聯(lián)起來,而實現(xiàn)這一切的核心就是Tapable,webpack中最核心的負責編譯的Compiler和負責創(chuàng)建bundles的Compilation都是Tapable的實例。Tapable暴露出掛載plugin的方法,使我們能 將plugin控制在webapack事件流上運行(如下圖)。
tapable庫暴露了很多Hook(鉤子)類,為插件提供掛載的鉤子。
const { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook, AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook } = require("tapable");
Tabable 用法
1.new Hook 新建鉤子
tapable 暴露出來的都是類方法,new 一個類方法獲得我們需要的鉤子。
class 接受數(shù)組參數(shù)options,非必傳。類方法會根據(jù)傳參,接受同樣數(shù)量的參數(shù)。
const hook1 = new SyncHook(["arg1", "arg2", "arg3"]);
2.使用 tap/tapAsync/tapPromise 綁定鉤子
tapable提供了同步&異步綁定鉤子的方法,并且他們都有綁定事件和執(zhí)行事件對應(yīng)的方法。
- | Async* | Sync* |
---|---|---|
綁定 | tapAsync/tapPromise/tap | tap |
執(zhí)行 | callAsync/promise | call |
3.call/callAsync 執(zhí)行綁定事件
const hook1 = new SyncHook(["arg1", "arg2", "arg3"]); //綁定事件到webapck事件流 hook1.tap("hook1", (arg1, arg2, arg3) => console.log(arg1, arg2, arg3)) //1,2,3 //執(zhí)行綁定的事件 hook1.call(1,2,3)
舉個例子
定義一個Car方法,在內(nèi)部hooks上新建鉤子。分別是同步鉤子 accelerate(accelerate接受一個參數(shù))、break、異步鉤子calculateRoutes
使用鉤子對應(yīng)的綁定和執(zhí)行方法
calculateRoutes使用tapPromise可以返回一個promise對象。
//引入tapable const { SyncHook, AsyncParallelHook } = require("tapable"); //創(chuàng)建類 class Car { constructor() { this.hooks = { accelerate: new SyncHook(["newSpeed"]), break: new SyncHook(), calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"]) }; } } const myCar = new Car(); //綁定同步鉤子 myCar.hooks.break.tap("WarningLampPlugin", () => console.log("WarningLampPlugin")); //綁定同步鉤子 并傳參 myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`)); //綁定一個異步Promise鉤子 myCar.hooks.calculateRoutes.tapPromise("calculateRoutes tapPromise", (source, target, routesList, callback) => { // return a promise return new Promise((resolve,reject)=>{ setTimeout(()=>{ console.log(`tapPromise to ${source} ${target} ${routesList}`) resolve(); },1000) }) }); //執(zhí)行同步鉤子 myCar.hooks.break.call(); myCar.hooks.accelerate.call("hello"); console.time("cost"); //執(zhí)行異步鉤子 myCar.hooks.calculateRoutes.promise("i", "love", "tapable").then(() => { console.timeEnd("cost"); }, err => { console.error(err); console.timeEnd("cost"); })
運行結(jié)果
WarningLampPlugin Accelerating to hello tapPromise to i love tapable cost: 1008.725ms
calculateRoutes也可以使用tapAsync綁定鉤子,注意:此時用callback結(jié)束異步回調(diào)。
myCar.hooks.calculateRoutes.tapAsync("calculateRoutes tapAsync", (source, target, routesList, callback) => { // return a promise setTimeout(() => { console.log(`tapAsync to ${source} ${target} ${routesList}`) callback(); }, 2000) }); myCar.hooks.calculateRoutes.callAsync("i", "like", "tapable", err => { console.timeEnd("cost"); if(err) console.log(err) })
運行結(jié)果
WarningLampPlugin Accelerating to hello tapAsync to i like tapable cost: 2007.045ms
進階一下~
到這里可能已經(jīng)學會使用tapable了,但是它如何與webapck/webpack插件關(guān)聯(lián)呢?
我們將剛才的代碼稍作改動,拆成兩個文件:Compiler.js、Myplugin.js
Compiler.js
把Class Car類名改成webpack的核心Compiler
接受options里傳入的plugins
將Compiler作為參數(shù)傳給plugin
執(zhí)行run函數(shù),在編譯的每個階段,都觸發(fā)執(zhí)行相對應(yīng)的鉤子函數(shù)。
const { SyncHook, AsyncParallelHook } = require("tapable"); class Compiler { constructor(options) { this.hooks = { accelerate: new SyncHook(["newSpeed"]), break: new SyncHook(), calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"]) }; let plugins = options.plugins; if (plugins && plugins.length > 0) { plugins.forEach(plugin => plugin.apply(this)); } } run(){ console.time("cost"); this.accelerate("hello") this.break() this.calculateRoutes("i", "like", "tapable") } accelerate(param){ this.hooks.accelerate.call(param); } break(){ this.hooks.break.call(); } calculateRoutes(){ const args = Array.from(arguments) this.hooks.calculateRoutes.callAsync(...args, err => { console.timeEnd("cost"); if (err) console.log(err) }); } } module.exports = Compiler
MyPlugin.js
引入Compiler
定義一個自己的插件。
apply方法接受 compiler參數(shù)。
給compiler上的鉤子綁定方法。
仿照webpack規(guī)則,向 plugins 屬性傳入 new 實例。
webpack 插件是一個具有 apply 方法的 JavaScript 對象。apply 屬性會被 webpack compiler 調(diào)用,并且 compiler 對象可在整個編譯生命周期訪問。
const Compiler = require("./Compiler") class MyPlugin{ constructor() { } apply(conpiler){//接受 compiler參數(shù) conpiler.hooks.break.tap("WarningLampPlugin", () => console.log("WarningLampPlugin")); conpiler.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`)); conpiler.hooks.calculateRoutes.tapAsync("calculateRoutes tapAsync", (source, target, routesList, callback) => { setTimeout(() => { console.log(`tapAsync to ${source}${target}${routesList}`) callback(); }, 2000) }); } } //這里類似于webpack.config.js的plugins配置 //向 plugins 屬性傳入 new 實例 const myPlugin = new MyPlugin(); const options = { plugins: [myPlugin] } let compiler = new Compiler(options) compiler.run()
運行結(jié)果
Accelerating to hello WarningLampPlugin tapAsync to iliketapable cost: 2009.273ms
改造后運行正常,仿照Compiler和webpack插件的思路慢慢得理順插件的邏輯成功。
更多其他Tabable方法
Webpack 通過 Plugin 機制讓其更加靈活,以適應(yīng)各種應(yīng)用場景。 在 Webpack 運行的生命周期中會廣播出許多事件,Plugin 可以監(jiān)聽這些事件,在合適的時機通過 Webpack 提供的 API 改變輸出結(jié)果。
一個最基礎(chǔ)的 Plugin 的代碼是這樣的:
class BasicPlugin{ // 在構(gòu)造函數(shù)中獲取用戶給該插件傳入的配置 constructor(options){ } // Webpack 會調(diào)用 BasicPlugin 實例的 apply 方法給插件實例傳入 compiler 對象 apply(compiler){ compiler.hooks.compilation.tap("BasicPlugin", compilation => { }); } } // 導出 Plugin module.exports = BasicPlugin;
在使用這個 Plugin 時,相關(guān)配置代碼如下:
const BasicPlugin = require("./BasicPlugin.js"); module.export = { plugins:[ new BasicPlugin(options), ] }
Compiler 和 Compilation
在開發(fā) Plugin 時最常用的兩個對象就是 Compiler 和 Compilation,它們是 Plugin 和 Webpack 之間的橋梁。 Compiler 和 Compilation 的含義如下:
Compiler 對象包含了 Webpack 環(huán)境所有的的配置信息,包含 options,loaders,plugins 這些信息,這個對象在 Webpack 啟動時候被實例化,它是全局唯一的,可以簡單地把它理解為 Webpack 實例;
Compilation 對象包含了當前的模塊資源、編譯生成資源、變化的文件等。當 Webpack 以開發(fā)模式運行時,每當檢測到一個文件變化,一次新的 Compilation 將被創(chuàng)建。Compilation 對象也提供了很多事件回調(diào)供插件做擴展。通過 Compilation 也能讀取到 Compiler 對象。
Compiler 和 Compilation 的區(qū)別在于:Compiler 代表了整個 Webpack 從啟動到關(guān)閉的生命周期,而 Compilation 只是代表了一次新的編譯。
常用 API插件可以用來修改輸出文件、增加輸出文件、甚至可以提升 Webpack 性能、等等,總之插件通過調(diào)用 Webpack 提供的 API 能完成很多事情。 由于 Webpack 提供的 API 非常多,有很多 API 很少用的上,又加上篇幅有限,下面來介紹一些常用的 API。
1、讀取輸出資源、代碼塊、模塊及其依賴
有些插件可能需要讀取 Webpack 的處理結(jié)果,例如輸出資源、代碼塊、模塊及其依賴,以便做下一步處理。
在 emit 事件發(fā)生時,代表源文件的轉(zhuǎn)換和組裝已經(jīng)完成,在這里可以讀取到最終將輸出的資源、代碼塊、模塊及其依賴,并且可以修改輸出資源的內(nèi)容。 插件代碼如下:
class MyPlugin { apply(compiler) { compiler.hooks.emit.tabAsync("MyPlugin", (compilation, callback) => { // compilation.chunks 存放所有代碼塊,是一個數(shù)組 compilation.chunks.forEach(function (chunk) { // chunk 代表一個代碼塊 // 代碼塊由多個模塊組成,通過 chunk.forEachModule 能讀取組成代碼塊的每個模塊 chunk.forEachModule(function (module) { // module 代表一個模塊 // module.fileDependencies 存放當前模塊的所有依賴的文件路徑,是一個數(shù)組 module.fileDependencies.forEach(function (filepath) { }); }); // Webpack 會根據(jù) Chunk 去生成輸出的文件資源,每個 Chunk 都對應(yīng)一個及其以上的輸出文件 // 例如在 Chunk 中包含了 CSS 模塊并且使用了 ExtractTextPlugin 時, // 該 Chunk 就會生成 .js 和 .css 兩個文件 chunk.files.forEach(function (filename) { // compilation.assets 存放當前所有即將輸出的資源 // 調(diào)用一個輸出資源的 source() 方法能獲取到輸出資源的內(nèi)容 let source = compilation.assets[filename].source(); }); }); // 這是一個異步事件,要記得調(diào)用 callback 通知 Webpack 本次事件監(jiān)聽處理結(jié)束。 // 如果忘記了調(diào)用 callback,Webpack 將一直卡在這里而不會往后執(zhí)行。 callback(); }) } }
2、監(jiān)聽文件變化
Webpack 會從配置的入口模塊出發(fā),依次找出所有的依賴模塊,當入口模塊或者其依賴的模塊發(fā)生變化時, 就會觸發(fā)一次新的 Compilation。
在開發(fā)插件時經(jīng)常需要知道是哪個文件發(fā)生變化導致了新的 Compilation,為此可以使用如下代碼:
// 當依賴的文件發(fā)生變化時會觸發(fā) watch-run 事件 compiler.hooks.watchRun.tap("MyPlugin", (watching, callback) => { // 獲取發(fā)生變化的文件列表 const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes; // changedFiles 格式為鍵值對,鍵為發(fā)生變化的文件路徑。 if (changedFiles[filePath] !== undefined) { // filePath 對應(yīng)的文件發(fā)生了變化 } callback(); });
默認情況下 Webpack 只會監(jiān)視入口和其依賴的模塊是否發(fā)生變化,在有些情況下項目可能需要引入新的文件,例如引入一個 HTML 文件。 由于 JavaScript 文件不會去導入 HTML 文件,Webpack 就不會監(jiān)聽 HTML 文件的變化,編輯 HTML 文件時就不會重新觸發(fā)新的 Compilation。 為了監(jiān)聽 HTML 文件的變化,我們需要把 HTML 文件加入到依賴列表中,為此可以使用如下代碼:
compiler.hooks.afterCompile.tap("MyPlugin", (compilation, callback) => { // 把 HTML 文件添加到文件依賴列表,好讓 Webpack 去監(jiān)聽 HTML 模塊文件,在 HTML 模版文件發(fā)生變化時重新啟動一次編譯 compilation.fileDependencies.push(filePath); callback(); });
3、修改輸出資源
有些場景下插件需要修改、增加、刪除輸出的資源,要做到這點需要監(jiān)聽 emit 事件,因為發(fā)生 emit 事件時所有模塊的轉(zhuǎn)換和代碼塊對應(yīng)的文件已經(jīng)生成好, 需要輸出的資源即將輸出,因此 emit 事件是修改 Webpack 輸出資源的最后時機。
所有需要輸出的資源會存放在 compilation.assets 中,compilation.assets 是一個鍵值對,鍵為需要輸出的文件名稱,值為文件對應(yīng)的內(nèi)容。
設(shè)置 compilation.assets 的代碼如下:
// 設(shè)置名稱為 fileName 的輸出資源 compilation.assets[fileName] = { // 返回文件內(nèi)容 source: () => { // fileContent 既可以是代表文本文件的字符串,也可以是代表二進制文件的 Buffer return fileContent; }, // 返回文件大小 size: () => { return Buffer.byteLength(fileContent, "utf8"); } }; callback();
讀取 compilation.assets 的代碼如下:
// 讀取名稱為 fileName 的輸出資源 const asset = compilation.assets[fileName]; // 獲取輸出資源的內(nèi)容 asset.source(); // 獲取輸出資源的文件大小 asset.size(); callback();實戰(zhàn)!寫一個插件
怎么寫一個插件?參照webpack官方教程Writing a Plugin。 一個webpack plugin由一下幾個步驟組成:
一個JavaScript類函數(shù)。
在函數(shù)原型 (prototype)中定義一個注入compiler對象的apply方法。
apply函數(shù)中通過compiler插入指定的事件鉤子,在鉤子回調(diào)中拿到compilation對象
使用compilation操縱修改webapack內(nèi)部實例數(shù)據(jù)。
異步插件,數(shù)據(jù)處理完后使用callback回調(diào)
下面我們舉一個實際的例子,帶你一步步去實現(xiàn)一個插件。
該插件的名稱取名叫 EndWebpackPlugin,作用是在 Webpack 即將退出時再附加一些額外的操作,例如在 Webpack 成功編譯和輸出了文件后執(zhí)行發(fā)布操作把輸出的文件上傳到服務(wù)器。 同時該插件還能區(qū)分 Webpack 構(gòu)建是否執(zhí)行成功。使用該插件時方法如下:
module.exports = { plugins:[ // 在初始化 EndWebpackPlugin 時傳入了兩個參數(shù),分別是在成功時的回調(diào)函數(shù)和失敗時的回調(diào)函數(shù); new EndWebpackPlugin(() => { // Webpack 構(gòu)建成功,并且文件輸出了后會執(zhí)行到這里,在這里可以做發(fā)布文件操作 }, (err) => { // Webpack 構(gòu)建失敗,err 是導致錯誤的原因 console.error(err); }) ] }
要實現(xiàn)該插件,需要借助兩個事件:
done:在成功構(gòu)建并且輸出了文件后,Webpack 即將退出時發(fā)生;
failed:在構(gòu)建出現(xiàn)異常導致構(gòu)建失敗,Webpack 即將退出時發(fā)生;
實現(xiàn)該插件非常簡單,完整代碼如下:
class EndWebpackPlugin { constructor(doneCallback, failCallback) { // 存下在構(gòu)造函數(shù)中傳入的回調(diào)函數(shù) this.doneCallback = doneCallback; this.failCallback = failCallback; } apply(compiler) { compiler.hooks.done.tab("EndWebpackPlugin", (stats) => { // 在 done 事件中回調(diào) doneCallback this.doneCallback(stats); }); compiler.hooks.failed.tab("EndWebpackPlugin", (err) => { // 在 failed 事件中回調(diào) failCallback this.failCallback(err); }); } } // 導出插件 module.exports = EndWebpackPlugin;
從開發(fā)這個插件可以看出,找到合適的事件點去完成功能在開發(fā)插件時顯得尤為重要。 在 工作原理概括 中詳細介紹過 Webpack 在運行過程中廣播出常用事件,你可以從中找到你需要的事件。
參考
tapable
compiler-hooks
Compilation Hooks
writing-a-plugin
深入淺出 Webpack
干貨!擼一個webpack插件
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/103948.html
摘要:馬上要出了,完全手寫一個優(yōu)化后的腳手架是不可或缺的技能。每個依賴項隨即被處理,最后輸出到稱之為的文件中,我們將在下一章節(jié)詳細討論這個過程。的事件流機制保證了插件的有序性,使得整個系統(tǒng)擴展性很好。 webpack馬上要出5了,完全手寫一個優(yōu)化后的腳手架是不可或缺的技能。 本文書寫時間 2019年5月9日 , webpack版本 4.30.0最新版本 本人所有代碼均手寫,親自試驗過可...
摘要:馬上要出了,完全手寫一個優(yōu)化后的腳手架是不可或缺的技能。每個依賴項隨即被處理,最后輸出到稱之為的文件中,我們將在下一章節(jié)詳細討論這個過程。的事件流機制保證了插件的有序性,使得整個系統(tǒng)擴展性很好。 webpack馬上要出5了,完全手寫一個優(yōu)化后的腳手架是不可或缺的技能。 本文書寫時間 2019年5月9日 , webpack版本 4.30.0最新版本 本人所有代碼均手寫,親自試驗過可...
摘要:馬上要出了,完全手寫一個優(yōu)化后的腳手架是不可或缺的技能。每個依賴項隨即被處理,最后輸出到稱之為的文件中,我們將在下一章節(jié)詳細討論這個過程。的事件流機制保證了插件的有序性,使得整個系統(tǒng)擴展性很好。 webpack馬上要出5了,完全手寫一個優(yōu)化后的腳手架是不可或缺的技能。 本文書寫時間 2019年5月9日 , webpack版本 4.30.0最新版本 本人所有代碼均手寫,親自試驗過可...
摘要:什么是可以引用官網(wǎng)的一幅圖解釋,我們可以看到,可以分析各個模塊的依賴關(guān)系,最終打包成我們常見的靜態(tài)文件,。我們暫時把通過傳文件路徑能返回文件信息的這個函數(shù)叫。 什么是webpack 可以引用官網(wǎng)的一幅圖解釋,我們可以看到webpack,可以分析各個模塊的依賴關(guān)系,最終打包成我們常見的靜態(tài)文件,.js 、 .css 、 .jpg 、.png。今天我們先不弄那么復雜,我們就介紹webpac...
前言 什么是webpack 本質(zhì)上,webpack 是一個現(xiàn)代 JavaScript 應(yīng)用程序的靜態(tài)模塊打包器(module bundler)。當 webpack 處理應(yīng)用程序時,它會遞歸地構(gòu)建一個依賴關(guān)系圖(dependency graph),其中包含應(yīng)用程序需要的每個模塊,然后將所有這些模塊打包成一個或多個 bundle。webpack 有哪些功能(代碼轉(zhuǎn)換 文件優(yōu)化 代碼分割 模塊合并 自...
閱讀 1882·2021-11-11 16:55
閱讀 2064·2021-10-08 10:13
閱讀 739·2019-08-30 11:01
閱讀 2155·2019-08-29 13:19
閱讀 3277·2019-08-28 18:18
閱讀 2620·2019-08-26 13:26
閱讀 579·2019-08-26 11:40
閱讀 1864·2019-08-23 17:17