1. 程式人生 > >打包工具的配置教程見的多了,但它們的執行原理你知道嗎?

打包工具的配置教程見的多了,但它們的執行原理你知道嗎?

前端模組化成為了主流的今天,離不開各種打包工具的貢獻。社群裡面對於webpack,rollup以及後起之秀parcel的介紹層出不窮,對於它們各自的使用配置分析也是汗牛充棟。為了避免成為一位“配置工程師”,我們需要來了解一下打包工具的執行原理,只有把核心原理搞明白了,在工具的使用上才能更加得心應手。

本文基於parcel核心開發者@ronami的開源專案minipack而來,在其非常詳盡的註釋之上加入更多的理解和說明,方便讀者更好地理解。

1、打包工具核心原理

顧名思義,打包工具就是負責把一些分散的小模組,按照一定的規則整合成一個大模組的工具。與此同時,打包工具也會處理好模組之間的依賴關係,最終這個大模組將可以被執行在合適的平臺中。

打包工具會從一個入口檔案開始,分析它裡面的依賴,並且再進一步地分析依賴中的依賴,不斷重複這個過程,直到把這些依賴關係理清挑明為止。

從上面的描述可以看到,打包工具最核心的部分,其實就是處理好模組之間的依賴關係,而minipack以及本文所要討論的,也是集中在模組依賴關係的知識點當中。

為了簡單起見,minipack專案直接使用ES modules規範,接下來我們新建三個檔案,並且為它們之間建立依賴:

  1. /* name.js */

  2. exportconst name ='World'

  1. /* message.js */

  2. import{ name }from'./name.js'

  3. exportdefault`Hello ${name}!`

  1. /* entry.js */

  2. import message from'./message.js'

  3. console.log(message)

它們的依賴關係非常簡單: entry.js → message.js → name.js,其中 entry.js將會成為打包工具的入口檔案。

但是,這裡面的依賴關係只是我們人類所理解的,如果要讓機器也能夠理解當中的依賴關係,就需要藉助一定的手段了。

2、依賴關係解析

新建一個js檔案,命名為 minipack.js,首先引入必要的工具。

  1. /* minipack.js */

  2. const fs =require('fs')

  3. const path =require('path')

  4. const

    babylon =require('babylon')

  5. const traverse =require('babel-traverse').default

  6. const{ transformFromAst }=require('babel-core')

接下來,我們會撰寫一個函式,這個函式接收一個檔案作為模組,然後讀取它裡面的內容,分析出其所有的依賴項。當然,我們可以通過正則匹配模組檔案裡面的 import關鍵字,但這樣做非常不優雅,所以我們可以使用 babylon這個js解析器把檔案內容轉化成抽象語法樹(AST),直接從AST裡面獲取我們需要的資訊。

得到了AST之後,就可以使用 babel-traverse去遍歷這棵AST,獲取當中關鍵的“依賴宣告”,然後把這些依賴都儲存在一個數組當中。

最後使用 babel-core的 transformFromAst方法搭配 babel-preset-env外掛,把ES6語法轉化成瀏覽器可以識別的ES5語法,並且為該js模組分配一個ID。

  1. let ID =0

  2. function createAsset (filename){

  3. // 讀取檔案內容

  4. const content = fs.readFileSync(filename,'utf-8')

  5. // 轉化成AST

  6. const ast = babylon.parse(content,{

  7.    sourceType:'module',

  8. });

  9. // 該檔案的所有依賴

  10. const dependencies =[]

  11. // 獲取依賴宣告

  12.  traverse(ast,{

  13. ImportDeclaration:({ node })=>{

  14.      dependencies.push(node.source.value);

  15. }

  16. })

  17. // 轉化ES6語法到ES5

  18. const{code}= transformFromAst(ast,null,{

  19.    presets:['env'],

  20. })

  21. // 分配ID

  22. const id = ID++

  23. // 返回這個模組

  24. return{

  25.    id,

  26.    filename,

  27.    dependencies,

  28.    code,

  29. }

  30. }

執行 createAsset('./example/entry.js'),輸出如下:

  1. { id:0,

  2.  filename:'./example/entry.js',

  3.  dependencies:['./message.js'],

  4.  code:'"use strict";\n\nvar _message = require("./message.js");\n\nvar _message2 = _interopRequireDefault(_message);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nconsole.log(_message2.default);'}

可見 entry.js檔案已經變成了一個典型的模組,且依賴已經被分析出來了。接下來我們就要遞迴這個過程,把“依賴中的依賴”也都分析出來,也就是下一節要討論的建立依賴關係圖集。

3、建立依賴關係圖集

新建一個名為 createGragh()的函式,傳入一個入口檔案的路徑作為引數,然後通過 createAsset()解析這個檔案使之定義成一個模組。

