随着前端工程化的不断发展,构建工具也在不断完善。做为大前端时代的新宠,webpack渐渐成为新时代前端工程师不可或缺的构建工具,随着webpack4的不断迭代,咱们享受着构建效率不断提高带来的快感,配置不断减小的温馨,也一直为重写的构建事件钩子机制煞费苦心,为插件各类不兼容心灰意冷,虽然过程痛苦,但结果老是美好的。经历了一番繁琐的配置后,我经常会想,这样一个精巧的工具,在构建过程当中作了什么?我也是抱着这样的好奇,潜心去翻阅相关书籍和官方文档,终于对其中原理有所了解,那么如今,就让咱们一块儿来逐步揭开webpack这个黑盒的神秘面纱,探寻其中的运行机制吧。css
本文将以三部份内容:Webpack运行机制、编写自定义webpack loader、编写自定义webpack plugin 直击webpack原理痛点,开启你通向高级前端工程师之路~html
本次webpack系列文章可参照项目:github.com/jerryOnlyZR… 。前端
本系列文章使用的webpack版本为4,若有其余版本问题可提issue或者直接在文章下方的评论区留言。node
在阅读本文以前,我就默认电脑前的你已经掌握了webpack的基本配置,可以独立搭建一款基于webpack的前端自动化构建体系,因此这篇文章不会教你如何配置或者使用webpack,天然具体概念我就不作介绍了,直面主题,开始讲解webpack原理。webpack
webpack的运行过程能够简单概述为以下流程:git
初始化配置参数 -> 绑定事件钩子回调 -> 肯定Entry逐一遍历 -> 使用loader编译文件 -> 输出文件github
接下来,咱们将对具体流程逐一介绍。web
在分析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 的官方文档会发现信息特别繁杂,可是在实际使用中,咱们只须要记住几个重要的事件就足够了。
在讲解webpack流程以前先附上一张我本身绘制的执行流程图:
webpack.config.js
文件,初始化本次构建的配置参数,而且执行配置文件中的插件实例化语句,生成Compiler传入plugin的apply方法,为webpack事件流挂上自定义钩子。require
语法替换成__webpack_require__
来模拟模块化操做。compilation.assets
上拿到所需数据,其中包括即将输出的资源、代码块Chunk等等信息。在1.2.2中,咱们看到了一个陌生的字眼——AST,上网一搜:
在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每一个节点都表示源代码中的一种结构。之因此说语法是“抽象”的,是由于这里的语法并不会表示出真实语法中出现的每一个细节。好比,嵌套括号被隐含在树的结构中,并无以节点的形式呈现;而相似于 if-condition-then 这样的条件跳转语句,可使用带有两个分支的节点来表示。 --维基百科
其实,你只要记着,AST是一棵树,像这样:
转换成AST的目的就是将咱们书写的字符串文件转换成计算机更容易识别的数据结构,这样更容易提取其中的关键信息,而这棵树在计算机上的表现形式,其实就是一个单纯的Object。
示例是一个简单的声明赋值语句,通过AST转化后各部份内容的含义就更为清晰明了了。
接下来,咱们来看看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
来处理数据。
纵观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之因此能大幅度提高构建效率的缘由之一。
在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.js
的module.rules
数组里加上这么一个对象就行了:
{
test: /\.js$/,
exclude: /node_modules/,
use: {
//这里是个人自定义loader的存放路径
loader: path.resolve('./loaders/index.js'),
options: {
test: 1
}
}
}
复制代码
这样,loader会去匹配全部以.js
后缀结尾的文件并在内容前追加{};
这样一段代码,咱们能够在输出文件中看到效果:
因此,拿到了文件内容,你想对字符串进行怎样得处理都由你自定义~你能够引入babel
库加个 babel(content)
,这样就实现了编译,也能够引入uglifyjs
对文件内容进行字符串压缩,一切工做都由你本身定义。
在咱们在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
}
复制代码
在前面的示例中,由于咱们一直loader是一个Funtion,因此咱们使用了return的方式导出loader处理后的数据,但其实这并非咱们最推荐的写法,在大多数状况下,咱们仍是更但愿使用 this.callback
方法去导出数据。若是改为这种写法,示例代码能够改写为:
module.exports = function(content){
//return "{};" + content
this.callback(null, "{};" + content)
}
复制代码
this.callback
能够传入四个参数(其中后两个参数能够省略),他们分别是:
通过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同此。
还记得咱们配置CSS编译时写的loader嘛,它们是长这样的:
在不少时候,咱们的 use 里不仅有一个loader,这些loader的执行顺序是从后往前的,你也能够把它理解为这个loaders数组的出栈过程。
webpack增量编译机制会观察每次编译时的变动文件,在默认状况下,webpack会对loader的执行结果进行缓存,这样可以大幅度提高构建速度,不过咱们也能够手动关闭它(虽然我不知道为何要关闭它,既然留了这么个API就蛮介绍下吧,欢迎补充),示例代码以下:
module.exports = function(content){
//关闭loader缓存
this.cacheable(false);
return "{};" + content
}
复制代码
在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"
}
复制代码
经过上述介绍,咱们明白了,loader其实就是一个“平平无奇”的Funtion,可以传入本次匹配到的文件内容供咱们自定义修改。
还记得咱们在前文讲到的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咱们也已经在前文介绍过了,若是你们不记得了,能够回过头再看看哦~
若是剖析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.js
里 require
并实例化就能够了:
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事件流上挂载回调的方法,在回调里执行你所需的操做。
若是咱们想赋予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时机里成功输出了。
来看一看已经被众人玩坏的 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
这个钩子向回调传入的数据,或许你能发现一片新大陆哦。