Webpack揭祕——走向高階前端的必經之路

隨着前端工程化的不斷髮展,構建工具也在不斷完善。做爲大前端時代的新寵,webpack漸漸成爲新時代前端工程師不可或缺的構建工具,隨着webpack4的不斷迭代,咱們享受着構建效率不斷提高帶來的快感,配置不斷減小的溫馨,也一直爲重寫的構建事件鉤子機制煞費苦心,爲插件各類不兼容心灰意冷,雖然過程痛苦,但結果老是美好的。經歷了一番繁瑣的配置後,我經常會想,這樣一個精巧的工具,在構建過程當中作了什麼?我也是抱着這樣的好奇,潛心去翻閱相關書籍和官方文檔,終於對其中原理有所瞭解,那麼如今,就讓咱們一塊兒來逐步揭開webpack這個黑盒的神祕面紗,探尋其中的運行機制吧。css

本文將以三部份內容:Webpack運行機制、編寫自定義webpack loader、編寫自定義webpack plugin 直擊webpack原理痛點,開啓你通向高級前端工程師之路~html

本次webpack系列文章可參照項目:github.com/jerryOnlyZR…前端

本系列文章使用的webpack版本爲4,若有其餘版本問題可提issue或者直接在文章下方的評論區留言。node

1.Webpack運行機制

1.1.webpack運行機制概述

在閱讀本文以前,我就默認電腦前的你已經掌握了webpack的基本配置,可以獨立搭建一款基於webpack的前端自動化構建體系,因此這篇文章不會教你如何配置或者使用webpack,天然具體概念我就不作介紹了,直面主題,開始講解webpack原理。webpack

webpack的運行過程能夠簡單概述爲以下流程:git

初始化配置參數 -> 綁定事件鉤子回調 -> 肯定Entry逐一遍歷 -> 使用loader編譯文件 -> 輸出文件github

接下來,咱們將對具體流程逐一介紹。web

1.2.webpack運行流程

1.2.1.webpack事件流初探

在分析webpack運行流程時,咱們能夠藉助一個概念,即是webpack的事件流機制。shell

什麼是webpack事件流?編程

Webpack 就像一條生產線,要通過一系列處理流程後才能將源文件轉換成輸出結果。 這條生產線上的每一個處理流程的職責都是單一的,多個流程之間有存在依賴關係,只有完成當前處理後才能交給下一個流程去處理。 插件就像是一個插入到生產線中的一個功能,在特定的時機對生產線上的資源作處理。 Webpack 經過 Tapable 來組織這條複雜的生產線。 Webpack 在運行過程當中會廣播事件,插件只須要監聽它所關心的事件,就能加入到這條生產線中,去改變生產線的運做。 Webpack 的事件流機制保證了插件的有序性,使得整個系統擴展性很好。 --吳浩麟《深刻淺出webpack》

咱們將webpack事件流理解爲webpack構建過程當中的一系列事件,他們分別表示着不一樣的構建週期和狀態,咱們能夠像在瀏覽器上監聽click事件同樣監聽事件流上的事件,而且爲它們掛載事件回調。咱們也能夠自定義事件並在合適時機進行廣播,這一切都是使用了webpack自帶的模塊 Tapable 進行管理的。咱們不須要自行安裝 Tapable ,在webpack被安裝的同時它也會一併被安裝,如需使用,咱們只須要在文件裏直接 require 便可。

Tapable的原理其實就是咱們在前端進階過程當中都會經歷的EventEmit,經過發佈者-訂閱者模式實現,它的部分核心代碼能夠歸納成下面這樣:

class SyncHook{
    constructor(){
        this.hooks = [];
    }

    // 訂閱事件
    tap(name, fn){
        this.hooks.push(fn);
    }

    // 發佈
    call(){
        this.hooks.forEach(hook => hook(...arguments));
    }
}
複製代碼

Tapable的具體內容能夠參照文章:《webpack4.0源碼分析之Tapable》 。其使用方法咱們會在後文中的「3.編寫自定義webpack plugin」模塊再作深刻介紹。

由於webpack4重寫了事件流機制,因此若是咱們翻閱 webpack hook 的官方文檔會發現信息特別繁雜,可是在實際使用中,咱們只須要記住幾個重要的事件就足夠了。

