Webpack打包流程細(xì)節(jié)源碼解析(P1)

說在前面:

這些文章均是本人花費(fèi)大量精力研究整理,如有轉(zhuǎn)載請(qǐng)聯(lián)系作者并注明引用,謝謝

本文的受眾人群不是webpack的小白,僅適合有一定基礎(chǔ)的前端工程師以及需要對(duì)webpack進(jìn)行研究但是在閱讀源碼的過程中有些小的細(xì)節(jié)不明白時(shí)進(jìn)行查閱理解使用,學(xué)藝不精,文章中很多地方可能有理解上的問題希望在評(píng)論區(qū)指正

首發(fā)于個(gè)人博客 https://github.com/879479119/879479119.github.io/issues/1,歡迎star

基本概念

webpack整個(gè)系統(tǒng)中的核心組件基本都是繼承自Tapable這個(gè)class的,內(nèi)部維護(hù)一個(gè)插件對(duì)象,key是我們指定了流程中會(huì)運(yùn)行的鉤子事件名,value則是一個(gè)列表_plugins數(shù)組,里面存放著在key的對(duì)應(yīng)時(shí)間能夠以特定的方式執(zhí)行的插件們。這里所說的特定方式可以是同步,異步,高階函數(shù),bail等等,各個(gè)地方的用法各有不同。

我們的插件主要通過自身的apply方法插入進(jìn)去,同時(shí)因?yàn)楹诵慕M件都是Tapable的,所以可能會(huì)在apply中繼續(xù)看到apply方法,就不用驚訝了

Webpack啟動(dòng)

webpack首先利用optimist對(duì)我們的命令行參數(shù)進(jìn)行格式化處理,得到的對(duì)象可以拿到很多參數(shù),然后與我們的webpack.config.js進(jìn)行合并,當(dāng)然webpack其實(shí)還默認(rèn)支持別名的配置文件「webpackfile.js」,這兩個(gè)配置文件都是直接可以通過node自身的require進(jìn)行引入的,不要把她們和webpack的打包中的require搞混淆了

啟動(dòng)過程

通過創(chuàng)建webpack實(shí)例過后,我們一般將得到的對(duì)象命名為compiler,可以對(duì)她進(jìn)行run操作,進(jìn)行此操作過后的對(duì)象便會(huì)開始下面那張的圖中的構(gòu)造過程,下面我將進(jìn)行盡可能詳細(xì)的描述,便于讀者在開發(fā)過程中對(duì)于整個(gè)插件系統(tǒng)有完整的了解

webpack

進(jìn)行run操作后,開始了一系列的準(zhǔn)備工作,這個(gè)時(shí)候

before-run

首當(dāng)其沖的執(zhí)行,進(jìn)行了對(duì)于文件系統(tǒng)的設(shè)置,inputFileSystem是我們手動(dòng)設(shè)置的,如果對(duì)于平時(shí)的構(gòu)建操作,她會(huì)選用普通的文件系統(tǒng),而對(duì)于使用dev-server的時(shí)候來說,其中使用到的dev-middleware就是一套實(shí)現(xiàn)了對(duì)應(yīng)文件系統(tǒng)接口的庫(kù),其內(nèi)部使用了memory-fs這個(gè)庫(kù)進(jìn)行對(duì)內(nèi)存中的虛擬文件進(jìn)行管理的操作。

webpack在很多地方參考了rollup,但是rollup卻沒有選擇文件系統(tǒng)這樣的操作,只能讀入固定的文件系統(tǒng),而寫出的時(shí)候我們能夠拿到代碼,當(dāng)然是可以寫到內(nèi)存里面也是沒毛病

run

然后進(jìn)入run流程中,這個(gè)時(shí)候第一個(gè)插件就開始工作了,不過為什么我沒配置插件還有插件進(jìn)行工作?之前也說到compiler對(duì)象是一個(gè)Tapable對(duì)象,自身不會(huì)帶有太多執(zhí)行邏輯,更多的是表明了業(yè)務(wù)流程,所以其實(shí)這里的插件是我們的內(nèi)置插件—CachePlugin,她和我們整個(gè)系統(tǒng)資源緩存相關(guān)的,還有一個(gè)RecordId也是和緩存相關(guān)的

進(jìn)入此階段檢查緩存過后(實(shí)際上是通過stat查看某個(gè)位置的文件是否存在),會(huì)調(diào)用readRecords的方法檢查之前的記錄recordsInputPath是否存在,由于是第一次編譯所以當(dāng)然是沒有的

檢查完成兩項(xiàng)緩存正式進(jìn)入compile函數(shù)進(jìn)行編譯階段的處理

compile函數(shù)

這個(gè)階段中最重要的對(duì)象就是compilition了,但是構(gòu)建她需要一個(gè)參數(shù),我們會(huì)在進(jìn)入compile階段時(shí)通過newCompilationParams方法給她提供相應(yīng)的normalMoudle和contextMoudle等的上下文工廠函數(shù),還有一個(gè)重要的compilcationDep存貯相應(yīng)的依賴便于以后收集依賴進(jìn)行打包以及代碼處理(整個(gè)打包過程會(huì)有大量的Dep關(guān)鍵詞出現(xiàn),每一次都是不同的依賴分析,但是最終還是會(huì)被放到一個(gè)名叫dependencies的數(shù)組中,另外其實(shí)入口模塊的依賴就是她自己,相當(dāng)于作為了打包起點(diǎn))

WX20170910-213840@2x
WX20170910-213840@2x

工廠函數(shù)的構(gòu)造過程中,將rule的匹配設(shè)置以及l(fā)oader處理等全部拿給了這些工廠函數(shù)進(jìn)行存儲(chǔ),設(shè)置為對(duì)應(yīng)的ruleSet對(duì)象(之前我找了好久沒找到,原來藏在這里),這個(gè)ruleSet和我們之后的loader處理密切相關(guān)

normal-module-factory

