圖解Webpack——實現Plugin

關注公衆號「執鳶者」,回覆「書籍」獲取大量前端學習資料,回覆「前端視頻」獲取大量前端教學視頻。前端

面試工做加分項!
Plugin是webpack生態系統的重要組成部分,其目的是解決loader沒法實現的其餘事,可用於執行範圍更廣的任務,爲webpack帶來很大的靈活性。目前存在的plugin並不能徹底知足全部的開發需求,因此定製化符合本身需求的plugin成爲學習webpack的必經之路。下面將逐步闡述plugin開發中幾個關鍵技術點並實現plugin。webpack

1、Webpack構建流程web

在實現本身的Plugin以前,須要瞭解一下Webpack的構建流程(下圖)。Webpack的構建流程相似於一條生產線(webpack的事件流機制),在特定時機會廣播對應的事件,插件就能夠監聽這些事件的發生,從而在特定的時機作特定的事情。面試

上圖中展現了Webpack構建流程的三個階段及每一個階段廣播出來的事件。json

  1. 初始化階段:啓動構建;緊接着從配置文件和Shell語句中讀取併合並參數,獲得最終參數;用上一步獲得的參數初始化Compiler對象並加載全部配置的插件。promise

  2. 從Entey出發,針對每一個Module串行調用對應的Loader去翻譯文件的內容,再找到該Module依賴的Module,遞歸地進行編譯處理,獲得每一個模塊被翻譯後的最終內容及它們之間的依賴關係。bash

  3. 根據入口和模塊之間的依賴關係,組裝成一個個包含多個模塊的Chunk,再將每一個Chunk轉換成一個單獨的文件加入到輸出列表中,最終將所需輸出文件的內容寫入文件系統中。微信

2、編寫Plugin

編寫Plugin主要分爲四個步驟:
app

  • 建立一個具名JavaScript函數異步

  • 在其原型上定義apply方法

  • 給webpack構建流程中相關事件掛載事件鉤子進行監聽

  • 鉤子函數觸發後,利用compiler、compilation等進行相關操做,達到須要的效果

2.1 基本結構

class MyPlugin {
constructor(options) {}

apply(compiler) {
console.log(compiler);
compiler.hooks.emit.tap('MyPlugin', () => {
console.log('plugin is used!!!');
})
}
}

module.exports = MyPlugin;

2.2 apply方法

apply 方法在安裝插件時,會被 webpack compiler 調用一次。apply 方法能夠接收一個 webpack compiler 對象的引用,從而能夠在回調函數中訪問到 compiler 對象。

2.3 鉤子函數

在webpack整個編譯過程當中暴露出來大量的Hook供內部/外部插件使用,將Hook註冊後便可實現對整個webpack事件流中對應事件的監聽。這些鉤子函數的核心是Tapable,該包暴露出不少鉤子類,利用這些類建立了上述鉤子函數。常見的鉤子主要有如下幾種:

暴露在compiler和compilation上的鉤子函數均是基於上述鉤子構建。

2.4 鉤子函數註冊方式

在plugin中,鉤子註冊方式有三種:tap、tapAsync、tapPromise,根據鉤子函數的類型採用相應的方法進行註冊。同步鉤子函數利用tap註冊,異步鉤子函數利用tap、tapAsync、tapPromise進行註冊。

  1. tap
    tap能夠用來註冊同步鉤子也能用來註冊異步鉤子。

compiler.hooks.compile.tap('MyPlugin', compilationParams => {
console.log('以同步方式觸及compile鉤子')
});
  1. tapAsync
    tapAsync可以用來註冊異步鉤子,並經過callback告知Webpack異步邏輯執行完畢。

compiler.hooks.run.tapAsync('MyPlugin', (compiler, callback) => {
console.log('tapAsync 異步');
callback();
});
  1. tapPromise
    tapPromise可以用來註冊異步鉤子,其經過返回Promise來告知Webpack異步邏輯執行完畢。(實現方式有兩種,一種是經過返回Promse函數,另外一種是利用async實現)。

// 方式一
compiler.hooks.run.tapPromise('MyPlugin', (compiler) => {
return new Promise(resolve => setTimeout(resolve, 1000)).then(() => {
console.log('tapPromise 異步')
})
})

