摘要:前端模塊化成為了主流的今天,離不開各種打包工具的貢獻。與此同時,打包工具也會處理好模塊之間的依賴關系,最終這個大模塊將可以被運行在合適的平臺中。至此,整一個打包工具已經完成。明白了當中每一步的目的,便能夠明白一個打包工具的運行原理。
前端模塊化成為了主流的今天,離不開各種打包工具的貢獻。社區里面對于webpack,rollup以及后起之秀parcel的介紹層出不窮,對于它們各自的使用配置分析也是汗牛充棟。為了避免成為一位“配置工程師”,我們需要來了解一下打包工具的運行原理,只有把核心原理搞明白了,在工具的使用上才能更加得心應手。
本文基于parcel核心開發者@ronami的開源項目minipack而來,在其非常詳盡的注釋之上加入更多的理解和說明,方便讀者更好地理解。
1、打包工具核心原理顧名思義,打包工具就是負責把一些分散的小模塊,按照一定的規則整合成一個大模塊的工具。與此同時,打包工具也會處理好模塊之間的依賴關系,最終這個大模塊將可以被運行在合適的平臺中。
打包工具會從一個入口文件開始,分析它里面的依賴,并且再進一步地分析依賴中的依賴,不斷重復這個過程,直到把這些依賴關系理清挑明為止。
從上面的描述可以看到,打包工具最核心的部分,其實就是處理好模塊之間的依賴關系,而minipack以及本文所要討論的,也是集中在模塊依賴關系的知識點當中。
為了簡單起見,minipack項目直接使用ES modules規范,接下來我們新建三個文件,并且為它們之間建立依賴:
/* name.js */ export const name = "World"
/* 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將會成為打包工具的入口文件。
但是,這里面的依賴關系只是我們人類所理解的,如果要讓機器也能夠理解當中的依賴關系,就需要借助一定的手段了。
2、依賴關系解析新建一個js文件,命名為minipack.js,首先引入必要的工具。
/* minipack.js */ const fs = require("fs") const path = require("path") const babylon = require("babylon") const traverse = require("babel-traverse").default const { 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 = 0 function createAsset (filename) { // 讀取文件內容 const content = fs.readFileSync(filename, "utf-8") // 轉化成AST const ast = babylon.parse(content, { sourceType: "module", }); // 該文件的所有依賴 const dependencies = [] // 獲取依賴聲明 traverse(ast, { ImportDeclaration: ({ node }) => { dependencies.push(node.source.value); } }) // 轉化ES6語法到ES5 const {code} = transformFromAst(ast, null, { presets: ["env"], }) // 分配ID const id = ID++ // 返回這個模塊 return { id, filename, dependencies, code, } }
運行createAsset("./example/entry.js"),輸出如下:
{ id: 0, filename: "./example/entry.js", dependencies: [ "./message.js" ], code: ""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);" }
可見entry.js文件已經變成了一個典型的模塊,且依賴已經被分析出來了。接下來我們就要遞歸這個過程,把“依賴中的依賴”也都分析出來,也就是下一節要討論的建立依賴關系圖集。
3、建立依賴關系圖集新建一個名為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"; var _message = require("./message.js"); var _message2 = _interopRequireDefault(_message); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } console.log(_message2.default);", mapping: { "./message.js": 1 } }, { id: 1, filename: "example/message.js", dependencies: [ "./name.js" ], code: ""use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _name = require("./name.js"); exports.default = "Hello " + _name.name + "!";", mapping: { "./name.js": 2 } }, { id: 2, filename: "example/name.js", dependencies: [], code: ""use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var name = exports.name = "world";", mapping: {} } ]
現在依賴關系圖集已經構建完成了,接下來就是把它們打包成一個多帶帶的,可直接運行的文件啦!
4、進行打包上一步生成的依賴關系圖集,接下來將通過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!”。
至此,整一個打包工具已經完成。
5、歸納總結經過上面幾個步驟,我們可以知道一個模塊打包工具,第一步會從入口文件開始,對其進行依賴分析,第二步對其所有依賴再次遞歸進行依賴分析,第三步構建出模塊的依賴圖集,最后一步根據依賴圖集使用CommonJS規范構建出最終的代碼。明白了當中每一步的目的,便能夠明白一個打包工具的運行原理。
最后再次感謝@ronami的開源項目minipack,其源碼有著更為詳細的注釋,非常值得大家閱讀。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/95449.html
摘要:本文總結了前端老司機經常問題的一些問題并結合個人總結給出了比較詳盡的答案。網易阿里騰訊校招社招必備知識點。此外還有網絡線程,定時器任務線程,文件系統處理線程等等。線程核心是引擎。主線程和工作線程之間的通知機制叫做事件循環。 showImg(https://segmentfault.com/img/bVbu4aB?w=300&h=208); 本文總結了前端老司機經常問題的一些問題并結合個...
摘要:本文總結了前端老司機經常問題的一些問題并結合個人總結給出了比較詳盡的答案。網易阿里騰訊校招社招必備知識點。此外還有網絡線程,定時器任務線程,文件系統處理線程等等。線程核心是引擎。主線程和工作線程之間的通知機制叫做事件循環。 showImg(https://segmentfault.com/img/bVbu4aB?w=300&h=208); 本文總結了前端老司機經常問題的一些問題并結合個...
摘要:本文總結了前端老司機經常問題的一些問題并結合個人總結給出了比較詳盡的答案。網易阿里騰訊校招社招必備知識點。此外還有網絡線程,定時器任務線程,文件系統處理線程等等。線程核心是引擎。主線程和工作線程之間的通知機制叫做事件循環。 showImg(https://segmentfault.com/img/bVbu4aB?w=300&h=208); 本文總結了前端老司機經常問題的一些問題并結合個...
閱讀 3560·2021-09-22 10:52
閱讀 1588·2021-09-09 09:34
閱讀 1990·2021-09-09 09:33
閱讀 758·2019-08-30 15:54
閱讀 2596·2019-08-29 11:15
閱讀 713·2019-08-26 13:37
閱讀 1667·2019-08-26 12:11
閱讀 2975·2019-08-26 12:00