【webpack】做了一夜動(dòng)畫,讓大家十分鐘搞懂Webpack
一、什么是webpack
webpack是一個(gè)打包工具,他的宗旨是一切靜態(tài)資源皆可打包。

二、原理分析
首先我們通過一個(gè)制作一個(gè)打包文件的原型。
假設(shè)有兩個(gè)js模塊,這里我們先假設(shè)這兩個(gè)模塊是復(fù)合commomjs標(biāo)準(zhǔn)的es5模塊。
語法和模塊化規(guī)范轉(zhuǎn)換的事我們先放一放,后面說。
我們的目的是將這兩個(gè)模塊打包為一個(gè)能在瀏覽器端運(yùn)行的文件,這個(gè)文件其實(shí)叫bundle.js。
比如
// index.js
var add = require('add.js').default
console.log(add(1 , 2))
// add.js
exports.default = function(a,b) {return a + b}
假設(shè)在瀏覽器中直接執(zhí)行這個(gè)程序肯定會(huì)有問題 最主要的問題是瀏覽器中沒有exports對(duì)象與require方法所以一定會(huì)報(bào)錯(cuò)。
我們需要通過模擬exports對(duì)象和require方法
1. 模擬exports對(duì)象
首先我們知道如果在nodejs打包的時(shí)候我們會(huì)使用fs.readfileSync()來讀取js文件。這樣的話js文件會(huì)是一個(gè)字符串。而如果需要將字符串中的代碼運(yùn)行會(huì)有兩個(gè)方法分別是new Function與Eval。
在這里面我們選用執(zhí)行效率較高的eval。

exports = {}
eval('exports.default = function(a,b) {return a + b}') // node文件讀取后的代碼字符串
exports.default(1,3)

上面這段代碼的運(yùn)行結(jié)果可以將模塊中的方法綁定在exports對(duì)象中。由于子模塊中會(huì)聲明變量,為了不污染全局我們使用一個(gè)自運(yùn)行函數(shù)來封裝一下。
var exports = {}
(function (exports, code) {
eval(code)
})(exports, 'exports.default = function(a,b){return a + b}')
2. 模擬require函數(shù)
require函數(shù)的功能比較簡(jiǎn)單,就是根據(jù)提供的file名稱加載對(duì)應(yīng)的模塊。
首先我們先看看如果只有一個(gè)固定模塊應(yīng)該怎么寫。

