手摸手写个webpack plugin

目录

  1. 基本概念
  2. 使用方法
  3. 源码学习
  4. 开发实战
  5. 其他

1. 基本概念

上周写了《手摸手写个webpack loader》,所以这周当然是来搞一搞plugin啦。

啥是plugin?

plugin (插件),是一个具有 apply 方法的 JavaScript 对象。

实际上,plugin就是一个函数

可以是普通的函数:

function MyPlugin(){}
复制代码

也可以是特殊的函数:

class MyPlugin{
  constructor(){}
}
// class实际上是函数的语法糖
复制代码

上周我们学习了loader,知道了loader是对符合类型规则的文件进行处理,入参和出参都是文件流,也因此loader有局限性,对于一些文件无关的操作,比如在打包开始前进行环境配置、打包结束后发送通知邮件等,loader就无能为力了。

而plugin的出现就是为了解决loader的局限性。

2. 使用方法

4步简单搭建一个webpack项目:

  1. 创建并cd进入文件夹test-webpack
  2. npm init -y,初始化项目
  3. npm install webpack webpack-cli –save-dev,安装webpack依赖
  4. 添加文件如下:
  • webpack.config.js
  • index.html
  • src
    • index.js

在项目中使用HtmlWebpackPlugin插件

HtmlWebpackPlugin插件为例,在我们的test-webpack项目中使用方法如下:

  1. 安装:npm i html-webpack-plugin -D
  2. 配置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 buildyarn run build,可以看到输出的dist文件夹下自动生成了index.html文件,并自动引入了生成的main.js脚本文件。

HtmlWebpackPlugin自动生成的index.html

到这里,我们已经了解了如何安装并在项目中配置使用插件。

接下来,我们去看亿下HtmlWebpackPlugin的源码,更进一步了解plugin。

3. 源码学习

从HtmlWebpackPlugin源码学习怎么写plugin

HtmlWebpackPlugin源码

上面是html-webpack-plugin/index.js折叠后的源码。

展开但并不完全展开class HtmlWebpackPlugin{}可以看到:

apply方法

看了上面这两张图,是不是更直观地理解了开篇那句: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源码的运行顺序看一下:

创建Compiler实例对象及调用plugin的apply方法的过程

webpack启动后会创建Compiler实例对象compiler[1],然后调用我们在plugin里定义的apply方法,并把compiler传给apply。

webpack/lib/webpack.js>createCompiler” loading=”lazy” class=”medium-zoom-image”></p>
<p>这里的<code>compiler</code>是<code>Compiler</code>类的一个实例对象;而<code>Compiler</code>类『扩展(extend)自<code>Tapable</code>类,用来注册和调用插件。』<a href=[2]

Compiler类提供了很多hook函数(钩子函数),我们可以在Compiler.js中看到都定义了哪些钩子函数:

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来查看是在哪里被调用。

这里的tapcall,可以简单理解为监听触发

监听:hooks.钩子函数.tap(name, callback),name是监听的事件名称,一般与plugin同名;callback是个函数,当钩子函数被触发(call)时会调用callback函数。

触发:hooks.钩子函数.call([args])

有点类似于onemit

这里以afterDone钩子函数为例,看看它是什么时候被调用的:

在webpack源码里搜索

Watching.js是在watch模式下才会被调用,比如你运行了webpack --watch,或者使用了webpack-dev-server,这里我们没开watch模式,所以只看Compiler.js即可:

finalCallback调用afterDone钩子函数

可以看到:

  • afterDone钩子函数是在finalCallback里被调用的;
  • finalCallback函数,顾名思义,就是最后的回调

Compiler.js中,多次调用了finalCallback(),比如:

当出现错误,调用finalCallback并直接return终止运行

阅读Compiler.js源码,可以发现:无论运行成功或失败,最后的最后都会执行finalCallback();换句话说就是,最后都会执行afterDone钩子函数的回调。

webpack源码:调用compiler.run()

小结

目前为止,我们

  • 知道了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打包后,发送通知邮件给指定邮箱。

实现步骤:

  1. 创建并cd进入文件夹,这里我们起名为yzz-webpack-test-plugin
  2. npm init -y初始化项目,生成package.json;
  3. 新增index.js文件,内容如下:
// yzz-webpack-test-plugin/index.js
class YzzWebpackTestPlugin {
  apply(compiler) {
    // do something ...
    console.log("进击!YzzWebpackTestPlugin");
  }
}

module.exports = YzzWebpackTestPlugin;
复制代码
  1. 在之前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() // 使用我们的自定义插件
  ],
};

复制代码
  1. 运行一下,看插件引入了没:
cd test-webpack
yarn run webpack or yarn run build
复制代码

运行结果:

控制台印出插件中定义的console

成功!

  1. 利用钩子函数afterDone,以便打包结束后进行后续操作:
compiler.hooks.afterDone.tap(pluginName, (stats) => {
  // TODO:在这里发送通知邮件
});
复制代码
  1. 安装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);
  }
}
复制代码
  1. 在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↓。

  1. 在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开发中还有几个比较重要的概念,这里因为没有用到,以及篇幅所限,主要是没时间,所以没有详细展开:

  1. Compilation[3]
  2. Tapable(tap/tapAsync/tapPromise)[4]

注:我们用的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插件包括:

  1. 一个JS函数或JS类;
  2. 定义apply方法;
  3. 利用钩子函数的tap方法
  4. 在钩子函数的回调里处理webpack内部实例数据(compilation/stats等)
  5. 如果是异步的tapAsync/tapPromise,在处理完成后需调用callback回调函数。

声明:本篇文章及项目仅为入门学习所作,请谨慎用于实际项目。如有错漏,欢迎指出~~~

参考文章

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享