進(jìn)入這個(gè)事件,傳進(jìn)去的修飾參數(shù)竟然是nmf(normal-module-factory),現(xiàn)在還沒有構(gòu)造compilition,同時(shí)因?yàn)樗彩抢^承自Tapable,所以有一些插件也會(huì)被插到nmf上面,另一個(gè)context是沒有編譯實(shí)體的,僅提供一個(gè)上下文環(huán)境。

上面可能說的有點(diǎn)糊,理一理,我們都知道webpack進(jìn)行打包的時(shí)候會(huì)找到指定名字的文件然后添加依賴打包進(jìn)去,但是這是知道明確文件名的時(shí)候做的操作(normal)。如果不知道具體的文件名,比如require('./module'+number)的時(shí)候,webpack系統(tǒng)實(shí)際上不能確定你用了哪一個(gè),只能夠把可能用上的包全部打包進(jìn)去,這個(gè)時(shí)候會(huì)用到的就是context進(jìn)行處理

說了上面一圈有什么用?實(shí)際上如果我們需要打包某個(gè)文件下的所有以jpg結(jié)尾的圖片,就可以使用這種方法或者是require.context進(jìn)行處理,全部打包進(jìn)去,詳情就看文檔吧

before-compile,compile

平淡無奇沒有操作

創(chuàng)建complication

創(chuàng)建了新的對(duì)象,同樣是一個(gè)插件系統(tǒng)對(duì)象,所有的插件鍵值都存儲(chǔ)在_plugins屬性中

this-complication

由于終于出現(xiàn)了complication對(duì)象,就算沒有什么要做的但是需要盡早注冊(cè)自己事件的插件就在這個(gè)階段登場(chǎng)注冊(cè)了,比如我們的CommonsChunkPlugin直接給優(yōu)化階段注冊(cè)了一個(gè)就走了

然后是JSONP相關(guān)的插件,她主要處理的是異步加載腳本的時(shí)候動(dòng)態(tài)添加script標(biāo)簽的事情,她也只是把插件插到了template上面,而不是compilation,這里綁定的處理大都會(huì)在最后打包的時(shí)候用到,所以hot-chunk其實(shí)和chunk區(qū)別不大,主要體現(xiàn)在加載方式不同,包裹不同而已

WX20170910-221030@2x
WX20170910-221030@2x

complication

根據(jù)我的配置,這個(gè)階段足足有38個(gè)插件,其中大部分還是來自wepack自身,由于使用Tapable帶來的高度解耦,讓人感覺webpack的核心是空的,只是一個(gè)走流程的架子,完全沒有實(shí)際的業(yè)務(wù)邏輯

  1. 模塊HMR插件,對(duì)compilation對(duì)象上的factory做了一個(gè)Map,把每種模塊對(duì)應(yīng)的工廠函數(shù)存了進(jìn)去,也對(duì)之后的mainTemplate等封裝的處理和代碼parse的處理過程進(jìn)行了注冊(cè)
  2. DefinePlugin的處理也是集中在parse的階段,進(jìn)行替換
  3. HashModuleId,使用set記錄已經(jīng)使用的id,避免重復(fù)
  4. LoaderOption,為normal-module-loader步驟注冊(cè),幫助loader解決設(shè)置問題
  5. FunctionMoudle,最重要的就是添加上我們平時(shí)見到最多的commonjs的模塊封裝方式,分為render和package兩個(gè)部分工作,第二部分主要是統(tǒng)計(jì)有些方法有沒有被用到,應(yīng)該是方便過后優(yōu)化的注釋
WX20170910-234440@2x
WX20170910-234440@2x
  1. NodeSource,主要是對(duì)node環(huán)境的一些表達(dá)式做特殊處理,比如global,process這樣的
  2. LoaderTarger,修改target,沒了
  3. EvalSourceMapDevToolPlugin,在moduleTemplate上面添加新的插件處理sourceMap
  4. MultiEntry和SingleEntry,這兩個(gè)都是很關(guān)鍵的入口插件,此階段僅僅是在之前提到的map上面設(shè)置好對(duì)應(yīng)的nmf,方便過后使用
  5. CompatibilityPlugin,主要是做兼容處理,比如json-loader在2版本之后就不需要手動(dòng)引入了,其實(shí)就是組件本身可以在這里開啟引入進(jìn)來的,在她的parse階段也會(huì)對(duì)require進(jìn)行處理,我們之后再說
  6. HarmonyModulePlugin, 在map中添加了大量處理import和export等的模板,另外還會(huì)在parse中注入幾個(gè)插件,是處理import這些語句用的
  7. AMDPlugin,CommonJs和上面的類似,她處理的是AMD形式引入的模塊
  8. LoaderPlugin,非常奇葩的兩段式,沒找到原因
  9. NodeStuff, 設(shè)置__dirname這種node環(huán)境下的變量,進(jìn)行替換
QQ20170915-115623@2x
QQ20170915-115623@2x
  1. RequireJsStuff, requirejs相關(guān)變量配置,如require.config

  2. API,配置相關(guān)縮寫,如果有需要可以直接在代碼中寫這些接口進(jìn)行使用,同時(shí)處理后的代碼可以根據(jù)這個(gè)進(jìn)行比對(duì)

    WX20170911-005110@2x
    WX20170911-005110@2x
  3. ConstPlugin,處理if語句和?:語句以及被設(shè)置為__resourceQuery的地方(這個(gè)東西她是在我們的require請(qǐng)求后面帶的那一堆query參數(shù)require('./a.js?sock:8000'),比如我們?cè)谑褂胐evServer的時(shí)候會(huì)加上上請(qǐng)求端口等,打包的時(shí)候就可以通過她獲取到)

  4. UseStrict, 由于use strict只能放在腳本最前面才起效,并且ast中解析出來的comment是放在別的位置的,所以把舊的use strict殺掉,過后統(tǒng)一一起加上

  5. RequireInclude,簡(jiǎn)直雞肋,沒有什么實(shí)際用途

  6. RequireEnsure,人如其名,會(huì)在parse階段加入插件進(jìn)行處理

  7. RequireContext,可以用來加載一整個(gè)文件目錄下的資源呢,不過還是主要用在了contextModule上面

  8. ImportPlugin,用做處理System.import,還有import-call?什么鬼

  9. SystemPlugin,向外提供System這個(gè)屌對(duì)象

  10. EnsureChunkCondition,利用set進(jìn)行篩選,清理輸入模塊中重復(fù)的部分,這里的ensure是確認(rèn),不是require.ensure里面那個(gè)

  11. RemoveParentModule,em。提取公共module,過后會(huì)講到

  12. RemoveEmptyChunks,正如名字一樣

  13. MergeDuplicateChunksPlugin,合并重復(fù)的chunk

  14. Flagincluded

  15. Occationaly

  16. FlagDep

  17. TemplatePath,過程中可以拿到結(jié)果的資源path和hash值,這對(duì)于html插件來說是很有意義的

  18. RecordId,

