开发一个简单的 webpack 插件

之前为了尝试实现 dalao 的一个 idea,浅浅地踩了一下 webpack 插件开发的坑。今天才想起来,趁着有时间,应该整理一下开发 webpack 插件的思路。

前言

首先呢,webpack 的强大功能其中有很大的一部分是离不开插件的,通过插件你几乎可以为所欲为,也能更充分地利用 webpack. 事实上 webpack 的配置什么的很多看起来很复杂,文档也很多,所以会给很多人一种它的插件也很难搞的错觉……和其它的程序类似, webpack 的插件也有一个模子,也就是基本框架。

为了让整理出来的东西看起来具体一些,我以前文提到的那个想法为例,这个想法具体如下:

在我们使用 webpack-dev-server 结合 webpack 开发的时候,webpack-dev-server 会把 console 中的错误和警告信息转发到浏览器端,但是单纯用 webpack --watch 的时候,webpack 除了会检测文件变动重新 compile 之外并不会把其他信息转发到浏览器上,以至于有时候我们发现了一些问题,到最后切到 console 才发现是编译的过程出现了偏差。所以有没有什么办法让 webpack --watch 的时候也能把错误和警告显示到浏览器的 console?

当然我不把具体的实现过程展开了。这样的需求显然可以通过 webpack 插件实现,监听 webpack 的 compile 事件,获取异常信息,然后前端和后端建立一个 socket 连接,实时输出这些信息。

实现

是的,以上所有的步骤都可以通过一个 webpack 插件做到。不多说废话,在开发之前稍微浏览一下 webpack 的官方开发文档还是很重要的:

How to write a plugin: https://webpack.js.org/development/how-to-write-a-plugin/#compiler-and-compilation Plugin API: https://webpack.js.org/api/

先来看看我们是怎么在 webpack 中应用插件的:

// webpack.config.js
plugins: [
  new webpack.HotModuleReplacementPlugin(),
  new webpack.NoEmitOnErrorsPlugin()
],

可以看到,我们在 webpack config 中的 plugins 字段里实例化一个对象来应用插件,所以我们的新插件也应该有一个 class. 官方文档中用的是 ES5 的 function + prototype 的写法,我们可以直接用上 ES6 的 class. 假设插件名字为 MyAwesomePlugin, 首先我们创建一个 *MyAwesomePlugin.js*: ```js module.exports = class MyAwesomePlugin { constructor(options) { this.options = options; }

apply(compiler) { console.log(‘Hello world!’); } };

在这个文件里我们 export 了一个叫做 ```MyAwesomePlugin``` 的类。其中有一个 ```apply()``` 方法,它表示在 webpack 初次加载完此插件的时候应该做的事情,也就是只会在 webpack 启动的时候被执行一次。接下来如果我们监听 compile 的事件等等,首先就要在这里写一下;假如我们的插件依赖于一个 express 的后端服务器,那么我们也可以在 ```apply()``` 里初始化 express。

然后我们试着应用这个插件,在你的 ```webpack.config.js``` 中:

js const MyAwesomePlugin = require(‘./MyAwesomePlugin’);

// …

plugins: [ // … new MyAwesomePlugin() ]

然后尝试着启动 webpack,如果一切正常,你应该在 console 中看到 ```Hello, world!``` 了。

接下来我们就开始做实事了,回头看看我们的需求,这是一个需要前后端配合的工作,首先我们需要在后端拥有一个 socket 服务器,然后监听 webpack 每次编译完后的结果,把信息通过这个 socket 服务器发送到浏览器。要实现这一步,还需要浏览器加载和后端服务器通信的相关 JS.启动 socket 服务器这件事我们在 ```apply()``` 方法中完成就可以了,接下来是事件监听。

说到事件监听,首先这里要区分一下 webpack 中的两个概念:```compiler``` 和 ```compilation```.

---

## compiler

首先你一定已经注意到上文的代码中,```apply()``` 方法传入了一个叫 ```compiler``` 的参数。这个所谓的 ```compiler``` 对象从字面意思上看是编译器的意思,实际上它也就指向了当前运行的 webpack 实例。这个实例包含了 webpack 的所有 options, loaders 和 plugins, 它随着 webpack 的启动而产生,可以说是 webpack 的“灵魂”。当我们运用一个插件时(即插件类被实例化,同时类中的 ```apply()``` 方法被调用的时候),```apply()``` 方法便会接收到一个指向这个 ```compiler``` 的参数,我们可以通过这个 ```compiler``` 访问整个 webpack 环境。

### compilation

对 ```compiler``` 有了一定的了解之后,大概我们就可以猜到 ```compilation``` 是干什么用的了。所谓 ```compilation``` 包含了 webpack 每次 build 后的详细信息,包括编译出的结果、错误信息、模块、编译后的资源、改变的文件和依赖等的当前状态,同时它提供了很多的事件挂钩,以便于插件来执行一些<s>黑魔法</s>。

---

具体的 API 可以在上文给出的链接中找到,这里我们直接贴出代码后再解释:

js apply(compiler) { compiler.plugin(‘compilation’, this.injectScriptToBundle.bind(this)); compiler.plugin(‘done’, this.onBuildCompleted.bind(this)); }


这一段代码中我们监听了 ```compiler``` 的两个事件(可以看到我们是用 ```compiler.plugin(hook, method)``` 方法注册事件的):

 - 第一是在每次文件变动,重新编译的时候,执行 ```this.injectScriptToBundle()``` 方法;
 - 第二是编译完成之后,执行 ```this.onBuildCompleted()``` 方法。

根据方法名我们大致可以构想一下,前一个方法用于把前端与后端交互用的 JS 代码注入到 bundle 中(显然,我们是不会直接把这样的调试用代码写到源代码里的),后一个方法大概就是把编译完成之后异常信息发送出去了。

我们先来看看 ```injectScriptToBundle()``` 方法:

js injectScriptToBundle(compilation) { compilation.mainTemplate(‘startup’, source => { return “\nconsole.log(‘Hello world!’);\n” + source; }); }

我已经截掉了一些对这篇文章没有什么用的代码。首先我们可以看到这个方法有一个参数,而这个参数正是上文提到的 ```compilation```. 这一段代码的作用就是在即将编译的源代码的开头部分插入一段我们自定义的 JS 代码。

对了,这里面出现了一个 ```mainTemplate``` 方法,具体可以看看 webpack 的官方文档:https://webpack.js.org/api/plugins/template/#src/components/Sidebar/Sidebar.jsx

然后接下来我们只需要在 ```onBuildCompleted()``` 方法中获取并向前端发送信息即可:

js onBuildCompleted(stats) { const detail = stats.toJson({ errorDetails: false });

this.sendWarnings(statsJson.warnings); this.sendErrors(statsJson.errors); } ```

忽略掉发送信息那部分的代码,我们只要看上半部分就好了。stats 参数随着 compilerdone 事件被触发后产生,包含本次编译的结果统计信息。它提供了一个 toJson() 方法,可以把这些信息转换成直观的 JSON,然后我们也就可以从这个 JSON 里拿到我们想要的内容了。

做好了这一切之后,大概已经实现了我们的目标了。上文的示例代码可以在这里找到。

推荐阅读