1. 前言

插件(plugins)是webpack中的一等功臣。正是由于有了诸多插件的存在,才使得webpack无所不能。在webpack源码中也是使用了大量的内部插件,插件要是用的好,可以让你的工作效率事半功倍。用了别人写的插件很爽,那么我们何不考虑编写一个自己的插件呢?本篇文章就来教你如何编写一个自己的webpack插件。

2. webpack运行机制

在编写插件之前,我们有必要先了解一下webpack的整体运行机制。Webpack的运行机制本质上是一种事件流的机制,在webpack运行过程中,从初始化参数、开始编译文件、确定入口、编译模块、编译完成、生成资源、输出资源、全部完成等等一系列过程,在这整个过程中的每一个节点,webpack都会向外广播一个事件,我们可以通过监听每一个事件,在合适的时机通过webpack提供的API做一些合适的事情。

通俗易懂的解释:

如果上面的文字你不理解的话,那么我们以一个通俗易懂的例子再加以解释,如下:

我们把webpack比作一个厨师,而把webpack的整个运行过程比作厨师要炒一道菜,菜名就叫酸辣土豆丝。那么从准备要做这道菜到这道菜做好盛盘大概一共要经历以下几道工序:洗菜——切菜——炒菜——盛盘。并且我们跟厨师约定好,当厨师每进行一道工序的时候就让他大喊出这道工序的名称,即让厨师广播出一个事件,例如当厨师开始洗菜的时候,就让厨师大喊:“洗菜”。如果没有其他要求,那么这道酸辣土豆丝在经过如上四道工序后就可以完成啦。但是,我们有一个额外要求,就是想让这道酸辣土豆丝更辣一些。而原本的厨师只会按照正常的工序做,并不会额外的加辣椒,那要怎么办呢?此时我们的插件就登场啦,我们就需要编写一个“加辣椒”的插件。编写之前我们先要考虑这个辣椒在什么时候加合适,也就是说我们该监听哪一道工序,很明显,在炒菜的时候加。那么我们就监听“炒菜”这道工序,一旦厨师广播了这个事件,即大喊“炒菜”的时候,我们就把辣椒加进去,这样最后做出来的酸辣土豆丝就会更辣一些。

总结来说:

Webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。

这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。

插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。

Webpack 通过 Tapable 来组织这条复杂的生产线。

Webpack在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。

Webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。

OK,相信现在你已经该对webpack的运行机制有个初步的理解了。接下来,我们就开始逐步编写插件。

3. 编写插件

我们知道,在我们通常使用别人的插件时往往是像如下方式使用的:

module.exports = {
entry:'',
output:{},
module: {},
plugins: [
new XXXPlugin(options)
]
}

从上面代码中我们可以看到,使用插件时往往都是new XXXPlugin(),那么换句话说一个插件就是一个类,使用该插件就是new一个该插件的实例,并且把插件所需要的配置参数传给该类的构造函数。那么我们就可以写出编写插件的第一步,如下:

class XXXPlugin{
// 在构造函数中获取用户给该插件传入的配置
constructor(options) {
this.options=options;
}
}
// 导出 Plugin
module.exports=XXXPlugin;

接着,通过阅读webpack源码webpack.js:46行我们知道:

if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}

webpack内部在调用插件时会调用插件实例的apply方法,并为其传入compiler对象,OK,那么编写插件的第二步就来了,给插件类添加apply方法:

class XXXPlugin{
// 在构造函数中获取用户给该插件传入的配置
constructor(options) {
this.options=options;
}
// Webpack 会调用 XXXPlugin 实例的 apply 方法给插件实例传入 compiler 对象
apply(compiler) { }
}
// 导出 Plugin
module.exports=XXXPlugin;

那么,这个complier又是什么?在这里我们有必要详细说明一下:

在开发 Plugin 时最常用的两个对象就是 CompilerCompilation,它们都继承自 Tapable,是 PluginWebpack 之间的桥梁。

CompilerCompilation 的含义如下:

  • Compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 optionsloaderplugin。当在 webpack 环境中应用一个插件时,插件将收到此 Compiler 对象的引用。可以使用它来访问 webpack 的主环境。
  • Compilation对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 Compilation,从而生成一组新的编译资源。一个 Compilation对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。Compilation对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。

CompilerCompilation的区别在于:Compiler代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation只是代表了一次新的编译。