。。。。。。。

make

首先是html-webpack-plugin

html的插件會(huì)在這里執(zhí)行,我們就可以跟著她進(jìn)行分析了,需要注意的是她把catch放在了then的前面,只是處理前面的操作拋出的錯(cuò)誤,更有針對(duì)性

runAsChild過后沒有相應(yīng)的事件處理了,其實(shí)也就是在子compiler中執(zhí)行了compile方法,開出來的新的異步執(zhí)行空間,具體請(qǐng)去看插件之中的源碼

這里會(huì)createChildCompiler產(chǎn)生一個(gè)新的compiler,作用是能夠得到父級(jí)的loader設(shè)置等等(issue說的)

同時(shí)也在原來的基礎(chǔ)上增加了一些插件,比如比較重要的new LibraryTemplatePlugin('HTML_WEBPACK_PLUGIN_RESULT', 'var'),還有改變了平臺(tái)成為node,之后在代碼進(jìn)行處理的時(shí)候會(huì)根據(jù)這個(gè)名字再把她消除掉

WX20170911-020009@2x
WX20170911-020009@2x

html插件通過向外面提供一些「接口」,也可以被定制,我們?cè)缇驼f過,這些東西都是插件,現(xiàn)在的子compiler也不例外,我們這里下面寫的這個(gè)事件就是一個(gè)鉤子,使用她的插件

WX20170911-081629@2x
WX20170911-081629@2x

make階段只是添加了這個(gè)新的compiler和一些異步任務(wù),真正的使用還在emit的時(shí)候

這里有一點(diǎn)關(guān)于WEBPACK的特點(diǎn)就是js 的在處理的時(shí)候得到的source是一堆js代碼,并不是我們最終輸出的html,這里把她拿到vm中去執(zhí)行一下才能拿到我們要的東西(HTML字符串),畢竟子compiler也只是打包而已,得到的代碼還是要執(zhí)行才能有相應(yīng)的結(jié)果

最終執(zhí)行返回的結(jié)果還是null,只不過會(huì)在asset中注冊(cè)上要新創(chuàng)建的html文件

nml使用的是和compiler里面不同的parser,規(guī)則也會(huì)有不同的地方

然后是single-entry-plugin

這個(gè)階段會(huì)用到我們之前在config文件或者是手動(dòng)配置時(shí)的entry參數(shù),看看會(huì)不會(huì)是單個(gè)入口文件

細(xì)心一些才能發(fā)現(xiàn),之前在html插件中,插入了一個(gè)single-entry-plugin,并且把入口設(shè)置成了我們之前的html或者ejs等模板文件,導(dǎo)致我們?cè)诘谝淮芜M(jìn)入此插件的時(shí)候會(huì)發(fā)現(xiàn)是那個(gè)模板文件

通過之前的Map獲取到相對(duì)應(yīng)的moduleFactory來創(chuàng)建模塊,這里得到的是一個(gè)nmf普通模塊

現(xiàn)在出現(xiàn)了一個(gè)特別變量semaphore,她是一個(gè)信標(biāo),本來是在多線程中使用的資源控制使用的,(其實(shí)這里的資源說的是并行處理的數(shù)目)默認(rèn)情況下是100的可用資源量,如果資源不足時(shí)就讓過后的請(qǐng)求掛起等待,這里的acquire方法就去申請(qǐng)一個(gè)資源,然后進(jìn)入了首個(gè)Module的create方法

factory(nmf)

nmf自身有一個(gè)處理函數(shù)被插入進(jìn)去,并且ExternalModuleFactory在nmf上面插入了一個(gè)處理函數(shù),

由于是兩個(gè)連續(xù)的過程調(diào)用,通過函數(shù)式的處理返回了一個(gè)回調(diào)方法factory,直接使用factory對(duì)我們的ejs文件進(jìn)行封裝

external的模塊都是nodejs的內(nèi)部模塊,這些模塊不能拿來打包

QQ20170915-115623@2x
QQ20170915-115623@2x

然后是multiple-entry-plugin

這里出現(xiàn)了我們?cè)O(shè)置的第一個(gè)index,多入口文件,進(jìn)入multiple開始進(jìn)行處理,其實(shí)內(nèi)部還是把multiple的每一項(xiàng),轉(zhuǎn)化成為了單獨(dú)的SingleEntry,并且所有的id都是從100000開始計(jì)數(shù)的

然后通過Map獲取到對(duì)應(yīng)的moduleFactory是Multiple的,并開始執(zhí)行create方法,create后會(huì)相同的執(zhí)行addMoudle的操作,但是這次和之前的html有不同,因?yàn)樗忻鞔_的依賴關(guān)系,能夠正確進(jìn)入處理函數(shù)得到結(jié)果

如何獲取module的id呢?module中的identifier方法會(huì)返回出表現(xiàn)當(dāng)前module特性的字符串包含「multi」「path」等等,把她的索引緩存到_modules里面,如果下次又出現(xiàn)了了相同id的模塊則直接退出

不過緩存又是放到另一個(gè)地方的,即cache里面,這里面緩存module的key就是在id前面加上一個(gè)m,或者是其她定義的緩存分組(這個(gè)機(jī)制暫時(shí)不知道有什么用),在添加進(jìn)緩存完成后返回true表示成功添加新模塊

