You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
//this.hooks.resolveif(scheme){//...}elseif(contextScheme){//...}// resource without scheme and without pathdefaultResolve(context);constdefaultResolve=context=>{if(/^($|\?)/.test(unresolvedResource)){//...}// resource without scheme and with pathelse{//创建解析器constnormalResolver=this.getResolver("normal",//dependencyType: "esm"dependencyType
? cachedSetProperty(resolveOptions||EMPTY_RESOLVE_OPTIONS,"dependencyType",dependencyType)
: resolveOptions);this.resolveResource(contextInfo,context,unresolvedResource,normalResolver,resolveContext,(err,resolvedResource,resolvedResourceResolveData)=>{//...continueCallback();});}};
returnthis._doBuild(options,compilation,resolver,fs,hooks,err=>{// if we have an error mark module as failed and exitif(err){//...}//...letresult;try{constsource=this._source.source();//调用parser转换成ASTresult=this.parser.parse(this._ast||source,{
source,current: this,module: this,compilation: compilation,options: options});}//...});
constonDependenciesSorted=err=>{if(err)returncallback(err);// early exit without changing parallelism back and forthif(sortedDependencies.length===0&&inProgressTransitive===1){returncallback();}// This is nested so we need to allow one additional taskthis.processDependenciesQueue.increaseParallelism();for(constitemofsortedDependencies){inProgressTransitive++;this.handleModuleCreation(item,err=>{// In V8, the Error objects keep a reference to the functions on the stack. These warnings &// errors are created inside closures that keep a reference to the Compilation, so errors are// leaking the Compilation object.if(err&&this.bail){if(inProgressTransitive<=0)return;inProgressTransitive=-1;// eslint-disable-next-line no-self-assignerr.stack=err.stack;onTransitiveTasksFinished(err);return;}if(--inProgressTransitive===0)onTransitiveTasksFinished();});}if(--inProgressTransitive===0)onTransitiveTasksFinished();};
//compiler.jscompile(callback){//...err=>{if(err)returncallback(err);//...logger.time("make hook");this.hooks.make.callAsync(compilation,err=>{logger.timeEnd("make hook");if(err)returncallback(err);logger.time("finish make hook");this.hooks.finishMake.callAsync(compilation,err=>{logger.timeEnd("finish make hook");if(err)returncallback(err);process.nextTick(()=>{logger.time("finish compilation");compilation.finish(err=>{logger.timeEnd("finish compilation");if(err)returncallback(err);logger.time("seal compilation");compilation.seal(err=>{//...});});});});});});
make hooks之后是finishMake hooks,在这里面会调用compilation.finish。
编译构建模块
上一篇讲了关于webpack初始化做了哪些工作,之后会调用make hook分步骤进行处理模块。
Make
make hooks注册了
EntryPlugin
,它会调用compilation.addEntry
处理入口模块首先会创建依赖,这个依赖叫EntryDependency,之后会将dep,options,context作为传参到compilation.addEntry
addEntry
因为支持MPA(multiple entry points),所以会判断有无设置过的entry。因为配置的时候是只有一个入口为
entry: "./index.js"
,所以在前面处理options时会转成对象,name为"main"addModuleTree
保存完EntryData后就会执行addModuleTree
首先会拿到对应的moduleFactory,它可以为
NormalModuleFactory
或ContextModuleFactory
。不同的依赖对应不同的处理方式 比如EntryDependency和ImportDependency对应NormalModuleFactory
, AMDRequireContextDependency 对应ContextModuleFactory
,它们都是之前初始化插件的时候注入到dependencyFactories
里的。handleModuleCreation
得到对应的moduleFactory后,handleModuleCreation将正式开始处理模块了,包括后续递归处理引入的外部依赖也是调用它
moduleGraph是个实例化对象,用于保存依赖,模块之间的引用信息等,用以后续的分析。
factorizeModule会调用
this.factorizeQueue.add
,用来解析模块,它是一个AsyncQueue。 Compilation包含几个AsyncQueue用来分步骤处理模块,每一个AsyncQueue就是一个taskAsyncQueue
Compilation为模块不同的操作分别定义了异步队列,队列提供了很多方法,比如可以判断队列是否在运行,停止,清除队列等, 可以传入parallelism控制并行运行的任务数量等(用于微调性能或者让处理模块时序正确保证分析结果)。AsyncQueue是webpack5才有的代码,Commit message如下
this.factorizeQueue.add会调用AsyncQueue的add方法。接着创建AsyncQueueEntry保存在AsyncQueue的_entries和_queued里。当可以运行时,会执行setImmediate(root._ensureProcessing)。
当执行_ensureProcessing的时候,会循环将队列拿出来,因为我们调用的是factorizeQueue,所以第一次循环取出来的addModule的队列是空的会被跳过。所以会执行
factorizeQueue
的_processor, 它是this._factorizeModule.bind(this)
factorizeModule
factorizeModule用于resolve模块信息,比如相对,绝对路径,packjson内容等。
第一步会先调用factory.create,然后将结果传回去,originModule表示当前模块是被谁引用的,因为处理的是入口文件,所以originModule为undefiend。所以factory.created的参数只用关注dependencies为
Array<EntryDependency>
,context为程序运行目录。NormalModuleFactory.create
create会初始化属性并调用钩子this.hooks.factorize.callAsync一共有两个插件。第一个是
ExternalModuleFactoryPlugin
,会对external配置的模块进行处理。第二个插件的名字叫NormalModuleFactory
,他是之前实例化NormalModuleFactory时注册进来的在实例化NormalModuleFactory的时候会注册两个插件,一个在
factorize
阶段,一个在resolve
阶段。factorize
钩子会调用resolve
钩子。resolve
下注册的hook代码如下NormalModuleFactory.hooks.resolve
首先会对context和request调用getScheme。context是执行目录,request就是我们的入口文件
/index.js
。getScheme会解析对应的字符串是否是某些URL方案。比如可能是file:///user/webpack/index.js
或者data:text/javascript;base64...
,如果是的话会丢到相应的dataUrlPlugin
或fileUrlPlugin处理
继续resolve hook之后的代码
如果没有scheme,会判断是否含有inline-loader,如果有会被拆分开来。比如
!style-loader!css-loader?modules!./styles.css
会拆成如下对象,然后丢进resolveRequestArray
进行相关处理接着会调用
defaultResolve(context)
生成模块解析器。this.getResolver
会调用ResolverFactory生成解析器,解析器用于解析文件绝对路径等信息。ResolverFactory内部的功能扩展于enhanced-resolve。enhanced-resolve是一个高度可配置的resolve库,比如我们经常配置resolve.alias用别名代替某些路径,配置resolve.extensions用于扩展名等。
根据getResolver传进来的参数会实例化enhanced-resolve并调用resolverFactory的钩子合并options
接着调用
this.resolveResource
,它会调用刚刚生成的resolver解析模块resolver.resolve会得到resolvedResource和resolvedResourceResolveData。
resolvedResource为
c:\\Users\\Administrator\\Desktop\\webpack\\index.js
,是执行webpack的入口文件的绝对路径。resolvedResourceResolveData会包含一些解析相关信息。接着回到this.resolveResource回调执行
continueCallback
函数首先会执行
this.ruleSet.exec
,之前的初始化篇讲过它是用来根据资源输出对应匹配的loader。解析出来的结果有三种类型,use
,use-post
和use-pre
,然后放置在对应的useLoadersPost
,useLoaders
和useLoadersPre
。在webpack1的时候有相应的配置,可以在loader处理之前或之后执行其他 预/后 处理loader。但是在v2后就被移除了。
之后会对每个loaders Array进行resolveRequestArray处理,因为没有preLoader和postLoader,所以我们只用关注中间的resolveRequestArray
async是一个异步库,map方法会循环array,然后把每个item给resolver.resolve执行,最后调用callback。resolver是之前调用的
this.getResolver("loader")
创建的LoaderResolver,resolver解析loader相应的绝对路径,比如传进去的为babel-loader
, 出来的就是node_modules包里入口地址C:\\Users\\Administrator\\Desktop\\webpack\\node_modules\\.pnpm\\babel-loader@8.2.3_ed870ac3ba52c4ec230ba2bc3dbb311c\\node_modules\\babel-loader\\lib\\index.js
。然后将结果回调给continueCallbackcontinueCallback会整合所有之前resolve的数据(资源信息,loader信息)到createData
createData还会初始化对应的parser和generator,对于不同的type会有不同的parser和generator,用于webpack处理不同的资源和输出不同代码
Parser
JavascriptParser
AssetParser
JsonParser
WebAssemblyParser
CssParser
Generator
webpack5也允许对Parser和Generator进行配置以获得某些功能或者更改输出配置
NormalModuleFactory.hooks.factorize
至此,resolve hooks的工作都做完了,将会回到factorize hooks。
之后便会根据createData实例化NormalModule,这样NormalModule就保存了当前模块的解析信息。一个模块会对应一个NormalModule,之后模块相关的操作都会在NormalModule进行。
this.hooks.module.call会有两个插件 ,一个判断packjson的有无sideEffects 配置,一个判断Rules有无sideEffects 配置。对模块设置sideEffects能够更友好的让webpack进行tree shaking
到这里,整个模块相关信息就解析完了, 将会开始回收栈,把结果返回给this.hooks.factorize,回到factory.create,再回到_startProcessing里的_processor
_handleResult会将一些属性赋值给AsyncQueueEntry对象并执行callback,还记得最初是由this.factorizeModule调用的吗
它将会用resolveData创建的NormalModule丢进this.addModule处理,
addModule AsyncQueue
this.addModule和this.factorizeModule一样也是AsyncQueue。
首先会获取模块的identifier,它会拼凑模块的type,request和layer。然后将module和对应的moduleGraph加入到map里。然后执行的this.addModule的回调
首先会循环dependencies并将当前module和依赖丢进setResolvedModule,originModule是父模块,比如
index.js
从module_c.js
引入了个变量,module_c
的originModule就是index
。ModuleGraph
setResolvedModule会执行一些ModuleGraph操作
ModuleGraph.js
里会包含ModuleGraph、ModuleGraphConnection、ModuleGraphModule,它们会收集模块间的依赖关系,并为后续处理提供信息ModuleGraph会被实例化,然后赋值到Compilation对象里,之后只要调用Compilation.ModuleGraph就能进行相关操作。
ModuleGraph有两个重要的Map,
_dependencyMap
和_moduleMap
。_dependencyMap接收Dependency为键,ModuleGraphConnection为值,Dependency比如为EntryDependency,ImportDependency等。
_moduleMap接收Module为键,ModuleGraphModule为值 , Module就是我们resloveData创建的NormalModule。
对于Map数据结构,之后就可以通过Module和Dependency对象直接方便找到对应的相关ModuleGraphConnection或ModuleGraphModule信息
ModuleGraphConnection保存引用信息,当前模块,父模块,依赖等。weak表示为弱依赖,用于处理一些SSR场景
incomingConnections保存模块的被引用集合,outgoingConnections保存模块的引用集合,集合对象都是ModuleGraphConnection。
ModuleGraphModule还保存着其他的信息,比如ExportsInfo,之后会用于保存模块的导出信息,并在优化的时候分析是否被使用来实现tree shaking
回到addModule
它会循环每个依赖并调用setResolvedModule
setResolvedModule会通过当前module新建ModuleGraphConnection实例,并保存在module对应的ModuleGraphModule的incomingConnections里。
之后如果有父模块就保存在outgoingConnections里,否则在_dependencyMap里保存connection。
最后会执行this.addmodule里的_handleModuleBuildAndDependencies,开始处理模块内容。
buildModule AsyncQueue
this.buildModule同样也是一个AsyncQueue,它最终会调用Module.build开始构建模块
module.build会初始化构建信息,源码,AST,buildInfo 等,然后调用_doBuild
_doBuild首先会创建loaderContext,用于runLoaders的参数,runLoaders来自loader-runner,这是webpack用于运行Loaders的库。
processResource用于该怎么读资源给Loader,因为我们的入口文件是"/indx.js",所以readResource hook会拿出
FileUriPlugin
,并进行fs.readFile,将文件Buffer取出。runLoaders会将result返回,它包含resourceBuffer和result,如果有相应的loader,result将会转换成js可操作的字符串。
下图是使用babel-loader生成的结果
最后会将结果传给processResult进行处理
processResult会要求loader必须返回Buffer或者String,然后会调用createSrouce对result处理,createSource会调用webpack-sources库,能够生成有或者无sourceMap的源码。
最后初始化AST,调用callback,callback会回到_doBuild。
_doBuild会把结果进行解析,还记得吗,这里的parser是在resolve阶段生成的,因为当前模块是js资源所以生成的是JavascriptParser。
JavascriptParser
JavascriptParser._parse函数用于解析js,在函数里会用acorn作为parser,然后转换成AST(抽象语法树),对于AST(抽象语法书)的树结构可以去astexplorer探索。
this.detectMode用于去检测是否含有
"use strict"
或"use asm"
,"use strict"
代表当前为严格模式,"use asm"
表示是使用asm.js编译出来的,帮助浏览器优化性能。preWalkStatements
preWalkStatements用于迭代声明变量的范围
preWalkStatement会遍历AST body,如果遇到最外层的变量声明,就定义变量到this.scope.definitions。definitions是个map结构,name为key,this.scope为值。
比如遇到
var a = 2
, 就符合VariableDeclaration
的case,然后将"a"保存到definitions map里,对应的this.scope的topLevelScope为true。当然如果在if,for等block是用let声明的,那么会跳过,后续再定义scope,因为它们不属于topLevelScope。blockPreWalkStatements
接着就是调用blockPreWalkStatements
在blockPreWalkStatements里,如果遇到的声明是ImportDeclaration的时候会调用blockPreWalkImportDeclaration。比如
import some form "some"
blockPreWalkImportDeclaration先会获取module名字,如
import { some } form "loadsh"
, source.value就为loadsh。然后会调用parser的import hook。HarmonyImportDependencyParserPlugin做的事情就是创建HarmonyImportSideEffectDependency并收集到模块的Dependencies数组里。
HarmonyImportSideEffectDependency是用于生成最后的import代码。比如
之后会根据不同的Import specifier给importSpecifier hooks传不同的参数,该钩子的插件会设置import变量的scope,跟平常变量不同的是,它会创建VariableInfo对象set到this.scope.definitions。VariableInfo除了包含作用域,还包含模块名字等信息。
如果遇到的是export,也会执行上述操作,不同的地方在于会创建不同的Dependency。
walkStatements
walkStatements除了会遍历块内变量并定义对应scope,还会做其他事情。
webpack很聪明,虽然有import,但如果没有使用的话,也不会去请求依赖,除非有变量引用了它。
当遇到
VariableDeclaration
,会从scope拿出这个变量引用信息,如果取出来的是VariableInfo
,说明是import的依赖,就会调用相关插件创建HarmonyImportSpecifierDependency
并添加到module.dependencies如果遇到
FunctionDeclaration
,并且为ImportExpression
。说明是个dynamic-imports,然后会调用ImportParserPlugin
插件进行处理。为了支持magic comments,
ImportParserPlugin
会解析出comments,并对其进行相关设置。之后会创建AsyncDependenciesBlock
,平常的import会添加到module.dependencies,但是动态导入是添加到module.block,而且处理优先级也在最后。到这里,buildModule的事情就基本完成了,将会回到buildModule回调。
processModuleDependencies AsyncQueue
在buildModule里会调用
processModuleDependencies
,它是个AsyncQueue,processor为_processModuleDependencies
_processModuleDependencies会循环模块的dependencies和block。前面说过dependencies里是正常的import,block里是Dynamic import,它们都会经过processDependency进行处理。
因为在parser的时候添加
HarmonyImportSpecifierDependency
依赖是根据代码引用位置,在dependencies里是乱序的。所以processDependency的作用就是根据import前后位置进行排序,然后将相同模块的几个依赖放在一起。排序完后调用onDependenciesSorted处理所有外部依赖
onDependenciesSorted会循环依赖,并执行handleModuleCreation,这个函数不就是最开始处理入口模块的吗?是的,从开头的handleModuleCreation到这就是webpack递归处理所有引入模块的过程。
当所有依赖都处理完后就会从processDependenciesQueue往后回收栈,回到buildQueue,factorizeModule,回到最初调用handleModuleCreation的地方
handleModuleCreation会停止所有任务队列并回调到make hook
finishMake
make hooks之后是finishMake hooks,在这里面会调用compilation.finish。
compilation.finish里大部分代码都在输出日志,但里面有个finishModules hooks里有个重要的插件
flagDependencyExportsPlugin
。它会将所有模块的export信息添加到NormalModule对应的ModuleGraphModule里,之后在优化阶段的时候用于分析是否被使用,是否需要tree shaking。到这里make的所有环节就结束了
总结
总的来说make环节主要就是在处理模块,webpack将处理模块分为几个步骤。
factorize会调用resolver去解析模块和loader的路径相关信息,并生成对应的paser和generator。
addModule会设置模块对应的ModuleGraph,ModuleGraph包含模块,外部依赖的引用信息,导出信息等。
buildModule会通过Loader-runner执行loaders,将资源转换成js能操作的目标,然后将源码paser成AST,并遍历AST Body,对不同的声明进行处理,在此期间会收集外部依赖,导出信息等。
processModuleDependencies会处理所有收集的依赖,并将依赖回到factorize,从而实现递归处理所有的模块
The text was updated successfully, but these errors were encountered: