什么是打包器
一個完整的 JavaScript 項目(比如各種前端SPA)由各種各樣的資源模塊(module)組成换吧,包括 JavaScript 代碼寞焙,CSS 樣式以及圖片等各種文件。打包器(module bundler)可以分析入口文件(entry)引用了哪些模塊,找到對應的文件债蜜,將其合并到一起。這樣執(zhí)行輸出文件(output)的時候究反,一個完整的項目會呈現(xiàn)出來寻定。
單頁面應用包含大量 JavaScript 代碼,為了合理地管理代碼精耐,開發(fā)時會將代碼拆分到不同文件里面特姐。在各個模塊的代碼編寫完成之后,bundler 可以幫我們把各個分散的 JS 文件合并起來黍氮,輸出一個完整的 JS 文件唐含。
對于樣式文件和圖片等資源,我們也可以指定如何處理它們沫浆。一般的處理方式是直接插入到 HTML 或者 JS 文件中捷枯,或者通過指定文件網(wǎng)絡地址(public path),在需要該文件的時候瀏覽器會通過網(wǎng)絡請求獲取到這些資源专执。
功能完備的打包器可以把各種資源模塊聚集到一起淮捆,生成完整的 web app。但本文作為開篇只討論如何實現(xiàn)一個 JavaSript bundler本股。
過程分析
一個最簡單的 JS bundler 可以幫助我們:
- 找到 entry JavaScript 文件引用到的所有其他 JS 文件攀痊,并將其合并到目標 JS 文件(output)中。
- 保證各個模塊的 JavaScript 代碼都在自己的作用域中執(zhí)行拄显,避免命名沖突苟径。
為了保證每個模塊的 JS 代碼都在自己的作用域中執(zhí)行,可以參考 Node 執(zhí)行 JS 代碼的方式躬审〖郑可以概括為 5 步:
- resolve:通過 require 中的 string 定位到文件的真實地址。
- load:加載這個文件承边。
- wrap:將引入的代碼包含在一個函數(shù)中遭殉,保證定義的變量只作用在本文件中。
- execute:執(zhí)行代碼博助。
- cache:緩存執(zhí)行結果险污。
而打包的流程可以概括為:
- 找到起始文件的依賴文件,將其加載并描述為一個資源模塊(asset)富岳,其包含的信息包括:
- id:唯一 id
- filename:絕對文件路徑
- code:模塊代碼蛔糯,將其包含在一個函數(shù)里面拯腮。并且需要把 ESM 的 import 和 export 改成 require 和 exports,這樣可以執(zhí)行函數(shù)參數(shù)里面的 require 和 exports渤闷,函數(shù)參數(shù)的 require 可以幫我們通過相對路徑找到實際文件疾瓮。
- dependencies:引入的模塊。
- mapping:記錄以來模塊的相對路徑和其模塊 id 的對應關系飒箭。
- 當依賴的文件有其他依賴的時候狼电,繼續(xù)加載依賴文件。最終生成一個依賴圖(dependency graph)弦蹂,包含所有模塊之間的依賴關系肩碟。
- 拼湊一個完整 string,包含所有模塊信息凸椿,并且執(zhí)行起始文件削祈。輸出這個 string。
代碼實現(xiàn)
轉換 JS 文件為資源模塊(asset):
const path = require('path');
const fs = require('fs');
const parser = require('babel-parser');
const { transformFromAst } = require('@babel/core');
const traverse = require('@babel/traverse').default;
let ID = 0;
function createAsset(filename) {
const content = fs.readFileSync(filename, 'utf-8');
const ast = parser.parse(content, {
sourceType: 'module',
});
const dependencies = [];
traverse(ast, {
importDelcaration: ({ node }) => {
dependencies.push(node.source.value);
}
});
const { code } = tranformFromAst(ast, null, {
presets: ['@babel/preset-env'],
});
return {
id: ID++,
filename,
dependencies,
code
};
}
生成依賴圖:
function createGraph(entry) {
const mainAsset = createAsset(entry);
const queue = [mainAsset];
queue.forEach(asset => {
asset.mapping = {};
const dirname = path.dirname(asset.filename);
asset.dependencies.forEach((relativePath) => {
const filename = path.join(dirname, relativePath);
const child = createAsset(filename);
asset.mapping[relativePath] = child.id;
queue.push(child);
});
});
return queue;
}
合并 string:
function createBundle(entry) {
const graph = createGraph(entry);
let modules = '{';
graph.forEach((asset) => {
modules +=
`${asset.id}: [function (requre, module, exports) { ${asset.code} }, ${JSON.stringify(asset.mapping)}],`;
});
modules += '}';
const result = `function(modules) {
function require(id) {
const [fn, mapping] = modules[id];
function localRequire(relativePath) {
require(mapping[relativePath]));
}
const module = { exports: {} };
fn(localRequire, module, exports);
return moduele.exports;
}
require(0);
}(${modules)`;
}
備注項目依賴
"dependencies": {
"@babel/core": "7.9.6",
"@babel/parser": "7.9.6",
"@babel/preset-env": "7.9.6",
"@babel/traverse": "7.9.6"
}