function require(file) {
var exports = {};
(function (exports, code) {
eval(code)
})(exports, 'exports.default = function(a,b){return a + b}')
return exports
}
var add = require('add.js').default
console.log(add(1 , 2))
完成了固定模塊,我們下面只需要稍加改動(dòng),將所有模塊的文件名和代碼字符串整理為一張key-value表就可以根據(jù)傳入的文件名加載不同的模塊了。
(function (list) {
function require(file) {
var exports = {};
(function (exports, code) {
eval(code);
})(exports, list[file]);
return exports;
}
require("index.js");
})({
"index.js": `
var add = require('add.js').default
console.log(add(1 , 2))
`,
"add.js": `exports.default = function(a,b){return a + b}`,
});
當(dāng)然要說明的一點(diǎn)是真正webpack生成的bundle.js文件中還需要增加模塊間的依賴關(guān)系。
叫做依賴圖(Dependency Graph)
類似下面的情況。
{
"./src/index.js": {
"deps": { "./add.js": "./src/add.js" },
"code": "....."
},
"./src/add.js": {
"deps": {},
"code": "......"
}
}
另外,由于大多數(shù)前端程序都習(xí)慣使用es6語法所以還需要預(yù)先將es6語法轉(zhuǎn)換為es5語法。
總結(jié)一下思路,webpack打包可以分為以下三個(gè)步驟:
分析依賴
ES6轉(zhuǎn)ES5
替換exports和require
下面進(jìn)入功能實(shí)現(xiàn)階段。
三、功能實(shí)現(xiàn)
我們的目標(biāo)是將以下兩個(gè)個(gè)互相依賴的ES6Module打包為一個(gè)可以在瀏覽器中運(yùn)行的一個(gè)JS文件(bundle.js)
處理模塊化 多模塊合并打包 - 優(yōu)化網(wǎng)絡(luò)請(qǐng)求
/src/add.js
export default (a, b) => a + b
/src/index.js
import add from "./add.js";
console.log(add(1 , 2))
1. 分析模塊
分析模塊分為以下三個(gè)步驟:
模塊的分析相當(dāng)于對(duì)讀取的文件代碼字符串進(jìn)行解析。這一步其實(shí)和高級(jí)語言的編譯過程一致。需要將模塊解析為抽象語法樹AST。我們借助babel/parser來完成。
AST (Abstract Syntax Tree)抽象語法樹 在計(jì)算機(jī)科學(xué)中,或簡(jiǎn)稱語法樹(Syntax tree),是源代碼語法結(jié)構(gòu)的一種抽象表示。它以樹狀的形式表現(xiàn)編程語言的語法結(jié)構(gòu),樹上的每個(gè)節(jié)點(diǎn)都表示源代碼中的一種結(jié)構(gòu)。( https://astexplorer.net/)
yarn add @babel/parser
yarn add @babel/traverse
yarn add @babel/core
yarn add @babel/preset-env
讀取文件
收集依賴
編譯與AST解析
const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");
function getModuleInfo(file) {
// 讀取文件
const body = fs.readFileSync(file, "utf-8");
// 轉(zhuǎn)化AST語法樹
const ast = parser.parse(body, {
sourceType: "module", //表示我們要解析的是ES模塊
});
// 依賴收集
const deps = {};
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
const abspath = "./" + path.join(dirname, node.source.value);
deps[node.source.value] = abspath;
},
});
// ES6轉(zhuǎn)成ES5
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
});
const moduleInfo = { file, deps, code };
return moduleInfo;
}
const info = getModuleInfo("./src/index.js");
console.log("info:", info);
運(yùn)行結(jié)果如下:

2. 收集依賴
上一步開發(fā)的函數(shù)可以單獨(dú)解析某一個(gè)模塊,這一步我們需要開發(fā)一個(gè)函數(shù)從入口模塊開始根據(jù)依賴關(guān)系進(jìn)行遞歸解析。最后將依賴關(guān)系構(gòu)成為依賴圖(Dependency Graph)
/**
* 模塊解析
* @param {*} file
* @returns
*/
function parseModules(file) {
const entry = getModuleInfo(file);
const temp = [entry];
const depsGraph = {};
getDeps(temp, entry);
temp.forEach((moduleInfo) => {
depsGraph[moduleInfo.file] = {
deps: moduleInfo.deps,
code: moduleInfo.code,
};
});
return depsGraph;
}
/**
* 獲取依賴
* @param {*} temp
* @param {*} param1
*/
function getDeps(temp, { deps }) {
Object.keys(deps).forEach((key) => {
const child = getModuleInfo(deps[key]);
temp.push(child);
getDeps(temp, child);
});
}
3. 生成bundle文件
這一步我們需要將剛才編寫的執(zhí)行函數(shù)和依賴圖合成起來輸出最后的打包文件。
function bundle(file) {
const depsGraph = JSON.stringify(parseModules(file));
return `(function (graph) {
function require(file) {
function absRequire(relPath) {
return require(graph[file].deps[relPath])
}
var exports = {};
(function (require,exports,code) {
eval(code)
})(absRequire,exports,graph[file].code)
return exports
}
require('${file}')
})(${depsGraph})`;
}
!fs.existsSync("./dist") && fs.mkdirSync("./dist");
fs.writeFileSync("./dist/bundle.js", content);
最后可以編寫一個(gè)簡(jiǎn)單的測(cè)試程序測(cè)試一下結(jié)果。
<script src="./dist/bundle.js"></script>

ok 學(xué)費(fèi)了。
后面有興趣的話大家可以在考慮一下如何加載css文件或者圖片base64 Vue SFC .vue。
