干货!撸一个webpack插件(内含tapable详解+webpack流程)
转载自掘金:https://juejin.im/post/5beb8875e51d455e5c4dd83f?utm_source=tuicool&utm_medium=referral#comment
目录
- Tabable是什么?
- Tabable 用法
- 进阶一下
- Tabable的其余方法
- webpack流程
- 总结
- 实战!写一个插件
Webpack可以将其了解是一种基于事件流的编程范例,一个插件合集。
而将这些插件控制在webapck事件流上的运行的就是webpack自己写的基础类Tapable。
Tapable暴露出挂载plugin的方法,使我们能 将plugin控制在webapack事件流上运行(如下图)。后面我们将看到核心的对象 Compiler、Compilation等都是继承于Tabable类。(如下图所示)

Tabable是什么?
tapable库暴露了很多Hook(钩子)类,为插件提供挂载的钩子。
const { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook, AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook } = require("tapable");
Tabable 用法
- 1.new Hook 新建钩子
- tapable 暴露出来的都是类方法,new 一个类方法取得我们需要的钩子。
- class 接受数组参数options,非必传。类方法会根据传参,接受同样数量的参数。
const hook1 = new SyncHook(["arg1", "arg2", "arg3"]);- 2.使用 tap/tapAsync/tapPromise 绑定钩子
tabpack提供了同步&异步绑定钩子的方法,并且他们都有绑定事件和执行事件对应的方法。
| Async* | Sync* |
|---|---|
| 绑定:tapAsync/tapPromise/tap | 绑定:tap |
| 执行:callAsync/promise | 执行:call |
- 3.call/callAsync 执行绑定事件
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)
- 举个栗子
- 定义一个Car方法,在内部hooks上新建钩子。分别是
同步钩子accelerate、break(accelerate接受一个参数)、异步钩子calculateRoutes - 使用钩子对应的
绑定和执行方法 - calculateRoutes使用
tapPromise可以返回一个promise对象。
- 定义一个Car方法,在内部hooks上新建钩子。分别是
//引入tapableconst { 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');})运行结果
WarningLampPluginAccelerating to hellotapPromise to ilovetapablecost: 1003.898mscalculateRoutes也可以使用tapAsync绑定钩子,注意:此时用callback结束异步回调。
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)})运行结果
WarningLampPluginAccelerating to hellotapAsync to iliketapablecost: 2007.850ms进阶一下~
到这里可能已经学会使用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 = CompilerMyPlugin.js
- 引入Compiler
- 定义一个自己的插件。
- apply方法接受 compiler参数。
webpack 插件是一个具备
apply方法的 JavaScript 对象。apply 属性会被 webpack compiler 调用,并且 compiler 对象可在整个编译生命周期访问。
- 给compiler上的钩子绑定方法。
- 仿照webpack规则,
向 plugins 属性传入 new 实例。
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 helloWarningLampPlugintapAsync to iliketapablecost: 2015.866ms改造后运行正常,仿照Compiler和webpack插件的思路慢慢得理顺插件的逻辑成功。
Tabable的其余方法
| type | function |
|---|---|
| Hook | 所有钩子的后缀 |
| Waterfall | 同步方法,但是它会传值给下一个函数 |
| Bail | 熔断:当函数有任何返回值,就会在当前执行函数中止 |
| Loop | 监听函数返回true表示继续循环,返回undefine表示结束循环 |
| Sync | 同步方法 |
| AsyncSeries | 异步串行钩子 |
| AsyncParallel | 异步并行执行钩子 |
我们可以根据自己的开发需求,选择适合的同步/异步钩子。
webpack流程
通过上面的阅读,我们知道了如何在webapck事件流上挂载钩子。
假设现在要自己设置一个插件更改最后产出资源的内容,我们应该把事件增加在哪个钩子上呢?哪一个步骤能拿到webpack编译的资源从而去修改?
所以接下来的任务是:理解webpack的流程。
贴一张淘宝团队分享的经典webpack流程图,再慢慢分析~