接下來,為了能夠挨個挨個地對模組進行依賴分析,所以我們維護一個數組,首先把第一個模組傳進去並進行分析。當這個模組被分析出還有其他依賴模組的時候,就把這些依賴模組也放進陣列中,然後繼續分析這些新加進去的模組,直到把所有的依賴以及“依賴中的依賴”都完全分析出來。

與此同時,我們有必要為模組新建一個 mapping屬性,用來儲存模組、依賴、依賴ID之間的依賴關係,例如“ID為0的A模組依賴於ID為2的B模組和ID為3的C模組”就可以表示成下面這個樣子:

  1. {

  2. 0:[function A (){},{'B.js':2,'C.js':3}]

  3. }

搞清楚了箇中道理,就可以開始編寫函數了。

  1. function createGragh (entry){

  2. // 解析傳入的檔案為模組

  3. const mainAsset = createAsset(entry)

  4. // 維護一個數組,傳入第一個模組

  5. const queue =[mainAsset]

  6. // 遍歷陣列,分析每一個模組是否還有其它依賴,若有則把依賴模組推進陣列

  7. for(const asset of queue){

  8.    asset.mapping ={}

  9. // 由於依賴的路徑是相對於當前模組,所以要把相對路徑都處理為絕對路徑

  10. const dirname = path.dirname(asset.filename)

  11. // 遍歷當前模組的依賴項並繼續分析

  12.    asset.dependencies.forEach(relativePath =>{

  13. // 構造絕對路徑

  14. const absolutePath = path.join(dirname, relativePath)

  15. // 生成依賴模組

  16. const child = createAsset(absolutePath)

  17. // 把依賴關係寫入模組的mapping當中

  18.      asset.mapping[relativePath]= child.id

  19. // 把這個依賴模組也推入到queue陣列中,以便繼續對其進行以來分析

  20.      queue.push(child)

  21. })

  22. }

  23. // 最後返回這個queue,也就是依賴關係圖集

  24. return queue

  25. }

可能有讀者對其中的 for...of ...迴圈當中的 queue.push有點迷,但是隻要嘗試過下面這段程式碼就能搞明白了:

  1. var numArr =['1','2','3']

  2. for(num of numArr){

  3.  console.log(num)

  4. if(num ==='3'){

  5.    arr.push('Done!')

  6. }

  7. }

嘗試執行一下 createGraph('./example/entry.js'),就能夠看到如下的輸出:

  1. [{ id:0,

  2.    filename:'./example/entry.js',

  3.    dependencies:['./message.js'],

  4.    code:'"use strict";\n\nvar _message = require("./message.js");\n\nvar _message2 = _interopRequireDefault(_message);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nconsole.log(_message2.default);',

  5.    mapping:{'./message.js':1}},

  6. { id:1,

  7.    filename:'example/message.js',

  8.    dependencies:['./name.js'],

  9.    code:'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\n\nvar _name = require("./name.js");\n\nexports.default = "Hello " + _name.name + "!";',

  10.    mapping:{'./name.js':2}},

  11. { id:2,

  12.    filename:'example/name.js',

  13.    dependencies:[],

  14.    code:'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nvar name = exports.name = \'world\';',

  15.    mapping:{}}]

現在依賴關係圖集已經構建完成了,接下來就是把它們打包成一個單獨的,可直接執行的檔案啦!

4、進行打包

上一步生成的依賴關係圖集,接下來將通過 CommomJS規範來實現載入。由於篇幅關係,本文不對 CommomJS規範進行擴充套件,有興趣的讀者可以參考@阮一峰 老師的一篇文章《瀏覽器載入 CommonJS 模組的原理與實現》,說得非常清晰。簡單來說,就是通過構造一個立即執行函式 (function(){})(),手動定義 module, exports和 require變數,最後實現程式碼在瀏覽器執行的目的。

接下來就是依據這個規範,通過字串拼接去構建程式碼塊。

  1. function bundle (graph){

  2. let modules =''

  3.  graph.forEach(mod =>{

  4.    modules +=`${mod.id}: [

  5.      function (require, module, exports) { ${mod.code} },

  6.      ${JSON.stringify(mod.mapping)},

  7.    ],`

  8. })

  9. const result =`

  10.    (function(modules) {

  11.      function require(id) {

  12.        const [fn, mapping] = modules[id];

  13.        function localRequire(name) {

  14.          return require(mapping[name]);

  15.        }

  16.        const module = { exports : {} };

  17.        fn(localRequire, module, module.exports);

  18.        return module.exports;

  19.      }

  20.      require(0);

  21.    })({${modules}})

  22.  `

  23. return result

  24. }