作者:frank
转发链接:https://mp.weixin.qq.com/s/LI-SkBoPA94Ply6Qes92PA
通过插件我们可以扩展webpack,在得当的机遇通过Webpack供应的 API 改变输出结果,使webpack可以实行更广泛的任务,拥有更强的构建能力。 本文将考试测验探索 webpack 插件的事情流程,进而去揭秘它的事情事理。同时须要你对webpack底层和构建流程的一些东西有一定的理解。

想要理解 webpack 的插件的机制,须要弄明白以下几个知识点:
一个大略的插件的构成webpack构建流程Tapable是如何把各个插件串联到一起的compiler以及compilation工具的利用以及它们对应的事宜钩子。插件基本构造plugins是可以用自身原型方法apply来实例化的工具。apply只在安装插件被Webpack compiler实行一次。apply方法传入一个webpck compiler的引用,来访问编译器回调。
一个大略的插件构造:classHelloPlugin{//在布局函数中获取用户给该插件传入的配置constructor(options){}//Webpack会调用HelloPlugin实例的apply方法给插件实例传入compiler工具apply(compiler){//在emit阶段插入钩子函数,用于特定时机处理额外的逻辑;compiler.hooks.emit.tap('HelloPlugin',(compilation)=>{//在功能流程完成后可以调用webpack供应的回调函数;});//如果事宜是异步的,会带两个参数,第二个参数为回调函数,在插件处理完任务时须要调用回调函数关照webpack,才会进入下一个处理流程。compiler.plugin('emit',function(compilation,callback){//支持处理逻辑//处理完毕后实行callback以关照Webpack//如果不实行callback,运行流程将会一贯卡在这不往下实行callback();});}}module.exports=HelloPlugin;
安装插件时, 只须要将它的一个实例放到Webpack config plugins 数组里面:
constHelloPlugin=require('./hello-plugin.js');varwebpackConfig={plugins:[newHelloPlugin({options:true})]};
先来剖析一下webpack Plugin的事情事理
读取配置的过程中会先实行 new HelloPlugin(options) 初始化一个 HelloPlugin 得到实在例。初始化 compiler 工具后调用 HelloPlugin.apply(compiler) 给插件实例传入 compiler 工具。插件实例在获取到 compiler 工具后,就可以通过compiler.plugin(事宜名称, 回调函数) 监听到 Webpack 广播出来的事宜。 并且可以通过 compiler 工具去操作 Webpack。webapck 构建流程在编写插件之前,还须要理解一下Webpack的构建流程,以便在得当的机遇插入得当的插件逻辑。
Webpack的基本构建流程如下:
校验配置文件 :读取命令行传入或者webpack.config.js文件,初始化本次构建的配置参数天生Compiler工具:实行配置文件中的插件实例化语句new MyWebpackPlugin(),为webpack事宜流挂上自定义hooks进入entryOption阶段:webpack开始读取配置的Entries,递归遍历所有的入口文件run/watch:如果运行在watch模式则实行watch方法,否则实行run方法compilation:创建Compilation工具回调compilation干系钩子,依次进入每一个入口文件(entry),利用loader对文件进行编译。通过compilation我可以可以读取到module的resource(资源路径)、loaders(利用的loader)等信息。再将编译好的文件内容利用acorn解析天生AST静态语法树。然后递归、重复的实行这个过程, 所有模块和和依赖剖析完成后,实行 compilation 的 seal 方法对每个 chunk 进行整理、优化、封装__webpack_require__来仿照模块化操作.emit:所有文件的编译及转化都已经完成,包含了终极输出的资源,我们可以在传入事宜回调的compilation.assets上拿到所需数据,个中包括即将输出的资源、代码块Chunk等等信息。//修正或添加资源compilation.assets['new-file.js']={source(){return'vara=1';},size(){returnthis.source().length;}};
afterEmit:文件已经写入磁盘完成done:完成编译
奉上一张滴滴云博客的WebPack 编译流程图,不喜好看笔墨讲解的可以看流程图理解影象
WebPack 编译流程图原图出自:https://blog.didiyun.com/index.php/2019/03/01/webpack/
看完之后,如果还是看不懂或者对缕不清webpack构建流程的话,建议通读一下全文,再回来看这段话,相信一定会对webpack构建流程有很更加深刻的理解。
理解事宜流机制 Tapablewebpack实质上是一种事宜流的机制,它的事情流程便是将各个插件串联起来,而实现这统统的核心便是Tapable。
Webpack 的 Tapable 事宜流机制担保了插件的有序性,将各个插件串联起来, Webpack 在运行过程中会广播事宜,插件只须要监听它所关心的事宜,就能加入到这条webapck机制中,去改变webapck的运作,使得全体系统扩展性良好。
Tapable也是一个小型的 library,是Webpack的一个核心工具。类似于node中的events库,核心事理便是一个订阅发布模式。浸染是供应类似的插件接口。
webpack中最核心的卖力编译的Compiler和卖力创建bundles的Compilation都是Tapable的实例,可以直接在 Compiler 和 Compilation 工具上广播和监听事宜,方法如下:
/广播事宜event-name为事宜名称,把稳不要和现有的事宜重名/compiler.apply('event-name',params);compilation.apply('event-name',params);/监听事宜/compiler.plugin('event-name',function(params){});compilation.plugin('event-name',function(params){});
Tapable类暴露了tap、tapAsync和tapPromise方法,可以根据钩子的同步/异步办法来选择一个函数注入逻辑。
tap 同步钩子
compiler.hooks.compile.tap('MyPlugin',params=>{console.log('以同步办法触及compile钩子。')})
tapAsync 异步钩子,通过callback回调见告Webpack异步实行完毕tapPromise 异步钩子,返回一个Promise见告Webpack异步实行完毕
compiler.hooks.run.tapAsync('MyPlugin',(compiler,callback)=>{console.log('以异步办法触及run钩子。')callback()})compiler.hooks.run.tapPromise('MyPlugin',compiler=>{returnnewPromise(resolve=>setTimeout(resolve,1000)).then(()=>{console.log('以具有延迟的异步办法触及run钩子')})})
Tabable用法
const{SyncHook,SyncBailHook,SyncWaterfallHook,SyncLoopHook,AsyncParallelHook,AsyncParallelBailHook,AsyncSeriesHook,AsyncSeriesBailHook,AsyncSeriesWaterfallHook}=require("tapable");
tapable
大略实现一个 SyncHookclassHook{constructor(args){this.taps=[]this.interceptors=[]//这个放在后面用this._args=args}tap(name,fn){this.taps.push({name,fn})}}classSyncHookextendsHook{call(name,fn){try{this.taps.forEach(tap=>tap.fn(name))fn(null,name)}catch(error){fn(error)}}}
tapable是如何将webapck/webpack插件关联的?
Compiler.js
const{AsyncSeriesHook,SyncHook}=require("tapable");//创建类classCompiler{constructor(){this.hooks={run:newAsyncSeriesHook(["compiler"]),//异步钩子compile:newSyncHook(["params"]),//同步钩子};},run(){//实行异步钩子this.hooks.run.callAsync(this,err=>{this.compile(onCompiled);});},compile(){//实行同步钩子并传参this.hooks.compile.call(params);}}module.exports=Compiler
MyPlugin.js
constCompiler=require('./Compiler')classMyPlugin{apply(compiler){//接管compiler参数compiler.hooks.run.tap("MyPlugin",()=>console.log('开始编译...'));compiler.hooks.compile.tapAsync('MyPlugin',(name,age)=>{setTimeout(()=>{console.log('编译中...')},1000)});}}//这里类似于webpack.config.js的plugins配置//向plugins属性传入new实例constmyPlugin=newMyPlugin();constoptions={plugins:[myPlugin]}letcompiler=newCompiler(options)compiler.run()
想要深入理解tapable的文章可以看看这篇文章:
webpack4核心模块tapable源码解析: https://www.cnblogs.com/tugenhua0707/p/11317557.html
理解Compiler(卖力编译)开拓插件首先要知道compiler和 compilation 工具是做什么的
Compiler 工具包含了当前运行Webpack的配置,包括entry、output、loaders等配置,这个工具在启动Webpack时被实例化,而且是全局唯一的。Plugin可以通过该工具获取到Webpack的配置信息进行处理。
如果看完这段话,你还是没理解compiler是做啥的,不要怕,接着看。 运行npm run build,把compiler的全部信息输出到掌握台上console.log(Compiler)。
compiler
//为了能更直不雅观的让大家看清楚compiler的构造,里面的大量代码利用省略号(...)代替。Compiler{_pluginCompat:SyncBailHook{...},hooks:{shouldEmit:SyncBailHook{...},done:AsyncSeriesHook{...},additionalPass:AsyncSeriesHook{...},beforeRun:AsyncSeriesHook{...},run:AsyncSeriesHook{...},emit:AsyncSeriesHook{...},assetEmitted:AsyncSeriesHook{...},afterEmit:AsyncSeriesHook{...},thisCompilation:SyncHook{...},compilation:SyncHook{...},normalModuleFactory:SyncHook{...},contextModuleFactory:SyncHook{...},beforeCompile:AsyncSeriesHook{...},compile:SyncHook{...},make:AsyncParallelHook{...},afterCompile:AsyncSeriesHook{...},watchRun:AsyncSeriesHook{...},failed:SyncHook{...},invalid:SyncHook{...},watchClose:SyncHook{...},infrastructureLog:SyncBailHook{...},environment:SyncHook{...},afterEnvironment:SyncHook{...},afterPlugins:SyncHook{...},afterResolvers:SyncHook{...},entryOption:SyncBailHook{...},infrastructurelog:SyncBailHook{...}},...outputPath:'',//输出目录outputFileSystem:NodeOutputFileSystem{...},inputFileSystem:CachedInputFileSystem{...},...options:{//Compiler工具包含了webpack的所有配置信息,entry、module、output、resolve等信息entry:['babel-polyfill','/Users/frank/Desktop/fe/fe-blog/webpack-plugin/src/index.js'],devServer:{port:3000},output:{...},module:{...},plugins:[MyWebpackPlugin{}],mode:'production',context:'/Users/frank/Desktop/fe/fe-blog/webpack-plugin',devtool:false,...performance:{maxAssetSize:250000,maxEntrypointSize:250000,hints:'warning'},optimization:{...},resolve:{...},resolveLoader:{...},infrastructureLogging:{level:'info',debug:false}},context:'/Users/frank/Desktop/fe/fe-blog/webpack-plugin',//高下文,文件目录requestShortener:RequestShortener{...},...watchFileSystem:NodeWatchFileSystem{//监听文件变革列表信息...}}
Compiler源码精简版代码解析
源码地址(948行):https://github.com/webpack/webpack/blob/master/lib/Compiler.js
const{SyncHook,SyncBailHook,AsyncSeriesHook}=require("tapable");classCompiler{constructor(){//1.定义生命周期钩子this.hooks=Object.freeze({//...只列举几个常用的常见钩子,更多hook就不列举了,有兴趣看源码done:newAsyncSeriesHook(["stats"]),//一次编译完成后实行,回调参数:statsbeforeRun:newAsyncSeriesHook(["compiler"]),run:newAsyncSeriesHook(["compiler"]),//在编译器开始读取记录前实行emit:newAsyncSeriesHook(["compilation"]),//在天生文件到output目录之前实行,回调参数:compilationafterEmit:newAsyncSeriesHook(["compilation"]),//在天生文件到output目录之后实行compilation:newSyncHook(["compilation","params"]),//在一次compilation创建后实行插件beforeCompile:newAsyncSeriesHook(["params"]),compile:newSyncHook(["params"]),//在一个新的compilation创建之前实行make:newAsyncParallelHook(["compilation"]),//完成一次编译之前实行afterCompile:newAsyncSeriesHook(["compilation"]),watchRun:newAsyncSeriesHook(["compiler"]),failed:newSyncHook(["error"]),watchClose:newSyncHook([]),afterPlugins:newSyncHook(["compiler"]),entryOption:newSyncBailHook(["context","entry"])});//...省略代码}newCompilation(){//创建Compilation工具回调compilation干系钩子constcompilation=newCompilation(this);//...一系列操作this.hooks.compilation.call(compilation,params);//compilation工具创建完成returncompilation}watch(){//如果运行在watch模式则实行watch方法,否则实行run方法if(this.running){returnhandler(newConcurrentCompilationError());}this.running=true;this.watchMode=true;returnnewWatching(this,watchOptions,handler);}run(callback){if(this.running){returncallback(newConcurrentCompilationError());}this.running=true;process.nextTick(()=>{this.emitAssets(compilation,err=>{if(err){//在编译和输出的流程中碰着非常时,会触发failed事宜this.hooks.failed.call(err)};if(compilation.hooks.needAdditionalPass.call()){//...//done:完成编译this.hooks.done.callAsync(stats,err=>{//创建compilation工具之前this.compile(onCompiled);});}this.emitRecords(err=>{this.hooks.done.callAsync(stats,err=>{});});});});this.hooks.beforeRun.callAsync(this,err=>{this.hooks.run.callAsync(this,err=>{this.readRecords(err=>{this.compile(onCompiled);});});});}compile(callback){constparams=this.newCompilationParams();this.hooks.beforeCompile.callAsync(params,err=>{this.hooks.compile.call(params);constcompilation=this.newCompilation(params);//触发make事宜并调用addEntry,找到入口js,进行下一步this.hooks.make.callAsync(compilation,err=>{process.nextTick(()=>{compilation.finish(err=>{//封装构建结果(seal),逐次对每个module和chunk进行整理,每个chunk对应一个入口文件compilation.seal(err=>{this.hooks.afterCompile.callAsync(compilation,err=>{//异步的事宜须要在插件处理完任务时调用回调函数关照Webpack进入下一个流程,//不然运行流程将会一贯卡在这不往下实行returncallback(null,compilation);});});});});});});}emitAssets(compilation,callback){constemitFiles=(err)=>{//...省略一系列代码//afterEmit:文件已经写入磁盘完成this.hooks.afterEmit.callAsync(compilation,err=>{if(err)returncallback(err);returncallback();});}//emit事宜发生时,可以读取到终极输出的资源、代码块、模块及其依赖,并进行修正(这是末了一次修正终极文件的机会)this.hooks.emit.callAsync(compilation,err=>{if(err)returncallback(err);outputPath=compilation.getPath(this.outputPath,{});mkdirp(this.outputFileSystem,outputPath,emitFiles);});}//...省略代码}
apply方法中插入钩子的一样平常形式如下:
//compiler供应了compiler.hooks,可以根据这些不同的时候去让插件做不同的事情。compiler.hooks.阶段.tap函数('插件名称',(阶段回调参数)=>{});compiler.run(callback)
理解Compilation
Compilation工具代表了一次资源版本构建。当运行 webpack 开拓环境中间件时,每当检测到一个文件变革,就会创建一个新的 compilation,从而天生一组新的编译资源。一个 Compilation 工具表现了当前的模块资源、编译天生资源、变革的文件、以及被跟踪依赖的状态信息,大略来讲便是把本次打包编译的内容存到内存里。Compilation 工具也供应了插件须要自定义功能的回调,以供插件做自定义处理时选择利用拓展。
大略来说,Compilation的职责便是构建模块和Chunk,并利用插件优化构建过程。
和 Compiler 用法相同,钩子类型不同,也可以在某些钩子上访问 tapAsync和 tapPromise。
掌握台输出console.log(compilation)
通过 Compilation 也能读取到 Compiler 工具。
源码2000多行,看不动了- -,有兴趣的可以自己看看。 https://github.com/webpack/webpack/blob/master/lib/Compilation.js
先容几个常用的Compilation HooksbuildModule(SyncHook):在模块开始编译之前触发,可以用于修正模
succeedModule(SyncHook):在模块开始编译之前触发,可以用于修正模块
finishModules(AsyncSeriesHook):当所有模块都编译成功后被调用
seal(SyncHook):当一次compilation停滞吸收新模块时触发
optimizeDependencies(SyncBailHook):在依赖优化的开始实行
optimize(SyncHook):在优化阶段的开始实行
optimizeModules(SyncBailHook):在模块优化阶段开始时实行,插件可以在这个钩子里实行对模块的优化,回调参数:modules
optimizeChunks(SyncBailHook):在代码块优化阶段开始时实行,插件可以在这个钩子里实行对代码块的优化,回调参数:chunks
optimizeChunkAssets(AsyncSeriesHook):优化任何代码块资源,这些资源存放在compilation.assets 上。一个 chunk 有一个 files 属性,它指向由一个chunk创建的所有文件。任何额外的 chunk 资源都存放在 compilation.additionalChunkAssets 上。回调参数:chunks
optimizeAssets(AsyncSeriesHook):优化所有存放在 compilation.assets的所有资源。回调参数:assets |
Compiler 和 Compilation 的差异Compiler 代表了全体 Webpack 从启动到关闭的生命周期,而 Compilation只是代表了一次新的编译,只要文件有改动,compilation就会被重新创建。
常用 API插件可以用来修正输出文件、增加输出文件、乃至可以提升 Webpack 性能、等等,总之插件通过调用Webpack 供应的 API 能完成很多事情。 由于 Webpack供应的 API 非常多,有很多 API 很少用的上,又加上篇幅有限,下面来先容一些常用的 API。
读取输出资源、代码块、模块及其依赖有些插件可能须要读取 Webpack 的处理结果,例如输出资源、代码块、模块及其依赖,以便做下一步处理。 在 emit 事宜发生时,代表源文件的转换和组装已经完成,在这里可以读取到终极将输出的资源、代码块、模块及其依赖,并且可以修正输出资源的内容。 插件代码如下:
classPlugin{apply(compiler){compiler.plugin('emit',function(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()方法能获取到输出资源的内容letsource=compilation.assets[filename].source();});});//这是一个异步事宜,要记得调用callback关照Webpack本次事宜监听处理结束。//如果忘却了调用callback,Webpack将一贯卡在这里而不会今后实行。callback();})}}
监听文件变革
Webpack 会从配置的入口模块出发,依次找出所有的依赖模块,当入口模块或者其依赖的模块发生变革时, 就会触发一次新的 Compilation。
在开拓插件时常常须要知道是哪个文件发生变革导致了新的 Compilation,为此可以利用如下代码:
//当依赖的文件发生变革时会触发watch-run事宜compiler.hooks.watchRun.tap('MyPlugin',(watching,callback)=>{//获取发生变革的文件列表constchangedFiles=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();});
3、修正输出资源
有些场景下插件须要修正、增加、删除输出的资源,要做到这点须要监听emit 事宜,由于发生 emit 事宜时所有模块的转换和代码块对应的文件已经天生好, 须要输出的资源即将输出,因此emit事宜是修正 Webpack 输出资源的末了机遇。
所有须要输出的资源会存放在 compilation.assets中,compilation.assets 是一个键值对,键为须要输出的文件名称,值为文件对应的内容。
设置 compilation.assets 的代码如下:
//设置名称为fileName的输出资源compilation.assets[fileName]={//返回文件内容source:()=>{//fileContent既可以是代表文本文件的字符串,也可以是代表二进制文件的BufferreturnfileContent;},//返回文件大小size:()=>{returnBuffer.byteLength(fileContent,'utf8');}};callback();
判断webpack利用了哪些插件
//判断当前配置利用利用了ExtractTextPlugin,//compiler参数即为Webpack在apply(compiler)中传入的参数functionhasExtractTextPlugin(compiler){//当前配置所有利用的插件列表constplugins=compiler.options.plugins;//去plugins中探求有没有ExtractTextPlugin的实例returnplugins.find(plugin=>plugin.__proto__.constructor===ExtractTextPlugin)!=null;}
以上4种方法来源于文章: [Webpack学习-Plugin] :http://wushaobin.top/2019/03/15/webpackPlugin/
管理 Warnings 和 Errors做一个实验,如果你在 apply函数内插入 throw new Error("Message"),会发生什么,终端会打印出 Unhandled rejection Error: Message。然后 webpack 中断实行。 为了不影响 webpack 的实行,要在编译期间向用户发出警告或缺点,则应利用 compilation.warnings 和 compilation.errors。
compilation.warnings.push("warning");compilation.errors.push("error");
文章中的案例demo代码展示
https://github.com/6fedcom/fe-blog/tree/master/webpack/plugin
webpack打包过程或者插件代码里该如何调试?在当前webpack项目工程文件夹下面,实行命令行:node --inspect-brk ./node_modules/webpack/bin/webpack.js --inline --progress
个中参数--inspect-brk便是以调试模式启动node:
终端会输出:
Debugger listening on ws://127.0.0.1:9229/1018c03f-7473-4d60-b62c-949a6404c81dFor help, see: https://nodejs.org/en/docs/inspector
谷歌浏览器输入 chrome://inspect/#devices
点击inspect
然后点一下Chrome调试器里的“连续实行”,断点就提留在我们设置在插件里的debugger断点了。推举webpack知识点文章手把手教你深入总结Webpack
玩转 webpack,使你的打包速率提升 90%
一周精通Vue(三)webpack、代码抽离
带你深度解锁Webpack系列(根本篇)
带你深度解锁Webpack系列(优化篇)
带你深度解锁Webpack系列(进阶篇)
Webpack优化总会让你不得不爱
2020年了,再不会webpack敲得代码就不喷鼻香了(近万字实战)
从构建进程间缓存设计 谈 Webpack5 优化和事情事理
手摸手,带你用合理的姿势利用webpack4(上)
手摸手,带你用合理的姿势利用webpack4(下)
是什么导致尤大大选择放弃Webpack?【vite 事理解析】
教你支配搭建一个Vue-cli4+Webpack移动端框架「实践」
作者:frank
转发链接:https://mp.weixin.qq.com/s/LI-SkBoPA94Ply6Qes92PA