1. webpack入口(webpack.config.js+shell options)
从配置文件package.json 和 Shell 语句中读取与合并参数,得出最终的参数;
每次在命令行输入 webpack 后,操作系统都会去调用
./node_modules/.bin/webpack这个 shell 脚本。这个脚本会去调用./node_modules/webpack/bin/webpack.js并追加输入的参数,如 -p , -w 。
2. 用yargs参数解析(optimist)
yargs.parse(process.argv.slice(2), (err, argv, output) => {})源码地址
3.webpack初始化
(1)构建compiler对象
let compiler = new Webpack(options)源码地址
(2)注册NOdeEnvironmentPlugin插件
new NodeEnvironmentPlugin().apply(compiler);源码地址
(3)挂在options中的基础插件,调用WebpackOptionsApply库初始化基础插件。
if (options.plugins && Array.isArray(options.plugins)) { for (const plugin of options.plugins) { if (typeof plugin === "function") { plugin.apply(compiler); } else { plugin.apply(compiler); } }}compiler.hooks.environment.call();compiler.hooks.afterEnvironment.call();compiler.options = new WebpackOptionsApply().process(options, compiler);源码地址
4. run 开始编译
if (firstOptions.watch || options.watch) { const watchOptions = firstOptions.watchOptions || firstOptions.watch || options.watch || {}; if (watchOptions.stdin) { process.stdin.on("end", function(_) { process.exit(); // eslint-disable-line }); process.stdin.resume(); } compiler.watch(watchOptions, compilerCallback); if (outputOptions.infoVerbosity !== "none") console.log("\nwebpack is watching the files…\n");} else compiler.run(compilerCallback);这里分为两种情况:
1)Watching:监听文件变化
2)run:执行编译
源码地址
5.触发compile
(1)在run的过程中,已经触发了少量钩子:beforeRun->run->beforeCompile->compile->make->seal (编写插件的时候,即可以将自己设置的方挂在对应钩子上,按照编译的顺序被执行)
(2)构建了关键的 Compilation对象
在run()方法中,执行了this.compile()
this.compile()中创立了compilation
this.hooks.beforeRun.callAsync(this, err => { ... this.hooks.run.callAsync(this, err => { ... this.readRecords(err => { ... this.compile(onCompiled); }); });});...compile(callback) { const params = this.newCompilationParams(); this.hooks.beforeCompile.callAsync(params, err => { ... this.hooks.compile.call(params); const compilation = this.newCompilation(params); this.hooks.make.callAsync(compilation, err => { ... compilation.finish(); compilation.seal(err => { ... this.hooks.afterCompile.callAsync(compilation, err ... return callback(null, compilation); }); }); }); });}源码地址
const compilation = this.newCompilation(params);Compilation负责整个编译过程,包含了每个构建环节所对应的方法。对象内部保留了对compiler的引用。
当 Webpack 以开发模式运行时,每当检测到文件变化,一次新的 Compilation 将被创立。
划重点:Compilation很重要!编译生产资源变换文件都靠它。
6.addEntry() make 分析入口文件创立板块对象
compile中触发make事件并调用addEntry
webpack的make钩子中, tapAsync注册了一个DllEntryPlugin, 就是将入口板块通过调用compilation。
这一注册在Compiler.compile()方法中被执行。
addEntry方法将所有的入口板块增加到编译构建队列中,开启编译流程。
DllEntryPlugin.js
compiler.hooks.make.tapAsync("DllEntryPlugin", (compilation, callback) => { compilation.addEntry( this.context, new DllEntryDependency( this.entries.map((e, idx) => { const dep = new SingleEntryDependency(e); dep.loc = { name: this.name, index: idx }; return dep; }), this.name ), this.name, callback );});源码地址
流程走到这里让我觉得很奇怪:刚刚还在Compiler.js中执行compile,怎样一下子就到了DllEntryPlugin.js?
这就要说道之前WebpackOptionsApply.process()初始化插件的时候,执行了compiler.hooks.entryOption.call(options.context, options.entry);
WebpackOptionsApply.js
class WebpackOptionsApply extends OptionsApply { process(options, compiler) { ... compiler.hooks.entryOption.call(options.context, options.entry); }}process
entryOption
DllPlugin.js
compiler.hooks.entryOption.tap("DllPlugin", (context, entry) => { const itemToPlugin = (item, name) => { if (Array.isArray(item)) { return new DllEntryPlugin(context, item, name); } throw new Error("DllPlugin: supply an Array as entry"); }; if (typeof entry === "object" && !Array.isArray(entry)) { Object.keys(entry).forEach(name => { itemToPlugin(entry[name], name).apply(compiler); }); } else { itemToPlugin(entry, "main").apply(compiler); } return true;});DllPlugin
其实addEntry方法,存在很多入口,SingleEntryPlugin也注册了compiler.hooks.make.tapAsync钩子。这里主要再强调一下WebpackOptionsApply.process()流程(233)。
入口有很多,有兴趣可以调试一下先后顺序~
7. 构建板块
compilation.addEntry中执行 _addModuleChain()这个方法主要做了两件事情。一是根据板块的类型获取对应的板块工厂并创立板块,二是构建板块。
通过 *ModuleFactory.create方法创立板块,(有NormalModule , MultiModule , ContextModule , DelegatedModule 等)对板块使用的loader进行加载。调用 acorn 解析经 loader 解决后的源文件生成笼统语法树 AST。遍历 AST,构建该板块所依赖的板块
addEntry(context, entry, name, callback) { const slot = { name: name, request: entry.request, module: null }; this._preparedEntrypoints.push(slot); this._addModuleChain( context, entry, module => { this.entries.push(module); }, (err, module) => { if (err) { return callback(err); } if (module) { slot.module = module; } else { const idx = this._preparedEntrypoints.indexOf(slot); this._preparedEntrypoints.splice(idx, 1); } return callback(null, module); } );}addEntry addModuleChain()源码地址
8. 封装构建结果(seal)
webpack 会监听 seal事件调用各插件对构建后的结果进行封装,要逐次对每个 module 和 chunk 进行整理,生成编译后的源码,合并,拆分,生成 hash 。 同时这是我们在开发时进行代码优化和功能增加的关键环节。
template.getRenderMainfest.render()通过模板(MainTemplate、ChunkTemplate)把chunk生产_webpack_requie()的格式。
9. 输出资源(emit)
把Assets输出到output的path中。
总结
webpack是一个插件合集,由 tapable 控制各插件在 webpack 事件流上运行。主要依赖的是compilation的编译板块和封装。
webpack 的入口文件其实就实例了Compiler并调用了run方法开启了编译,webpack的主要编译都按照下面的钩子调用顺序执行。
- Compiler:beforeRun 清理缓存
- Compiler:run 注册缓存数据钩子
- Compiler:beforeCompile
- Compiler:compile 开始编译
- Compiler:make 从入口分析依赖以及间接依赖板块,创立板块对象
- Compilation:buildModule 板块构建
- Compiler:normalModuleFactory 构建
- Compilation:seal 构建结果封装, 不可再更改
- Compiler:afterCompile 完成构建,缓存数据
- Compiler:emit 输出到dist目录
一个 Compilation 对象包含了当前的板块资源、编译生成资源、变化的文件等。
Compilation 对象也提供了很多事件回调供插件做扩展。
Compilation中比较重要的部分是assets 假如我们要借助webpack帮你生成文件,就要在assets上增加对应的文件信息。
compilation.getStats()能得到生产文件以及chunkhash的少量信息。等等
实战!写一个插件
这次尝试写一个简单的插件,帮助我们去除webpack打包生成的bundle.js中多余的注释
<figure style=”display: block; margin: 22px auto; text-align: center;”>[图片上传中…(image-2d5386-1542186773727-1)]
<figcaption style=”display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;”></figcaption>
</figure>
怎样写一个插件?
参照webpack官方教程Writing a Plugin
一个webpack plugin由一下几个步骤组成:
- 一个JavaScript类函数。
- 在函数原型 (prototype)中定义一个注入
compiler对象的apply方法。 apply函数中通过compiler插入指定的事件钩子,在钩子回调中拿到compilation对象- 使用compilation操纵修改webapack内部实例数据。
- 异步插件,数据解决完后使用callback回调
完成插件初始架构
在之前说Tapable的时候,写了一个MyPlugin类函数,它已经满足了webpack plugin结构的前两点(一个JavaScript类函数,在函数原型 (prototype)中定义一个注入compiler)
现在我们要让Myplugin满足后三点。首先,使用compiler指定的事件钩子。
class MyPlugin{ constructor() { } apply(conpiler){ 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) }); }}插件的常用对象
| 对象 | 钩子 |
|---|---|
| Compiler | run,compile,compilation,make,emit,done |
| Compilation | buildModule,normalModuleLoader,succeedModule,finishModules,seal,optimize,after-seal |
| Module Factory | beforeResolver,afterResolver,module,parser |
| Module | |
| Parser | program,statement,call,expression |
| Template | hash,bootstrap,localVars,render |
编写插件
class MyPlugin { constructor(options) { this.options = options this.externalModules = {} } apply(compiler) { var reg = /("([^\\\"]*(\\.)?)*")|('([^\\\']*(\\.)?)*')|(\/{2,}.*?(\r|\n))|(\/\*(\n|.)*?\*\/)|(\/\*\*\*\*\*\*\/)/g compiler.hooks.emit.tap('CodeBeautify', (compilation)=> { Object.keys(compilation.assets).forEach((data)=> { let content = compilation.assets[data].source() // 欲解决的文本 content = content.replace(reg, function (word) { // 去除注释后的文本 return /^\/{2,}/.test(word) || /^\/\*!/.test(word) || /^\/\*{3,}\//.test(word) ? "" : word; }); compilation.assets[data] = { source(){ return content }, size(){ return content.length } } }) }) }}module.exports = MyPlugin第一步,使用compiler的emit钩子
emit事件是将编译好的代码发射到指定的stream中触发,在这个钩子执行的时候,我们能从回调函数返回的compilation对象上拿到编译好的stream。
compiler.hooks.emit.tap('xxx',(compilation)=>{})第二步,访问compilation对象,我们用绑定提供了编译 compilation 引用的emit钩子函数,每一次编译都会拿到新的 compilation 对象。这些 compilation 对象提供了少量钩子函数,来钩入到构建流程的很多步骤中。
compilation中会返回很多内部对象,不完全截图如下所示:
<figure style=”display: block; margin: 22px auto; text-align: center;”>[图片上传中…(image-982c4e-1542186773727-0)]
<figcaption style=”display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;”></figcaption>
</figure>
其中,我们需要的是compilation.assets
assetsCompilation { assets: { 'js/index/main.js': CachedSource { _source: [Object], _cachedSource: undefined, _cachedSize: undefined, _cachedMaps: {} } }, errors: [], warnings: [], children: [], dependencyFactories: ArrayMap { keys: [ [Object], [Function: MultiEntryDependency], [Function: SingleEntryDependency], [Function: LoaderDependency], [Object], [Function: ContextElementDependency], values: [ NullFactory {}, [Object], NullFactory {} ] }, dependencyTemplates: ArrayMap { keys: [ [Object], [Object], [Object] ], values: [ ConstDependencyTemplate {}, RequireIncludeDependencyTemplate {}, NullDependencyTemplate {}, RequireEnsureDependencyTemplate {}, ModuleDependencyTemplateAsRequireId {}, AMDRequireDependencyTemplate {}, ModuleDependencyTemplateAsRequireId {}, AMDRequireArrayDependencyTemplate {}, ContextDependencyTemplateAsRequireCall {}, AMDRequireDependencyTemplate {}, LocalModuleDependencyTemplate {}, ModuleDependencyTemplateAsId {}, ContextDependencyTemplateAsRequireCall {}, ModuleDependencyTemplateAsId {}, ContextDependencyTemplateAsId {}, RequireResolveHeaderDependencyTemplate {}, RequireHeaderDependencyTemplate {} ] }, fileTimestamps: {}, contextTimestamps: {}, name: undefined, _currentPluginApply: undefined, fullHash: 'f4030c2aeb811dd6c345ea11a92f4f57', hash: 'f4030c2aeb811dd6c345', fileDependencies: [ '/Users/mac/web/src/js/index/main.js' ], contextDependencies: [], missingDependencies: [] }优化所有 chunk 资源(asset)。资源(asset)会以key-value的形式被存储在
compilation.assets。
第三步,遍历assets。
1)assets数组对象中的key是资源名,在Myplugin插件中,遍历Object.key()我们拿到了
main.cssbundle.jsindex.html2)调用Object.source() 方法,得到资源的内容
compilation.assets[data].source() 3)用正则,去除注释
Object.keys(compilation.assets).forEach((data)=> { let content = compilation.assets[data].source() content = content.replace(reg, function (word) { return /^\/{2,}/.test(word) || /^\/\*!/.test(word) || /^\/\*{3,}\//.test(word) ? "" : word; })});第四步,升级compilation.assets[data]对象
compilation.assets[data] = { source(){ return content }, size(){ return content.length }}第五步 在webpack中引用插件
webpack.config.js
const path = require('path')const MyPlugin = require('./plugins/MyPlugin')module.exports = { entry:'./src/index.js', output:{ path:path.resolve('dist'), filename:'bundle.js' }, plugins:[ ... new MyPlugin() ]}插件地址
参考资料
- taobaofed.org/blog/2016/0…
- github.com/webpack/tap…
- zoumiaojiang.com/article/wha…
- webpack.js.org/api/plugins…
- webpack.js.org/contribute/…
- webpack.docschina.org/api/plugins…

1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 干货!撸一个webpack插件(内含tapable详解+webpack流程)