使用 Compiler 对象就可以访问 webpack 的主环境,就可以监听到webpack在运行过程中所广播出的一系列事件。OK,到这里我们才搞明白,一个插件的核心部分原来是在apply方法里,在该方法里通过监听webpack在合适时机的广播出的事件然后做一些合适的事情。此时,你可能还有个疑问:我们该监听哪个事件?或者说webpack在整个流程中都广播了哪些事件?接下来,我们就通过阅读源码来webpack看看在整个运行过程中都广播出了哪些事件。

4. webpack工作流程

Webpack 的工作流程是一个串行的过程,从启动到结束会依次执行以下流程:

  • 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
  • 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的run方法开始执行编译;
  • 确定入口:根据配置中的 entry 找出所有的入口文件
  • 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行编译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  • 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
  • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  • 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

按照广播的事件又可大致分为三大阶段:初始化阶段、编译阶段、输出阶段。三个阶段所发生的部分常用事件如下。

4.1 初始化阶段

事件名 解释
初始化参数 从配置文件和Shell 语句中读取与合并参数,得出最终的参数。
实例化 Compiler 1.用上一步得到的参数初始化 Compiler 实例 2.Compiler 负责文件监听和启动编译 3.Compiler 实例中包含了完整的 Webpack 配置,全局只有一个 Compiler 实例。
加载插件 1.依次调用插件的 apply 方法,让插件可以监听后续的所有事件节点。 同时给插件传入 compiler 实例的引用,以方便插件通过 compiler 调用 Webpack 提供的 API。
environment 开始应用Node.js 风格的文件系统到compiler 对象,以方便后续的文件寻找和读取
entry-option 读取配置的 Entrys,为每个 Entry 实例化一个对应的 EntryPlugin,为后面该 Entry 的递归解析工作做准备
after-plugins 调用完所有内置的和配置的插件的apply 方法
after-resolvers 根据配置初始化resolver, resolver 负责在文件系统中寻找指定路径的文件

4.2 编译阶段

事件名 解释
run 启动一次新的编译
watch-run 和run 类似,区别在于它是在监听模式下启动编译,在这个事件中可以获取是哪些文件发生了变化从而导致重新启动一次新的编译
compile 该事件是为了告诉插件一次新的编译将要启动,同时会给插件传入compiler 对象。
compilation 当 Webpack 以开发模式运行时,每当检测到文件变化,一次新的 Compilation 将被创建。 一个 Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。 Compilation 对象也提供了很多事件回调供插件做扩展。
make 一个新的 Compilation 创建完毕主开始编译
after-compile 一次Compilation 执行完成
invalid 当遇到文件不存在、文件编译错误等异常时会触发该事件,该事件不会导致Webpack 退出

4.3 输出阶段

事件名 解释
seal 所有模块及其依赖的模块都通过Loader 转换完成,根据依赖关系开始生成Chunk
addChunk 生成资源
createChunkAssets 创建资源
getRenderManifest 获得要渲染的描述文件
render 渲染源码
afterCompile 编译结束
shouldEmit 所有需要输出的文件已经生成好,询问插件哪些文件需要输出,哪些不需要。
emit 确定好要输出哪些文件后,执行文件输出,可以在这里获取和修改输出内容。
emitRecords 写入记录
done 成功完成一次完整的编译和输出流程
failed 如果在编译和输出的流程中遇到异常,导致Webpack 退出, 就会直接跳转到本步骤,插件可以在本事件中获取具体的错误原因

更多的事件钩子请求查看这里:Compiler对象事件Compilation对象事件

5. 继续编写插件

知道了广播了哪些事件之后,我们就可以在apply中监听事件并编写相应的逻辑啦。如下:

class XXXPlugin{
// 在构造函数中获取用户给该插件传入的配置
constructor(options) {
this.options=options;
}
// Webpack 会调用 XXXPlugin 实例的 apply 方法给插件实例传入 compiler 对象
apply(compiler) {
// 监听某个事件
compiler.hooks.'compiler事件名称'.tap('XXXPlugin', (compilation)=> {
//编写相应逻辑
});
}
}
// 导出 Plugin
module.exports=XXXPlugin;

当我们监听Compiler对象事件中的compilation事件时,此时可以在回调函数中再次监听compilation对象里的事件,如下:

