因爲如今社區有太多的零配置腳手架,致使平常業務開發中基本不會關注webpack
的原理,甚至一些具體配置都不會去看。javascript
因爲疫情嚴重被困在家,無聊中透露着寂寞,我就按照着官方40分鐘教你寫webpack,學着實現一個小型的webpack
, 經過這次實踐簡單瞭解webpack
的打包原理。java
由於涉及到 ES6 轉 ES5,因此咱們首先須要安裝一些 Babel 相關的工具node
yarn add babylon babel-core babel-traverse babel-preset-env
複製代碼
爲了方便調試查看, 另外安裝一些輔助包(高亮代碼/格式代碼)webpack
yarn add cli-highlight js-beautify
複製代碼
package.json
文件中添加script
命令web
"bundle": "node bundler.js | js-beautify | highlight"
複製代碼
咱們按照官方操做,建立三個待打包文件(entry.js
-> message.js
-> name.js
)json
// entry.js 入口文件
import message from './message.js';
console.log(message);
// message.js
import {name} from './name.js';
export default `hello ${name}!`;
// name.js
export const name = 'world';
複製代碼
在根目錄建立bundler.js
, 編寫打包代碼, 這裏面主要包括三個函數數組
// 據入口文件獲取文件信息, 獲取當前js文件的依賴信息
function createAsset(filename) {//代碼略}
// 從入口開始分析全部依賴項,造成依賴圖,採用廣度遍歷
function createGraph(entry) {//代碼略}
// 根據生成的依賴關係圖,生成瀏覽器可執行文件
function bundle(graph) {//代碼略}
複製代碼
目錄結構瀏覽器
- example
- entry.js
- message.js
- name.js
- bundler.js
複製代碼
如何獲取依賴呢,其實思路很簡單:bash
webpack
的入口配置,指向一個文件, 經過這個文件的路徑讀取文件的信息AST
(抽象語法樹)AST
中找到它所依賴的模塊, 獲取其相對路徑,組成json
格式數據const fs = require('fs')
const path = require('path')
const babylon = require('babylon')
const traverse = require('babel-traverse').default
const babel = require('babel-core')
let ID = 0
// 根據入口文件獲取文件信息, 獲取當前js文件的依賴信息
function createAsset(filename) {
//獲取文件,返回值是字符串
const content = fs.readFileSync(filename, 'utf-8')
// babylon 轉換成 AST
const ast = babylon.parse(content, {
sourceType: 'module'
})
// 用來存儲當前文件所依賴的文件路徑
const dependencies = []
// 遍歷 ast
traverse(ast, {
ImportDeclaration: ({ node }) => {
// 把當前依賴的模塊加入到數組中,其實這存的是字符串,
dependencies.push(node.source.value)
}
})
// 建立id, 方便以後找到依賴關係,下面會講
const id = ID++
// 這邊主要把ES6 的代碼轉成 ES5
const { code } = babel.transformFromAst(ast, null, {
presets: ['env']
});
return {
id,
filename,
dependencies,
code
}
}
複製代碼
接下來在末行添加代碼babel
console.log(createAsset('./example/entry.js'))
複製代碼
運行yarn bundle
命令,查看生成的數據:
{
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 依賴 message.js
), 那以後就能夠經過遍歷的方式,查詢message.js
以後的依賴關係,造成數組數據
// 從入口開始分析全部依賴項,造成依賴圖,採用廣度遍歷
function createGraph(entry) {
// 如上所示,表示入口文件的依賴
const mainAsset = createAsset(entry)
// 既然要廣度遍歷確定要有一個隊列,第一個元素確定是 從 "./example/entry.js" 返回的信息
const queue = [mainAsset]
for (const asset of queue) {
// 獲取相對路徑
const dirname = path.dirname(asset.filename)
// 新增一個屬性來保存子依賴項的數據
// 保存相似 這樣的數據結構 ---> {"./message.js" : 1}
// 對應上面的 id, 方便找到依賴關係
asset.mapping = {}
// 根據依賴添加數組元素
asset.dependencies.forEach(relativePath => {
const absolutePath = path.join(dirname, relativePath)
// 得到子依賴(子模塊)的依賴項、代碼、模塊id,文件名
const child = createAsset(absolutePath)
asset.mapping[relativePath] = child.id
queue.push(child)
})
}
return queue
}
複製代碼
接下來跟一樣進行測試
const graph = createGraph('./example/entry.js')
console.log(graph)
複製代碼
獲得以下數據:
[
{
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: {}
}
]
複製代碼
接下來就是最重要的一步,以上已經生成的依賴關係數據,而且都有對應的code
, 那如今要作的就是把這些code
整合起來,讓它能夠在瀏覽器端運行。
首先看一下代碼的大體結構:
function bundle(graph) {
let modules = "";
//循環依賴關係,並把每一個模塊中的代碼存在function做用域裏
graph.forEach(mod => {
modules += `${mod.id}:[ function (require, module, exports){ ${mod.code} }, ${JSON.stringify(mod.mapping)}, ],`;
});
const result = ` (function(modules) { // 代碼略 })({${modules}}) `;
return result;
}
複製代碼
這塊稍微有些複雜,一步一步來。
首先咱們把全部的代碼都轉換成了ES5
,而且生成了依賴關係圖,如今要作的第一步就是把代碼整合在一塊兒,而作到這一步的總體思路就是:建立匿名函數,遍歷整個依賴關係數組,生成一個函數對象看成參數傳進去
生成的結構如圖所示
(function(modules) {
})({
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);
},
// 導入模塊對應的 id
{
"./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';
},
{},
],
})
複製代碼
由上圖能夠看到entry.js
代碼通過Babel
轉碼後是什麼樣子
// 原代碼
import message from './message.js';
console.log(message)
// 轉換成 ES5
"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);
複製代碼
這代碼放在瀏覽器中確定是沒法運行的
VM689:1 Uncaught ReferenceError: require is not defined
at <anonymous>:1:16
複製代碼
Babel
將咱們 ES6
的模塊化代碼轉換爲了 CommonJS
, 而如今須要在瀏覽器端運行CommonJS
代碼就須要一些操做,這也就是匿名函數內部所要作的事情, 具體實現就是模擬建立require
方法,返回值是導入模塊的export
值
例如entry.js
中var _message = require("./message.js");
, 如何獲取message.js
的內容獲取它的返回值?
(function(modules) {
function require(id) {
// 根據id獲取 function 和 mapping
const [fn, mapping] = modules[id];
function localRequire(relativePath){
//根據模塊的路徑在mapping中找到對應的模塊id
return require(mapping[relativePath]);
}
const module = {exports:{}};
//執行每一個模塊的代碼。
//對應上方的 function(require, module, exports)
fn(localRequire,module,module.exports);
return module.exports;
}
// 導入entry.js 代碼
require(0)
})(modules)
複製代碼
乍一看可能有點蒙圈,請對照上方每塊轉換成ES5
的代碼,我簡單解釋一下具體的流程:
require(0)
執行entry.js
代碼,也就是執行fn(localRequire,module,module.exports);
require("./message.js")
這裏,這裏的require
就是上方傳入的localRequire
,傳入了"./message.js"
,根據mapping
獲取ID
也就是1
, 至關於執行了一次require(1)
,但此時尚未獲取到message
的值name.js
中,又執行了require("./name.js");
,一模一樣,也就是require(2)
var name = exports.name = 'world';
, 此時傳入的exports
終於有了值,require
方法返回了module.exports
的值,也就是說var _name = require("./name.js");
, name
的值是'world'
exports.default = "hello " + _name.name + "!";
也獲取了require("./message.js")
的返回值就我淺薄的理解而言,就是模擬建立並遞歸調用require
方法,外部暴露module.export
並做爲返回值
完整的bundle
函數以下所示:
function bundle(graph) {
let modules = "";
//循環依賴關係,並把每一個模塊中的代碼存在function做用域裏
graph.forEach(mod => {
modules += `${mod.id}:[ function (require, module, exports){ ${mod.code} }, ${JSON.stringify(mod.mapping)}, ],`;
});
//require, module, exports 是 cjs的標準不能再瀏覽器中直接使用,因此這裏模擬cjs模塊加載,執行,導出操做。
const result = ` (function(modules){ //建立require函數, 它接受一個模塊ID(這個模塊id是數字0,1,2) ,它會在咱們上面定義 modules 中找到對應是模塊. function require(id){ const [fn, mapping] = modules[id]; function localRequire(relativePath){ //根據模塊的路徑在mapping中找到對應的模塊id return require(mapping[relativePath]); } const module = {exports:{}}; //執行每一個模塊的代碼。 fn(localRequire,module,module.exports); return module.exports; } //執行入口文件, require(0); })({${modules}}) `;
return result;
}
複製代碼
const graph = createGraph("./example/entry.js");
const result = bundle(graph);
// 打包生成文件
fs.writeFileSync("./bundle.js", result);
複製代碼
瀏覽器端完美運行