前端模塊化成為了主流的本日,離不開種種打包東西的孝敬。社區內里關於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";\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模塊”就可以示意成下面這個模樣:
{
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: {} } ]
如今依靠關聯圖集已構建完成了,接下來就是把它們打包成一個零丁的,可直接運轉的文件啦!
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
範例構建出終究的代碼。邃曉了當中每一步的目標,便可以邃曉一個打包東西的運轉原理。