圖解Webpack——實現Plugin

2021-02-21 執鳶者

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

面試工作加分項!
Plugin是webpack生態系統的重要組成部分,其目的是解決loader無法實現的其他事,可用於執行範圍更廣的任務,為webpack帶來很大的靈活性。目前存在的plugin並不能完全滿足所有的開發需求,所以定製化符合自己需求的plugin成為學習webpack的必經之路。下面將逐步闡述plugin開發中幾個關鍵技術點並實現plugin。

一、Webpack構建流程

在實現自己的Plugin之前,需要了解一下Webpack的構建流程(下圖)。Webpack的構建流程類似於一條生產線(webpack的事件流機制),在特定時機會廣播對應的事件,插件就可以監聽這些事件的發生,從而在特定的時機做特定的事情。

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

初始化階段:啟動構建;緊接著從配置文件和Shell語句中讀取並合併參數,得到最終參數;用上一步得到的參數初始化Compiler對象並加載所有配置的插件。

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

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

二、編寫Plugin

編寫Plugin主要分為四個步驟:

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進行註冊。

tap
tap可以用來註冊同步鉤子也能用來註冊異步鉤子。

compiler.hooks.compile.tap('MyPlugin', compilationParams => {
console.log('以同步方式觸及compile鉤子')
});

tapAsync
tapAsync能夠用來註冊異步鉤子,並通過callback告知Webpack異步邏輯執行完畢。

compiler.hooks.run.tapAsync('MyPlugin', (compiler, callback) => {
console.log('tapAsync 異步');
callback();
});

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 對象。

三、自定義鉤子函數

難道鉤子函數只有官方給的哪些嗎?肯定不是的,我們能夠自己按照自己的需求實現一個鉤子函數,實現該鉤子函數主要分為以下幾個步驟:

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

將自己定義的鉤子函數掛載到compiler或compilation上

在需要的位置註冊該鉤子函數

在你所需要的時機觸發對應的鉤子函數

// 該代碼中有註冊的同步鉤子函數和異步鉤子函數和其觸發過程
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;

四、實現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」獲取本節的思維導圖,回復「書籍」獲取大量前端學習資料,回復「前端視頻」獲取大量前端教學視頻)