class XXXPlugin{
// 在构造函数中获取用户给该插件传入的配置
constructor(options) {
this.options=options;
}
// Webpack 会调用 XXXPlugin 实例的 apply 方法给插件实例传入 compiler 对象
apply(compiler) {
// 监听某个事件
compiler.hooks.compilation.tap('XXXPlugin', (compilation)=> {
compilation.hooks.'compilation事件名称'.tap('optimize', () => {
//编写相应逻辑 });
});
}
}
// 导出 Plugin
module.exports=XXXPlugin;

5.1 异步插件

有一些事件使用了异步钩子AsyncHook,此时在监听这些事件的时候我们就需要使用tapAsynctapPromise,并且还需要额外传入一个 callback 回调函数,在插件运行结束时,必须调用这个回调函数,如下:

class XXXPlugin{
// 在构造函数中获取用户给该插件传入的配置
constructor(options) {
this.options=options;
}
// Webpack 会调用 XXXPlugin 实例的 apply 方法给插件实例传入 compiler 对象
apply(compiler) {
// 监听某个事件
compiler.hooks.emit.tapAsync('XXXPlugin', (compilation,callback)=> {
setTimeout(function () {
console.log('异步任务完成');
callback();
},500);
});
}
}
// 导出 Plugin
module.exports=XXXPlugin;

OK,以上就是编写一个webpack插件所有需要用到的东西,接下里我们就来编写一个简单的插件做个示例。

6. 插件示例

在这里我们编写一个简单的插件,该插件的功能是:将webpack输出的所有文件打包压缩成一个zip压缩包,该插件接收一个配置参数filename,表示最终生成的压缩包文件名称。其代码如下:

定义ZipPlugin插件:

const { RawSource } = require("webpack-sources");
const JSZip = require("jszip");
class ZipPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
compiler.hooks.emit.tapAsync("ZipPlugin", (compilation, callback) => {
var zip = new JSZip();
for (let filename in compilation.assets) {
const source = compilation.assets[filename].source();
zip.file(filename, source);
}
zip.generateAsync({ type: "nodebuffer" }).then(content => {
compilation.assets[this.options.filename || 'fileList.zip'] = new RawSource(content);
callback();
});
});
}
}
module.exports = ZipPlugin;

代码说明:

  1. 首先获取到用户传入的配置对象,将其存入this.options内。
  2. 我们要想在webpack输出完成后将输出的所有文件打包成zip压缩包,那么就必须监听webpack输出完成后的那个事件,即emit事件。并且该事件为异步事件,所以使用tabAsync监听。
  3. 通过webpack的输出资源对象compilation.assets获取到输出的所有文件,遍历这些文件,使用JSZip包将这些文件都放入一个zip压缩包内。
  4. 然后将这个压缩包挂载到webpack的输出资源对象compilation.assets上,使其跟随输出资源一同被输出,该压缩包的文件名使用用户传入的filename,如果用户未传入则使用默认的名字fileList.zip
  5. 最后由于是异步事件,在运行结束时,必须调用callback()

使用ZipPlugin插件:

const ZipPlugin  = require('./src/plugins/ZipPlugin');

module.exports = {
entry: './src/index.js',
output: {
path: path.resolve('dist'),
filename: 'bundle.js'
},
plugins: [
// 使用ZipPlugin插件
new ZipPlugin({
filename:'NLRX.zip'
})
]
}

7. 总结

那么我们可以简单总结一下,一个插件的构成部分:

  • 一个插件就是一个类。
  • 在插件类上定义一个 apply 方法。
  • apply方法中监听一个 webpack 自身的事件钩子。
  • 处理 webpack 内部实例的特定数据。
  • 功能完成后调用 webpack 提供的回调。

相信读完这篇文章,你应该就能写出一个自己的插件了。

(完)