1.2.2.webpack運行流程詳解

在講解webpack流程以前先附上一張我本身繪製的執行流程圖:

  • 首先,webpack會讀取你在命令行傳入的配置以及項目裏的 webpack.config.js 文件,初始化本次構建的配置參數,而且執行配置文件中的插件實例化語句,生成Compiler傳入plugin的apply方法,爲webpack事件流掛上自定義鉤子。
  • 接下來到了entryOption階段,webpack開始讀取配置的Entries,遞歸遍歷全部的入口文件
  • Webpack接下來就開始了compilation過程。會依次進入其中每個入口文件(entry),先使用用戶配置好的loader對文件內容進行編譯(buildModule),咱們能夠從傳入事件回調的compilation上拿到module的resource(資源路徑)、loaders(通過的loaders)等信息;以後,再將編譯好的文件內容使用acorn解析生成AST靜態語法樹(normalModuleLoader),分析文件的依賴關係逐個拉取依賴模塊並重覆上述過程,最後將全部模塊中的require語法替換成__webpack_require__來模擬模塊化操做。
  • emit階段,全部文件的編譯及轉化都已經完成,包含了最終輸出的資源,咱們能夠在傳入事件回調的compilation.assets 上拿到所需數據,其中包括即將輸出的資源、代碼塊Chunk等等信息。

1.2.3.什麼是AST?

在1.2.2中,咱們看到了一個陌生的字眼——AST,上網一搜:

在計算機科學中,抽象語法樹(Abstract Syntax Tree,AST),或簡稱語法樹(Syntax tree),是源代碼語法結構的一種抽象表示。它以樹狀的形式表現編程語言的語法結構,樹上的每一個節點都表示源代碼中的一種結構。之因此說語法是「抽象」的,是由於這裏的語法並不會表示出真實語法中出現的每一個細節。好比,嵌套括號被隱含在樹的結構中,並無以節點的形式呈現;而相似於 if-condition-then 這樣的條件跳轉語句,可使用帶有兩個分支的節點來表示。 --維基百科

其實,你只要記着,AST是一棵樹,像這樣:

轉換成AST的目的就是將咱們書寫的字符串文件轉換成計算機更容易識別的數據結構,這樣更容易提取其中的關鍵信息,而這棵樹在計算機上的表現形式,其實就是一個單純的Object。

示例是一個簡單的聲明賦值語句,通過AST轉化後各部份內容的含義就更爲清晰明瞭了。

1.2.4.webpack輸出結果解析

接下來,咱們來看看webpack的輸出內容。若是咱們沒有設置splitChunk,咱們只會在dist目錄下看到一個main.js輸出文件,過濾掉沒用的註釋還有一些目前不須要去考慮的Funciton,獲得的代碼大概是下面這樣:

(function (modules) {
  // 緩存已經加載過的module的exports
  // module在exports以前仍是有js須要執行的,緩存的目的就是優化這一過程
  // The module cache
  var installedModules = {};

  // The require function
  /** * 模擬CommonJS require() * @param {String} moduleId 模塊路徑 */
  function __webpack_require__(moduleId) {

    // Check if module is in cache
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache)
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    };

    // 執行單個module JS Function並填充installedModules與module
    // function mudule(module, __webpack_exports__[, __webpack_require__])
    // Execute the module function
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    // Flag the module as loaded
    module.l = true;

    // Return the exports of the module
    return module.exports;
  }

  // expose the modules object (__webpack_modules__)
  __webpack_require__.m = modules;

  // expose the module cache
  __webpack_require__.c = installedModules;

 ......

  // __webpack_public_path__
  __webpack_require__.p = "";

  // 加載Entry並返回Entry的exports
  // Load entry module and return exports
  return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
  // modules其實就是一個對象,鍵是模塊的路徑,值就是模塊的JS Function
  ({
    "./src/index.js": function (module, __webpack_exports__, __webpack_require__) {
 "use strict";
      eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _module_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./module.js */ \"./src/module.js\");\n/* harmony import */ var _module_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_module_js__WEBPACK_IMPORTED_MODULE_0__);\n{};\nconsole.log(_module_js__WEBPACK_IMPORTED_MODULE_0___default.a.s);\n\n//# sourceURL=webpack:///./src/index.js?");
    },
    "./src/module.js": function (module, exports) {
      eval("{};var s = 123;\nconsole.log(s);\nmodule.exports = {\n s: s\n};\n\n//# sourceURL=webpack:///./src/module.js?");
    }
  });