// 方式二
compiler.hooks.run.tapPromise('MyPlugin', async (compiler) => {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('tapPromise 異步 async')
})

2.5 Compiler和Compilation

Compiler和Compilation是Plugin和Webpack之間的橋樑,因此瞭解其具體含義相當重要,其含義以下:

  • Compiler 對象包含了 Webpack 環境的全部配置信息,包含options、loaders、plugins等信息。這個對象在 Webpack 啓動時被實例化,它是全局惟一的,能夠簡單地將它理解爲 Webpack 實例。

  • Compilation 對象包含了當前的模塊資源、編譯生成資源、變化的文件等。當 Webpack以開發模式運行時,每當檢測到一個文件發生變化,便有一次新的 Compilation 被 建立。Compilation對象也提供了不少事件回調供插件進行擴展。經過 Compilation也能讀取到 Compiler 對象。

3、自定義鉤子函數

難道鉤子函數只有官方給的哪些嗎?確定不是的,咱們可以本身按照本身的需求實現一個鉤子函數,實現該鉤子函數主要分爲如下幾個步驟:

  1. 調用tabable包,選擇合適的鉤子

  2. 將本身定義的鉤子函數掛載到compiler或compilation上

  3. 在須要的位置註冊該鉤子函數

  4. 在你所須要的時機觸發對應的鉤子函數

// 該代碼中有註冊的同步鉤子函數和異步鉤子函數和其觸發過程
const { SyncHook, AsyncSeriesHook } = require('tapable');

class MyHookPlugin {
constructor() {}

apply(compiler) {
// 掛載
compiler.hooks.myHook = new SyncHook(['arg1', 'arg2']);
compiler.hooks.myAsyncHook = new AsyncSeriesHook(['arg1', 'arg2']);

// 註冊
// 同步
compiler.hooks.myHook.tap('myHook', (arg1, arg2) => {
console.log('本身定義的鉤子函數被觸發', arg1, arg2);
})

// 異步1
compiler.hooks.myAsyncHook.tapAsync('myHook', (arg1, arg2, callback) => {
console.log('異步鉤子 tapAsync ', arg1, arg2);
callback();
});
// 異步2
compiler.hooks.myAsyncHook.tapPromise('myHook', (arg1, arg2) => {
return new Promise((resolve) => {
resolve({arg1, arg2});
}).then((context) => {
console.log('異步鉤子 tapPromise', context)
})
});

compiler.hooks.environment.tap('myHookPlugin', () => {
// 觸發
// 同步
compiler.hooks.myHook.call(1, 2);
// 異步1
compiler.hooks.myAsyncHook.callAsync(1, 2, err => {
console.log('觸發完畢…… callAsync')
})
// 異步2
compiler.hooks.myAsyncHook.promise(1, 2).then(err => {
console.log('觸發完畢…… promise')
})
})
}
}

module.exports = MyHookPlugin;

4、實現Plugin

本節是Plugin實戰,在生成資源到output目錄以前生成資源清單。

class MyPlugin {
constructor(options) {

}

apply(compiler) {
compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
console.log('plugin is used!!!', "emit事件");
const mainfest = {}
for (const name of Object.keys(compilation.assets)) {
// compilation.assets[name].size()獲取輸出文件的大小;compilation.assets[name].source()獲取內容
mainfest[name] = compilation.assets[name].size();
console.log(compilation.assets[name].source())
}

compilation.assets['mainfest.json'] = {
source() {
return JSON.stringify(mainfest);
},
size() {
return this.source().length;
}
};

callback();
});
}
}

module.exports = MyPlugin;

注:本文只是起到拋磚引玉的做用,但願各位大佬多多指點。

相關章節
圖解Webpack————基礎篇
圖解Webpack————優化篇
圖解Webpack————實現Loader

歡迎你們關注公衆號(回覆「深刻淺出Webpack」獲取深刻淺出Webpack的pdf版本,回覆「webpack05」獲取本節的思惟導圖,回覆「書籍」獲取大量前端學習資料,回覆「前端視頻」獲取大量前端教學視頻)


本文分享自微信公衆號 - 執鳶者(gh_4918fcc10eab)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索