本文首發(fā)于個人博客 https://github.com/879479119/879479119.github.io/issues/2,歡迎star
轉(zhuǎn)載請聯(lián)系作者
此篇博客緊承上一篇,上片討論了我們的webpack整個處理單個文件的流程,這一節(jié)主要說一說webpack的文件打包問題,其實(shí)本身是比較簡單的,但是有異步塊和html-plugin的加入,使這個步驟變得尤為復(fù)雜,這里先介紹幾個重要的概念:
Module,模塊,我們的入口文件就是一個模塊
Block,一個新的資源塊,我們在最后進(jìn)行打包的時候,塊里的東西會被打包成一個新的資源
Dependency,依賴而已,所有依賴如果不進(jìn)行處理會被打包到一起,然后通過她們存好的ID在最后使用的時候拿出來使用
-
Variables,不知道干什么用,暫時的使用中一直為空,最近才發(fā)現(xiàn)他會給我們的代碼里面注入一些IIFE函數(shù),這個過程叫做variable injection(變量注入)綁定一些需要計(jì)算的特殊值,比如global,process這一類,直接替換不太好,這是最終打包時的部分代碼,可以看到我們的variable最后會被處理成為一個立即執(zhí)行的函數(shù),拼接出來的字符串參數(shù)在這里是module和global
QQ20170926-213001@2x在下面的call中參數(shù)進(jìn)行拼湊時,通過我們的sourceId得到需要引入的對應(yīng)資源
QQ20170926-213511@2xQQ20170926-214118@2x?
上一節(jié)中,我們成功的對每個文件進(jìn)行了處理,并通過了process的方法對所有入口文件以及他們的依賴文件進(jìn)行了處理,獲得了最初的依賴文件列表,現(xiàn)在我們就可以對資源的依賴進(jìn)行優(yōu)化處理,本片的內(nèi)容將從webpack/lib/Compiler.js:510的斷點(diǎn)開始逐步的對源碼進(jìn)行分析
seal
在seal之前,由于一輪compilition已經(jīng)執(zhí)行完成,先調(diào)用finish方法進(jìn)行收尾處理與之對應(yīng)的是我們注冊的finish-modules事件,
這里我們首先看到的又是index.ejs這個老朋友,由于他是單獨(dú)的文件經(jīng)過了loader處理沒有獲得額外的處理函數(shù)的依賴,所以最終這里看到的module實(shí)際上是它的js外殼包起來的ejs文件,此階段也還沒有進(jìn)行資源hash的注入等等
這里有一個FlagDependencyExportsPlugin進(jìn)行了操作,聽名字可能就聽出來了,他是對我們資源中的export使用進(jìn)行一個標(biāo)志的作用,和我們最終做出的tree shaking效果可能是相關(guān)的
調(diào)用seal事件處理
處理我們的preparedChunk,這個東西是我們剛好在進(jìn)行addEntry的時候添加上的不知道你們還記不記得,中途就沒有添加過新的,所以講道理,一個entry是只用一個的,但是這里使用了一個數(shù)組不知道有什么用意

然后把這個入口模塊添加到了block里面,過后打包也是從block里面拿數(shù)據(jù),block里面的東西會被打包成為單獨(dú)的文件,但是還是工作在之前的上下文中,這里可以通過看一下這里的import即是我們之前在路由文件中通過import函數(shù)設(shè)置引入的動態(tài)加載路由資源

進(jìn)入到processDependenciesBlockForChunk函數(shù),就開始處理我們之前做好準(zhǔn)備的block了,這里這是一個不斷處理依賴的過程,但是沒有使用遞歸的做法,畢竟文件太多了,不斷的進(jìn)行遞歸會浪費(fèi)很多空間,取而代之的是使用queue進(jìn)行記錄,處理過程中不斷把新的需要處理的模塊放到queue里面等待下一步處理
在每一步的處理中
-
首先處理variable,這個東西簡直罕見,不過它也是依賴模塊,像這個地方的他就是在替換瀏覽器環(huán)境的時候用到的變量依賴,可能會再之后的處理用,像是一些polyfill可能就是這樣的工作方式
QQ20170923-184047@2x 然后是dependency,向當(dāng)前的chunk上添加module,并且這個module的集合還是個set,也就是相同的模塊是不會再添加進(jìn)去了,所以這里如果是新的模塊的話會給之前的queue上面push一項(xiàng)新的資源上去
最后處理block資源,會添加新的block資源,并且按照一個Map,如果父模塊是新的block,則為他開辟一個項(xiàng)目,把我們的模塊和對應(yīng)的依賴放進(jìn)去最后得到一個Map類型的chunkDependencies,在我們這里處理應(yīng)該是只有入口模塊,在底下的dep數(shù)組中掛載剩下的異步block才對,但是事與愿違
處理完這一波循環(huán)依賴過后,本身的依賴樹結(jié)構(gòu)變得扁平化,之前一層一層的模塊通過dependency連接起來作為一個樹的結(jié)構(gòu),而現(xiàn)在變成了頂層最終的幾個chunk