buildModule

非常重要的一項(xiàng)操作,loader的應(yīng)用和AST的解析都是在這一步進(jìn)行的,可以說除去打包優(yōu)化的所有重要功能都是在這里進(jìn)行的

build-module的時(shí)候只是sourceMap的插件把useSourceMap開啟了,然后沒了

正式調(diào)用module的build方法,比較重要的是傳遞了設(shè)置和相關(guān)的文件系統(tǒng)

不過mutiple的處理很水,直接什么也沒做就構(gòu)造完成了,把built置成了true

處理完成一個(gè)模塊后釋放之前的標(biāo)識(shí)semaphore,又回到了100個(gè)的默認(rèn)值

現(xiàn)在才是真操作,processModuleDependencies將做遞歸操作,為每一個(gè)模塊的依賴進(jìn)行獲取和處理

然后又進(jìn)入了nmf的create函數(shù)里面,現(xiàn)在又是before-resolve,這次和ejs那次不一樣,能夠拿到的是nmf的plugin,不過這次能夠執(zhí)行操作了

factory是通過waterfall將插件組合成為一個(gè)函數(shù)對(duì)結(jié)果進(jìn)行處理,resolver也是一樣的道理將所有操作合成為一個(gè)函數(shù)進(jìn)行處理

在resolver里面把request進(jìn)行了拆分,得到最終的資源文件所在位置和需要使用到的loaders,并行的執(zhí)行兩步操作,一步是resolveRequestArray用于一個(gè)一個(gè)的檢查這個(gè)模塊上應(yīng)用上了哪些loaders的擴(kuò)展,一個(gè)接一個(gè)的進(jìn)行執(zhí)行

QQ20170915-171239@2x
QQ20170915-171239@2x

在進(jìn)行實(shí)實(shí)在在的doResolve操作的時(shí)候使用到了一個(gè)操作棧的概念,每一次處理得文件會(huì)按照一定規(guī)律將名字緩存起來,放到函數(shù)上面作為屬性保存;

QQ20170915-142845@2x
QQ20170915-142845@2x

最后進(jìn)行runMormal做最普通的處理,然后添加了很多生命周期棧,應(yīng)該說這里面的操作都是在為編譯的實(shí)際過程做準(zhǔn)備,并且做了一堆準(zhǔn)備工作過后最后還主要是返回了package.json里面的東西回去繼續(xù)異步處理

NormalModuleFactory.js:100

然后從第一個(gè)參數(shù)中提取到剛才我們分析出來需要使用的loader,再到ruleSet對(duì)象中跑一遍,先是exec然后_run方法,進(jìn)行一個(gè)個(gè)的規(guī)則匹配工作,看看當(dāng)前文件到底要使用哪些loader

這里需要分辨一下,這個(gè)地方的是按照webpack.config.js里面的規(guī)則,正則表達(dá)式來進(jìn)行匹配的,但是我們說的上面的那一次resolver處理實(shí)際上是對(duì)類似于require('index.js?babel-loader')這種形式進(jìn)行的操作

QQ20170915-175107@2x
QQ20170915-175107@2x

經(jīng)過一系列的匹配我們知道了現(xiàn)在需要的只有babel-loader進(jìn)行處理,所以返回一個(gè)result,如果我們有enforce的需要,會(huì)把loader掛載到對(duì)應(yīng)的時(shí)機(jī)上面處理,但是實(shí)際執(zhí)行的時(shí)候并行執(zhí)行了三種loader

然后她又來到了我們之前執(zhí)行過的resolveRequestArray方法,就是從request中取出需要使用的loader那一步用到的東西。(NormalModuleFactory.js:247)

當(dāng)處理完的時(shí)候進(jìn)入回調(diào)的時(shí)候神TM的results中得到的loader又變成了空數(shù)組!合著我辛辛苦苦拿到的loader最后又喂了狗(未解之謎,正常的流程操作在下面,請(qǐng)接著看)

從傷痛之中走出來發(fā)現(xiàn),底下執(zhí)行回調(diào)函數(shù)的時(shí)候并沒有直接執(zhí)行,而是放到了nextTick里面,過后試一下拿出來會(huì)怎么樣

由于回調(diào)回去會(huì)用到解析器進(jìn)行詞法分析,這里調(diào)用了getParser方法嘗試拿到一個(gè)解析器,如果沒有現(xiàn)成的的話就創(chuàng)建一個(gè)新的,順便把她緩存下來之后使用

進(jìn)入創(chuàng)建解析器的函數(shù),我們會(huì)發(fā)現(xiàn)她創(chuàng)建好新的對(duì)象過后會(huì)把自身上安裝上插件「二元計(jì)算」「typeof計(jì)算」等等,另外還發(fā)現(xiàn)在處理字符串的split,substr等操作函數(shù)時(shí),會(huì)有特別的處理,由此可見其實(shí)在這一步里面parser其實(shí)會(huì)做少量的字符串優(yōu)化

創(chuàng)建完成之后,得到了parser她也是繼承自Tapable的,那么我們其她組件也可以慢慢發(fā)揮自己的作用了,通過把自己在parse階段要做的處理apply上去,比如之前說的import和require.ensure等等就是在這里找到指定的表達(dá)式進(jìn)行替換處理的

解析時(shí)工作的插件

