这是我参与第四届青训营笔记创作活动的第十三天
今天这篇我们开始对Plugin"下手",了解Plugin原理以及用法,最后试着简单实现一些简易的Plugin。话不多说,我们开始吧!
Plugin是什么?
很多知名工具,如:VS Code.Web Storm、ChromeFirefox、Babel、Webpack、Rollup、Eslint、Vue、Redux、Quill、Axios等等,都设计了所谓“插件”架构,为什么?
让我们用webpack来具体展开说说。
如图所示,这是webpack编译的一个过程,这是一个特别复杂的过程,那么新人如果需要了解整个流程细节的话上手成本高,加上功能迭代成本高,牵一发动全身,没有插件会导致功能僵化,作为开源项目而言缺乏成长性也总起来也就是心智成本高、可维护性低、生命力弱。而通过插件我们可以扩展 webpack,加入自定义的构建行为,使 webpack 可以执行更广泛的任务,拥有更强的构建能力,而且插件架构有一个精髓就是:对扩展开发,对修改封闭。
由于插件的优势,甚至webpack本身的很多功能也是基于插件实现的。
Plugin 工作原理
webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。webpack 通过 Tapable 来组织这条复杂的生产线。 webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。
——「深入浅出 Webpack」
站在代码逻辑的角度就是:webpack 在编译代码过程中,会触发一系列 Tapable 钩子事件,插件所做的,就是找到相应的钩子,往上面挂上自己的任务,也就是注册事件,这样,当 webpack 构建的时候,插件注册的事件就会随着钩子的触发而执行了。
Webpack 内部的钩子
什么是钩子
钩子的本质就是:事件。为了方便我们直接介入和控制编译过程,webpack 把编译过程中触发的各类关键事件封装成事件接口暴露了出来。这些接口被很形象地称做:hooks(钩子)。开发插件,离不开这些钩子。
钩子的核心信息:
时机:编译过程的特定节点,Webpack 会以钩子形式通知插件此刻正在发生什么事情;
上下文:通过 tapable 提供的回调机制,以参数方式传递上下文信息;
交互:在上下文参数对象中附带了很多存在side effect 的交互接口,插件可以通过这些接口改变
举个例子:
时机:compier.hooks.compilation
参数:compilation
交互:dependencyFactories.set
Tapable
Tapable 为 webpack 提供了统一的插件接口(钩子)类型定义,它是 webpack 的核心功能库。webpack 中目前有十种 hooks,在 Tapable 源码中可以看到,他们是:
// https://github.com/webpack/tapable/blob/master/lib/index.js
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");
Tapable 还统一暴露了三个方法给插件,用于注入不同类型的自定义构建行为:
-
tap:可以注册同步钩子和异步钩子。 -
tapAsync:回调方式注册异步钩子。 -
tapPromise:Promise 方式注册异步钩子。
Plugin 构建对象
Compiler
compiler 对象中保存着完整的 Webpack 环境配置,每次启动 webpack 构建时它都是一个独一无二,仅仅会创建一次的对象。
这个对象会在首次启动 Webpack 时创建,我们可以通过 compiler 对象上访问到 Webapck 的主环境配置,比如 loader 、 plugin 等等配置信息。
它有以下主要属性:
compiler.options可以访问本次启动 webpack 时候所有的配置文件,包括但不限于 loaders 、 entry 、 output 、 plugin 等等完整配置信息。
compiler.inputFileSystem和compiler.outputFileSystem可以进行文件操作,相当于 Nodejs 中 fs。
compiler.hooks可以注册 tapable 的不同种类 Hook,从而可以在 compiler 生命周期中植入不同的逻辑。
Compilation
compilation 对象代表一次资源的构建,compilation 实例能够访问所有的模块和它们的依赖。
一个 compilation 对象会对构建依赖图中所有模块,进行编译。 在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、 分块(chunk)、哈希(hash)和重新创建(restore)。
它有以下主要属性:
-
compilation.modules可以访问所有模块,打包的每一个文件都是一个模块。 -
compilation.chunkschunk 即是多个 modules 组成而来的一个代码块。入口文件引入的资源组成一个 chunk,通过代码分割的模块又是另外的 chunk。 -
compilation.assets可以访问本次打包生成所有文件的结果。 -
compilation.hooks可以注册 tapable 的不同种类 Hook,用于在 compilation 编译模块阶段进行逻辑添加以及修改。
生命周期简图
开发一个插件
最简单的插件
- plugins/test-plugin.js
class TestPlugin {constructor() {
console.log("TestPlugin constructor()");}
// 1. webpack读取配置时,new TestPlugin() ,会执行插件 constructor 方法
// 2. webpack创建 compiler 对象
// 3. 遍历所有插件,调用插件的 apply 方法apply(compiler) {
console.log("TestPlugin apply()");}
}
module.exports = TestPlugin;
注册 hook
const { Compilation } = require("webpack");
/*
1. webpack加载webpack.config.js中所有的配置,此时就会new TestPlugin(),执行插件的constructor
2.webpack创建compiler对象
3.遍历所有的plugins插件,调用插件的apply方法
4.执行剩下编译流程(触发各个hooks事件)
*/
class TestPlugin {
constructor() {
console.log(" TestPlugin constructor");
}
apply(compiler) {
console.log(" TestPlugin apply");
// 由文档可知,environment是同步钩子,所以需要使用tap注册
compiler.hooks.environment.tap(" TestPlugin", () => {
console.log(" TestPlugin environment");
})
// 由文档可知,emit是异步串行钩子
compiler.hooks.emit.tap(" TestPlugin", (compilation) => {
console.log("TestPlugin emit 111");
})
compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {
setTimeout(() => {
console.log("TestPlugin emit 222");
callback();
}, 2000);
});
compiler.hooks.emit.tapPromise("TestPlugin", (compilation) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("TestPlugin emit 333");
resolve();
}, 1000);
});
});
// 由文档可知,make是异步并行钩子 AsyncParallelHook
compiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => {
// 需要在compilation hooks触发前才能使用
compilation.hooks.seal.tap("TestPlugin", () => {
console.log("TestPlugin seal");
});
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;
运行结果
启动调试
通过调试查看 compiler 和 compilation 对象数据情况。
- package.json 配置指令
"scripts": {
"debug": "node --inspect-brk ./node_modules/webpack-cli/bin/cli.js"
},
- 运行指令
npm run debug
此时控制台输出以下内容:
PS C:\Users\86176\Desktop\source> npm run debug
> source@1.0.0 debug
> node --inspect-brk ./node_modules/webpack-cli/bin/cli.js
Debugger listening on ws://127.0.0.1:9229/629ea097-7b52-4011-93a7-02f83c75c797
For help, see: https://nodejs.org/en/docs/inspecto
- 打开 Chrome 浏览器,F12 打开浏览器调试控制台。
此时控制台会显示一个绿色的图标
-
点击绿色的图标进入调试模式。
-
在需要调试代码处用
debugger打断点,代码就会停止运行,从而调试查看数据情况。
思考题
1、Loader与插件有什么区别?
答:
(1)Loader主要是用来解析和检测对应资源,负责源文件从输入到输出的转换,它专注于实现资源模块加载,数据格式的转换,因为webpack只能处理js数据因此通过部分loader可以将webpack不能识别的数据转换成可识别的数据,在一定程度上可以将其视为管道,一个loader可以视为一个处理环节;Plugin主要是通过webpack内部的钩子机制,在webpack构建的不同阶段执行一些额外的工作,它的插件是一个函数或者是一个包含apply方法的对象,接受一个compile对象,通过webpack的钩子来处理资源。
(2) loader开发方式是通过module.exports导出一个函数,该函数默认参数source(即要处理的资源文件),在函数体中处理资源(loader里配置响应的loader后),通过return返回最终打包后的结果(这里返回的结果需为字符串形式)。plugin开发方式是通过钩子机制实现,插件必须是一个函数或包含apply方法的对象,在方法体内通过webpack提供的API获取资源做响应处理
将处理完的资源通过webpack提供的方法返回该资源。
2、“钩子”有什么作用?如何监听钩子函数?
答:
(1)钩子(Hook)是Windows消息处理机制的一个平台,应用程序可以在上面设置子程序以监听定窗口的某种消息,而且所监听的窗口可以是由其他他进程所创建的。当消息到达后,在目标窗口处理函数之前处理它。钩子机制允许应用程序截获处理window消息或特定事件。钩子实际上是一个处理消息的程序段,通过系统调用,把它挂入系统。每当特定的消息发出,在没有到达目的窗口前,钩子程序就先捕获该消息,亦即钩子函数先得到控制权。这时钩子函数即可以加工处理(改变)该消息,也可以不作处理而继续传递该消息,还可以强制结束消息的传递。
(2)这是webpack提供的编译函数模板,监听webpack的钩子事件,然后会触发 compilation,和插件执行完成的回调。
compiler.plugin('webpacksEventHook', function(compilation,callback) {
console.log('this is customer plugins');
callback();
});
参考:
至此,关于插件的就介绍到这里。最后的最后,谢谢大家这么厉害还来看我,如果发现问题或者需要补充的点麻烦大家通过评论告诉我。博取众长,共同进步!