从0开始编写webpack插件的更多相关文章

  1. 编写webpack 插件

    Webpack插件为第三方开发者释放了Webpack的最大可能性.利用多级回调开发者可以把他们自己的需要的功能引入到Webpack里面来.Build插件比Build loader 更进一步.因为你需要 ...

  2. 【Web】阿里icon图标webpack插件(webpack-qc-iconfont-plugin)详解

    webpack-qc-iconfont-plugin webpack-qc-iconfont-plugin是一个webpack插件,可以轻松地帮你将阿里icon的图标项目下载至本地 开发初衷 之前已经 ...

  3. 【babel+小程序】记“编写babel插件”与“通过语法解析替换小程序路由表”的经历

    话不多说先上图,简要说明一下干了些什么事.图可能太模糊,可以点svg看看 背景 最近公司开展了小程序的业务,派我去负责这一块的业务,其中需要处理的一个问题是接入我们web开发的传统架构--模块化开发. ...

  4. Qt 显示透明flash和编写QtWebkit插件

    Qt 有两种方法可以显示flash. 1. 通过QAxWidget 调用com形式显示flash, 需要本机安装IE flash插件 2. 直接通过qwebview显示flash, 需要下载webki ...

  5. 编写jQuery插件--实现返回顶部插件

    国庆过去一周多了,作为IT界的具有严重’工作狂‘性质的宅人,居然还没走出玩耍的心情,拖了程序猿的脚后跟了.最近工作不顺,心情不佳,想吐槽下公司,想了还是厚道点,以彼之道还施彼身,觉得自己也和他们同流合 ...

  6. 使用Qt编写模块化插件式应用程序

    动态链接库技术使软件工程师们兽血沸腾,它使得应用系统(程序)可以以二进制模块的形式灵活地组建起来.比起源码级别的模块化,二进制级别的模块划分使得各模块更加独立,各模块可以分别编译和链接,模块的升级不会 ...

  7. [翻译]如何编写GIMP插件(一)

    近期想尝试编写gimp插件,在gimp官网看到了三篇简明教程,顺便翻译了下,由于本人英文,计算机知识有限,文中难免有warning,error出现,欢迎指正. <How to write a G ...

  8. 探寻 webpack 插件机制

    webpack 可谓是让人欣喜又让人忧,功能强大但需要一定的学习成本.在探寻 webpack 插件机制前,首先需要了解一件有意思的事情,webpack 插件机制是整个 webpack 工具的骨架,而 ...

  9. 前端html、CSS快速编写代码插件-Emmet使用方法技巧详解

    前端html.CSS快速编写代码插件-Emmet使用方法技巧详解   Emmet的前身是大名鼎鼎的Zen coding,如果你从事Web前端开发的话,对该插件一定不会陌生.它使用仿CSS选择器的语法来 ...

随机推荐

  1. ADB命令无法导出文件到物理机上处理办法

    因为想查看一下脚本生成的sqlite文件.就想导出文件,,结果导出adb pull命令一直报错.使用su也是错误的..最后发现adb pull 不能再adb的命令状态下执行.需要退出adb命令.然后直 ...

  2. 10个值得深思的_PHP_面试问题

    Q1 第一个问题关于弱类型 $str1 = 'yabadabadoo'; $str2 = 'yaba'; if (strpos($str1,$str2)) { echo "\"&q ...

  3. 在chrome浏览器中调用IE浏览器并访问(openIE.reg自定义协议)

    在谷歌浏览器中有4种方法调用IE浏览器.如下: c++ socket通过浏览器在ie中打开指定url : vb生成exe,url访问exe启动ie并打开指定url : 通过socket实现通过http ...

  4. LAMP环境部署物联网项目

    今天来在LAMP环境下搭建一个PHP项目,开始之前,先来普及下物联网常识: 物联网,即Internet of Things,简写IOT.让所有能行使独立功能的普通物体实现互联互通的网络,通过物联网可以 ...

  5. 2. SOFAJRaft源码分析—JRaft的定时任务调度器是怎么做的?

    看完这个实现之后,感觉还是要多看源码,多研究.其实JRaft的定时任务调度器是基于Netty的时间轮来做的,如果没有看过Netty的源码,很可能并不知道时间轮算法,也就很难想到要去使用这么优秀的定时调 ...

  6. SLAM中的卡方分布

    视觉slam中相邻帧特征点匹配时,动辄上千个特征点,匹配错误的是难免的,而误匹配势必会对位姿精度以及建图精度造成影响,那么如何分辨哪些是误匹配的点对儿呢?如果已知两帧的的单应矩阵,假设单应矩阵是没有误 ...

  7. [书籍翻译] 《JavaScript并发编程》第五章 使用Web Workers

    本文是我翻译<JavaScript Concurrency>书籍的第五章 使用Web Workers,该书主要以Promises.Generator.Web workers等技术来讲解Ja ...

  8. 机器学习:eclipse中调用weka的Classifier分类器代码Demo

    weka中实现了很多机器学习算法,不管实验室研究或者公司研发,都会或多或少的要使用weka,我的理解是weka是在本地的SparkML,SparkML是分布式的大数据处理机器学习算法,数据量不是很大的 ...

  9. SpringBoot系列:Spring Boot集成Spring Cache,使用RedisCache

    前面的章节,讲解了Spring Boot集成Spring Cache,Spring Cache已经完成了多种Cache的实现,包括EhCache.RedisCache.ConcurrentMapCac ...

  10. eclipse提交代码到GitHub

    1.配置git 2.右键项目--> Team--> Share Project... 3.右键项目--> Team--> Commit...