按照舊例梳理一下有哪些插件應(yīng)用到了這上面,詳細(xì)的作用不做梳理,不然能講一年:

  1. HotModuleReplacement,主要是做模塊熱替換的處理(這玩意兒都能講一個(gè)專題)

    1. webpack_hash變成webpack_require.h()對(duì)照前面的看看這個(gè)函數(shù)具體會(huì)變成什么作用

    2. 把module.hot的計(jì)算值根據(jù)開發(fā)環(huán)境還是什么的進(jìn)行替換,這里的模塊熱替換也可以去看看相關(guān)的知識(shí)點(diǎn),她的依賴圖譜也是一個(gè)樹形的結(jié)構(gòu),牽一發(fā)而動(dòng)全身,如果本層沒有處理這個(gè)更新那么會(huì)向上一層傳遞

    3. 調(diào)用module.hot.accept的時(shí)候,會(huì)根據(jù)里面的參數(shù)進(jìn)行轉(zhuǎn)換,這個(gè)地方再次引入了插件。。。簡(jiǎn)直了。。如果帶參數(shù)就是callback,沒有就是withoutCallback,居然是在HarmonyImportDependencyParser里面引入的兩個(gè)處理模塊

      if (module.hot) {
        module.hot.accept('./print.js', function() {
          console.log('Accepting the updated printMe module!');
          printMe();
        })
      }
      

      這里做的處理就是吧前面的request收集起來,做成包的依賴

    4. module.hot.decline….

      QQ20170915-195157@2x
      QQ20170915-195157@2x
  2. DefinePlugin,把我們?cè)趐lugin中設(shè)置好的表達(dá)式進(jìn)行替換就是了

    1. 主要的處理是在can-rename中進(jìn)行的,不過一般不好保證這個(gè)插件能比較完美的執(zhí)行

    2. 如果出現(xiàn)循環(huán)替換的怎么辦,a->b,b->a,這樣的情況下,直接返回原本代碼的計(jì)算值

      QQ20170915-203536@2x
      QQ20170915-203536@2x
  3. NodeSource,就是處理global,process,console,Buffer,setImmediate這幾個(gè)

    1. 那么global是怎么替換的呢,就像下面這樣,把她用來替換,不過這里面這個(gè)(1,eval)我是真沒搞懂,還有webpack里面的(0,module)都很trick的感覺

      QQ20170915-204951@2x
      QQ20170915-204951@2x
      QQ20170915-205646@2x
      QQ20170915-205646@2x
    2. 那么其她呢?其實(shí)都是依賴的node-libs-browser這個(gè)package里面的內(nèi)容,不過其實(shí)有些模塊還是沒有實(shí)現(xiàn)的,當(dāng)然都是無可避免的,比如dns,cluster這一類,簡(jiǎn)單看看console的實(shí)現(xiàn),其她自行查閱

      QQ20170915-205819@2x
      QQ20170915-205819@2x
  4. Compatibility,主要是針對(duì)browserify做的一些處理,不過我沒有用過這個(gè)東西,她好像是require后面可以帶上兩個(gè)參數(shù)同時(shí)添加上一個(gè)新的ConstDependecy

  5. HarmonyModules,里面包含了好幾個(gè)插件

  1. HarmonyDetectionParser主要是在program階段,處理是import和export等語句,如果有這兩個(gè)關(guān)鍵字出現(xiàn)的話,就把這個(gè)模塊當(dāng)成HarmonyCompatibility依賴處理,添加進(jìn)入依賴列表,注意這個(gè)module是從parser.state.module中取出來的

  2. HarmonyImportDependencyParser,根據(jù)import尋找依賴,把找到的依賴通過HarmonyImportDependency添加進(jìn)依賴列表中;如果有import specifier好像會(huì)換成imported var過后會(huì)有另一個(gè)插件來處理,把她變成HarmonyImportSpecifierDependency的依賴,這個(gè)specifier應(yīng)該說的是引入部分模塊那種操作

  3. HarmonyExportDependencyParser,對(duì)export做了類似的處理,不過有點(diǎn)看不懂

  4. 上面這些注冊(cè)的調(diào)用是在哪里執(zhí)行的呢?當(dāng)然就是在parser處理得過程中啦,由于名字都是一一對(duì)應(yīng)的所以我們只需要簡(jiǎn)單的搜索一下就能知道「import specifier」是在Parser.js:656開始進(jìn)行處理的,可以往回觀察她的邏輯

    WX20170911-001528@2x
    WX20170911-001528@2x
  5. AMD,安裝了兩個(gè)插件,第一個(gè)處理了所有和require相關(guān)的依賴加載(復(fù)雜異常)有很多的parser類似于「call require:amd:array」這種應(yīng)該是parser階段做的特殊處理;第二個(gè)是處理define加載相關(guān)解析操作的,和前一個(gè)差的不多;剩下的就是對(duì)typeof等等的一些處理了

  6. COMMONJs,就是處理module.exports這種啦,當(dāng)然還有require,同樣為了保證給require賦值時(shí)不導(dǎo)致undefine的尷尬,插件會(huì)加上一個(gè)var require;

  7. NodeStuff,有什么用呢?當(dāng)然就是把文檔中所寫的那些nodeAPI給替換掉啊,瀏覽器環(huán)境可是沒有什么__dirname這種東西的,當(dāng)然還有module相關(guān)的什么id,loaded之類的東西

  8. RequirejsStuff,有些小用處

    1. 讓require.config和requirejs.config返回undefined

    2. 把require.version換成0.0.0,這樣我們可以看看當(dāng)前的系統(tǒng)是不是使用的webpack打包咯~,畢竟只有requirejs參與的話這里就會(huì)是她的版本了

    3. require.onError是加載錯(cuò)誤的回調(diào)函數(shù),會(huì)轉(zhuǎn)變成webpack.oe,請(qǐng)對(duì)照之前說的列表看看怎么操作的,不過有了import可以用catch處理這個(gè)也沒那么重要了

      WX20170911-005110@2x
      WX20170911-005110@2x
  9. API,還是官方文檔里寫的那些表達(dá)式的處理,__wepack_require__我們上面說的oe也在這里面哦

  10. Const,主要是處理表達(dá)式的true或者false,進(jìn)行表達(dá)式的計(jì)算直接替換做優(yōu)化,另外還有一個(gè)__resourceQuery是和熱加載相關(guān)的

    QQ20170915-191630@2x
    QQ20170915-191630@2x
  11. UseStrict,處理的時(shí)候添加了一個(gè)空的ConstDependency,和一個(gè)issue有關(guān),不這樣處理位置可能不對(duì)issue:1970

  12. Require.include,沒鳥用,下一個(gè)

  13. Require.ensure,除了基本的typeof處理等,加載了一個(gè)插件RequireEnsureDependenciesBlockParser處理異步加載,她最多的處理參數(shù)可以達(dá)到4個(gè)。。而處理的邏輯里面會(huì)發(fā)現(xiàn),這次操作并沒有把當(dāng)前得到的模塊添加到parser的依賴上面,而是直接賦值了一個(gè)給parser.state.current

  14. RequireContext,好東西啊,不過不常用,之后再解釋

  15. Import,這個(gè)import和之前的Harmony插件有什么區(qū)別呢?區(qū)別就是這個(gè)import其實(shí)是webpack里面那個(gè)System.import 和 import函數(shù),進(jìn)來處理的時(shí)候呢,首先會(huì)把她的第一個(gè)參數(shù)進(jìn)行計(jì)算處理,然后判斷這個(gè)東西計(jì)算出來是不是一個(gè)字符串,如果是直接可以計(jì)算出是某個(gè)確定的字符串的話,那我們就可以直接引入相對(duì)應(yīng)的模塊

    ? 1. 從源碼才看出來,其實(shí)這個(gè)加載支持幾種方式,有l(wèi)azy模式只是其中一種,她還可以是eager和weak的方式

    ? 2. 當(dāng)是其她兩種方式的時(shí)候會(huì)加入對(duì)應(yīng)type的Dependency,但是如果是lazy模式下面則會(huì)作為一個(gè)新的block添加,這個(gè)block繼承自AsyncDependenciesBlock,就是平時(shí)的異步模塊

    ? 3. 但如果不是字符串的時(shí)候怎么辦呢,這個(gè)時(shí)候創(chuàng)建的就是我們的ContextDependency了,這個(gè)東西會(huì)根據(jù)我們已經(jīng)知道的模塊信息進(jìn)行模塊查找,匹配的都打包到一起i(未驗(yàn)證)

  16. System,處理System這個(gè)變量,不知道有什么鳥用,現(xiàn)在她上面其實(shí)只有import一個(gè)方法,對(duì)她的set,get,register都做了報(bào)錯(cuò)處理

