目录
- 基本概念
- 使用方法
- 源码学习
- 开发实战
- 其他
1. 基本概念
上周写了《手摸手写个webpack loader》,所以这周当然是来搞一搞plugin啦。
啥是plugin?
plugin
(插件),是一个具有apply
方法的 JavaScript 对象。
实际上,plugin就是一个函数。
可以是普通的函数:
function MyPlugin(){}
复制代码
也可以是特殊的函数:
class MyPlugin{
constructor(){}
}
// class实际上是函数的语法糖
复制代码
上周我们学习了loader,知道了loader是对符合类型规则的文件进行处理,入参和出参都是文件流,也因此loader有局限性,对于一些文件无关的操作,比如在打包开始前进行环境配置、打包结束后发送通知邮件等,loader就无能为力了。
而plugin的出现就是为了解决loader的局限性。
2. 使用方法
4步简单搭建一个webpack项目:
- 创建并cd进入文件夹
test-webpack
- npm init -y,初始化项目
- npm install webpack webpack-cli –save-dev,安装webpack依赖
- 添加文件如下:
- webpack.config.js
- index.html
- src
- index.js
在项目中使用HtmlWebpackPlugin插件
以HtmlWebpackPlugin插件为例,在我们的test-webpack项目中使用方法如下:
- 安装:
npm i html-webpack-plugin -D
- 配置webpack.config.js:
// test-webpack/webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: "./src/index.js",
mode: "development",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist"),
},
plugins: [
new HtmlWebpackPlugin({
title: "Webpack~",
})
};
复制代码
配置文件中的plugins数组就是我们配置plugin插件的地方。
HtmlWebpackPlugin
插件可以帮我们在dist文件夹下自动生成index.html文件,并在其中自动引入打包后的css和js脚本文件。
配置好后,在package.json中添加脚本,方便我们执行打包指令:
"script": {
"build": "webpack"
}
复制代码
运行npm run build
或yarn run build
,可以看到输出的dist
文件夹下自动生成了index.html
文件,并自动引入了生成的main.js
脚本文件。
到这里,我们已经了解了如何安装并在项目中配置使用插件。
接下来,我们去看亿下HtmlWebpackPlugin
的源码,更进一步了解plugin。
3. 源码学习
从HtmlWebpackPlugin源码学习怎么写plugin
上面是html-webpack-plugin/index.js折叠后的源码。
展开但并不完全展开class HtmlWebpackPlugin{}
可以看到:
看了上面这两张图,是不是更直观地理解了开篇那句:plugin
(插件),是一个具有 apply
方法的 JavaScript 对象。
接着展开apply
:
apply (compiler) {
compiler.hooks.initialize.tap('HtmlWebpackPlugin', () => {
// 此处省略xxx字
});
}
复制代码
好了,问题来了:
- 1)apply方法什么时候被调用?
- 2)入参compiler是个啥?
- 3)
compiler.hooks.initialize.tap
是嘛意思?
要知道这几个问题的答案,就得去看webpack源码了,走起——
从webpack源码学习plugin实现原理
让我们运行yarn run webpack
,然后跟着webpack源码的运行顺序看一下:
webpack启动后会创建Compiler实例对象compiler[1],然后调用我们在plugin里定义的apply方法,并把compiler传给apply。
[2]
Compiler
类提供了很多hook函数(钩子函数),我们可以在Compiler.js中看到都定义了哪些钩子函数:
当webpack调用了plugin.apply(compiler)
后,我们就可以在自定义的plugin里访问到当前的compiler实例对象。然后利用compiler提供的钩子函数,在钩子函数的回调函数里(tap方法的第二个参数)自定义我们想要的操作。
比如上面提到的HtmlWebpackPlugin插件,就是利用了initialize
这个钩子函数:
apply (compiler) {
compiler.hooks.initialize.tap('HtmlWebpackPlugin', () => {
// 在这里自定义操作,当钩子函数被call时,就会执行
// 可以通过hooks.initialize.call来查看钩子函数在源码中的运行时机
});
}
复制代码
这些钩子函数会在webpack运行的生命周期里被调用,可以通过在webpack源码中搜索hooks.钩子函数名.call
来查看是在哪里被调用。
这里的tap
和call
,可以简单理解为监听
和触发
。
监听:hooks.钩子函数.tap(name, callback)
,name是监听的事件名称,一般与plugin同名;callback是个函数,当钩子函数被触发(call)时会调用callback函数。
触发:hooks.钩子函数.call([args])
。
有点类似于on
和emit
。
这里以afterDone
钩子函数为例,看看它是什么时候被调用的:
Watching.js是在watch模式下才会被调用,比如你运行了webpack --watch
,或者使用了webpack-dev-server
,这里我们没开watch模式,所以只看Compiler.js即可:
可以看到:
afterDone
钩子函数是在finalCallback
里被调用的;finalCallback
函数,顾名思义,就是最后的回调;
Compiler.js中,多次调用了finalCallback()
,比如:
阅读Compiler.js源码,可以发现:无论运行成功或失败,最后的最后都会执行finalCallback()
;换句话说就是,最后都会执行afterDone
钩子函数的回调。
小结
目前为止,我们
- 知道了plugin是:一个具有
apply
方法的 JavaScript 对象; - 以HtmlWebpackPlugin为例,知道了如何在webpack项目中安装、引入和使用plugin;以及plugin里有一个apply方法、apply方法的入参是个Compiler实例对象;
- 并且阅读了webpack源码,知道了
- webpack启动后就会创建Compiler实例对象;
- 然后调用
plugin.apply(compiler)
,把当前Compiler实例对象传给plugin; - 最后在webpack运行的生命周期里通过
hooks.钩子函数名.call
来调用钩子函数中的回调函数。
- 无论打包成功或失败,钩子函数
afterDone
都会在最后被调用(这一点和我们接下来要写的plugin有关)。
4. 开发实战
学习完基本概念和原理,接下来就是实战了。
一句话需求:在webpack打包后,发送通知邮件给指定邮箱。
实现步骤:
- 创建并cd进入文件夹,这里我们起名为
yzz-webpack-test-plugin
; npm init -y
初始化项目,生成package.json;- 新增index.js文件,内容如下:
// yzz-webpack-test-plugin/index.js
class YzzWebpackTestPlugin {
apply(compiler) {
// do something ...
console.log("进击!YzzWebpackTestPlugin");
}
}
module.exports = YzzWebpackTestPlugin;
复制代码
- 在之前test-webpack项目里引入这个插件:
// test-webpack/webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const YzzWebpackTestPlugin = require(path.resolve("../yzz-webpack-test-plugin")); // 这里用的是本地的相对路径引入
module.exports = {
entry: "./src/index.js",
mode: "development",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist"),
},
plugins: [
new HtmlWebpackPlugin({
title: "Webpack~",
}),
new YzzWebpackTestPlugin() // 使用我们的自定义插件
],
};
复制代码
- 运行一下,看插件引入了没:
cd test-webpack
yarn run webpack or yarn run build
复制代码
运行结果:
成功!
- 利用钩子函数afterDone,以便打包结束后进行后续操作:
compiler.hooks.afterDone.tap(pluginName, (stats) => {
// TODO:在这里发送通知邮件
});
复制代码
- 安装
nodemailer
插件,用于发送邮件:
npm i nodemailer -S
具体使用方法参见nodemailer官方文档:nodemailer.com/about/
发送邮件的核心代码:
// 改自博客:https://www.cnblogs.com/jackson-yqj/p/10154296.html
function emailTo(host, fromEmail, password, toEmail, subject, html, callback) {
const transporter = nodemailer.createTransport({
host: host,
auth: {
user: fromEmail,
pass: password // 如果发送邮箱是QQ邮箱,则为授权码
}
});
var mailOptions = {
from: fromEmail, // 发送者
to: toEmail, // 接受者,可以同时发送多个,以逗号隔开
subject: subject, // 标题
};
if (html != undefined) {
mailOptions.html = html;// html
}
var result = {
httpCode: 200,
message: '发送成功!',
}
try {
transporter.sendMail(mailOptions, function (err, info) {
if (err) {
result.httpCode = 500;
result.message = err;
callback(result);
return;
}
callback(result);
});
} catch (err) {
result.httpCode = 500;
result.message = err;
callback(result);
}
}
复制代码
- 在afterDone钩子函数中调用emailTo:
compiler.hooks.afterDone.tap(pluginName, (stats) => {
const { fromEmail, password, toEmail, host } = this.options;
if (!fromEmail || !password || !toEmail || !host) {
console.log("邮箱配置参数错误!");
} else if (stats) {
const subject = stats.hasErrors() ? "[ERROR]webpack打包失败" : "[SUCCESS]webpack打包成功";
const html = stats.toString() + `<br><div>${"打包时间:" + new Date(stats.startTime).toLocaleString() + "-" + new Date(stats.endTime).toLocaleString()}</div>`;
emailTo(host, fromEmail, password, toEmail, subject, html, function (data) {
console.log(data);
})
}
});
复制代码
这里的options参数需要在配置插件的时候传入,见步骤8↓。
- 在test-webpack/webpack.config.js配置中传入
options
参数:
plugins: [
new YzzWebpackTestPlugin({
fromEmail: "xx@qq.com", // 发送方邮箱
password: "xxx", // 如果是QQ邮箱,则为QQ邮箱授权码
toEmail: "xx@163.com", // 接收方邮箱
host: "smtp.qq.com" // QQ邮箱服务器
})
],
复制代码
让我们运行webpack看看效果:
完成!✅
当然,我们还可以npm publish
把自定义的plugin发布到npm仓库,这样就可以通过npm install yzz-webpack-test-plugin -D
安装插件,引入时也不需要再解析路径:
const YzzWebpackTestPlugin = require("yzz-webpack-test-plugin");
复制代码
Demo已开源:github.com/youzouzou/y…
5. 其他
在plugin开发中还有几个比较重要的概念,这里因为没有用到,以及篇幅所限,主要是没时间,所以没有详细展开:
注:我们用的afterDone是个SyncHook,不支持异步模式。
官网上有篇《Writing a Plugin》[5],里面提到:
A plugin for webpack consists of:
- A named JavaScript function or a JavaScript class.
- Defines apply method in its prototype.
- Specifies an event hook to tap into.
- Manipulates webpack internal instance specific data.
- Invokes webpack provided callback after functionality is complete.
一个webpack插件包括:
- 一个JS函数或JS类;
- 定义apply方法;
- 利用钩子函数的tap方法
- 在钩子函数的回调里处理webpack内部实例数据(compilation/stats等)
- 如果是异步的tapAsync/tapPromise,在处理完成后需调用callback回调函数。
声明:本篇文章及项目仅为入门学习所作,请谨慎用于实际项目。如有错漏,欢迎指出~~~
参考文章
- [1] compiler实例:webpack.js.org/api/node/#c…
- [2] compiler钩子:webpack.docschina.org/api/compile…
- [3] Compilation:webpack.docschina.org/api/compila…
- [4] Tapable:github.com/webpack/tap…
- [5] Writing a Plugin:webpack.docschina.org/contribute/…