複製代碼

咱們都知道其實webpack在瀏覽器實現模塊化的本質就是將全部的代碼都注入到同一個JS文件裏,如今咱們能夠清晰明瞭地看出webpack最後生成的也不過只是一個IIFE,咱們引入的全部模塊都被一個function給包起來組裝成一個對象,這個對象做爲IIFE的實參被傳遞進去。

但若是咱們配置了splitChunk,這時候輸出的文件就和你的Chunk掛鉤了,代碼也變了模樣:

//@file: dist/common/runtime.js
 // 當配置了splitChunk以後,此時IIFE的形參modules就成了擺設,
 // 真正的module還有chunk都被存放在了一個掛載在window上的全局數組`webpackJsonp`上了
 (function(modules) { // webpackBootstrap
	 // install a JSONP callback for chunk loading
	 /** * webpackJsonpCallback 處理chunk數據 * @param {Array} data [[chunkId(chunk名稱)], modules(Object), [...other chunks(全部須要的chunk)]] */
 	function webpackJsonpCallback(data) {
        // chunk的名稱,若是是entry chunk也就是咱們entry的key
 		var chunkIds = data[0];
        // 依賴模塊
 		var moreModules = data[1];
 		var executeModules = data[2];

 		// add "moreModules" to the modules object,
 		// then flag all "chunkIds" as loaded and fire callback
 		var moduleId, chunkId, i = 0, resolves = [];
 		for(;i < chunkIds.length; i++) {
 			chunkId = chunkIds[i];
 			if(installedChunks[chunkId]) {
 				resolves.push(installedChunks[chunkId][0]);
 			}
 			installedChunks[chunkId] = 0;
 		}
 		for(moduleId in moreModules) {
 			if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
 				modules[moduleId] = moreModules[moduleId];
 			}
 		}
 		if(parentJsonpFunction) parentJsonpFunction(data);

 		while(resolves.length) {
 			resolves.shift()();
 		}

 		// add entry modules from loaded chunk to deferred list
 		deferredModules.push.apply(deferredModules, executeModules || []);

 		// run deferred modules when all chunks ready
 		return checkDeferredModules();
 	};
 	function checkDeferredModules() {
 		var result;
 		for(var i = 0; i < deferredModules.length; i++) {
 			var deferredModule = deferredModules[i];
 			var fulfilled = true;
 			for(var j = 1; j < deferredModule.length; j++) {
 				var depId = deferredModule[j];
 				if(installedChunks[depId] !== 0) fulfilled = false;
 			}
 			if(fulfilled) {
 				deferredModules.splice(i--, 1);
 				result = __webpack_require__(__webpack_require__.s = deferredModule[0]);
 			}
 		}
 		return result;
 	}

 	// The module cache
 	var installedModules = {};

	// 緩存chunk,同理module
 	// object to store loaded and loading chunks
 	// undefined = chunk not loaded, null = chunk preloaded/prefetched
 	// Promise = chunk loading, 0 = chunk loaded
 	var installedChunks = {
 		"common/runtime": 0
 	};

 	var deferredModules = [];

 	// The require function
 	function __webpack_require__(moduleId) {
 		// Check if module is in cache
 		if(installedModules[moduleId]) {
 			return installedModules[moduleId].exports;
 		}
 		// Create a new module (and put it into the cache)
 		var module = installedModules[moduleId] = {
 			i: moduleId,
 			l: false,
 			exports: {}
 		};
 		// Execute the module function
 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
 		// Flag the module as loaded
 		module.l = true;
 		// Return the exports of the module
 		return module.exports;
 	}


 	// expose the modules object (__webpack_modules__)
 	__webpack_require__.m = modules;

 	// expose the module cache
 	__webpack_require__.c = installedModules;

 	......

 	// __webpack_public_path__
 	__webpack_require__.p = "";

 	var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
 	var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
 	jsonpArray.push = webpackJsonpCallback;
 	jsonpArray = jsonpArray.slice();
 	for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
 	var parentJsonpFunction = oldJsonpFunction;


 	// run deferred modules from other chunks
 	checkDeferredModules();
 })([]);
