本文參考webpack
創始人 Tobias Koppers 的視頻 Webpack founder Tobias Koppers demos bundling live by hand,梳理webpack
打包過程。javascript
咱們準備一個極簡單的項目來進行打包,目錄結構和內容以下:html
+-- src
| +-- big.js
| +-- helloWorld.js
| +-- index.js
| +-- lazy.js
複製代碼
index.jsjava
import helloWorld from './helloWorld'
const node = document.createElement("div")
node.innerHTML = helloWorld + 'loading...'
import(/* webpackChunkName: "async" */ './lazy').then(({ default: lazy }) => {
node.innerHTML = helloWorld + lazy
})
document.body.appendChild(node)
複製代碼
helloWorld.jsnode
import big from './big'
const helloWorld = big('hello world!')
export default helloWorld
複製代碼
big.jswebpack
export default (val) => {
return val && val.toUpperCase()
}
複製代碼
lazy.jsgit
import big from './big'
const lazy = big("lazy loaded!")
export default lazy
複製代碼
咱們先來看下webpack打包以後的結果,省略了一些代碼,可是大致能夠看到,全部分散的文件最終變成一個當即執行函數,參數是文件(模塊)隊列數組。es6
/******/ (function(modules) {// webpackBootstrap
/******/ // ...
/******/ })({
/************************************************************************/
/***/ "./src/big.js":
/***/ (function(module, __webpack_exports__, __webpack_require__) {}),
/***/ "./src/index.js":
/***/ (function(module, __webpack_exports__, __webpack_require__) {
/***/ })
/******/ });
複製代碼
咱們的目標是經過人工打包的方式生成這樣一個當即執行函數,經過必定的串聯邏輯,將全部的模塊整合到一塊兒。github
能夠看到項目模塊之間有這樣的引用關係,入口文件引入了helloWorld
和lazy
,helloWorld
和lazy
分別又引入了big
web
* src/index.js (ESM)
# ./helloWorld
# (async) ./lazy
- src/helloWorld.js
- (async) src/lazy.js
* src/helloWorld.js (ESM)
# ./big
- src/big.js
* src/big.js
* src/lazy.js (ESM)
# ./big
- src/big.js
複製代碼
打包以後將生成兩個文件,一個主文件main.js,一個是動態引入的async.js。其中,main是async的父文件,main中有的模塊,asycn能夠不引入。main文件裏面已經包含了src/big.js
,這裏進行優化,打包後的async.js
不須要包含src/big.js
以下圖所示:json
- src/index.js
- src/helloWorld.js
- src/big.js
複製代碼
async.js (parent:main)
- src/lazy.js
- src/big.js ( in parent)---delete
複製代碼
如今劃分一下模塊,能夠看到入口文件--index.js
,咱們將它import
的文件直接串聯當成第一個模塊。這裏只有引入一個模塊helloWorld
(lazy是打包進去async.js
暫不考慮)。由此能夠劃分紅三個模塊,咱們手動爲每一個模塊賦予一個id
(中括號中的數字)。
* [0]src/index.js (ESM) + 1modules
# ./helloWorld
# (async) ./lazy
- src/helloWorld.js
- (async) src/lazy.js
- src/big.js
* [1]src/big.js
* [2]src/lazy.js (ESM)
# ./big
- src/big.js
複製代碼
咱們知道webpack
把分散的代碼經過import
和export
串成一個當即執行函數(IIFE),參數是模塊對象數組。 其中模塊對象是一個這樣的結構:
{
[moduleId]: function() {
// 模塊代碼
}
}
複製代碼
如今來處理一下每一個文件的import
和export
。
對於每個模塊,要保證有獨立的做用域,用一個funtion
去包裹。而且傳入兩個參數,用來實現import
和export
的功能。 index.js + 1 modules(hellowWorld.js)
(function(__require__, exports) {
let X = __require__(1)
const helloWorld = X.default('hello world!')
const node = document.createElement("div")
node.innerHTML = helloWorld + 'loading...'
// 先看普通的import
// import(/* webpackChunkName: "async" */ './lazy').then(({ default: lazy }) => {
// node.innerHTML = helloWorld + lazy
// })
document.body.appendChild(node)
})
複製代碼
big.js
(function(__require__, exports) {
exports.default = (val) => {
return val && val.toUpperCase()
}
})
複製代碼
import的功能就是: 1.執行目標模塊的代碼; 2.導出目標模塊的export
內容給外部使用。 以下__require__
函數的實現,
function __require__(id) {
// 設置一個緩存,有的話直接返回
if(cache[id]) return cache[id].exports
var module = {
exports: {}
};
// 一、執行當前模塊的內容,這個modules[id]就是咱們剛纔對每一個模塊封裝的那個方法
modules[id](__require__, module.exports, module)
cache[id] = module
// 二、導出當前模塊的export內容給外部使用
return module.exports
}
複製代碼
runtime.js
!(function(modules){
function __require__ (id) {
var module = {
exports: []
}
modules[id](__require__, module.exports, module);
return module.exports
}
__require__(0)
})(
{
0: (function(__require__, exports) {
let X = __require__(1)
const helloWorld = X.default('hello world!')
const node = document.createElement("div")
node.innerHTML = helloWorld + 'loading...'
// import(/* webpackChunkName: "async" */ './lazy').then(({ default: lazy }) => {
// node.innerHTML = helloWorld + lazy
// }
document.body.appendChild(node)
}),
1: (function(__require__, exports) {
exports.default = (val) => {
return val && val.toUpperCase()
}
})
}
)
複製代碼
在index.html上引入這個文件,打開就能看到結果了。至此,咱們完成了最基本的手動打包流程。
如今還剩下對lazy.js
的打包,它是做爲一個單獨的文件,按需引入的。咱們但願使用的時候是這樣的,加載完模塊,而後進行require: index.js (bundled)
// import(/* webpackChunkName: "async" */ './lazy').then(({ default: lazy }) => {
// node.innerHTML = helloWorld + lazy
// })
__require__.loadChunk(0)
.then(__require__.bind(null, 3))
.then(function(Y){
node.innerHTML = helloWorld + Y.default
})
複製代碼
請求一個文件地址,獲得文件中的數據,這個過程用相似jsonp
的方式來實現。
首先是下載文件,這個過程是異步的,要用一個promise
來封裝。下載完成,還須要解析出數據才能執行下一步。因此,promise
的回調函數resolve
下載完成先放在一個全局變量chunkResolves
當中,等解析出數據以後再調用它。
runtime.js
// 每一個模塊下載(promise)完成對應的resolve
let chunkResolves = {};
__require__.loadChunk = function(chunkId) {
return new Promise(resolve => {
chunkResolves[chunkId] = resolve
let script = document.createElement('script')
script.src = 'src/' + {0: 'async'}[chunkId]+ '.js'
document.head.appendChild(script)
})
}
複製代碼
根據jsonp
的原理,下載下來的模塊對象須要用一個callback
(這裏是requireJsonp
)包裹,變成一個可執行的腳本,下載完成以後在本地執行這個callback
才能解析出模塊對象。因此手動對異步的模塊進行一個封裝: async.js
window.requireJsonp( 0, {
3: (function(__require__, exports) {
let X = __require__(1)
const lazy = X.default("lazy loaded!")
exports.default = lazy
})
})
複製代碼
而且咱們應提早聲明好window.requireJsonp
這個回調函數。咱們把下載獲得的動態模塊對象添加到當即執行函數參數的個模塊對象,就回到了普通的模塊打包的狀況,這時候解析完成,執行promise
的resolve
,算是整個異步加載的過程結束。 runtime.js
!(function(modules){
function __require__ (id) {
// ...
}
// 每一個模塊下載(promise)完成對應的resolve
let chunkResolves = {};
window.requireJsonp = function(chunkId, newModules) {
for (const id in newModules) {
modules[id] = newModules[id]
chunkResolves[chunkId]();
}
}
__require__(0)
})({
//...模塊對象
})
複製代碼
這樣,咱們就完成了人工打包一個項目的簡單流程。接下來看要怎麼用代碼來實現自動打包。
咱們參考開源項目minipack,來看看要怎麼實現一個簡易的打包工具。 先不看詳細的細節,咱們主要的步驟就是:
// 解析模塊
function createAsset(filename) {}
// 生成依賴圖
function createGraph(entry){}
// 打包
function bundle(graph){}
const graph = createGraph('./src/index.js')
const result = bundle(graph)
複製代碼
所依賴的工具
const fs = require('fs');
const path = require('path');
const babylon = require('babylon'); // 將文件轉化成AST
const traverse = require('babel-traverse').default; // 尋找依賴關係
const {transformFromAst} = require('babel-core'); // 將 AST 轉化成 ES5
複製代碼
主要就是把文本文件轉化成語法樹,拿到import
和export
知道模塊之間的依賴關係,再把語法樹轉換成ES5。
能夠了解一下語法樹,以下圖所示,能夠拿到每句代碼對應的信息。
function createAsset(filename) {
// 讀一個文件,獲得一個文件內容的字符串
const content = fs.readFileSync(filename, 'utf-8');
// 咱們經過 babylon 這個 javascript 解析器來理解 import 進來的字符串
const ast = babylon.parse(content, {
sourceType: 'module',
});
// 該模塊所依賴的模塊的相對路徑放在這個 dependencies 數組
const dependencies = [];
// import聲明
traverse(ast, {
// es6 的模塊是靜態的,不能導入一個變量或者有條件的導入另外一個模塊
ImportDeclaration: ({node}) => {
// 所依賴的模塊的路徑
dependencies.push(node.source.value);
},
});
// 遞增設置模塊ID
const id = ID++;
// AST -> ES5
const {code} = transformFromAst(ast, null, {
presets: ['env'],
});
// 返回模塊的信息
return {
id,
filename,
dependencies,
code,
};
}
複製代碼
index文件解析後輸出以下內容
import helloWorld from './helloWorld'
const node = document.createElement("div")
node.innerHTML = helloWorld + 'loading...'
document.body.appendChild(node)
複製代碼
{ id: 0,
filename: './src/index.js',
dependencies: [ './helloWorld.js' ],
code: '"use strict";\n\n
var _helloWorld = require("./helloWorld.js");\n\n
var _helloWorld2 = _interopRequireDefault(_helloWorld);\n\n
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : {
default: obj }; }\n\n
var node = document.createElement("div");\nn
ode.innerHTML = _helloWorld2.default + \'loading...\';\n\n
document.body.appendChild(node);'
}
複製代碼
// 咱們須要知道單個模塊的依賴,而後從入口文件開始,提取依賴圖
function createGraph(entry) {
// 從第一個文件開始,首先解析index文件
const mainAsset = createAsset(entry);
// 定義一個依賴隊列,一開始的時候只有入口文件
const queue = [mainAsset];
// 遍歷 queue,廣度優先
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);
// 子模塊`路徑-id`map
asset.mapping[relativePath] = child.id;
// 每個子模塊加入依賴圖隊列,進行遍歷
queue.push(child);
});
}
複製代碼
輸出的依賴圖長這樣:
[ { id: 0,
filename: './src/index.js',
dependencies: [ './helloWorld.js' ],
code:
'"use strict";\n\nvar _helloWorld = require("./helloWorld.js");\n\nvar _helloWorld2 = _interopRequireDefault(_helloWorld);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nvar node = document.createElement("div");\nnode.innerHTML = _helloWorld2.default + \'loading...\';\n// import(/* webpackChunkName: "async" */ \'./lazy\').then(({ default: lazy }) => {\n// node.innerHTML = helloWorld + lazy\n// })\ndocument.body.appendChild(node);',
mapping: { './helloWorld.js': 1 }
},
{ id: 1,
filename: 'src\\helloWorld.js',
dependencies: [ './big.js' ],
code:
'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\n\nvar _big = require("./big.js");\n\nvar _big2 = _interopRequireDefault(_big);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nvar helloWorld = (0, _big2.default)(\'hello world!\');\nexports.default = helloWorld;',
mapping: { './big.js': 2 }
},
{ id: 2,
filename: 'src\\big.js',
dependencies: [],
code:
'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\n\nexports.default = function (val) {\n return val && val.toUpperCase();\n};',
mapping: {}
} ]
複製代碼
modules
// 最終咱們要生成一個自執行函數,參數是模塊依賴圖
// (function() {})()
function bundle(graph) {
let modules = '';
graph.forEach(mod => {
// 利用 createAsset 解析的時候,咱們是把 import 轉化成 commonJs 的 require
// 模塊`id-路徑`的map,由於咱們轉化以後的代碼的require是使用相對路徑.寫一個map,拿到模塊id的時候能夠知道該模塊對應的路徑
// { './relative/path': 1 }.
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;
}
複製代碼
生成的模塊對象參數跟咱們第一部分手動打包的模塊對像是同樣的:
0: [
function (require, module, exports) {
"use strict";
var _helloWorld = require("./helloWorld.js");
var _helloWorld2 = _interopRequireDefault(_helloWorld);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var node = document.createElement("div");
node.innerHTML = _helloWorld2.default + 'loading...';
// import(/* webpackChunkName: "async" */ './lazy').then(({ default:
lazy }) => {
// node.innerHTML = helloWorld + lazy
// })
document.body.appendChild(node);
},
{"./helloWorld.js":1},
],
1: [
function (require, module, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _big = require("./big.js");
var _big2 = _interopRequireDefault(_big);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var helloWorld = (0, _big2.default)('hello world!');
exports.default = helloWorld;
},
{"./big.js":2},
],
2: [
function (require, module, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = function (val) {
return val && val.toUpperCase();
};
},
{},
],
複製代碼
至此咱們完成了一個簡易的模塊打包器。
參考文獻:
Webpack founder Tobias Koppers demos bundling live by hand
github 項目 minipack