parser.state.dependendies好像是一個(gè)特別重要的東西

另外要注意的是我們現(xiàn)在調(diào)用的是nmf的createParser,所以只會(huì)有類似于params.mormalModuleFactory.plugin("parser")這種才會(huì)在這個(gè)步驟進(jìn)行注冊(cè)操作,其她如hot或者chunk這一類的會(huì)在自己的周期中進(jìn)行注冊(cè)

至此,parser創(chuàng)建完畢,返回回去

create-module

然后創(chuàng)建一個(gè)真正的NomalModule對(duì)象,進(jìn)行一些沒有實(shí)際插件工作的操作,過后再次進(jìn)入到我們的doBuild過程中,調(diào)用runLoader的方法使用loadLoader加載我們將要使用的loader

進(jìn)入loadLoader函數(shù)中,首先會(huì)檢查是否有System這個(gè)對(duì)象,以及System.import這個(gè)東西,但是講道理這個(gè)東西應(yīng)該是在我們將要處理的文件中出現(xiàn)的東西,為什么會(huì)在我們的工具代碼中出現(xiàn)呢?這點(diǎn)暫時(shí)不得而知,不過有可能是因?yàn)檫@一段代碼也有可能被打包進(jìn)入我們的工程文件,然后通過webpack進(jìn)行處理;在發(fā)現(xiàn)沒有這種方法將資源引入過后,webpack會(huì)選擇使用require的方式把loader加載進(jìn)來(注意這里的require是node的require,即同步加載的require :loaderRunner/lib/loadLoader.js:2

在這過后調(diào)用iteratePitchingLoaders方法,不斷遞歸的檢查還有沒有其她的loader加入(pitch指的就是我們的loader是否是從這個(gè)位置開始執(zhí)行,所以如果當(dāng)pitch為undefined的時(shí)候會(huì)導(dǎo)致她不斷的遞歸處理,直到到達(dá)最前面一個(gè)loader或者是剛好是pitch的loader)

這里我們只有一個(gè)ejs的loader,那么就會(huì)進(jìn)入processResource中開始對(duì)資源進(jìn)行處理

  1. 進(jìn)來過后又會(huì)添加依賴,不過這次的依賴不是添加到我們的module或者是compilation上面,而是添加到了loaderContext維護(hù)的一個(gè)數(shù)組上
  2. 利用我們?cè)O(shè)置好的資源加載函數(shù)獲取到資源(畢竟現(xiàn)在可能把文件系統(tǒng)設(shè)置為了內(nèi)存中或者是webDV等)
  3. 獲取到資源過后又開始進(jìn)行一個(gè)遞歸,不明白為什么全部的邏輯都用那一個(gè)函數(shù)處理,讓人頭大,總之好不容易是拿到了我們要的資源,現(xiàn)在是buffer格式的,利用createSource方法把她的字符串類型和buffer類型都放到_source里面存好便于以后處理(注意這里的buffer解碼成string的時(shí)候沒有設(shè)置選項(xiàng)直接是UTF-8)
  4. 這個(gè)時(shí)候總算是回到了我們doBuild函數(shù)的回調(diào)函數(shù)之中對(duì)資源進(jìn)行操作了
  5. 這里提到我們可以對(duì)資源進(jìn)行noParse的設(shè)置,反正檢查了一下,設(shè)置項(xiàng)中好像沒看到這個(gè)東西
  6. 常使用我們準(zhǔn)備好的parser來處理文件了,話說loader呢????這里webpack使用的是acorn,像平時(shí)的babel使用的是babylon來解析的一樣(不過對(duì)于她們兩個(gè)來說有一個(gè)解析出來的是ESTree,另一個(gè)是BabelASTTree,規(guī)范不同導(dǎo)致不兼容,所以最后選擇的是acorn,至于babylon網(wǎng)上有人說就是acorn的魔改版本,不再擴(kuò)展,link:https://blog.zsxsoft.com/post/28

聊一聊AST相關(guān)的解析

  1. 利用acorn新創(chuàng)建一個(gè)parser,這個(gè)parser和我們之前提到的設(shè)置好的parser不一樣,這個(gè)是實(shí)際上內(nèi)部真正處理內(nèi)容的parser,之前那個(gè)相當(dāng)于是外部的一層封裝便于我們使用各種插件對(duì)她進(jìn)行處理
  2. 獲取設(shè)置,根據(jù)我們的配置進(jìn)行處理,可以看到開啟了dynamicImport插件的使用;然后按照設(shè)置的ESMA解析版本對(duì)關(guān)鍵詞的過濾進(jìn)行設(shè)置如「let」「default」「import」等等
  3. 根據(jù)8開始到當(dāng)前版本往回獲取保留的關(guān)鍵字,像我們?cè)O(shè)置的是6,所以這里保留關(guān)鍵字還剩下「await」「enum」,再往后就把她們和完全沒有版本實(shí)現(xiàn)的一些關(guān)鍵字?jǐn)?shù)組拼接起來,比如「static」「private」等
  4. 加載我們?cè)O(shè)置好的插件進(jìn)來進(jìn)行處理,進(jìn)行parse操作
    1. 利用skipSpace篩選跳過所有空格和注釋
    2. 讀入第一個(gè)token,然后開始進(jìn)行無盡的循環(huán),直到eof
    3. 讀出來的數(shù)據(jù)全部放到node的body里面
    4. 完了過后有會(huì)調(diào)用next方法,里面又會(huì)調(diào)用nextToken方法相當(dāng)于做了一次檢查
    5. 然后這一步處理完成加上type的標(biāo)簽,最外面一層的type就是Program,里面的是各種各樣的表達(dá)式或者塊部分,相當(dāng)于這個(gè)樹的每一個(gè)節(jié)點(diǎn)都有自己的type,這也是我們之前注冊(cè)的parser插件們得以正常工作的前提
    6. 最終得到了整個(gè)的抽象語法樹,但是要知道注釋是單獨(dú)抽出來的沒有放到語法樹里面
  5. 拿到了AST開始執(zhí)行我們之前綁定的事件,比如首當(dāng)其沖的program就開始處理啦