可以看到我們最終在這個入口(entry)設(shè)置中拿到了9個chunk,她們都有_modules屬性,我們的所有依賴都是放到這里面的,是用的一個Set進(jìn)行存儲,其中的依賴關(guān)系則是通過origins和reason等標(biāo)識進(jìn)行模塊間關(guān)系的連接的
還可以將我們的入口chunk和異步加載的chunk進(jìn)行一些對比(上面的是入口文件),下面的chunk中出現(xiàn)的origins就是指向我們之前的router那個module
這個圖里也可以看到,兩個chunk實(shí)際上按照自己的路子搜集了所有的依賴,結(jié)果導(dǎo)致了_modules的文件數(shù)量都達(dá)到了一千多個,這就是我們常使用的CommonChunk插件需要處理的地方了,稍后進(jìn)行討論

這輪處理我們成功的把主要的入口module和異步加載的模塊區(qū)分開了,然后開始按照類似的邏輯處理我們的第一個入口模塊
這個時候拿到chunkDependencies進(jìn)行處理,這就是之前那個存儲block的東西,但是有個很奇怪的地方,就是這里面居然只有三個chunk,而不是和上面的一樣是9個也不是只有一個入口模塊,這就讓人無從下手了(我異步加載的模塊并不是一樣的,而且這些模塊之間沒有沒相互依賴)

