手把手教你学习webapck插件plugin的开发
准备知识
作用
通过插件我们可以拓展webpack,加入自定义的构建行为,使webpack可以执行更广泛的任务,拥有更强的构建能力。
工作原理
webpack在编译过程中,会触发一些列Tapable钩子事件,插件所做的,就是找到相应的钩子,往上面挂上自己的任务,也就是注册事件。这样,当webpack构建的时候,插件注册的事件就会随着钩子的触发而执行了。
webpack内部的钩子
什么是钩子
钩子的本质就是:事件。为了方便我们直接介入和控制编译过程,webpack把编译过程中触发的各类关键事件封装成了事件接口暴露了出来。这些接口被很形象的称作:hooks(钩子)。开发插件,离不开这些钩子。
Tapble
Tapble为webpack提供了统一的接口(钩子)类型定义,他是webpack核心功能库,webpack中目前有很多中hooks,在Tapble源码中可以看到,他们是:
exports.__esModule = true;
exports.SyncHook = require("./SyncHook");
exports.SyncBailHook = require("./SyncBailHook");
exports.SyncWaterfallHook = require("./SyncWaterfallHook");
exports.SyncLoopHook = require("./SyncLoopHook");
exports.AsyncParallelHook = require("./AsyncParallelHook");
exports.AsyncParallelBailHook = require("./AsyncParallelBailHook");
exports.AsyncSeriesHook = require("./AsyncSeriesHook");
exports.AsyncSeriesBailHook = require("./AsyncSeriesBailHook");
exports.AsyncSeriesLoopHook = require("./AsyncSeriesLoopHook");
exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook");
exports.HookMap = require("./HookMap");
exports.MultiHook = require("./MultiHook");
Tapble还统一暴露了三个方法给插件,用于注入不同类型的自定义构建行为:
- tap:可以注册同步和异步钩子
- tapAsync:回调方式注册异步钩子
- tapPromise:Promise方式注册异步钩子
Plugin构建对象
Compiler
compiler对象中保存着完整的webpack环境配置,每次启动webpack构建时它都是一个独一无二,仅仅创建一次的对象。这个对象会在首次启动webpack时构建,我们可以通过compiler对象访问到webpack的主环境配置,如loader、plugin等配置信息。
compiler主要有以下属性:
属性名 | |
---|---|
options | 可以访问本次启动的webpack时的所有配置文件,如loaders、entry、output、plugin等等配置信息。 |
inputFileSystem | 可以进行文件操作,功能如Node.js内的fs |
outputFileSystem | 可以进行文件操作,功能如Node.js内的fs |
hooks | 可以注册Tabable的不同种类Hook,从而可以在compiler生命周期中植入不同的逻辑 |
Compilation
compilation对象代表一次资源的构建,compilation实例能够访问所有的模块和它们的依赖
一个compilation对象会对构建依赖图中所有模块,进行编译。在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、分块(chunk)、哈希(hash)和重新创建(restore)
它主要有以下属性
属性名 | |
---|---|
modules | 可以访问所有模块,打包的每一个文件都是一个模块。 |
chunks | chunk 即是多个 modules 组成而来的一个代码块。入口文件引入的资源组成一个 chunk,通过代码分割的模块又是另外的 chunk。 |
assets | 可以访问本次打包生成所有文件的结果。 |
hooks | 可以注册 tapable 的不同种类 Hook,用于在 compilation 编译模块阶段进行逻辑添加以及修改。 |
生命周期简图
插件开发基础示例
插件基础结构示例
一个插件由以下构成
- 一个具名 JavaScript 函数。
- 在它的原型上定义 apply 方法。
- 指定一个触及到 webpack 本身的 事件钩子。
- 操作 webpack 内部的实例特定数据。
- 在实现功能后调用 webpack 提供的 callback。
// 一个 JavaScript class
class MyExampleWebpackPlugin {
// 将 `apply` 定义为其原型方法,此方法以 compiler 作为参数
apply(compiler) {
// 指定要附加到的事件钩子函数
compiler.hooks.emit.tapAsync('MyExampleWebpackPlugin',
(compilation, callback) => {
......
callback();
}
);
}
}
手写一个最简单的插件
创建一个test-plugin.js的文件
class TestPlugin {
constructor() {
console.log("我的第一个插件");
}
apply(){
}
}
module.exports = TestPlugin;
webpack.config.js中引入并使用
const path = require("path");
const TestPlugin = require("./plugins/test-plugin");
module.exports = {
entry: "./src/main.js",
output: {
path: path.resolve(__dirname, "./dist"),
filename: "js/[name].js",
},
module: {
rules: [],
},
plugins: [
new TestPlugin(),
],
mode: "production",
};
命令行执行 webpack,可以看到插件正常执行了。
webpack执行插件的流程:
-
webpack创建compiler对象
-
遍历所有plugins中插件,调用插件的apply方法
-
执行剩下编译流程(触发各个hooks事件)
注:如果没有写apply函数,webpack会报错:Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema.
注册hooks
钩子函数:v4.webpack.docschina.org/api/compile…
钩子函数的调用,采用下面方法。
这个方法,接受两个参数,第一个参数是当前定义的类名,第二个参数是回调函数,如
// 一个 JavaScript class
class MyExampleWebpackPlugin {
// 将 `apply` 定义为其原型方法,此方法以 compiler 作为参数
apply(compiler) {
// 指定要附加到的事件钩子函数
compiler.hooks.emit.tapAsync('MyExampleWebpackPlugin',
(compilation, callback) => {
......
callback();
}
);
}
}
注,回调函数的参数需要查看对应的钩子函数需不需要参数,如果没有写,代表没有参数。如:
如果有,回调函数的参数就是文档上写的,如:
environment钩子
根据environment钩子,我们来在插件内增加一些逻辑
class TestPlugin {
constructor() {
console.log("---------------------------------------------------------------");
}
apply(compiler) {
// 由文档可知,environment是同步钩子,所以需要使用tap注册
compiler.hooks.environment.tap("TestPlugin", () => {
console.log("我是environment钩子");
});
}
}
module.exports = TestPlugin;
执行打包命令
可以发现,这个钩子会自动执行。
我们在学习几个常用的钩子
emit钩子
钩子类型 | 执行时机 | 参数 |
---|---|---|
AsyncSeriesHook(异步串行钩子) | 生成资源到 output 目录之前。 | compilation |
根据这个钩子,我们体验下三种调用方式的异同。
class TestPlugin {
constructor() {
console.log("-------------------------------------------------------------------------------");
}
apply(compiler) {
// 由文档可知,environment是同步钩子,所以需要使用tap注册
compiler.hooks.environment.tap("TestPlugin", () => {
console.log("我是environment钩子");
});
// emitA:由文档可知,emit是异步串行钩子 AsyncSeriesHook
compiler.hooks.emit.tap("TestPlugin", (compilation) => {
console.log("emit AAAAAAAAA");
});
// emitB:
compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {
setTimeout(() => {
console.log("emit BBBBBBBBB");
// 使用tapAsync的调用方式,必须返回callback()代表结束
callback();
}, 2000);
});
// emitC:
compiler.hooks.emit.tapPromise("TestPlugin", (compilation) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("emit CCCCCCCCC");
// tapPromise的方式通过 resolve() 或 reject() 结束函数
resolve();
}, 1000);
});
});
}
}
module.exports = TestPlugin;
通过示例,我们可以发现三种函数的写法是不一样,这是我们要注意的。然后,我们可以思考一下,代码执行后的打印顺序。
你可能有一个地方比较疑惑,emit是异步钩子,为什么还可以通过tap来注册我们的插件?
异步钩子可以通过tap方法来注册插件,但是回调函数内部只能写同步代码,不能写异步代码。
上述代码执行的顺序是:
通过这个示例,我们可以得出这样的结论
- apply内的钩子函数是从上往下依次执行的
- 执行过程中,遇到异步任务,会等异步任务执行完毕继续向下执行
- 三种函数写法是不一样的
我们在看看异步并行钩子的执行顺序
make钩子
钩子类型 | 执行时机 | 参数 |
---|---|---|
AsyncParallelHook(异步串行钩子) | --- | compilation |
/*
1. webpack加载webpack.config.js中所有配置,此时就会new TestPlugin(), 执行插件的constructor
2. webpack创建compiler对象
3. 遍历所有plugins中插件,调用插件的apply方法
4. 执行剩下编译流程(触发各个hooks事件)
*/
class TestPlugin {
constructor() {
console.log("-------------------------------------------------------------------------------");
}
apply(compiler) {
......
// 由文档可知,make是异步并行钩子 AsyncParallelHook
compiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => {
// 需要在compilation hooks触发前注册才能使用
setTimeout(() => {
console.log("TestPlugin make 111");
callback();
}, 3000);
});
compiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => {
setTimeout(() => {
console.log("TestPlugin make 222");
callback();
}, 1000);
});
compiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => {
setTimeout(() => {
console.log("TestPlugin make 333");
callback();
}, 2000);
});
}
}
module.exports = TestPlugin;
根据执行结果,我们可以看出,对于异步并行钩子,是谁快谁先结束。
注:每个钩子也有自己优先级,比如make的优先级是高于emit的
compilation 钩子
compilation作为compiler钩子的回调函数参数,它也是有钩子函数的,用法是compiler钩子一直的。
使用node调试代码
我们如果想看compilation或compiler里面具体有什么内容,在命令行打印基本上是看不来东西的,实在太乱太多了!!!!我们可以借助node调试的方式进行查看。
首先,我们在项目内打个断点
class TestPlugin {
constructor() {
console.log("-------------------------------------------------------------------------------");
}
apply(compiler) {
debugger
console.log("compiler");
// emitA:由文档可知,emit是异步串行钩子 AsyncSeriesHook
compiler.hooks.emit.tap("TestPlugin", (compilation) => {
console.log("compilation");
});
}
}
module.exports = TestPlugin;
我们在package.json中增加一段node代码
"scripts": {
"debug": "node --inspect-brk ./node_modules/webpack-cli/bin/cli.js"
}
- -- insepct 是node内置的调试指令(借助浏览器控制台实现调试)
- -brk 指在代码的首个位置打一个断点
- ./node_modules/webpack-cli/bin/cli.js 指webpack打包命令一旦执行时就进入调试状态
我们在命令行执行 npm run debug
然后打开谷歌浏览器任意一个界面,进入控制台,稍等片刻,会发现出现一个node标志
点进去,就可以调试了!第一个断点是-brk生效了,这是webpack cli.js 的第一句代码
我们点击继续执行脚本,然后就可以调试了
现在,可以方便的看compiler里面到底是什么东西了!
转载自:https://juejin.cn/post/7174208031964528648