前言
webpack
大家应该都耳熟能详了。个人感觉,webpack
的本质就是让一堆的 Loader
和 Plugin
在webpack
的可支配范围内,有序可控的执行,最终生成一堆可在浏览器中执行的 code 和 一些状态信息。而这些 Loader
和 Plugin
,有用户自定义的,也有webpack 自己内部定义的。
Loader
的运行机制,不是这篇文章讲述的内容,有需要的朋友,可以看下我之前的这篇文章:webpack之 loader。
webpack
的设计思想还是很好的,我觉得这个思想和渐进增强有异曲同工之妙。它自己实现一套打包的主流程,然后在 Complier
和 Complation
对象上暴露出一些钩子,这些钩子起到了类似于生命周期
的作用,允许用户在不同的打包阶段通过钩子来增加用户所需的各种各样的功能。webpack
聪明的将一些不确定因素抛给使用者去处理,也就是:标准我来定,细节和扩展你自己弄。
举个栗子,Complier
对象上的钩子,就有这么多 compiler钩子
那么这些钩子是什么呢?它们其实都是 tapable
的实例。
那么我们怎么调用这些钩子呢?通过编写 webpack Plugin
。
今天我们来分析下 webpack
插件机制的基石 - tapable
。考虑到 wepback4
,是使用 tapable
的 1.1.0
版本,所以这篇文章就用 1.1.0
的版本来分析。
注意:
1.x.x
版本和0.x.x
版本,使用方式是有区别的,代码也被重构过。
如果对 tapable
还未有了解的朋友,可以参考下这里:
这才是官方的tapable中文文档
概念与用法
概念
我觉得tapable
整个库其实就是一套 发布订阅模式
的实现,类似 nodejs 的 EventEmitter
。
有的道友说是生产者消费者模式
, 发布订阅模式本质上也是一种生产者消费者模式,至于他们的区别,应该就是发布订阅模式的功能更单一,而生产者消费者模式的抽象级别更高。到底是 发布订阅模式
还是 生产者消费者模式
,我不能确定,有待考究,聪明的你如果知道的话,可以评论告诉下我哦。
用法
tapable
支持三种方式注册插件名称 ,分别是 tap
, tapAsync
, tapPromise
。
tap
表示使用的同步钩子,tapAsync
和 tapPromise
表示使用的是异步钩子。
与此对应的,支持三种调用方式 call
, callAsync
,promise
,注意需要一一对应。
用 SyncHook
举个栗子
const { SyncHook } = require('tapable');
// 初始化时传入参数名称
const myHook = new SyncHook(['name', 'age']);
// 添加事件
myHook.tap('pluginName1', (name, age) => console.log('pluginName1', name, age))
myHook.tap('pluginName2', (name, age) => console.log('pluginName2', name, age))
// 触发
myHook.call('jk', 26);
// 输出
// pluginName1 jk 26
// pluginName2 jk 26
复制代码
有木有发现和我们使用 addEventListener
添加事件非常相似?是的,就是这么像。所以不要怕它,我们只需要注意, 声明 Hook
时,传入预置的参数名称,然后用 tap
监听事件,用 call
传入参数触发事件。
用起来和 jQuery
的 on
与 trigger
一个意思,当然内部处理流程是不一样。
这里我们用的是 SyncHook
,还有 Async
类型的 Hook
,也就是异步钩子,用起来也是类似的。
tapAsync
监听,callAsync
触发
tapPromise
监听,promise
触发
代码如下:
const { AsyncParallelHook } = require("tapable");
class Model {
constructor() {
this.hooks = {
asyncHook: new AsyncParallelHook(['name']),
promiseHook: new AsyncParallelHook(['age'])
};
}
callAsyncHook(name, callback) {
this.hooks.asyncHook.callAsync(name, err => {
if (err) return callback(err);
callback(null);
});
}
callPromiseHook(age) {
return this.hooks.promiseHook.promise(age).then(res => console.log(res));
}
}
const model = new Model();
// Async 方式监听事件
model.hooks.asyncHook.tapAsync('AsyncPluginName', (name, callback) => {
const pluginName = 'AsyncPluginName';
setTimeout( () => {
console.log(pluginName, name);
}, 2000);
});
model.callAsyncHook('jk');
// 2秒后输出:AsyncPluginName jk
// Promise 方式监听事件
model.hooks.promiseHook.tapPromise('PromisePluginName', (age) => {
const pluginName = 'PromisePluginName';
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(pluginName, age);
resolve(pluginName);
}, 4000);
});
});
model.callPromiseHook(26);
// 4秒后输出:PromisePluginName 26
复制代码
实践:编写一个 Plugin
Webpack Plugin 有一套固定格式,这里以 Async event hooks 举例
其他的类型,可参考官方文档
const pluginName = 'HelloWorldPlugin';
class HelloWorldPlugin {
apply(complier) {
compiler.hooks.someHook.tapAsync(
pluginName,
(compilation, callback) => {
// Do something async...
setTimeout( () => {
console.log('Done with async work...');
callback();
}, 1000);
}
)
}
}
module.exports = HelloWorldPlugin;
复制代码
有的同学可能会好奇,为什么在 webpack 的 plugins 参数里,配置一些插件的实例,就会出现神奇的效果。
因为 webpack 在初始化 时,会遍历 plugins
参数中的实例,依次调用实例的 apply
方法,并将 complier
作为参数。
源码出自: webpack\lib\webpack.js
到这里应该能明白,为什么 webpack
的插件需要按照那样的格式去写了。
基于 HTMLWebpackPlugin 扩展一个 Plugin
目录结构如图
MyPlugin.js 代码如下,我这里是直接 copy HTMLWebpackPlugin
官方文档的。
const HtmlWebpackPlugin = require('html-webpack-plugin');
const pluginName = 'MyPlugin';
class MyPlugin {
apply(compiler) {
compiler.hooks.compilation.tap(pluginName, (compilation) => {
console.log('The compiler is starting a new compilation...')
// Staic Plugin interface |compilation |HOOK NAME | register listener
HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync(
pluginName, // <-- Set a meaningful name here for stacktraces
(data, cb) => {
// Manipulate the content
data.html += 'The Magic Footer'
// Tell webpack to move on
cb(null, data)
}
)
})
}
}
module.exports = MyPlugin;
复制代码
注意一点:HTMLWebpackPlugin
默认安装 3.2.0
版本,这个版本还只是支持旧版的插件写法,没有 HtmlWebpackPlugin.getHooks
这个方法。我这里安装的是最新的master分支。
一切就绪后,npm run prod
,可看到 dist/index.html
的内容如下:
一个简单的插件算是写好并运行成功了。
写在最后
希望本文能对读者有帮助。
如果有错误的地方,还请指出。
谢谢阅读。
代码在此:webpack-plugin-test
参考
Writing a Plugin
html-webpack-plugin
这才是官方的tapable中文文档
不满足于只会使用系列: tapable
生产者消费者模式与订阅发布者模式的区别
推荐两篇好文:
Webpack基本架构浅析
Webpack 核心模块 tapable 解析