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

說在前面:

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

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

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

基本概念

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

我們的插件主要通過自身的apply方法插入進去,同時因為核心組件都是Tapable的,所以可能會在apply中繼續(xù)看到apply方法,就不用驚訝了

Webpack啟動

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

啟動過程

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

webpack

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

before-run

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

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

run

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

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

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

compile函數(shù)

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

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

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

normal-module-factory

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

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

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

before-compile,compile

平淡無奇沒有操作

創(chuàng)建complication

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

this-complication

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

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

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

complication

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

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

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

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

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

  5. RequireInclude,簡直雞肋,沒有什么實際用途

  6. RequireEnsure,人如其名,會在parse階段加入插件進行處理

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

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

  9. SystemPlugin,向外提供System這個屌對象

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

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

  12. RemoveEmptyChunks,正如名字一樣

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

  14. Flagincluded

  15. Occationaly

  16. FlagDep

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

  18. RecordId,

。。。。。。。

make

首先是html-webpack-plugin

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

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

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

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

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

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

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

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

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

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

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

然后是single-entry-plugin

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

細心一些才能發(fā)現(xiàn),之前在html插件中,插入了一個single-entry-plugin,并且把入口設(shè)置成了我們之前的html或者ejs等模板文件,導(dǎo)致我們在第一次進入此插件的時候會發(fā)現(xiàn)是那個模板文件

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

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

factory(nmf)

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

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

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

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

然后是multiple-entry-plugin

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

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

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

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

buildModule

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

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

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

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

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

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

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

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

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

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

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

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

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

NormalModuleFactory.js:100

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

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

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

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

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

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

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

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

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

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

解析時工作的插件

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

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

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

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

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

      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,把我們在plugin中設(shè)置好的表達式進行替換就是了

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

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

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

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

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

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

  5. HarmonyModules,里面包含了好幾個插件

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

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

  3. HarmonyExportDependencyParser,對export做了類似的處理,不過有點看不懂

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

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

  6. COMMONJs,就是處理module.exports這種啦,當(dāng)然還有require,同樣為了保證給require賦值時不導(dǎo)致undefine的尷尬,插件會加上一個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參與的話這里就會是她的版本了

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

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

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

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

  12. Require.include,沒鳥用,下一個

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

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

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

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

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

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

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

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

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

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

create-module

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

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

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

這里我們只有一個ejs的loader,那么就會進入processResource中開始對資源進行處理

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

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

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

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

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

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

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

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

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

success-module

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

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

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

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

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

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

loader執(zhí)行操作

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

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

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

然后我們一直說的處理BOM頭,這個BOM頭究竟是個啥,其實她就是0xFEFF,我們在第一個字符找到了她要記得清除哦,不然鬼知道會解析出來的什么鬼東西

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

babel-loader

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,157評論 2 375

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