webpack构建的详细流程探底
作为模块加载和打包神器,只需配置几个文件,加载各种loader就可以享受无痛流程化开发。但对于webpack这样一个复杂度较高的插件集合,它的整体流程及思想对我们来说还是很透明的。
本文旨在搞清楚从命令行下敲下webpack命令,或者配置npmscript后执行package.json中的命令,到工程目录下出现打包的后的bundle文件的过程中,webpack都替我们做了哪些工作。
测试用webpack版本为webpack@3.4.1
webpack.config.js中定义好相关配置,包括entry、output、module、plugins等,命令行执行webpack命令,webpack便会根据配置文件中的配置进行打包处理文件,并生成最后打包后的文件。
第一步:执行webpack命令时,发生了什么?(bin/webpack.js)
命令行执行webpack时,如果全局命令行中未找到webpack命令的话,执行本地的node-modules/bin/webpack.js文件。
在bin/webpack.js中使用yargs库解析了命令行的参数,处理了webpack的配置对象options,调用processOptions()函数。
//处理编译相关,核心函数 functionprocessOptions(options){ //promise风格的处理,暂时还没遇到这种情况的配置 if(typeofoptions.then==="function"){...} //处理传入的options为数组的情况 varfirstOptions=[].concat(options)[0]; varstatsPresetToOptions=require("../lib/Stats.js").presetToOptions; //设置输出的options varoutputOptions=options.stats; if(typeofoutputOptions==="boolean"||typeofoutputOptions==="string"){ outputOptions=statsPresetToOptions(outputOptions); }elseif(!outputOptions){ outputOptions={}; } //处理各种现实相关的参数 ifArg("display",function(preset){ outputOptions=statsPresetToOptions(preset); }); ... //引入lib下的webpack.js,入口文件 varwebpack=require("../lib/webpack.js"); //设置最大错误追踪堆栈 Error.stackTraceLimit=30; varlastHash=null; varcompiler; try{ //编译,这里是关键,需要进入lib/webpack.js文件查看 compiler=webpack(options); }catch(e){ //错误处理 varWebpackOptionsValidationError=require("../lib/WebpackOptionsValidationError"); if(einstanceofWebpackOptionsValidationError){ if(argv.color) console.error("\u001b[1m\u001b[31m"+e.message+"\u001b[39m\u001b[22m"); else console.error(e.message); process.exit(1);//eslint-disable-lineno-process-exit } throwe; } //显示相关参数处理 if(argv.progress){ varProgressPlugin=require("../lib/ProgressPlugin"); compiler.apply(newProgressPlugin({ profile:argv.profile })); } //编译完后的回调函数 functioncompilerCallback(err,stats){} //watch模式下的处理 if(firstOptions.watch||options.watch){ varwatchOptions=firstOptions.watchOptions||firstOptions.watch||options.watch||{}; if(watchOptions.stdin){ process.stdin.on("end",function(){ process.exit(0);//eslint-disable-line }); process.stdin.resume(); } compiler.watch(watchOptions,compilerCallback); console.log("\nWebpackiswatchingthefiles…\n"); }else //调用run()函数,正式进入编译过程 compiler.run(compilerCallback); }
第二步:调用webpack,返回compiler对象的过程(lib/webpack.js)
如下图所示,lib/webpack.js中的关键函数为webpack,其中定义了编译相关的一些操作。
"usestrict"; constCompiler=require("./Compiler"); constMultiCompiler=require("./MultiCompiler"); constNodeEnvironmentPlugin=require("./node/NodeEnvironmentPlugin"); constWebpackOptionsApply=require("./WebpackOptionsApply"); constWebpackOptionsDefaulter=require("./WebpackOptionsDefaulter"); constvalidateSchema=require("./validateSchema"); constWebpackOptionsValidationError=require("./WebpackOptionsValidationError"); constwebpackOptionsSchema=require("../schemas/webpackOptionsSchema.json"); //核心方法,调用该方法,返回Compiler的实例对象compiler functionwebpack(options,callback){...} exports=module.exports=webpack; //设置webpack对象的常用属性 webpack.WebpackOptionsDefaulter=WebpackOptionsDefaulter; webpack.WebpackOptionsApply=WebpackOptionsApply; webpack.Compiler=Compiler; webpack.MultiCompiler=MultiCompiler; webpack.NodeEnvironmentPlugin=NodeEnvironmentPlugin; webpack.validate=validateSchema.bind(this,webpackOptionsSchema); webpack.validateSchema=validateSchema; webpack.WebpackOptionsValidationError=WebpackOptionsValidationError; //对外暴露一些插件 functionexportPlugins(obj,mappings){...} exportPlugins(exports,{...}); exportPlugins(exports.optimize={},{...});
接下来看在webpack函数中主要定义了哪些操作
//核心方法,调用该方法,返回Compiler的实例对象compiler functionwebpack(options,callback){ //验证是否符合格式 constwebpackOptionsValidationErrors=validateSchema(webpackOptionsSchema,options); if(webpackOptionsValidationErrors.length){ thrownewWebpackOptionsValidationError(webpackOptionsValidationErrors); } letcompiler; //传入的options为数组的情况,调用MultiCompiler进行处理,目前还没遇到过这种情况的配置 if(Array.isArray(options)){ compiler=newMultiCompiler(options.map(options=>webpack(options))); }elseif(typeofoptions==="object"){ //配置options的默认参数 newWebpackOptionsDefaulter().process(options); //初始化一个Compiler的实例 compiler=newCompiler(); //设置context的默认值为进程的当前目录,绝对路径 compiler.context=options.context; //定义compiler的options属性 compiler.options=options; //Node环境插件,其中设置compiler的inputFileSystem,outputFileSystem,watchFileSystem,并定义了before-run的钩子函数 newNodeEnvironmentPlugin().apply(compiler); //应用每个插件 if(options.plugins&&Array.isArray(options.plugins)){ compiler.apply.apply(compiler,options.plugins); } //调用environment插件 compiler.applyPlugins("environment"); //调用after-environment插件 compiler.applyPlugins("after-environment"); //处理compiler对象,调用一些必备插件 compiler.options=newWebpackOptionsApply().process(options,compiler); }else{ thrownewError("Invalidargument:options"); } if(callback){ if(typeofcallback!=="function")thrownewError("Invalidargument:callback"); if(options.watch===true||(Array.isArray(options)&&options.some(o=>o.watch))){ constwatchOptions=Array.isArray(options)?options.map(o=>o.watchOptions||{}):(options.watchOptions||{}); returncompiler.watch(watchOptions,callback); } compiler.run(callback); } returncompiler; }
webpack函数中主要做了以下两个操作,
- 实例化Compiler类。该类继承自Tapable类,Tapable是一个基于发布订阅的插件架构。webpack便是基于Tapable的发布订阅模式实现的整个流程。Tapable中通过plugins注册插件名,以及对应的回调函数,通过apply,applyPlugins,applyPluginsWater,applyPluginsAsync等函数以不同的方式调用注册在某一插件下的回调。
- 通过WebpackOptionsApply处理webpackcompiler对象,通过compiler.apply的方式调用了一些必备插件,在这些插件中,注册了一些plugins,在后面的编译过程中,通过调用一些插件的方式,去处理一些流程。
第三步:调用compiler的run的过程(Compiler.js)
run()调用
run函数中主要触发了before-run事件,在before-run事件的回调函数中触发了run事件,run事件中调用了readRecord函数读取文件,并调用compile()函数进行编译。
compile()调用
compile函数中定义了编译的相关流程,主要有以下流程:
- 创建编译参数
- 触发before-compile事件,
- 触发compile事件,开始编译
- 创建compilation对象,负责整个编译过程中具体细节的对象
- 触发make事件,开始创建模块和分析其依赖
- 根据入口配置的类型,决定是调用哪个plugin中的make事件的回调。如单入口的entry,调用的是SingleEntryPlugin.js下make事件注册的回调函数,其他多入口同理。
- 调用compilation对象的addEntry函数,创建模块以及依赖。
- make事件的回调函数中,通过seal封装构建的结果
- run方法中定义的onCompiled回调函数被调用,完成emit过程,将结果写入至目标文件
compile函数的定义
compile(callback){ //创建编译参数,包括模块工厂和编译依赖参数数组 constparams=this.newCompilationParams(); //触发before-compile事件,开始整个编译过程 this.applyPluginsAsync("before-compile",params,err=>{ if(err)returncallback(err); //触发compile事件 this.applyPlugins("compile",params); //构建compilation对象,compilation对象负责具体的编译细节 constcompilation=this.newCompilation(params); //触发make事件,对应的监听make事件的回调函数在不同的EntryPlugin中注册,比如singleEntryPlugin this.applyPluginsParallel("make",compilation,err=>{ if(err)returncallback(err); compilation.finish(); compilation.seal(err=>{ if(err)returncallback(err); this.applyPluginsAsync("after-compile",compilation,err=>{ if(err)returncallback(err); returncallback(null,compilation); }); }); }); }); }
【问题】make事件触发后,有哪些插件中注册了make事件并得到了运行的机会呢?
以单入口entry配置为例,在EntryOptionPlugin插件中定义了,不同配置的入口应该调用何种插件进行解析。不同配置的入口插件中注册了对应的make事件回调函数,在make事件触发后被调用。
如下所示:
一个插件的apply方法是一个插件的核心方法,当说一个插件被调用时主要是其apply方法被调用。
EntryOptionPlugin插件在webpackOptionsApply中被调用,其内部定义了使用何种插件来解析入口文件。
constSingleEntryPlugin=require("./SingleEntryPlugin"); constMultiEntryPlugin=require("./MultiEntryPlugin"); constDynamicEntryPlugin=require("./DynamicEntryPlugin"); module.exports=classEntryOptionPlugin{ apply(compiler){ compiler.plugin("entry-option",(context,entry)=>{ functionitemToPlugin(item,name){ if(Array.isArray(item)){ returnnewMultiEntryPlugin(context,item,name); }else{ returnnewSingleEntryPlugin(context,item,name); } } //判断entry字段的类型去调用不同的入口插件去处理 if(typeofentry==="string"||Array.isArray(entry)){ compiler.apply(itemToPlugin(entry,"main")); }elseif(typeofentry==="object"){ Object.keys(entry).forEach(name=>compiler.apply(itemToPlugin(entry[name],name))); }elseif(typeofentry==="function"){ compiler.apply(newDynamicEntryPlugin(context,entry)); } returntrue; }); } };
entry-option事件被触发时,EntryOptionPlugin插件做了这几个事情:
判断入口的类型,通过entry字段来判断,对应了entry字段为stringobjectfunction的三种情况
每种不同的类型调用不同的插件去处理入口的配置。大致处理逻辑如下:
- 数组类型的entry调用multiEntryPlugin插件去处理,对应了多入口的场景
- function的entry调用了DynamicEntryPlugin插件去处理,对应了异步chunk的场景
- string类型的entry或者object类型的entry,调用SingleEntryPlugin去处理,对应了单入口的场景
【问题】entry-option事件是在什么时机被触发的呢?
如下代码所示,是在WebpackOptionsApply.js中,先调用处理入口的EntryOptionPlugin插件,然后触发entry-option事件,去调用不同类型的入口处理插件。
注意:调用插件的过程也就是一个注册事件以及回调函数的过程。
WebpackOptionApply.js
//调用处理入口entry的插件 compiler.apply(newEntryOptionPlugin()); compiler.applyPluginsBailResult("entry-option",options.context,options.entry);
前面说到,make事件触发时,对应的回调逻辑都在不同配置入口的插件中注册的。下面以SingleEntryPlugin为例,说明从make事件被触发,到编译结束的整个过程。
SingleEntryPlugin.js
classSingleEntryPlugin{ constructor(context,entry,name){ this.context=context; this.entry=entry; this.name=name; } apply(compiler){ //compilation事件在初始化Compilation对象的时候被触发 compiler.plugin("compilation",(compilation,params)=>{ constnormalModuleFactory=params.normalModuleFactory; compilation.dependencyFactories.set(SingleEntryDependency,normalModuleFactory); }); //make事件在执行compile的时候被触发 compiler.plugin("make",(compilation,callback)=>{ constdep=SingleEntryPlugin.createDependency(this.entry,this.name); //编译的关键,调用Compilation中的addEntry,添加入口,进入编译过程。 compilation.addEntry(this.context,dep,this.name,callback); }); } staticcreateDependency(entry,name){ constdep=newSingleEntryDependency(entry); dep.loc=name; returndep; } } module.exports=SingleEntryPlugin;
Compilation中负责具体编译的细节,包括如何创建模块以及模块的依赖,根据模板生成js等。如:addEntry,buildModule,processModuleDependencies等。
Compilation.js
addEntry(context,entry,name,callback){ constslot={ name:name, module:null }; this.preparedChunks.push(slot); //添加该chunk上的module依赖 this._addModuleChain(context,entry,(module)=>{ entry.module=module; this.entries.push(module); module.issuer=null; },(err,module)=>{ if(err){ returncallback(err); } if(module){ slot.module=module; }else{ constidx=this.preparedChunks.indexOf(slot); this.preparedChunks.splice(idx,1); } returncallback(null,module); }); }
_addModuleChain(context,dependency,onModule,callback){ conststart=this.profile&&Date.now(); ... //根据模块的类型获取对应的模块工厂并创建模块 constmoduleFactory=this.dependencyFactories.get(dependency.constructor); ... //创建模块,将创建好的模块module作为参数传递给回调函数 moduleFactory.create({ contextInfo:{ issuer:"", compiler:this.compiler.name }, context:context, dependencies:[dependency] },(err,module)=>{ if(err){ returnerrorAndCallback(newEntryModuleNotFoundError(err)); } letafterFactory; if(this.profile){ if(!module.profile){ module.profile={}; } afterFactory=Date.now(); module.profile.factory=afterFactory-start; } constresult=this.addModule(module); if(!result){ module=this.getModule(module); onModule(module); if(this.profile){ constafterBuilding=Date.now(); module.profile.building=afterBuilding-afterFactory; } returncallback(null,module); } if(resultinstanceofModule){ if(this.profile){ result.profile=module.profile; } module=result; onModule(module); moduleReady.call(this); return; } onModule(module); //构建模块,包括调用loader处理文件,使用acorn生成AST,遍历AST收集依赖 this.buildModule(module,false,null,null,(err)=>{ if(err){ returnerrorAndCallback(err); } if(this.profile){ constafterBuilding=Date.now(); module.profile.building=afterBuilding-afterFactory; } //开始处理收集好的依赖 moduleReady.call(this); }); functionmoduleReady(){ this.processModuleDependencies(module,err=>{ if(err){ returncallback(err); } returncallback(null,module); }); } }); }
_addModuleChain主要做了以下几件事情:
- 调用对应的模块工厂类去创建module
- buildModule,开始构建模块,收集依赖。构建过程中最耗时的一步,主要完成了调用loader处理模块以及模块之间的依赖,使用acorn生成AST的过程,遍历AST循环收集并构建依赖模块的过程。此处可以深入了解webpack使用loader处理模块的原理。
第四步:模块build完成后,使用seal进行module和chunk的一些处理,包括合并、拆分等。
Compilation的seal函数在make事件的回调函数中进行了调用。
seal(callback){ constself=this; //触发seal事件,提供其他插件中seal的执行时机 self.applyPlugins0("seal"); self.nextFreeModuleIndex=0; self.nextFreeModuleIndex2=0; self.preparedChunks.forEach(preparedChunk=>{ constmodule=preparedChunk.module; //将module保存在chunk的origins中,origins保存了module的信息 constchunk=self.addChunk(preparedChunk.name,module); //创建一个entrypoint constentrypoint=self.entrypoints[chunk.name]=newEntrypoint(chunk.name); //将chunk创建的chunk保存在entrypoint中,并将该entrypoint的实例保存在chunk的entrypoints中 entrypoint.unshiftChunk(chunk); //将module保存在chunk的_modules数组中 chunk.addModule(module); //module实例上记录chunk的信息 module.addChunk(chunk); //定义该chunk的entryModule属性 chunk.entryModule=module; self.assignIndex(module); self.assignDepth(module); self.processDependenciesBlockForChunk(module,chunk); }); self.sortModules(self.modules); self.applyPlugins0("optimize"); while(self.applyPluginsBailResult1("optimize-modules-basic",self.modules)|| self.applyPluginsBailResult1("optimize-modules",self.modules)|| self.applyPluginsBailResult1("optimize-modules-advanced",self.modules)){/*empty*/} self.applyPlugins1("after-optimize-modules",self.modules); while(self.applyPluginsBailResult1("optimize-chunks-basic",self.chunks)|| self.applyPluginsBailResult1("optimize-chunks",self.chunks)|| self.applyPluginsBailResult1("optimize-chunks-advanced",self.chunks)){/*empty*/} self.applyPlugins1("after-optimize-chunks",self.chunks); self.applyPluginsAsyncSeries("optimize-tree",self.chunks,self.modules,functionsealPart2(err){ if(err){ returncallback(err); } self.applyPlugins2("after-optimize-tree",self.chunks,self.modules); while(self.applyPluginsBailResult("optimize-chunk-modules-basic",self.chunks,self.modules)|| self.applyPluginsBailResult("optimize-chunk-modules",self.chunks,self.modules)|| self.applyPluginsBailResult("optimize-chunk-modules-advanced",self.chunks,self.modules)){/*empty*/} self.applyPlugins2("after-optimize-chunk-modules",self.chunks,self.modules); constshouldRecord=self.applyPluginsBailResult("should-record")!==false; self.applyPlugins2("revive-modules",self.modules,self.records); self.applyPlugins1("optimize-module-order",self.modules); self.applyPlugins1("advanced-optimize-module-order",self.modules); self.applyPlugins1("before-module-ids",self.modules); self.applyPlugins1("module-ids",self.modules); self.applyModuleIds(); self.applyPlugins1("optimize-module-ids",self.modules); self.applyPlugins1("after-optimize-module-ids",self.modules); self.sortItemsWithModuleIds(); self.applyPlugins2("revive-chunks",self.chunks,self.records); self.applyPlugins1("optimize-chunk-order",self.chunks); self.applyPlugins1("before-chunk-ids",self.chunks); self.applyChunkIds(); self.applyPlugins1("optimize-chunk-ids",self.chunks); self.applyPlugins1("after-optimize-chunk-ids",self.chunks); self.sortItemsWithChunkIds(); if(shouldRecord) self.applyPlugins2("record-modules",self.modules,self.records); if(shouldRecord) self.applyPlugins2("record-chunks",self.chunks,self.records); self.applyPlugins0("before-hash"); //创建hash self.createHash(); self.applyPlugins0("after-hash"); if(shouldRecord) self.applyPlugins1("record-hash",self.records); self.applyPlugins0("before-module-assets"); self.createModuleAssets(); if(self.applyPluginsBailResult("should-generate-chunk-assets")!==false){ self.applyPlugins0("before-chunk-assets"); //使用template创建最后的js代码 self.createChunkAssets(); } self.applyPlugins1("additional-chunk-assets",self.chunks); self.summarizeDependencies(); if(shouldRecord) self.applyPlugins2("record",self,self.records); self.applyPluginsAsync("additional-assets",err=>{ if(err){ returncallback(err); } self.applyPluginsAsync("optimize-chunk-assets",self.chunks,err=>{ if(err){ returncallback(err); } self.applyPlugins1("after-optimize-chunk-assets",self.chunks); self.applyPluginsAsync("optimize-assets",self.assets,err=>{ if(err){ returncallback(err); } self.applyPlugins1("after-optimize-assets",self.assets); if(self.applyPluginsBailResult("need-additional-seal")){ self.unseal(); returnself.seal(callback); } returnself.applyPluginsAsync("after-seal",callback); }); }); }); }); }
在seal中可以发现,调用了很多不同的插件,主要就是操作chunk和module的一些插件,生成最后的源代码。其中createHash用来生成hash,createChunkAssets用来生成chunk的源码,createModuleAssets用来生成Module的源码。在createChunkAssets中判断了是否是入口chunk,入口的chunk用mainTemplate生成,否则用chunkTemplate生成。
第五步:通过emitAssets将生成的代码输入到output的指定位置
在compiler中的run方法中定义了compile的回调函数onCompiled,在编译结束后,会调用该回调函数。在该回调函数中调用了emitAsset,触发了emit事件,将文件写入到文件系统中的指定位置。
总结
webpack的源码通过采用Tapable控制其事件流,并通过plugin机制,在webpack构建过程中将一些事件钩子暴露给plugin,使得开发者可以通过编写相应的插件来自定义打包。
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。
参考文章:
细说webpack之流程篇
webpack源码解析