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

前端模塊化成爲了主流的今天,離不開各類打包工具的貢獻。社區裏面對於webpack,rollup以及後起之秀parcel的介紹層出不窮,對於它們各自的使用配置分析也是汗牛充棟。爲了不成爲一位「配置工程師」,咱們須要來了解一下打包工具的運行原理,只有把核心原理搞明白了,在工具的使用上才能更加駕輕就熟。php

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

一、打包工具核心原理

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

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

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

爲了簡單起見,minipack項目直接使用ES modules規範,接下來咱們新建三個文件,而且爲它們之間創建依賴:webpack

 
 
  
  
           
  
  
  1. web

  2. swift

/* name.js */export const name = 'World'
 
 
  
  
           
  
  
  1. 數組

  2. 瀏覽器

/* message.js */import { name } from './name.js'export default `Hello ${name}!`
 
 
  
  
           
  
  
/* entry.js */import message from './message.js'console.log(message)

它們的依賴關係很是簡單: entry.js → message.js → name.js,其中 entry.js將會成爲打包工具的入口文件。

可是,這裏面的依賴關係只是咱們人類所理解的,若是要讓機器也可以理解當中的依賴關係,就須要藉助必定的手段了。

二、依賴關係解析

新建一個js文件,命名爲 minipack.js,首先引入必要的工具。

 
 
  
  
           
  
  
/* minipack.js */const fs = require('fs')const path = require('path')const babylon = require('babylon')const traverse = require('babel-traverse').defaultconst { transformFromAst } = require('babel-core')

接下來,咱們會撰寫一個函數,這個函數接收一個文件做爲模塊,而後讀取它裏面的內容,分析出其全部的依賴項。固然,咱們能夠經過正則匹配模塊文件裏面的 import關鍵字,但這樣作很是不優雅,因此咱們可使用 babylon這個js解析器把文件內容轉化成抽象語法樹(AST),直接從AST裏面獲取咱們須要的信息。

獲得了AST以後,就可使用 babel-traverse去遍歷這棵AST,獲取當中關鍵的「依賴聲明」,而後把這些依賴都保存在一個數組當中。