還記得之前說的use strict加入了一個(gè)constDependency嗎,其實(shí)這玩意兒沒什么用,她不會(huì)添加新的依賴,給人感覺只是一個(gè)干凈的占位符,不過另外還有其她類型的依賴,她們就是有各種各樣的特殊作用了

其實(shí)啊,這些依賴呢,本身就是存儲(chǔ)了相應(yīng)的位置信息,還有需要添加的模板,她們都有一個(gè)非常重要的屬性Template,這個(gè)東西能夠在最后加入進(jìn)文檔的時(shí)候把她們的內(nèi)容進(jìn)行添加操作

繼續(xù)進(jìn)行prewalk的處理,這里我們簡(jiǎn)單的舉一些例子,比如第一次進(jìn)來的var Sockjs首先會(huì)進(jìn)行一個(gè)var-XXX的綁定操作,然后才是var Sockjs的操作,所以要是真正開發(fā)的時(shí)候這里面的解析順序還是非常值得注意的問題,在處理完成過后就會(huì)在defination數(shù)組之中加入我們新創(chuàng)建的變量名字,以便后續(xù)的處理過程使用。

QQ20170916-131359@2x
QQ20170916-131359@2x

進(jìn)行完成prewalk過后就是我們的walk操作,在這一步中

  1. 進(jìn)行statement的綁定操作,沒有綁定的操作
  2. 之前進(jìn)行過一次的prewalk操作,操作并記錄了一些變量的名字,現(xiàn)在在walk的時(shí)候我們可以對(duì)變量進(jìn)行改名操作等等,但是rename操作有個(gè)特點(diǎn),那就是在rename之前必須有個(gè)can-rename XXX得返回true,判斷這個(gè)變量是可以進(jìn)行rename操作的
  3. 對(duì)于call expression這種表達(dá)式,會(huì)實(shí)時(shí)的調(diào)用evaluate相關(guān)的綁定進(jìn)行替換,就不再贅述,總之我們之前插件中綁定的解析器處理函數(shù)都在這里起作用就對(duì)了

success-module

在回調(diào)函數(shù)中,由于完成了一個(gè)文件的解析處理,這里我們把semaphore還回去一個(gè),即釋放回去一個(gè)資源,同時(shí)由于這里的遞歸參數(shù)被設(shè)置為了true,我們會(huì)繼續(xù)尋找已處理模塊的依賴(注意這里的模塊概念,我們的input設(shè)置的一個(gè)鍵名即對(duì)應(yīng)了一個(gè)chunk,而不是只數(shù)組的每個(gè)值對(duì)應(yīng)一個(gè)模塊)

突然發(fā)現(xiàn)這個(gè)block的單位值得拿出來說一說,在這個(gè)地方添加依賴的時(shí)候首先就是會(huì)執(zhí)行addDependenciesBlock,這樣算是把整個(gè)模塊當(dāng)成是一個(gè)block來處理了,然后再處理里面的子block和dependencies等等,全部添加進(jìn)去

QQ20170915-135718@2x
QQ20170915-135718@2x

既然有入口,那么肯定就免不了循環(huán)的尋找依賴了,現(xiàn)在又會(huì)調(diào)用我們之前使用過的addModuleDependencies方法,進(jìn)行依賴的尋找,以及所依賴模塊的依賴遞歸處理

單獨(dú)說一下NullFactory,她會(huì)處理我們之前提到的ConstDependency,整個(gè)create函數(shù)毛也沒干直接就執(zhí)行了回調(diào)函數(shù),暫時(shí)沒發(fā)現(xiàn)有什么用,所以你也知道ConstDependency只是占位了

不過如果是碰到正常的模塊的話比如說Coomonjs的依賴,她在map中其實(shí)對(duì)應(yīng)的就是一個(gè)普通的nmf,這個(gè)時(shí)候就會(huì)把這個(gè)模塊普普通通的進(jìn)行像之前解析入口文件一樣的操作

loader執(zhí)行操作