相關焦點

  • 如何編寫一個 Webpack Plugin
    ❝compiler 對象代表了完整的 webpack 環境配置,在初始化 compiler 對象之後,通過調用插件實例的 apply 方法,作為其參數傳入。這個對象在啟動 webpack 時被一次性建立,並包含了 webpack 環境的所有的配置信息,包括 options,loader 和 plugin。
  • Webpack的使用指南-Webpack的常用解決方案
    1.自動構建HTML,可壓縮空格,可給引用的js加版本號或隨機數:html-webpack-plugin解決方案:使用插件 html-webpack-pluginwebpack.config.js如下:module.exports = {entry: '.
  • webpack教程:如何從頭開始設置 webpack 5
    我們希望HTML文件自動引入這個生成 js 文件,所以我們將使用html-webpack-plugin創建一個HTML模板。注意:在安裝HtmlWebpackPlugin後,你會看到一個DeprecationWarning,因為插件在升級到webpack 5後還沒有完全擺脫deprecation警告。Clean我們還需要設置clean-webpack-plugin,在每次構建後清除dist文件夾中的所有內容。
  • 18款Webpack插件,總會有你想要的!
    03、clean-webpack-pluginclean-webpack-plugin用於在打包前清理上一次項目生成的bundle文件,它會根據output.path自動清理文件夾;這個插件在生產環境用的頻率非常高,因為生產環境經常會通過hash生成很多bundle文件,如果不進行清理的話每次都會生成新的,導致文件夾非常龐大。
  • Webpack 3 從入門到放棄
    而 plugin (插件),顧名思義,是用來增加 webpack 的功能的,作用於整個 webpack 的構建過程。在 webpack 這個大公司中,loader 是保安大叔,負責對進入公司的不同人員的處理,而 plugin 則是公司裡不同職位的職員,負責公司裡的各種不同業務,每增加一種新型的業務需求,我們就需要增加一種 plugin。
  • 重學webpack4之基礎篇
    >mini-css-extract-plugin和optimize-css-assets-webpack-plugin提取css,建議使用contenthashmodule: { rules: [ { test: /.s?
  • 18個常用 webpack插件,總會有適合你的!
    3、clean-webpack-pluginclean-webpack-plugin 用於在打包前清理上一次項目生成的 bundle 文件,它會根據 output.path 自動清理文件夾;這個插件在生產環境用的頻率非常高,因為生產環境經常會通過 hash 生成很多 bundle 文件,如果不進行清理的話每次都會生成新的,導致文件夾非常龐大。
  • 前端必備技能 webpack - 4. webpack處理CSS資源
    配置文件,對創建項目過程有疑問的同學,可以查看 前端必備技能 webpack - 2. webpack環境安裝 的第四部分。這個時候,我們需要藉助一個插件將 css 文件單獨提取出來:npm i mini-css-extract-plugin -D  安裝完畢後我們修改一下項目的目錄,並創建一些文件:
  • Webpack打包全世界
    到webpack4.0之後,不用自己添加配置文件也可以直接運行了,這個點讚。入口(entry)webpack中提供了多種定義入口屬性的方式。提供單個入口語法和對象語法。不過呢,常用的配置方法是對象方法,用來包含你引入的第三方庫。比如我常用的vue開發框架,帶入到webpack配置文件裡面就需要如下配置。
  • 你必須知道的 webpack 插件原理分析
    webpack 自身只支持 js 和 json 這兩種格式的文件,對於其他文件需要通過 loader 將其轉換為 commonJS 規範的文件後,webpack 才能解析到。plugin 是一個擴展器,它豐富了 webpack 本身,針對是 loader 結束後,webpack 打包的整個過程,它並不直接操作文件,而是基於事件機制工作,會監聽 webpack 打包過程中的某些節點,執行廣泛的任務。
  • webpack系列---loader
    webpack本身只能打包Javascript文件,對於其他資源例如 css,圖片,或者其他的語法集比如jsx,是沒有辦法加載的。 這就需要對應的loader將資源轉化,加載進來。所謂 loader 只是一個導出為函數的 JavaScript 模塊。
  • 擼一個webpack插件!!
    而將這些插件控制在webapck事件流上的運行的就是webpack自己寫的基礎類Tapable。Tapable暴露出掛載plugin的方法,使我們能將插件控制在webapack事件流上運行(如下圖)。後面我們將看到核心的對象 Compiler,Compilation等都是繼承於Tabable類。Tabable是什麼?
  • 前端工程化(ES6模塊化和webpack打包css,less,scss,圖片,字體...
    通過模塊化形式,實現列表隔行變色效果*/注意:此時項目運行會有錯誤,因為import $ from "jquery";這句代碼屬於ES6的新語法代碼,在瀏覽器中可能會存在兼容性問題 所以我們需要webpack來幫助我們解決這個問題。
  • webpack4 處理頁面
    圖文編輯:開三金前言:1.plugins 是webpack4的插件集合,可以用一些插件擴展webpack4 的功能,就比如HtmlWebpackPlugin2.HtmlWebpackPlugin的主要作用是:為html文件中引入的外部資源如script
  • Webpack 的 Bundle Split 和 Code Split 區別和應用
    :將每個 npm 包單獨分離出來這裡我們需要使用到 webpack.HashedModuleIdsPlugin 這個插件參考官方文檔(https://webpack.js.org/plugins/split-chunks-plugin/#optimization-splitchunks)直接上代碼:const path = require
  • Webpack 多入口配置,看這篇,足夠了
    本文由於是入口和出口相關的配置,所以內容主要圍繞著 entry 、 output 和一個重要的 webpack 插件 html-webpack-plugin,這個插件是跟打包出來的 HTML 文件密切相關,主要有下面幾個作用:根據模版生成 HTML 文件;給生成的 HTML 文件引入外部資源比如 link、 script 等;改變每次引入的外部文件的
  • 【webpack】webpack 中最易混淆的 5 個知識點
    但是最近看了一下 webpack4 的文檔,發現 webpack官網的 指南[1] 寫的還不錯,跟著這份指南學會 webpack4 基礎配置完全不是問題,想系統學習 webpack 的朋友可以看一下。其實 webpack 懶加載是用內置的一個插件 SplitChunksPlugin[6] 實現的,這個插件裡面有些默認配置項[7],比如說 automaticNameDelimiter,默認的分割符就是 ~,所以最後的文件名才會出現這個符號,這塊兒內容我就不引申了,感興趣的同學可以自己研究一下。
  • webpack的幾個常見loader源碼淺析,以及動手實現一個md2html-loader
    前言本文會帶你簡單的認識一下webpack的loader,動手實現一個利用md轉成抽象語法樹,再轉成html字符串的loader。順便簡單的了解一下幾個style-loader,vue-loader,babel-loader的源碼以及工作流程。
  • webpack4.x 系列-entry(入口)
    之前寫過webpack4系列文章,本次對webpack4進行更再次研究學習webpack官網webpck中文網中文網更新慢
  • 【Webpack】654- 了不起的 Webpack Scope Hoisting 學習指南
    _0__util_js__ = __webpack_require__(1);    console.log(__WEBPACK_IMPORTED_MODULE_0__util_js__["a"]);  }),  (function (module, __webpack_exports__, __webpack_require__) {    __webpack_exports