喜聞樂見進(jìn)行第二次處理,首先取出一個chunk拿到對應(yīng)的存儲在value中的deps,對每一個項(xiàng)目添加上了他們的parent,但是有個組件就是用來removeParent的
在RemoveParentModulesPlugin這個插件中,針對每個module都做了處理,看看這些模塊在哪些chunk之中有被使用到,把他們所存在的chunks按照id記錄下來,并改變她們的reason為幾種統(tǒng)一的chunk組合數(shù)組。這樣就做到了每個module知道了自己被哪些chunk使用,但是從之前的單一reason到現(xiàn)在的多reason具體不知道有什么用(恩。。可能是為作用域提升做準(zhǔn)備)
然后嘛,移除空的模塊,不需要多解釋
然后這層處理就算完啦,主要進(jìn)行了模塊的依賴梳理和拆分,并為他們添加上了指向父節(jié)點(diǎn)的指針(話說之前不是有origins嗎)
sortModules
對模塊進(jìn)行排序工作,不過只是按照索引進(jìn)行排序罷了,那個按照出現(xiàn)概率進(jìn)行排序處理的插件不是在這里工作的
optimize
optimize-modules-advanced(還有另外兩個)
又是那個flag的插件進(jìn)行了處理,但是只是把所有模塊的used設(shè)置為了true,還有為一些被依賴的module設(shè)置上他們的usedExports為true
ChunkConditions插件用于監(jiān)視模塊上是否有chunkCondition函數(shù),并返回他的執(zhí)行結(jié)果,如果有模塊的此函數(shù)返回了false,那么將會重寫這個模塊(重寫即是重新添加進(jìn)入parent的鏈接以及reason等的設(shè)置)并且還會返回true,到至此過程不斷執(zhí)行直至condition全部OK
RemoveParentModulesPlugin這個插件的作用有點(diǎn)玄乎,看樣子是對每個chunk進(jìn)行處理,看對于多個chunk中都有的某一些module,會直接把他們的reason設(shè)置為主要的入口chunk,而后把當(dāng)前chunk中的module移除掉(話說這個事情不是應(yīng)該Common來做嗎)
然后移除所有空的模塊,再就是移除重復(fù)的模塊了(話說一直用set神他媽還會有重復(fù)的)
CommonChunk
因?yàn)檫@些優(yōu)化處理的插件都是放在一個while循環(huán)中的,所以如果對于他這種等冪操作做的一些優(yōu)化就是利用自己的文件路徑名做了一個標(biāo)志位,檢查確認(rèn)只執(zhí)行一次就好了
-
由于我們在設(shè)置中取好了名字叫vender,
那么這個地方就會直接從產(chǎn)生的chunk中拿到這個要處理的chunk資源,也就是說這里實(shí)際上拿到的還是chunk中依賴的內(nèi)容,而不是全部的node_modules中的內(nèi)容,那么為什么會出現(xiàn)基本上所有node包中的資源都被打包到vender里面的情況呢?因?yàn)槲覀冞@里做的minChunk函數(shù)實(shí)際上是對有所依賴的chunk才做到了過濾的這里有兩個概念,一個是target,一個是affected,其中target就死我們設(shè)置好的用來存儲提取公共文件的一個chunk,而affected是我們其他需要被提取資源的包,經(jīng)過一些篩選最終得到的是我們的index模塊,然后這里也理所當(dāng)然的對所有index的依賴進(jìn)行篩選,導(dǎo)致最后所有的node_modules里面的資源都被放到vender中QQ20170924-123025@2x 在vender中添加了過后,當(dāng)然還要把原來chunk中的依賴全部移除掉,也就是簡單的刪除操作
刪除了不用我說,也會給新的CommonChunk添加上哪些被刪除模塊的鏈接,經(jīng)典的操作,給雙方都添加上指針
然后最后再把我們的index的parent指向vender,畢竟現(xiàn)在index中的資源已經(jīng)完全依賴vender了,然后處理了entry,也添加上了新的依賴
返回true,導(dǎo)致在進(jìn)行一次優(yōu)化,不過我們在開始的時候會做判斷,這個插件相當(dāng)于不會再執(zhí)行功能了
然后進(jìn)行各種優(yōu)化,比如出現(xiàn)的概率大的放到前面,這里還是做了module和chunk兩種優(yōu)化,也是有毛病,就像我們的react項(xiàng)目中可以知道react的使用次數(shù)最多,那么他就被放到了最錢前面,緊隨其后的是echart等
HashedModuleIdsPlugin插件為我們的模塊計(jì)算出它的id,默認(rèn)是通過md5進(jìn)行計(jì)算,解出來的是base64的,而且計(jì)算的參數(shù)也僅僅只是通過模塊的libId進(jìn)行hash,而這個libhash只是相對位置,連絕對的都不是,所以算下來這個東西能夠當(dāng)成單個文件的hash了
applyModuleId,到這里你可能會想,誒之前不是已經(jīng)設(shè)置好每個元素的id了嗎,為什么還要搞這么個函數(shù)專門處理,我們在上一個生成id的時候?qū)嶋H上得到的id是根據(jù)我們的設(shè)置進(jìn)行了截?cái)嗟模瑢?shí)際上拿到的hash碰撞的概率非常大,我們看看下面這個篩選的處理就可以知道,1885個模塊里面竟然又3個重復(fù)的id,這種時候就要特殊處理了

