本文示例源代码请戳 github博客,建议你们动手敲敲代码。
webpack本质上是一种事件流的机制,它的工做流程就是将各个插件串联起来,而实现这一切的核心就是Tapable,webpack中最核心的负责编译的Compiler和负责建立bundles的Compilation都是Tapable的实例。Tapable暴露出挂载plugin的方法,使咱们能 将plugin控制在webapack事件流上运行(以下图)。css
tapable库暴露了不少Hook(钩子)类,为插件提供挂载的钩子。webpack
const { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook, AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook } = require("tapable");
Tabable 用法git
1.new Hook 新建钩子github
const hook1 = new SyncHook(["arg1", "arg2", "arg3"]);
2.使用 tap/tapAsync/tapPromise
绑定钩子
tapable提供了同步&异步绑定钩子的方法,而且他们都有绑定事件和执行事件对应的方法。web
- | Async* | Sync* |
---|---|---|
绑定 | tapAsync/tapPromise/tap |
tap |
执行 | callAsync/promise |
call |
3.call/callAsync
执行绑定事件segmentfault
const hook1 = new SyncHook(["arg1", "arg2", "arg3"]); //绑定事件到webapck事件流 hook1.tap('hook1', (arg1, arg2, arg3) => console.log(arg1, arg2, arg3)) //1,2,3 //执行绑定的事件 hook1.call(1,2,3)
举个例子api
Car
方法,在内部hooks
上新建钩子。分别是同步钩子 accelerate
(accelerate
接受一个参数)、break
、异步钩子calculateRoutes
calculateRoutes
使用tapPromise
能够返回一个promise
对象。//引入tapable const { SyncHook, AsyncParallelHook } = require('tapable'); //建立类 class Car { constructor() { this.hooks = { accelerate: new SyncHook(["newSpeed"]), break: new SyncHook(), calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"]) }; } } const myCar = new Car(); //绑定同步钩子 myCar.hooks.break.tap("WarningLampPlugin", () => console.log('WarningLampPlugin')); //绑定同步钩子 并传参 myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`)); //绑定一个异步Promise钩子 myCar.hooks.calculateRoutes.tapPromise("calculateRoutes tapPromise", (source, target, routesList, callback) => { // return a promise return new Promise((resolve,reject)=>{ setTimeout(()=>{ console.log(`tapPromise to ${source} ${target} ${routesList}`) resolve(); },1000) }) }); //执行同步钩子 myCar.hooks.break.call(); myCar.hooks.accelerate.call('hello'); console.time('cost'); //执行异步钩子 myCar.hooks.calculateRoutes.promise('i', 'love', 'tapable').then(() => { console.timeEnd('cost'); }, err => { console.error(err); console.timeEnd('cost'); })
运行结果数组
WarningLampPlugin Accelerating to hello tapPromise to i love tapable cost: 1008.725ms
calculateRoutes
也可使用tapAsync
绑定钩子,注意:此时用callback
结束异步回调。promise
myCar.hooks.calculateRoutes.tapAsync("calculateRoutes tapAsync", (source, target, routesList, callback) => { // return a promise setTimeout(() => { console.log(`tapAsync to ${source} ${target} ${routesList}`) callback(); }, 2000) }); myCar.hooks.calculateRoutes.callAsync('i', 'like', 'tapable', err => { console.timeEnd('cost'); if(err) console.log(err) })
运行结果服务器
WarningLampPlugin Accelerating to hello tapAsync to i like tapable cost: 2007.045ms
进阶一下~
到这里可能已经学会使用tapable了,可是它如何与webapck/webpack
插件关联呢?
咱们将刚才的代码稍做改动,拆成两个文件:Compiler.js
、Myplugin.js
Compiler.js
Class Car
类名改为webpack
的核心Compiler
options
里传入的plugins
Compiler
做为参数传给plugin
run
函数,在编译的每一个阶段,都触发执行相对应的钩子函数。const { SyncHook, AsyncParallelHook } = require('tapable'); class Compiler { constructor(options) { this.hooks = { accelerate: new SyncHook(["newSpeed"]), break: new SyncHook(), calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"]) }; let plugins = options.plugins; if (plugins && plugins.length > 0) { plugins.forEach(plugin => plugin.apply(this)); } } run(){ console.time('cost'); this.accelerate('hello') this.break() this.calculateRoutes('i', 'like', 'tapable') } accelerate(param){ this.hooks.accelerate.call(param); } break(){ this.hooks.break.call(); } calculateRoutes(){ const args = Array.from(arguments) this.hooks.calculateRoutes.callAsync(...args, err => { console.timeEnd('cost'); if (err) console.log(err) }); } } module.exports = Compiler
MyPlugin.js
Compiler
apply
方法接受 compiler
参数。compiler
上的钩子绑定方法。webpack
规则,向 plugins
属性传入 new
实例。webpack
插件是一个具备apply
方法的JavaScript
对象。apply
属性会被webpack compiler
调用,而且compiler
对象可在整个编译生命周期访问。
const Compiler = require('./Compiler') class MyPlugin{ constructor() { } apply(conpiler){//接受 compiler参数 conpiler.hooks.break.tap("WarningLampPlugin", () => console.log('WarningLampPlugin')); conpiler.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`)); conpiler.hooks.calculateRoutes.tapAsync("calculateRoutes tapAsync", (source, target, routesList, callback) => { setTimeout(() => { console.log(`tapAsync to ${source}${target}${routesList}`) callback(); }, 2000) }); } } //这里相似于webpack.config.js的plugins配置 //向 plugins 属性传入 new 实例 const myPlugin = new MyPlugin(); const options = { plugins: [myPlugin] } let compiler = new Compiler(options) compiler.run()
运行结果
Accelerating to hello WarningLampPlugin tapAsync to iliketapable cost: 2009.273ms
改造后运行正常,仿照Compiler和webpack插件的思路慢慢得理顺插件的逻辑成功。
更多其余Tabable方法
Webpack
经过 Plugin
机制让其更加灵活,以适应各类应用场景。 在 Webpack
运行的生命周期中会广播出许多事件,Plugin
能够监听这些事件,在合适的时机经过 Webpack
提供的 API
改变输出结果。
一个最基础的 Plugin 的代码是这样的:
class BasicPlugin{ // 在构造函数中获取用户给该插件传入的配置 constructor(options){ } // Webpack 会调用 BasicPlugin 实例的 apply 方法给插件实例传入 compiler 对象 apply(compiler){ compiler.hooks.compilation.tap('BasicPlugin', compilation => { }); } } // 导出 Plugin module.exports = BasicPlugin;
在使用这个 Plugin 时,相关配置代码以下:
const BasicPlugin = require('./BasicPlugin.js'); module.export = { plugins:[ new BasicPlugin(options), ] }
Compiler 和 Compilation
在开发 Plugin
时最经常使用的两个对象就是 Compiler
和 Compilation
,它们是 Plugin
和 Webpack
之间的桥梁。 Compiler
和 Compilation
的含义以下:
Compiler 和 Compilation 的区别在于:Compiler 表明了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是表明了一次新的编译。
插件能够用来修改输出文件、增长输出文件、甚至能够提高 Webpack 性能、等等,总之插件经过调用 Webpack 提供的 API 能完成不少事情。 因为 Webpack 提供的 API 很是多,有不少 API 不多用的上,又加上篇幅有限,下面来介绍一些经常使用的 API。
一、读取输出资源、代码块、模块及其依赖
有些插件可能须要读取 Webpack 的处理结果,例如输出资源、代码块、模块及其依赖,以便作下一步处理。
在 emit 事件发生时,表明源文件的转换和组装已经完成,在这里能够读取到最终将输出的资源、代码块、模块及其依赖,而且能够修改输出资源的内容。 插件代码以下:
class MyPlugin { apply(compiler) { compiler.hooks.emit.tabAsync('MyPlugin', (compilation, callback) => { // compilation.chunks 存放全部代码块,是一个数组 compilation.chunks.forEach(function (chunk) { // chunk 表明一个代码块 // 代码块由多个模块组成,经过 chunk.forEachModule 能读取组成代码块的每一个模块 chunk.forEachModule(function (module) { // module 表明一个模块 // module.fileDependencies 存放当前模块的全部依赖的文件路径,是一个数组 module.fileDependencies.forEach(function (filepath) { }); }); // Webpack 会根据 Chunk 去生成输出的文件资源,每一个 Chunk 都对应一个及其以上的输出文件 // 例如在 Chunk 中包含了 CSS 模块而且使用了 ExtractTextPlugin 时, // 该 Chunk 就会生成 .js 和 .css 两个文件 chunk.files.forEach(function (filename) { // compilation.assets 存放当前全部即将输出的资源 // 调用一个输出资源的 source() 方法能获取到输出资源的内容 let source = compilation.assets[filename].source(); }); }); // 这是一个异步事件,要记得调用 callback 通知 Webpack 本次事件监听处理结束。 // 若是忘记了调用 callback,Webpack 将一直卡在这里而不会日后执行。 callback(); }) } }
二、监听文件变化
Webpack 会从配置的入口模块出发,依次找出全部的依赖模块,当入口模块或者其依赖的模块发生变化时, 就会触发一次新的 Compilation。
在开发插件时常常须要知道是哪一个文件发生变化致使了新的 Compilation,为此可使用以下代码:
// 当依赖的文件发生变化时会触发 watch-run 事件 compiler.hooks.watchRun.tap('MyPlugin', (watching, callback) => { // 获取发生变化的文件列表 const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes; // changedFiles 格式为键值对,键为发生变化的文件路径。 if (changedFiles[filePath] !== undefined) { // filePath 对应的文件发生了变化 } callback(); });
默认状况下 Webpack 只会监视入口和其依赖的模块是否发生变化,在有些状况下项目可能须要引入新的文件,例如引入一个 HTML 文件。 因为 JavaScript 文件不会去导入 HTML 文件,Webpack 就不会监听 HTML 文件的变化,编辑 HTML 文件时就不会从新触发新的 Compilation。 为了监听 HTML 文件的变化,咱们须要把 HTML 文件加入到依赖列表中,为此可使用以下代码:
compiler.hooks.afterCompile.tap('MyPlugin', (compilation, callback) => { // 把 HTML 文件添加到文件依赖列表,好让 Webpack 去监听 HTML 模块文件,在 HTML 模版文件发生变化时从新启动一次编译 compilation.fileDependencies.push(filePath); callback(); });
三、修改输出资源
有些场景下插件须要修改、增长、删除输出的资源,要作到这点须要监听 emit
事件,由于发生 emit
事件时全部模块的转换和代码块对应的文件已经生成好, 须要输出的资源即将输出,所以 emit
事件是修改 Webpack
输出资源的最后时机。
全部须要输出的资源会存放在 compilation.assets
中,compilation.assets
是一个键值对,键为须要输出的文件名称,值为文件对应的内容。
设置 compilation.assets
的代码以下:
// 设置名称为 fileName 的输出资源 compilation.assets[fileName] = { // 返回文件内容 source: () => { // fileContent 既能够是表明文本文件的字符串,也能够是表明二进制文件的 Buffer return fileContent; }, // 返回文件大小 size: () => { return Buffer.byteLength(fileContent, 'utf8'); } }; callback();
读取 compilation.assets
的代码以下:
// 读取名称为 fileName 的输出资源 const asset = compilation.assets[fileName]; // 获取输出资源的内容 asset.source(); // 获取输出资源的文件大小 asset.size(); callback();
怎么写一个插件?参照webpack官方教程Writing a Plugin。 一个webpack plugin由一下几个步骤组成:
下面咱们举一个实际的例子,带你一步步去实现一个插件。
该插件的名称取名叫 EndWebpackPlugin
,做用是在 Webpack
即将退出时再附加一些额外的操做,例如在 Webpack
成功编译和输出了文件后执行发布操做把输出的文件上传到服务器。 同时该插件还能区分 Webpack
构建是否执行成功。使用该插件时方法以下:
module.exports = { plugins:[ // 在初始化 EndWebpackPlugin 时传入了两个参数,分别是在成功时的回调函数和失败时的回调函数; new EndWebpackPlugin(() => { // Webpack 构建成功,而且文件输出了后会执行到这里,在这里能够作发布文件操做 }, (err) => { // Webpack 构建失败,err 是致使错误的缘由 console.error(err); }) ] }
要实现该插件,须要借助两个事件:
done
:在成功构建而且输出了文件后,Webpack 即将退出时发生;failed
:在构建出现异常致使构建失败,Webpack 即将退出时发生;实现该插件很是简单,完整代码以下:
class EndWebpackPlugin { constructor(doneCallback, failCallback) { // 存下在构造函数中传入的回调函数 this.doneCallback = doneCallback; this.failCallback = failCallback; } apply(compiler) { compiler.hooks.done.tab('EndWebpackPlugin', (stats) => { // 在 done 事件中回调 doneCallback this.doneCallback(stats); }); compiler.hooks.failed.tab('EndWebpackPlugin', (err) => { // 在 failed 事件中回调 failCallback this.failCallback(err); }); } } // 导出插件 module.exports = EndWebpackPlugin;
从开发这个插件能够看出,找到合适的事件点去完成功能在开发插件时显得尤其重要。 在 工做原理归纳 中详细介绍过 Webpack 在运行过程当中广播出经常使用事件,你能够从中找到你须要的事件。
参考
tapable
compiler-hooks
Compilation Hooks
writing-a-plugin
深刻浅出 Webpack
干货!撸一个webpack插件