複製代碼
//@file: dist/common/utils.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["common/utils"], {
  "./src/index.js": function (module, __webpack_exports__, __webpack_require__) {
 "use strict";
    eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _module_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./module.js */ \"./src/module.js\");\n/* harmony import */ var _module_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_module_js__WEBPACK_IMPORTED_MODULE_0__);\n{};\nconsole.log(_module_js__WEBPACK_IMPORTED_MODULE_0___default.a.s);\n\n//# sourceURL=webpack:///./src/index.js?");
  },
  "./src/module.js": function (module, exports) {
    eval("{};var s = 123;\nconsole.log(s);\nmodule.exports = {\n s: s\n};\n\n//# sourceURL=webpack:///./src/module.js?");
  }
}]);
複製代碼

這時候,IIFE的形參也變成了擺設,全部咱們的模塊都被放在了一個名爲 webpackJsonp 的全局數組上,經過IIFE裏的 webpackJsonpCallback 來處理數據。

1.3.總結

縱觀webpack構建流程,咱們能夠發現整個構建過程主要花費時間的部分也就是遞歸遍歷各個entry而後尋找依賴逐個編譯的過程,每次遞歸都須要經歷 String->AST->String 的流程,通過loader還須要處理一些字符串或者執行一些JS腳本,介於node.js單線程的壁壘,webpack構建慢一直成爲它飽受詬病的緣由。這也是happypack之因此能大火的緣由,咱們能夠來看一段happypack的示例代碼:

// @file: webpack.config.js
const HappyPack = require('happypack');
const os = require('os');
// 開闢一個線程池
// 拿到系統CPU的最大核數,讓happypack將編譯工做灌滿全部CPU核
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

module.exports = {
  // ...
  plugins: [
    new HappyPack({
      id: 'js',
      threadPool: happyThreadPool,
      loaders: [ 'babel-loader' ]
    }),

    new HappyPack({
      id: 'styles',
      threadPool: happyThreadPool,
      loaders: [ 'style-loader', 'css-loader', 'less-loader' ]
    })
  ]
};
複製代碼

你們若是有用過pm2的話就能很容易明白了,其實原理是一致的,都是利用了node.js原生的cluster模塊去開闢多進程執行構建,不過在4以後你們就能夠不用去糾結這一問題了,多進程構建已經被集成在webpack自己上了,除了增量編譯,這也是4之因此能大幅度提高構建效率的緣由之一。

2.編寫自定義webpack loader

2.1.讓webpack loader現出原型

在webpack中,真正起編譯做用的即是咱們的loader,也就是說,平時咱們進行babel的ES6編譯,SCSS、LESS等編譯都是在loader裏面完成的,在你不知道loader的本質以前你必定會以爲這是個很高大上的東西,正如計算機學科裏的編譯原理同樣,裏面必定有許多繁雜的操做。但實際上,loader只是一個普通的funciton,他會傳入匹配到的文件內容(String),你只須要對這些字符串作些處理就行了。一個最簡單的loader大概是這樣:

/** * loader Function * @param {String} content 文件內容 */
module.exports = function(content){
    return "{};" + content
}
複製代碼

使用它的方式和babel-loader同樣,只須要在webpack.config.jsmodule.rules數組裏加上這麼一個對象就行了:

{
    test: /\.js$/,
    exclude: /node_modules/,
       use: {
           //這裏是個人自定義loader的存放路徑
           loader: path.resolve('./loaders/index.js'),
           options: {
              test: 1
           }
       }
}
複製代碼

這樣,loader會去匹配全部以.js後綴結尾的文件並在內容前追加{};這樣一段代碼,咱們能夠在輸出文件中看到效果:

因此,拿到了文件內容,你想對字符串進行怎樣得處理都由你自定義~你能夠引入babel庫加個 babel(content) ,這樣就實現了編譯,也能夠引入uglifyjs對文件內容進行字符串壓縮,一切工做都由你本身定義。

2.2.Loader實戰經常使用技巧