- 因?yàn)榻M件不知道我們的id會不會是數(shù)字,或者是字符串的hash,所以會先判斷數(shù)字,然后拿到最大的那一個,在她上面新添加新的id,肯定就不會沖突了嘛
- 如果不是數(shù)字的時候,那么還是會執(zhí)行類似的過程,只不過最終打包出來會發(fā)現(xiàn)有一些模塊的名稱是數(shù)字的,那就是沖突的模塊新添加的id啦
執(zhí)行sortItemsWithModuleIds依據(jù)id進(jìn)行排序,不只是最外面的chunk,就連reason里的id也會被重新排序,也是蠻逗的,這里直接用的是id做比較并沒有判斷類型,也就是說把數(shù)字和字符串會混到一起,就算你是class也會拿valueOf出來比較,想想還是蠻刺激的,不過其實(shí)比較完成也沒有太特殊的用途就這么隨意一點(diǎn)也好
中間一些處理recordId的我忽略掉了
hash
然后開始處理hash了,這里的hash具體使用了哪些參數(shù)和長度是多少呢
可以在此階段添加hashSalt即噪聲,給hash值添加一些特征
進(jìn)入mainTemplate的處理函數(shù)中,添加了一些字符串參數(shù)和數(shù)字參數(shù),并且調(diào)用了mainTemplate的hash插件,但是她們的執(zhí)行過程并不是保證我們最后生成的文件中能夠有結(jié)果的hash值,便于請求對應(yīng)的資源文件,而是僅僅在hash的過程中添加了一些干擾的路徑參數(shù)等
最終一輪hash下來,chunk會得到自己的renderHash,而compilation會得到一個針對編譯過程的hash,這個hash就跟我們的所有資源扯上關(guān)聯(lián)啦,所以每次都是新的
createModule/ChunkAssets
創(chuàng)建模塊資源咯~
- 先來看看index.ejs文件如何處理自己的資源文件,空的。。。assets對象里面存儲的就是我們需要新創(chuàng)建出來的文件。。然而他是一個空對象
- 然后處理chunk的資源文件,我們要生成的文件是在這里生成的。所以說這個東西也特么能算一個chunk咯,不過為什么這個html文件可以沒有js那些頭啊尾的內(nèi)容給包起來呢?
- 然后把我們的資源存入緩存中,這里的緩存鍵名實(shí)際上就是我們的模塊id前面加個c而已,照這樣緩存起來,如果沒有緩存結(jié)果的話再根據(jù)hasRunTime函數(shù),判斷chunk是入口還是拆分出來的chunk,根據(jù)mainTemplate或chunkTemplate的render函數(shù)進(jìn)行渲染結(jié)果的操作
bootstrap
說到底,這里就是在拼接字符串的過程,但是其實(shí)我們使用的應(yīng)用模塊中有時require有時webpack_require是怎么來的呢,到處都看到的requireFn為什么不直接設(shè)置成為require或者加上webpack的形式呢
先調(diào)用bootstrap的插件,執(zhí)行封裝頭的過程,這里首先會拿到HotModule的一些插件處理,主要是插入模塊熱替換的一些工具,相關(guān)源碼在/webpack/lib/HotModuleReplacement.runtime.js中,下次講模塊熱替換會進(jìn)行專門的分析
-
在此緊要關(guān)頭,又觸發(fā)了hot-bootstrap的操作,NodeMainTemplatePlugin也來湊熱鬧,拿到了我們默認(rèn)設(shè)置的熱替換資源json文件名和操作的update的js文件名字,然后順勢又把a(bǔ)sset-path的事件給調(diào)用了,把我們的模式文件名化為了表達(dá)式的存在,便于過后直接進(jìn)行替換存儲
如[name].[chunk].js變成了"" + chunkId + "." + hotCurrentHash + ".hot-update.js"類似的樣子
然后取出并返回我們的模板函數(shù)內(nèi)容,這里的模板函數(shù)沒有使用字符串的方式進(jìn)行存儲,而是直接使用的獲取函數(shù)toString的方式拿到其中的內(nèi)容,再對一些特殊變量名的位置進(jìn)行替換,豈不是美滋滋(模板有兩種一種同步一種異步)
把我們剛才得到的hot資源還有源碼資源等全部合并壓縮為字符串,我們暫且就叫這一部分叫bootstrap吧
local-val
- 添加installed-module這個變量來記錄我們本地已經(jīng)使用過的模塊
require
- MainTemplate會在一個注入的對象module-obj上添加模塊的基本屬性,hot相關(guān)插件會添加,這個組件本身的hot處理以及它的parent(這也是模塊熱更新的基礎(chǔ)之一)
- strictModuleExceptionHandling用于選擇是否用try....catch包裹住我們的業(yè)務(wù)代碼,當(dāng)為true的時候會抱起來,執(zhí)行出了錯就把當(dāng)前的模塊從緩存中刪掉,好像什么也沒發(fā)生一樣,但是這樣別的模塊就完全無法得到它的內(nèi)容了,所以也算是從另一種角度講的strict了,是不是很神奇的操作
require-extension
Main中處理了大部分和web-requi這個變量相關(guān)的值,并且設(shè)置了通過_esMoudle來確定是ES6模塊還是COMMONjs模塊,過后再看是否需要default把需要的模塊導(dǎo)出
這里是通過defineProperity的方法定義的getter,但是這樣也導(dǎo)致了我們的模塊如果不做特殊處理,不能夠兼容上古瀏覽器。而且還有一點(diǎn)值得注意的是過度使用定義對象屬性的方法會導(dǎo)致較大的性能損失
NodeMain又要放什么洋屁呢?chunk太少了沒放出來
-
Hot,看起來只是利用wr來設(shè)置了一下自己的相關(guān)變量掛載對象而已,回憶起來其實(shí)好多模塊都是拿過去干這個事
QQ20170925-212941@2x
startup
- Main,如果存在入口模塊,那么開始拼接他的依賴頭列表,最重要的當(dāng)然就是把我們的入口模塊的id記錄到其中,相當(dāng)于就是在這里找到最初的模塊開始執(zhí)行的所有業(yè)務(wù)操作,首先來到的是Hotmodule生成的相關(guān)hot模塊
render
-
Main,創(chuàng)建一個ConcatSource用于拼接資源,可以看到下面這一段決定了整個文件的結(jié)構(gòu),首先是我們拼裝好的bootstrap(bootstrap里面存上了我們啟動模塊的id,可以用于流程的發(fā)起工作),然后就是緊隨其后的參數(shù)了,想必大家也都知道,這些參數(shù)就是我們的所有模塊了,不過一直不知道為什么webpack毛病老是要在前面加星號的注釋
QQ20170925-215634@2x 然后就是依次處理每個module了,通過ModuleTemplate的render方法進(jìn)行處理,在其中搜集他的資源,按照經(jīng)典的從大到小的順序搜尋chunk,block,dependency。在這個處理過程中又出現(xiàn)了variable,之前一次看到的時候以為他是充當(dāng)了一個代替變量的作用,這次呢,看樣子實(shí)際上好像是被注入到了我們最外層的wrapper函數(shù)中當(dāng)做參數(shù)使用誒!
-
承接上文現(xiàn)在執(zhí)行module方法,EvalSourceMapDevToolModuleTemplatePlugin分別取出了我們資源的source和map,注意webpack中有很多地方都是喜歡用reduce對資源進(jìn)行處理,沒發(fā)現(xiàn)對性能有多大的提升,只不過讓你少在外面建立一個對象,看起來更優(yōu)雅
QQ20170926-003413@2x -
我們仔細(xì)看看創(chuàng)建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
發(fā)現(xiàn)這里其實(shí)針對chrome的bug做了些處理,才有了現(xiàn)在這種獵奇的webpack-internal:///格式的路徑名字,所以以后看到不要驚慌了,1版本的時候是對應(yīng)文件的,升級到3就不是了,想知道具體是什么BUG可以去issue找找
這里由于我們當(dāng)時設(shè)置的sourceMap就是eval-cheap的所以最后得到的代碼內(nèi)容也就變成了上面一個eval全部抱起來,下面一個sourceMap的base64link而已
處理完成模塊的封裝我們就來渲染吧,執(zhí)行render事件,千萬不要搞糊了,這些事件名稱很多重復(fù)的,但是他們是針對不同的Tapable組件作用的,比如現(xiàn)在執(zhí)行這個就是綁定在MainTeplate上面
-
FunctionModuleTemplatePlugin閃亮登場,就是她把我們的每個模塊單獨(dú)分裝起來的,比較短就直接貼代碼了,可以看到wr這個參數(shù)要添加上去還是得花些功夫,因?yàn)橹挥性谛枰玫侥切┘虞d函數(shù)相關(guān)的時候才會用到,如果一個模塊已經(jīng)不依賴別的模塊的話,那么再把他添加上是沒有意義的,還有就是上面的module和exports有時會按照需求變成webpack打頭的;記得之前說的use strict嗎?,需要的話就在這里統(tǒng)一加上了
QQ20170926-010334@2x 全部都拼湊好啦!至此,模塊構(gòu)建完成!剩下的就只是把他們打包放到文件中!
這些文章寫的都有點(diǎn)水,相當(dāng)于是閱讀源碼時候做的筆記了,看看圖個樂子吧