打包工具運行原理你知道嗎?

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

本文基于parcel核心開發(fā)者@ronami的開源項目minipack而來,在其非常詳盡的注釋之上加入更多的理解和說明,方便讀者更好地理解。

1、打包工具核心原理

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

打包工具會從一個入口文件開始,分析它里面的依賴,并且再進一步地分析依賴中的依賴,不斷重復(fù)這個過程,直到把這些依賴關(guān)系理清挑明為止。

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

為了簡單起見,minipack項目直接使用ES modules規(guī)范,接下來我們新建三個文件,并且為它們之間建立依賴:

  1. /* name.js */

  2. export const name = 'World'

  1. /* message.js */

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

  3. export default `Hello ${name}!`

  1. /* entry.js */

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

  3. console.log(message)

它們的依賴關(guān)系非常簡單:?entry.js?→?message.js?→?name.js,其中?entry.js將會成為打包工具的入口文件。

但是,這里面的依賴關(guān)系只是我們?nèi)祟愃斫獾?,如果要讓機器也能夠理解當(dāng)中的依賴關(guān)系,就需要借助一定的手段了。

2、依賴關(guān)系解析

新建一個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')

接下來,我們會撰寫一個函數(shù),這個函數(shù)接收一個文件作為模塊,然后讀取它里面的內(nèi)容,分析出其所有的依賴項。當(dāng)然,我們可以通過正則匹配模塊文件里面的?import關(guān)鍵字,但這樣做非常不優(yōu)雅,所以我們可以使用?babylon這個js解析器把文件內(nèi)容轉(zhuǎn)化成抽象語法樹(AST),直接從AST里面獲取我們需要的信息。

得到了AST之后,就可以使用?babel-traverse去遍歷這棵AST,獲取當(dāng)中關(guān)鍵的“依賴聲明”,然后把這些依賴都保存在一個數(shù)組當(dāng)中。

最后使用?babel-core的?transformFromAst方法搭配?babel-preset-env插件,把ES6語法轉(zhuǎn)化成瀏覽器可以識別的ES5語法,并且為該js模塊分配一個ID。

  1. let ID = 0

  2. function createAsset (filename) {

  3. ?// 讀取文件內(nèi)容

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

  5. ?// 轉(zhuǎn)化成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. ?// 轉(zhuǎn)化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文件已經(jīng)變成了一個典型的模塊,且依賴已經(jīng)被分析出來了。接下來我們就要遞歸這個過程,把“依賴中的依賴”也都分析出來,也就是下一節(jié)要討論的建立依賴關(guān)系圖集。

3、建立依賴關(guān)系圖集

新建一個名為?createGragh()的函數(shù),傳入一個入口文件的路徑作為參數(shù),然后通過?createAsset()解析這個文件使之定義成一個模塊。

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

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

  1. {

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

  3. }

搞清楚了個中道理,就可以開始編寫函數(shù)了。

  1. function createGragh (entry) {

  2. ?// 解析傳入的文件為模塊

  3. ?const mainAsset = createAsset(entry)

  4. ?// 維護一個數(shù)組,傳入第一個模塊

  5. ?const queue = [mainAsset]

  6. ?// 遍歷數(shù)組,分析每一個模塊是否還有其它依賴,若有則把依賴模塊推進數(shù)組

  7. ?for (const asset of queue) {

  8. ? ?asset.mapping = {}

  9. ? ?// 由于依賴的路徑是相對于當(dāng)前模塊,所以要把相對路徑都處理為絕對路徑

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

  11. ? ?// 遍歷當(dāng)前模塊的依賴項并繼續(xù)分析

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

  13. ? ? ?// 構(gòu)造絕對路徑

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

  15. ? ? ?// 生成依賴模塊

  16. ? ? ?const child = createAsset(absolutePath)

  17. ? ? ?// 把依賴關(guān)系寫入模塊的mapping當(dāng)中

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

  19. ? ? ?// 把這個依賴模塊也推入到queue數(shù)組中,以便繼續(xù)對其進行以來分析

  20. ? ? ?queue.push(child)

  21. ? ?})

  22. ?}

  23. ?// 最后返回這個queue,也就是依賴關(guān)系圖集

  24. ?return queue

  25. }

可能有讀者對其中的?for...of?...循環(huán)當(dāng)中的?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: {} } ]

現(xiàn)在依賴關(guān)系圖集已經(jīng)構(gòu)建完成了,接下來就是把它們打包成一個單獨的,可直接運行的文件啦!

4、進行打包

上一步生成的依賴關(guān)系圖集,接下來將通過?CommomJS規(guī)范來實現(xiàn)加載。由于篇幅關(guān)系,本文不對?CommomJS規(guī)范進行擴展,有興趣的讀者可以參考@阮一峰 老師的一篇文章《瀏覽器加載 CommonJS 模塊的原理與實現(xiàn)》,說得非常清晰。簡單來說,就是通過構(gòu)造一個立即執(zhí)行函數(shù)?(function?()?{})(),手動定義?module,?exports和?require變量,最后實現(xiàn)代碼在瀏覽器運行的目的。

接下來就是依據(jù)這個規(guī)范,通過字符串拼接去構(gòu)建代碼塊。

  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. }

最后運行?bundle(createGraph('./example/entry.js')),輸出如下:

  1. (function (modules) {

  2. ?function require(id) {

  3. ? ?const [fn, mapping] = modules[id];

  4. ? ?function localRequire(name) {

  5. ? ? ?return require(mapping[name]);

  6. ? ?}

  7. ? ?const module = { exports: {} };

  8. ? ?fn(localRequire, module, module.exports);

  9. ? ?return module.exports;

  10. ?}

  11. ?require(0);

  12. })({

  13. ?0: [

  14. ? ?function (require, module, exports) {

  15. ? ? ?"use strict";

  16. ? ? ?var _message = require("./message.js");

  17. ? ? ?var _message2 = _interopRequireDefault(_message);

  18. ? ? ?function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

  19. ? ? ?console.log(_message2.default);

  20. ? ?},

  21. ? ?{ "./message.js": 1 },

  22. ?], 1: [

  23. ? ?function (require, module, exports) {

  24. ? ? ?"use strict";

  25. ? ? ?Object.defineProperty(exports, "__esModule", {

  26. ? ? ? ?value: true

  27. ? ? ?});

  28. ? ? ?var _name = require("./name.js");

  29. ? ? ?exports.default = "Hello " + _name.name + "!";

  30. ? ?},

  31. ? ?{ "./name.js": 2 },

  32. ?], 2: [

  33. ? ?function (require, module, exports) {

  34. ? ? ?"use strict";

  35. ? ? ?Object.defineProperty(exports, "__esModule", {

  36. ? ? ? ?value: true

  37. ? ? ?});

  38. ? ? ?var name = exports.name = 'world';

  39. ? ?},

  40. ? ?{},

  41. ?],

  42. })

這段代碼將能夠直接在瀏覽器運行,輸出“Hello world!”。

至此,整一個打包工具已經(jīng)完成。

5、歸納總結(jié)

經(jīng)過上面幾個步驟,我們可以知道一個模塊打包工具,第一步會從入口文件開始,對其進行依賴分析,第二步對其所有依賴再次遞歸進行依賴分析,第三步構(gòu)建出模塊的依賴圖集,最后一步根據(jù)依賴圖集使用?CommonJS規(guī)范構(gòu)建出最終的代碼。明白了當(dāng)中每一步的目的,便能夠明白一個打包工具的運行原理。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容