最後使用 babel-core的 transformFromAst方法搭配 babel-preset-env插件,把ES6語法轉化成瀏覽器能夠識別的ES5語法,而且爲該js模塊分配一個ID。

 
 
  
  
           
  
  
let ID = 0function createAsset (filename) {// 讀取文件內容const content = fs.readFileSync(filename, 'utf-8')// 轉化成ASTconst ast = babylon.parse(content, {sourceType: 'module',});// 該文件的全部依賴const dependencies = []// 獲取依賴聲明traverse(ast, {ImportDeclaration: ({ node }) => {dependencies.push(node.source.value);}})// 轉化ES6語法到ES5const {code} = transformFromAst(ast, null, {presets: ['env'],})// 分配IDconst id = ID++// 返回這個模塊return {id,filename,dependencies,code,}}

運行 createAsset('./example/entry.js'),輸出以下:

 
 
  
  
           
  
  
{ id: 0,filename: './example/entry.js',dependencies: [ './message.js' ],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文件已經變成了一個典型的模塊,且依賴已經被分析出來了。接下來咱們就要遞歸這個過程,把「依賴中的依賴」也都分析出來,也就是下一節要討論的創建依賴關係圖集。

三、創建依賴關係圖集

新建一個名爲 createGragh()的函數,傳入一個入口文件的路徑做爲參數,而後經過 createAsset()解析這個文件使之定義成一個模塊。

接下來,爲了可以挨個挨個地對模塊進行依賴分析,因此咱們維護一個數組,首先把第一個模塊傳進去並進行分析。當這個模塊被分析出還有其餘依賴模塊的時候,就把這些依賴模塊也放進數組中,而後繼續分析這些新加進去的模塊,直到把全部的依賴以及「依賴中的依賴」都徹底分析出來。

與此同時,咱們有必要爲模塊新建一個 mapping屬性,用來儲存模塊、依賴、依賴ID之間的依賴關係,例如「ID爲0的A模塊依賴於ID爲2的B模塊和ID爲3的C模塊」就能夠表示成下面這個樣子:

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

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

 
 
  
  
           
  
  
function createGragh (entry) {// 解析傳入的文件爲模塊const mainAsset = createAsset(entry)// 維護一個數組,傳入第一個模塊const queue = [mainAsset]// 遍歷數組,分析每個模塊是否還有其它依賴,如有則把依賴模塊推動數組for (const asset of queue) {asset.mapping = {}// 因爲依賴的路徑是相對於當前模塊,因此要把相對路徑都處理爲絕對路徑const dirname = path.dirname(asset.filename)// 遍歷當前模塊的依賴項並繼續分析asset.dependencies.forEach(relativePath => {// 構造絕對路徑const absolutePath = path.join(dirname, relativePath)// 生成依賴模塊const child = createAsset(absolutePath)// 把依賴關係寫入模塊的mapping當中asset.mapping[relativePath] = child.id// 把這個依賴模塊也推入到queue數組中,以便繼續對其進行以來分析queue.push(child)})}// 最後返回這個queue,也就是依賴關係圖集return queue}

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

 
 
  
  
           
  
  
var numArr = ['1', '2', '3']for (num of numArr) {console.log(num)if (num === '3') {arr.push('Done!')}}

嘗試運行一下 createGraph('./example/entry.js'),就可以看到以下的輸出:

 
 
  
  
           
  
  
[ { id: 0,filename: './example/entry.js',dependencies: [ './message.js' ],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);',mapping: { './message.js': 1 } },{ id: 1,filename: 'example/message.js',dependencies: [ './name.js' ],code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\n\nvar _name = require("./name.js");\n\nexports.default = "Hello " + _name.name + "!";',mapping: { './name.js': 2 } },{ id: 2,filename: 'example/name.js',dependencies: [],code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nvar name = exports.name = \'world\';',mapping: {} } ]

如今依賴關係圖集已經構建完成了,接下來就是把它們打包成一個單獨的,可直接運行的文件啦!

四、進行打包

上一步生成的依賴關係圖集,接下來將經過 CommomJS規範來實現加載。因爲篇幅關係,本文不對 CommomJS規範進行擴展,有興趣的讀者能夠參考@阮一峯 老師的一篇文章《瀏覽器加載 CommonJS 模塊的原理與實現》,說得很是清晰。簡單來講,就是經過構造一個當即執行函數 (function () {})(),手動定義 module, exports和 require變量,最後實現代碼在瀏覽器運行的目的。

接下來就是依據這個規範,經過字符串拼接去構建代碼塊。

 
 
  
  
           
  
  
function bundle (graph) {let modules = ''graph.forEach(mod => {modules += `${mod.id}: [function (require, module, exports) { ${mod.code} },${JSON.stringify(mod.mapping)},],`})const result = `(function(modules) {function require(id) {const [fn, mapping] = modules[id];function localRequire(name) {return require(mapping[name]);}const module = { exports : {} };fn(localRequire, module, module.exports);return module.exports;}require(0);})({${modules}})`return result}

最後運行 bundle(createGraph('./example/entry.js')),輸出以下:

 
 
  
  
           
  
  
(function (modules) {function require(id) {const [fn, mapping] = modules[id];function localRequire(name) {return require(mapping[name]);}const module = { exports: {} };fn(localRequire, module, module.exports);return module.exports;}require(0);})({0: [function (require, module, exports) {"use strict";var _message = require("./message.js");var _message2 = _interopRequireDefault(_message);function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }console.log(_message2.default);},{ "./message.js": 1 },], 1: [function (require, module, exports) {"use strict";Object.defineProperty(exports, "__esModule", {value: true});var _name = require("./name.js");exports.default = "Hello " + _name.name + "!";},{ "./name.js": 2 },], 2: [function (require, module, exports) {"use strict";Object.defineProperty(exports, "__esModule", {value: true});var name = exports.name = 'world';},{},],})

這段代碼將可以直接在瀏覽器運行,輸出「Hello world!」。

至此,整一個打包工具已經完成。

五、概括總結

通過上面幾個步驟,咱們能夠知道一個模塊打包工具,第一步會從入口文件開始,對其進行依賴分析,第二步對其全部依賴再次遞歸進行依賴分析,第三步構建出模塊的依賴圖集,最後一步根據依賴圖集使用 CommonJS規範構建出最終的代碼。明白了當中每一步的目的,便可以明白一個打包工具的運行原理。

本文同步分享在 博客「grain先森」(JianShu)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索