本文首发于个人博客 https://github.com/879479119/879479119.github.io/issues/2,欢迎star
转载请联系作者
此篇博客紧承上一篇,上片讨论了我们的webpack整个处理单个文件的流程,这一节主要说一说webpack的文件打包问题,其实本身是比较简单的,但是有异步块和html-plugin的加入,使这个步骤变得尤为复杂,这里先介绍几个重要的概念:
Module,模块,我们的入口文件就是一个模块
Block,一个新的资源块,我们在最后进行打包的时候,块里的东西会被打包成一个新的资源
Dependency,依赖而已,所有依赖如果不进行处理会被打包到一起,然后通过她们存好的ID在最后使用的时候拿出来使用
-
Variables,不知道干什么用,暂时的使用中一直为空,最近才发现他会给我们的代码里面注入一些IIFE函数,这个过程叫做variable injection(变量注入)绑定一些需要计算的特殊值,比如global,process这一类,直接替换不太好,这是最终打包时的部分代码,可以看到我们的variable最后会被处理成为一个立即执行的函数,拼接出来的字符串参数在这里是module和global
在下面的call中参数进行拼凑时,通过我们的sourceId得到需要引入的对应资源
上一节中,我们成功的对每个文件进行了处理,并通过了process的方法对所有入口文件以及他们的依赖文件进行了处理,获得了最初的依赖文件列表,现在我们就可以对资源的依赖进行优化处理,本片的内容将从webpack/lib/Compiler.js:510的断点开始逐步的对源码进行分析
seal
在seal之前,由于一轮compilition已经执行完成,先调用finish方法进行收尾处理与之对应的是我们注册的finish-modules事件,
这里我们首先看到的又是index.ejs这个老朋友,由于他是单独的文件经过了loader处理没有获得额外的处理函数的依赖,所以最终这里看到的module实际上是它的js外壳包起来的ejs文件,此阶段也还没有进行资源hash的注入等等
这里有一个FlagDependencyExportsPlugin进行了操作,听名字可能就听出来了,他是对我们资源中的export使用进行一个标志的作用,和我们最终做出的tree shaking效果可能是相关的
调用seal事件处理
处理我们的preparedChunk,这个东西是我们刚好在进行addEntry的时候添加上的不知道你们还记不记得,中途就没有添加过新的,所以讲道理,一个entry是只用一个的,但是这里使用了一个数组不知道有什么用意
然后把这个入口模块添加到了block里面,过后打包也是从block里面拿数据,block里面的东西会被打包成为单独的文件,但是还是工作在之前的上下文中,这里可以通过看一下这里的import即是我们之前在路由文件中通过import函数设置引入的动态加载路由资源
进入到processDependenciesBlockForChunk函数,就开始处理我们之前做好准备的block了,这里这是一个不断处理依赖的过程,但是没有使用递归的做法,毕竟文件太多了,不断的进行递归会浪费很多空间,取而代之的是使用queue进行记录,处理过程中不断把新的需要处理的模块放到queue里面等待下一步处理
在每一步的处理中
-
首先处理variable,这个东西简直罕见,不过它也是依赖模块,像这个地方的他就是在替换浏览器环境的时候用到的变量依赖,可能会再之后的处理用,像是一些polyfill可能就是这样的工作方式
然后是dependency,向当前的chunk上添加module,并且这个module的集合还是个set,也就是相同的模块是不会再添加进去了,所以这里如果是新的模块的话会给之前的queue上面push一项新的资源上去
最后处理block资源,会添加新的block资源,并且按照一个Map,如果父模块是新的block,则为他开辟一个项目,把我们的模块和对应的依赖放进去最后得到一个Map类型的chunkDependencies,在我们这里处理应该是只有入口模块,在底下的dep数组中挂载剩下的异步block才对,但是事与愿违
处理完这一波循环依赖过后,本身的依赖树结构变得扁平化,之前一层一层的模块通过dependency连接起来作为一个树的结构,而现在变成了顶层最终的几个chunk
可以看到我们最终在这个入口(entry)设置中拿到了9个chunk,她们都有_modules属性,我们的所有依赖都是放到这里面的,是用的一个Set进行存储,其中的依赖关系则是通过origins和reason等标识进行模块间关系的连接的
还可以将我们的入口chunk和异步加载的chunk进行一些对比(上面的是入口文件),下面的chunk中出现的origins就是指向我们之前的router那个module
这个图里也可以看到,两个chunk实际上按照自己的路子搜集了所有的依赖,结果导致了_modules的文件数量都达到了一千多个,这就是我们常使用的CommonChunk插件需要处理的地方了,稍后进行讨论
这轮处理我们成功的把主要的入口module和异步加载的模块区分开了,然后开始按照类似的逻辑处理我们的第一个入口模块
这个时候拿到chunkDependencies进行处理,这就是之前那个存储block的东西,但是有个很奇怪的地方,就是这里面居然只有三个chunk,而不是和上面的一样是9个也不是只有一个入口模块,这就让人无从下手了(我异步加载的模块并不是一样的,而且这些模块之间没有没相互依赖)
喜闻乐见进行第二次处理,首先取出一个chunk拿到对应的存储在value中的deps,对每一个项目添加上了他们的parent,但是有个组件就是用来removeParent的
在RemoveParentModulesPlugin这个插件中,针对每个module都做了处理,看看这些模块在哪些chunk之中有被使用到,把他们所存在的chunks按照id记录下来,并改变她们的reason为几种统一的chunk组合数组。这样就做到了每个module知道了自己被哪些chunk使用,但是从之前的单一reason到现在的多reason具体不知道有什么用(恩。。可能是为作用域提升做准备)
然后嘛,移除空的模块,不需要多解释
然后这层处理就算完啦,主要进行了模块的依赖梳理和拆分,并为他们添加上了指向父节点的指针(话说之前不是有origins吗)
sortModules
对模块进行排序工作,不过只是按照索引进行排序罢了,那个按照出现概率进行排序处理的插件不是在这里工作的
optimize
optimize-modules-advanced(还有另外两个)
又是那个flag的插件进行了处理,但是只是把所有模块的used设置为了true,还有为一些被依赖的module设置上他们的usedExports为true
ChunkConditions插件用于监视模块上是否有chunkCondition函数,并返回他的执行结果,如果有模块的此函数返回了false,那么将会重写这个模块(重写即是重新添加进入parent的链接以及reason等的设置)并且还会返回true,到至此过程不断执行直至condition全部OK
RemoveParentModulesPlugin这个插件的作用有点玄乎,看样子是对每个chunk进行处理,看对于多个chunk中都有的某一些module,会直接把他们的reason设置为主要的入口chunk,而后把当前chunk中的module移除掉(话说这个事情不是应该Common来做吗)
然后移除所有空的模块,再就是移除重复的模块了(话说一直用set神他妈还会有重复的)
CommonChunk
因为这些优化处理的插件都是放在一个while循环中的,所以如果对于他这种等幂操作做的一些优化就是利用自己的文件路径名做了一个标志位,检查确认只执行一次就好了
-
由于我们在设置中取好了名字叫vender,
那么这个地方就会直接从产生的chunk中拿到这个要处理的chunk资源,也就是说这里实际上拿到的还是chunk中依赖的内容,而不是全部的node_modules中的内容,那么为什么会出现基本上所有node包中的资源都被打包到vender里面的情况呢?因为我们这里做的minChunk函数实际上是对有所依赖的chunk才做到了过滤的这里有两个概念,一个是target,一个是affected,其中target就死我们设置好的用来存储提取公共文件的一个chunk,而affected是我们其他需要被提取资源的包,经过一些筛选最终得到的是我们的index模块,然后这里也理所当然的对所有index的依赖进行筛选,导致最后所有的node_modules里面的资源都被放到vender中 在vender中添加了过后,当然还要把原来chunk中的依赖全部移除掉,也就是简单的删除操作
删除了不用我说,也会给新的CommonChunk添加上哪些被删除模块的链接,经典的操作,给双方都添加上指针
然后最后再把我们的index的parent指向vender,毕竟现在index中的资源已经完全依赖vender了,然后处理了entry,也添加上了新的依赖
返回true,导致在进行一次优化,不过我们在开始的时候会做判断,这个插件相当于不会再执行功能了
然后进行各种优化,比如出现的概率大的放到前面,这里还是做了module和chunk两种优化,也是有毛病,就像我们的react项目中可以知道react的使用次数最多,那么他就被放到了最钱前面,紧随其后的是echart等
HashedModuleIdsPlugin插件为我们的模块计算出它的id,默认是通过md5进行计算,解出来的是base64的,而且计算的参数也仅仅只是通过模块的libId进行hash,而这个libhash只是相对位置,连绝对的都不是,所以算下来这个东西能够当成单个文件的hash了
applyModuleId,到这里你可能会想,诶之前不是已经设置好每个元素的id了吗,为什么还要搞这么个函数专门处理,我们在上一个生成id的时候实际上得到的id是根据我们的设置进行了截断的,实际上拿到的hash碰撞的概率非常大,我们看看下面这个筛选的处理就可以知道,1885个模块里面竟然又3个重复的id,这种时候就要特殊处理了
- 因为组件不知道我们的id会不会是数字,或者是字符串的hash,所以会先判断数字,然后拿到最大的那一个,在她上面新添加新的id,肯定就不会冲突了嘛
- 如果不是数字的时候,那么还是会执行类似的过程,只不过最终打包出来会发现有一些模块的名称是数字的,那就是冲突的模块新添加的id啦
执行sortItemsWithModuleIds依据id进行排序,不只是最外面的chunk,就连reason里的id也会被重新排序,也是蛮逗的,这里直接用的是id做比较并没有判断类型,也就是说把数字和字符串会混到一起,就算你是class也会拿valueOf出来比较,想想还是蛮刺激的,不过其实比较完成也没有太特殊的用途就这么随意一点也好
中间一些处理recordId的我忽略掉了
hash
然后开始处理hash了,这里的hash具体使用了哪些参数和长度是多少呢
可以在此阶段添加hashSalt即噪声,给hash值添加一些特征
进入mainTemplate的处理函数中,添加了一些字符串参数和数字参数,并且调用了mainTemplate的hash插件,但是她们的执行过程并不是保证我们最后生成的文件中能够有结果的hash值,便于请求对应的资源文件,而是仅仅在hash的过程中添加了一些干扰的路径参数等
最终一轮hash下来,chunk会得到自己的renderHash,而compilation会得到一个针对编译过程的hash,这个hash就跟我们的所有资源扯上关联啦,所以每次都是新的
createModule/ChunkAssets
创建模块资源咯~
- 先来看看index.ejs文件如何处理自己的资源文件,空的。。。assets对象里面存储的就是我们需要新创建出来的文件。。然而他是一个空对象
- 然后处理chunk的资源文件,我们要生成的文件是在这里生成的。所以说这个东西也特么能算一个chunk咯,不过为什么这个html文件可以没有js那些头啊尾的内容给包起来呢?
- 然后把我们的资源存入缓存中,这里的缓存键名实际上就是我们的模块id前面加个c而已,照这样缓存起来,如果没有缓存结果的话再根据hasRunTime函数,判断chunk是入口还是拆分出来的chunk,根据mainTemplate或chunkTemplate的render函数进行渲染结果的操作
bootstrap
说到底,这里就是在拼接字符串的过程,但是其实我们使用的应用模块中有时require有时webpack_require是怎么来的呢,到处都看到的requireFn为什么不直接设置成为require或者加上webpack的形式呢
先调用bootstrap的插件,执行封装头的过程,这里首先会拿到HotModule的一些插件处理,主要是插入模块热替换的一些工具,相关源码在/webpack/lib/HotModuleReplacement.runtime.js中,下次讲模块热替换会进行专门的分析
-
在此紧要关头,又触发了hot-bootstrap的操作,NodeMainTemplatePlugin也来凑热闹,拿到了我们默认设置的热替换资源json文件名和操作的update的js文件名字,然后顺势又把asset-path的事件给调用了,把我们的模式文件名化为了表达式的存在,便于过后直接进行替换存储
如[name].[chunk].js变成了"" + chunkId + "." + hotCurrentHash + ".hot-update.js"类似的样子
然后取出并返回我们的模板函数内容,这里的模板函数没有使用字符串的方式进行存储,而是直接使用的获取函数toString的方式拿到其中的内容,再对一些特殊变量名的位置进行替换,岂不是美滋滋(模板有两种一种同步一种异步)
把我们刚才得到的hot资源还有源码资源等全部合并压缩为字符串,我们暂且就叫这一部分叫bootstrap吧
local-val
- 添加installed-module这个变量来记录我们本地已经使用过的模块
require
- MainTemplate会在一个注入的对象module-obj上添加模块的基本属性,hot相关插件会添加,这个组件本身的hot处理以及它的parent(这也是模块热更新的基础之一)
- strictModuleExceptionHandling用于选择是否用try....catch包裹住我们的业务代码,当为true的时候会抱起来,执行出了错就把当前的模块从缓存中删掉,好像什么也没发生一样,但是这样别的模块就完全无法得到它的内容了,所以也算是从另一种角度讲的strict了,是不是很神奇的操作
require-extension
Main中处理了大部分和web-requi这个变量相关的值,并且设置了通过_esMoudle来确定是ES6模块还是COMMONjs模块,过后再看是否需要default把需要的模块导出
这里是通过defineProperity的方法定义的getter,但是这样也导致了我们的模块如果不做特殊处理,不能够兼容上古浏览器。而且还有一点值得注意的是过度使用定义对象属性的方法会导致较大的性能损失
NodeMain又要放什么洋屁呢?chunk太少了没放出来
-
Hot,看起来只是利用wr来设置了一下自己的相关变量挂载对象而已,回忆起来其实好多模块都是拿过去干这个事
startup
- Main,如果存在入口模块,那么开始拼接他的依赖头列表,最重要的当然就是把我们的入口模块的id记录到其中,相当于就是在这里找到最初的模块开始执行的所有业务操作,首先来到的是Hotmodule生成的相关hot模块
render
-
Main,创建一个ConcatSource用于拼接资源,可以看到下面这一段决定了整个文件的结构,首先是我们拼装好的bootstrap(bootstrap里面存上了我们启动模块的id,可以用于流程的发起工作),然后就是紧随其后的参数了,想必大家也都知道,这些参数就是我们的所有模块了,不过一直不知道为什么webpack毛病老是要在前面加星号的注释
然后就是依次处理每个module了,通过ModuleTemplate的render方法进行处理,在其中搜集他的资源,按照经典的从大到小的顺序搜寻chunk,block,dependency。在这个处理过程中又出现了variable,之前一次看到的时候以为他是充当了一个代替变量的作用,这次呢,看样子实际上好像是被注入到了我们最外层的wrapper函数中当做参数使用诶!
-
承接上文现在执行module方法,EvalSourceMapDevToolModuleTemplatePlugin分别取出了我们资源的source和map,注意webpack中有很多地方都是喜欢用reduce对资源进行处理,没发现对性能有多大的提升,只不过让你少在外面建立一个对象,看起来更优雅
-
我们仔细看看创建sourceMap的最终操作
const footer = self.sourceMapComment.replace(/\[url\]/g, `data:application/json;charset=utf-8;base64,${new Buffer(JSON.stringify(sourceMap), "utf8").toString("base64")}`) + `\n//# sourceURL=webpack-internal:///${module.id}\n`; // workaround for chrome bug
发现这里其实针对chrome的bug做了些处理,才有了现在这种猎奇的webpack-internal:///格式的路径名字,所以以后看到不要惊慌了,1版本的时候是对应文件的,升级到3就不是了,想知道具体是什么BUG可以去issue找找
这里由于我们当时设置的sourceMap就是eval-cheap的所以最后得到的代码内容也就变成了上面一个eval全部抱起来,下面一个sourceMap的base64link而已
处理完成模块的封装我们就来渲染吧,执行render事件,千万不要搞糊了,这些事件名称很多重复的,但是他们是针对不同的Tapable组件作用的,比如现在执行这个就是绑定在MainTeplate上面
-
FunctionModuleTemplatePlugin闪亮登场,就是她把我们的每个模块单独分装起来的,比较短就直接贴代码了,可以看到wr这个参数要添加上去还是得花些功夫,因为只有在需要用到那些加载函数相关的时候才会用到,如果一个模块已经不依赖别的模块的话,那么再把他添加上是没有意义的,还有就是上面的module和exports有时会按照需求变成webpack打头的;记得之前说的use strict吗?,需要的话就在这里统一加上了
全部都拼凑好啦!至此,模块构建完成!剩下的就只是把他们打包放到文件中!
这些文章写的都有点水,相当于是阅读源码时候做的笔记了,看看图个乐子吧