2.2.1.拿到loader的用戶自定義配置

在咱們在webpack.config.js書寫loader配置時,常常會見到 options 這樣一個配置項,這就是webpack爲用戶提供的自定義配置,在咱們的loader裏,若是要拿到這樣一個配置信息,只須要使用這個封裝好的庫 loader-utils 就能夠了:

const loaderUtils = require("loader-utils");

module.exports = function(content){
    // 獲取用戶配置的options
    const options = loaderUtils.getOptions(this);
    console.log('***options***', options)
    return "{};" + content
}
複製代碼

2.2.2.loader導出數據的形式

在前面的示例中,由於咱們一直loader是一個Funtion,因此咱們使用了return的方式導出loader處理後的數據,但其實這並非咱們最推薦的寫法,在大多數狀況下,咱們仍是更但願使用 this.callback 方法去導出數據。若是改爲這種寫法,示例代碼能夠改寫爲:

module.exports = function(content){
    //return "{};" + content
    this.callback(null, "{};" + content)
}
複製代碼

this.callback 能夠傳入四個參數(其中後兩個參數能夠省略),他們分別是:

  • error:Error | null,當loader出錯時向外跑出一個Error
  • content:String | Buffer,通過loader編譯後須要導出的內容
  • sourceMap:爲方便調試生成的編譯後內容的source map
  • ast: 本次編譯生成的AST靜態語法樹,以後執行的loader能夠直接使用這個AST,能夠省去重複生成AST的過程

2.2.3.異步loader

通過2.2.2咱們能夠發現,不管是使用return仍是 this.callback 的方式,導出結果的執行都是同步的,假如咱們的loader裏存在異步操做,好比拉取請求等等又該怎麼辦呢?

熟悉ES6的朋友都知道最簡單的解決方法即是封裝一個Promise,而後用async-await徹底無視異步問題,示例代碼以下:

module.exports = async function(content){
    function timeout(delay) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve("{};" + content)
            }, delay)
        })
    }
    const data = await timeout(1000)
    return data
}
複製代碼

但若是node的版本不夠,咱們還有原始的土方案 this.async ,調用這個方法會返回一個callback Function,在適當時候執行這個callback就能夠了,上面的示例代碼能夠改寫爲:

module.exports = function(content){
    function timeout(delay) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve("{};" + content)
            }, delay)
        })
    }
    const callback = this.async()
    timeout(1000).then(data => {
        callback(null, data)
    })
}
複製代碼

更老版本的node同此。

2.2.4.loaders的執行順序

還記得咱們配置CSS編譯時寫的loader嘛,它們是長這樣的:

在不少時候,咱們的 use 裏不僅有一個loader,這些loader的執行順序是從後往前的,你也能夠把它理解爲這個loaders數組的出棧過程。

2.2.5.loader緩存

webpack增量編譯機制會觀察每次編譯時的變動文件,在默認狀況下,webpack會對loader的執行結果進行緩存,這樣可以大幅度提高構建速度,不過咱們也能夠手動關閉它(雖然我不知道爲何要關閉它,既然留了這麼個API就蠻介紹下吧,歡迎補充),示例代碼以下:

module.exports = function(content){
    //關閉loader緩存
    this.cacheable(false);
    return "{};" + content
}
複製代碼

2.2.6.pitch鉤子全程傳參

在loader文件裏你能夠exports一個命名爲 pitch 的函數,它會先於全部的loader執行,就像這樣:

module.exports.pitch = (remaining, preceding, data) => {
    console.log('***remaining***', remaining)
    console.log('***preceding***', preceding)
    // data會被掛在到當前loader的上下文this上在loaders之間傳遞
    data.value = "test"
}
複製代碼

它能夠接受三個參數,最重要的就是第三個參數data,你能夠爲其掛在一些所需的值,一個rule裏的全部的loader在執行時都能拿到這個值。

module.exports = function(content){
    //***this data*** test
    console.log('***this data***', this.data.value)
    return "{};" + content
}

module.exports.pitch = (remaining, preceding, data) => {
    data.value = "test"
}
複製代碼

2.3.總結

經過上述介紹,咱們明白了,loader其實就是一個「平平無奇」的Funtion,可以傳入本次匹配到的文件內容供咱們自定義修改。

3.編寫自定義webpack plugin

3.1.溫習一下webpack事件流

還記得咱們在前文講到的webpack事件流,你還記得webpack有哪些經常使用的事件嗎?webpack插件起到的做用,就是爲這些事件掛載回調,或者執行指定腳本。

咱們在文章裏也提到,webpack的事件流是經過 Tapable 實現的,它就和咱們的EventEmit同樣,是這一系列的事件的生成和管理工具,它的部分核心代碼就像下面這樣:

class SyncHook{
    constructor(){
        this.hooks = [];
    }

    // 訂閱事件
    tap(name, fn){
        this.hooks.push(fn);
    }

    // 發佈
    call(){
        this.hooks.forEach(hook => hook(...arguments));
    }
}
複製代碼

webpack hook 上的全部鉤子都是 Tapable 的示例,因此咱們能夠經過 tap 方法監聽事件,使用 call 方法廣播事件,就像官方文檔介紹的這樣:

compiler.hooks.someHook.tap(/* ... */);
複製代碼

幾個比較經常使用的hook咱們也已經在前文介紹過了,若是你們不記得了,能夠回過頭再看看哦~

3.2.什麼是webpack plugin

若是剖析webpack plugin的本質,它實際上和webpack loader同樣簡單,其實它只是一個帶有apply方法的class。

//@file: plugins/myplugin.js
class myPlugin {
    constructor(options){
        //用戶自定義配置
        this.options = options
        console.log(this.options)
    }
    apply(compiler) {
        console.log("This is my first plugin.")
    }
}

module.exports = myPlugin
複製代碼

這樣就實現了一個簡單的webpack plugin,若是咱們要使用它,只須要在webpack.config.jsrequire 並實例化就能夠了:

const MyPlugin = require('./plugins/myplugin-4.js')

module.exports = {
    ......,
    plugins: [
        new MyPlugin("Plugin is instancing.")
    ]
}
複製代碼

你們如今確定也都想起來了,每次咱們須要使用某個plugin的時候都須要new一下實例化,天然,實例過程當中傳遞的參數,也就成爲了咱們的構造函數裏拿到的options了。

而實例化全部plugin的時機,即是在webpack初始化全部參數的時候,也就是事件流開始的時候。因此,若是配合 shell.js 等工具庫,咱們就能夠在這時候執行文件操做等相關腳本,這就是webpack plugin所作的事情。

若是你想在指定時機執行某些腳本,天然可使用在webpack事件流上掛載回調的方法,在回調裏執行你所需的操做。

3.3.Tapable新用

若是咱們想賦予webpack事件流咱們的自定義事件可以實現嘛?

答案固然是必須能夠啊老鐵!

自定義webpack事件流事件須要幾步?四步:

  • 引入Tapable並找到你想用的hook,同步hook or 異步hook 在這裏應有盡有 -> webpack4.0源碼分析之Tapable

    const { SyncHook } = require("tapable");
    複製代碼
  • 實例化Tapable中你所須要的hook並掛載在compiler或compilation上

    compiler.hooks.myHook = new SyncHook(['data'])
    複製代碼
  • 在你須要監聽事件的位置tap監聽

    compiler.hooks.myHook.tap('Listen4Myplugin', (data) => {
        console.log('@Listen4Myplugin', data)
    })
    複製代碼
  • 在你所須要廣播事件的時機執行call方法並傳入數據

    compiler.hooks.environment.tap(pluginName, () => {
           //廣播自定義事件
           compiler.hooks.myHook.call("It's my plugin.")
    });
    複製代碼

完整代碼實現能夠參考我在文章最前方貼出的項目,大概就是下面這樣:

如今個人自定義插件裏實例化一個hook並掛載在webpack事件流上

// @file: plugins/myplugin.js
const pluginName = 'MyPlugin'
// tapable是webpack自帶的package,是webpack的核心實現
// 不須要單獨install,能夠在安裝過webpack的項目裏直接require
// 拿到一個同步hook類
const { SyncHook } = require("tapable");
class MyPlugin {
    // 傳入webpack config中的plugin配置參數
    constructor(options) {
        // { test: 1 }
        console.log('@plugin constructor', options);
    }

