目录
- 基本概念
- 使用方法
- 源码学习
- 开发实战
- 其他
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]
[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/…























![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)