由于之前的模塊在node_module里面,成功的避開了設(shè)置好的經(jīng)歷loader處理的過程,所以這里先單獨(dú)拿出來說過后補(bǔ)上去

可以看到這次好不容易啊,我們的loaders數(shù)目終于變成了1,總算是可以進(jìn)行babel的處理了,還有需要注意的問題就是這個(gè)東西她不知道是又開了一個(gè)進(jìn)程還是怎么的,如果別的地方打了斷點(diǎn)是進(jìn)不來的,花了好多時(shí)間嘗試,inspect的機(jī)制也有待了解

終于可以正式進(jìn)行處理了好興奮,這里也會(huì)把我們的輸入?yún)?shù)進(jìn)行格式化(其實(shí)就是拿到的文件資源buffer,webpack也是很聰明的,如果不使用buffer這樣的原始內(nèi)存空間,那么項(xiàng)目的大小和資源大小就會(huì)收到限制了)

然后我們一直說的處理BOM頭,這個(gè)BOM頭究竟是個(gè)啥,其實(shí)她就是0xFEFF,我們?cè)诘谝粋€(gè)字符找到了她要記得清除哦,不然鬼知道會(huì)解析出來的什么鬼東西

然后進(jìn)入了神秘的runSyncOrAsync函數(shù),可能執(zhí)行同步或者異步操作,看來就是拿給我們的loader來做決定了

babel-loader

  1. 進(jìn)來插件里面當(dāng)然會(huì)禮貌性的檢查一下我們有沒有.babelrc的配置文件,不過并沒有主動(dòng)的找,而是看我們有沒有設(shè)置

  2. 由于我們的loader要執(zhí)行異步操作,這里便先執(zhí)行一下webpack要求的this.async方法,主要就是跑回去把isSync變成了false,下次檢查的時(shí)候就知道這個(gè)不是同步處理了

  3. 我們發(fā)現(xiàn)loader的默認(rèn)緩存路徑是在node_module/.cache/babel-loader里面,而且由于沒有使用外部的fs系統(tǒng),她的內(nèi)容是確實(shí)的存在在硬盤中的,檢查目錄的時(shí)候也是用的mkdirp確定目錄存在

  4. 在進(jìn)行debug操作的時(shí)候要記得把之前的緩存刪掉不然會(huì)直接拿到舊的數(shù)據(jù)

  5. 反正終于通過read方法拿到了我們需要的文件,這個(gè)時(shí)候就嘗試調(diào)用transform方法進(jìn)行轉(zhuǎn)化工作,這里的編譯函數(shù)就是index.js:38中的transpile函數(shù),可以看到很多用void 0而不是undefined,除了代碼壓縮的時(shí)候我還少有看到這么干的

  6. babel-core的代碼看起來賊難受,本身也是從es6的代碼轉(zhuǎn)化過來的,這算是自舉了?23333333

  7. 總之嘛,算是把她解析好了看看處理完成什么樣子,算是包含了注釋單獨(dú)抽出來,ast樹結(jié)構(gòu),這個(gè)babel解析的樹肯定還是和之前說的那個(gè)有些不一樣的,解析出來的代碼code,我們的sourceMap,以及在處理過程中得到的所有token居然也保留了下來,這個(gè)時(shí)候返回我們處理的結(jié)果,但是只返回了code,map和metadata這就讓人很難受了啊,如果這個(gè)語法樹要是能夠直接拿去給檢查依賴用多好省了不少時(shí)間

  8. 返回出來的結(jié)果把她緩存到我們之前說過的目錄里面,以便下次使用加快編譯

  9. 我們留意一下編譯出來的代碼,會(huì)發(fā)現(xiàn)每處外部加載包被調(diào)用之前都會(huì)有(0, XXX)的寫法,到現(xiàn)在還沒發(fā)現(xiàn)到底拿來干嘛

  10. 看了一下存下來的metadata發(fā)現(xiàn)她還是存下來了import進(jìn)去的依賴,看來過后還是可以使用的嘛

  11. 然后會(huì)讓metaData的訂閱者們首先處理一下這些依賴,講道理我們也可以在這里做些手腳,不過現(xiàn)在發(fā)現(xiàn)是沒有東西進(jìn)行操作

  12. 最終執(zhí)行回調(diào)退出過程,這次還是沒有吧metadata帶走!所以metadata還是沒能翻身!

  13. 回到我們熟悉的runSyncOrAsync的回調(diào)之中LoaderRunner.js:233再次執(zhí)行iterateNormalLoaders,為什么會(huì)這樣呢?當(dāng)然是因?yàn)檫€是要去這個(gè)韓束里面判斷我們是否還有l(wèi)oader要對(duì)她進(jìn)行處理咯,事實(shí)證明是沒有的,有空我們看看less文件怎么辦

  14. 隨著調(diào)用棧的不斷退出!我們終于又回到了doBuild中,開始了新一輪解析AST的征程!是不是有毛病!

說了一下babel是這樣處理的,那么其她資源文件是怎么做的呢?

less文件的處理會(huì)把文件進(jìn)行轉(zhuǎn)碼最終變成字符串傳給下游,css和style等組件并不會(huì)直接把資源做多大的處理,她們更多的是添加依賴進(jìn)去module里面,這些添加的依賴是一些工具函數(shù),最終會(huì)幫助資源進(jìn)行封裝工作

這里可以做一下思考為什么webpack的設(shè)計(jì)者會(huì)讓實(shí)際上越后處理資源的loader放到列表的前面呢?

另外還有一點(diǎn)就是css從某個(gè)版本開始沒有直接使用字符串存放我們的css資源了,取而代之的是使用了base64的字符串,如果支持的情況下會(huì)使用atob的方式對(duì)資源進(jìn)行解碼,這樣處理好像是對(duì)于sourceMap更加方便

在github原文中有詳細(xì)的核心插件作用介紹,歡迎查看 :)

【1】淘寶FED-細(xì)說 webpack 之流程篇 http://taobaofed.org/blog/2016/09/09/webpack-flow/

【2】zsx的博客 https://blog.zsxsoft.com/post/28

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容