    apply(compiler) {
        console.log('@plugin apply');
        // 實例化自定義事件
        compiler.hooks.myPlugin = new SyncHook(['data'])

        compiler.hooks.environment.tap(pluginName, () => {
            //廣播自定義事件
            compiler.hooks.myPlugin.call("It's my plugin.")
            console.log('@environment');
        });

        // compiler.hooks.compilation.tap(pluginName, (compilation) => {
            // 你也能夠在compilation上掛載hook
            // compilation.hooks.myPlugin = new SyncHook(['data'])
            // compilation.hooks.myPlugin.call("It's my plugin.")
        // });
    }
}
module.exports = MyPlugin
複製代碼

在監聽插件裏監聽個人自定義事件

// @file: plugins/listen4myplugin.js
class Listen4Myplugin {
    apply(compiler) {
        // 在myplugin environment 階段被廣播
        compiler.hooks.myPlugin.tap('Listen4Myplugin', (data) => {
            console.log('@Listen4Myplugin', data)
        })
    }
}

module.exports = Listen4Myplugin
複製代碼

在webpack配置裏引入兩個插件並實例化

// @file: webpack.config.js
const MyPlugin = require('./plugins/myplugin-4.js')
const Listen4Myplugin = require('./plugins/listen4myplugin.js')

module.exports = {
    ......,
    plugins: [
        new MyPlugin("Plugin is instancing."),
        new Listen4Myplugin()
    ]
}
複製代碼

輸出結果就是這樣:

咱們拿到了call方法傳入的數據,而且成功在environment時機裏成功輸出了。

3.4.實戰剖析

來看一看已經被衆人玩壞的 html-webpack-plugin ,咱們發如今readme底部有這樣一段demo:

function MyPlugin(options) {
  // Configure your plugin with options...
}

MyPlugin.prototype.apply = function (compiler) {
  compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
    console.log('The compiler is starting a new compilation...');

    compilation.hooks.htmlWebpackPluginAfterHtmlProcessing.tapAsync(
      'MyPlugin',
      (data, cb) => {
        data.html += 'The Magic Footer'

        cb(null, data)
      }
    )
  })
}

module.exports = MyPlugin
複製代碼

若是你認真讀完了上個板塊的內容,你會發現,這個 htmlWebpackPluginAfterHtmlProcessing 不就是這個插件本身掛載在webpack事件流上的自定義事件嘛,它會在生成輸出文件準備注入HTML時調用你自定義的回調,並向回調裏傳入本次編譯後生成的資源文件的相關信息以及待注入的HTML文件的內容(字符串形式)供咱們自定義操做。在項目搜一下這個鉤子:

這不和咱們在3.2裏說的同樣嘛,先實例化咱們所須要的hook,從名字就能夠看出來只有第一個是同步鉤子,另外幾個都是異步鉤子。而後再找找事件的廣播:

和咱們剛剛介紹的如出一轍對吧,只不過異步鉤子使用promise方法去廣播,其餘不就徹底是咱們自定義事件的流程。你們若是有興趣能夠去打下console看看 htmlWebpackPluginAfterHtmlProcessing 這個鉤子向回調傳入的數據,或許你能發現一片新大陸哦。

硬廣

咱們團隊招人啦!!!歡迎加入字節跳動商業變現前端團隊,咱們在作的技術建設有:前端工程化體系升級、團隊 Node 基建搭建、前端一鍵式 CI 發佈工具、組件服務化支持、前端國際化通用解決方案、重依賴業務系統微前端改造、可視化頁面搭建系統、商業智能 BI 系統、前端自動化測試等等等等,擁有近百號人的北上杭大前端團隊,必定會有你感興趣的領域,若是你想要加入咱們,歡迎點擊個人內推通道:

✨✨✨✨✨

內推傳送門(黃金招聘季,點擊獲取字節跳動內推機會!)

校招專屬入口(字節跳動校招內推碼: HTZYCHN,投遞連接: 加入字節跳動-招聘)

✨✨✨✨✨

若是你想了解咱們部門的平常生(dòu)活(bī)以及工做環(fú)境(lì),也能夠點擊這裏瞭解噢~

相關文章
相關標籤/搜索