早期JavaScript只需要實(shí)現(xiàn)簡(jiǎn)單的頁(yè)面交互,幾行代碼即可搞定。隨著瀏覽器性能的提升以及前端技術(shù)的不斷發(fā)展,JavaScript代碼日益膨脹,此時(shí)就需要一個(gè)完善的模塊化機(jī)制來(lái)解決這個(gè)問(wèn)題。因此誕生了CommonJS(NodeJS), AMD(sea.js), ES6 Module(ES6, Webpack), CMD(require.js)等模塊化規(guī)范。
什么是模塊化?
模塊化是一種處理復(fù)雜系統(tǒng)分解為更好的可管理模塊的方式,用來(lái)分割,組織和打包軟件。每一個(gè)模塊完成一個(gè)特定的子功能,所有的模塊按照某種方式組裝起來(lái),成為一個(gè)整體,完成整個(gè)系統(tǒng)的所有要求功能。
模塊化的好處是什么?
模塊間解耦,提高模塊的復(fù)用性。
避免命名沖突。
分離以及按需加載。
提高系統(tǒng)的維護(hù)性。
JS模塊化的演進(jìn)
JavaScript模塊化的發(fā)展進(jìn)程了。早期JavaScript模塊化模式比較簡(jiǎn)單粗暴,將一個(gè)模塊定義為一個(gè)全局函數(shù)
function module1() {
// code
}
function module2() {
// code
}
// 這種方案非常簡(jiǎn)單,但問(wèn)題也很明顯:污染全局命名空間,引起命名沖突或數(shù)據(jù)不安全,而且模塊間的依賴關(guān)系并不明顯。
在此基礎(chǔ)上,又有了namespace模式,利用一個(gè)對(duì)象來(lái)對(duì)模塊進(jìn)行包裝
var module1 = {
data: { }, // 數(shù)據(jù)區(qū)域
func1: function() {}
func2: function() {}
}
// 這種方案的問(wèn)題依然是數(shù)據(jù)不安全,外面能直接修改module1的data
因此又有了IIFE模式,利用自執(zhí)行函數(shù)(閉包)
!function(window) {
var data = {};
function func1() {
data.hello = "hello";
}
function func2() {
data.world = "world";
}
window.module1 = { func1, func2 };
} (window)
//數(shù)據(jù)定義為私有,外部只能通過(guò)模塊暴露的方法來(lái)對(duì)data進(jìn)行操作,但這依然沒(méi)有解決模塊依賴的問(wèn)題
基于IIFE,又提出了一種新的模塊化方案,即在IIFE的基礎(chǔ)上引入了依賴(現(xiàn)代模塊化的基石,Webpack、NodeJS等模塊化都是基于此實(shí)現(xiàn)的)
!function (window, module2) {
var data = {};
function func1() {
data.world = "world";
module2.hello();
}
window.module1 = { func1 };
} (window, { hello: function() {}, });
// 這樣使IIFE模塊化的依賴關(guān)系變得更明顯,又保證了IIFE模塊化獨(dú)有的特性。
什么是 webpack
webpack 是一個(gè)現(xiàn)代 JavaScript 應(yīng)用程序的靜態(tài)模塊打包器(module bundler)。當(dāng) webpack 處理應(yīng)用程序時(shí),它會(huì)遞歸地構(gòu)建一個(gè)依賴關(guān)系圖(dependency graph),其中包含應(yīng)用程序需要的每個(gè)模塊,然后將所有這些模塊打包成一個(gè)或多個(gè) bundle。
我們知道了模塊打包器會(huì)將多個(gè)文件打包成一個(gè)文件,那么打包后的文件到底是什么樣的了,我們必須知道這個(gè)才能夠進(jìn)行具體實(shí)現(xiàn),因此我們查看以下 webpack 打包后的效果。示例:假設(shè)我們?cè)谕粋€(gè)文件夾下有以下幾個(gè)文件:文件:index.js
let action = require("./action.js").action; // 引入aciton.js
let name = require("./name.js").name; // 引入name.js
let message = `${name} is ${action}`;
// index.js文件中引入了action.js和name.js。
文件:action.js
let action = "making webpack";
exports.action = action;
文件:name.js
let familyName = require("./family-name.js").name;
exports.name = `${familyName} 阿爾伯特`;
// 文件name.js又引入了family-name.js文件。
文件:family-name.js
exports.name = "haiyingsitan";
接下來(lái)我們使用 webpack 進(jìn)行打包,并去除打包后的注釋,得到如下代碼:
(() => {
var __webpack_modules__ = ({
"./action.js": ((__unused_webpack_module, exports) => {
let action = "making webpack";
exports.action = action;
}),
"./family-name.js": ((__unused_webpack_module, exports) => {
exports.name = "haiyingsitan";
}),
"./name.js": ((__unused_webpack_module, exports, __webpack_require__) => {
let familyName = __webpack_require__( /*! ./family-name.js */ "./family-name.js").name;
exports.name = `${familyName} 阿爾伯特`;
})
});
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
if (__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
__webpack_modules__[moduleId](module, module.exports, __webpack_require__ "moduleId");
return module.exports;
}
(() => {
let action = __webpack_require__( "./action.js").action;
let name = __webpack_require__( "./name.js").name;
let message = `${name} is ${action}`;
console.log(message);
})();
})();
我們進(jìn)一步簡(jiǎn)化它
(() => {
// 獲取所有的依賴
var modules = {
"./action.js": (module, exports) => {
let action = "making webpack";
exports.action = action;
},
// ... 其他代碼
};
// require對(duì)應(yīng)的模塊函數(shù)執(zhí)行
function __webpack_require__(moduleId) {
// 其他實(shí)現(xiàn)
return module.exports;
}
// 入口函數(shù)立即執(zhí)行
let entryFn = () => {
let action = __webpack_require__("./action.js").action;
let name = __webpack_require__("./name.js").name;
let message = `${name} is ${action}`;
console.log(message);
};
entryFn();
})();
我們可以發(fā)現(xiàn),文件最終打包后就是一個(gè)立即執(zhí)行函數(shù)。這個(gè)函數(shù)由三部分組成:
1、模塊集合 這個(gè)模塊集合是所有模塊的集合,以路徑作為key值,模塊內(nèi)容作為value值。當(dāng)我們需要使用某個(gè)模塊時(shí),直接從這個(gè)模塊集合中進(jìn)行獲取即可。為什么需要這個(gè)模塊集合了?試想一下,如果我們遇到require("./action.js"),那么這個(gè)action.js到底對(duì)應(yīng)的是哪個(gè)模塊了?因此,我們必須能夠獲取到所有的模塊,并對(duì)他們進(jìn)行區(qū)分(使用模塊id或者模塊名稱),到時(shí)候直接從這個(gè)模塊集合中通過(guò)模塊id或者模塊名進(jìn)行獲取即可。
2、模塊函數(shù)執(zhí)行 每一個(gè)模塊對(duì)應(yīng)于一個(gè)函數(shù),當(dāng)遇到require(xxx)的時(shí)候?qū)嶋H上就是去執(zhí)行引入的這個(gè)模塊函數(shù)。
3、入口文件立即執(zhí)行(執(zhí)行模塊的函數(shù)) 我們都知道一個(gè)模塊的打包,必須有一個(gè)入口文件,而且這個(gè)文件必須立即執(zhí)行,才能獲取到所有的依賴。其實(shí)入口文件,也是一個(gè)模塊,立即執(zhí)行這個(gè)模塊對(duì)應(yīng)的函數(shù)即可。
我們可以發(fā)現(xiàn)每個(gè)模塊實(shí)際上就是在外層套上了一個(gè)函數(shù)的外殼。為什么要把文件內(nèi)容放入到一個(gè)函數(shù)中了,這是因?yàn)槲覀兌贾滥K化最重要的一個(gè)特點(diǎn)就是環(huán)境隔離,各個(gè)模塊之間互不影響。
如果我們想要實(shí)現(xiàn)同樣的功能,只需要同時(shí)實(shí)現(xiàn):模塊集合,模塊執(zhí)行和入口函數(shù)立即執(zhí)行即可。

也就是說(shuō),我們最終要實(shí)現(xiàn)的就是這樣的一個(gè)集合。到目前為止,我們要實(shí)現(xiàn)的功能是:
1、給每個(gè)文件內(nèi)容加殼
2、每個(gè)模塊以路徑作為模塊 id
3、將所有的模塊合在一起形成一個(gè)集合
我們看下具體的實(shí)現(xiàn)如下:
const fs = require("fs");
let modules = {};
const fileToModule = function (path) {
const fileContent = fs.readFileSync(path).toString();
return {
id: path, // 這里以路徑作為模塊id
code: `function(require,exports){ // 這里加殼了
${fileContent.toString()};
}`,
};
};
let result = fileToModule("./index.js");
modules[result["id"]] = result.code;
console.log("modules=",modules);
輸出的結(jié)果為:
modules= {
'./index.js':'function(require,exports){\n let action = require("./action.js").action;\r\nlet name = require("./name.js").name;\r\nlet message = `${name} is ${action}`;\r\nconsole.log(message);\r\n;\n }'
從上面我們可以看出,我們成功地將入口文件加殼轉(zhuǎn)化成一個(gè)模塊,并且給其命名,然后添加到模塊對(duì)象中去了。但是我們發(fā)現(xiàn)我們的文件中其實(shí)還依賴了./action.js和./name.js,然而我們無(wú)法獲取到他們的模塊內(nèi)容。因此,我們需要處理require引入的模塊。也就是說(shuō)要找到當(dāng)前模塊中的所有依賴,然后解析這些依賴將其放入模塊集合中。
獲取當(dāng)前模塊的所有依賴
// const action = require("./action.js")
function getDependencies(fileContent) {
let reg = /require\(['"](.+? "'"")['"]\)/g;
let result = null;
let dependencies = [];
while ((result = reg.exec(fileContent))) {
dependencies.push(result[1]);
}
return dependencies;
}
這里我們使用了正則判斷,只要是require("")或者require('')這種格式的都當(dāng)作模塊引入進(jìn)行處理(這種處理有點(diǎn)問(wèn)題,我們暫時(shí)先不管,等到下面進(jìn)行優(yōu)化)。然后把所有的引入都放到一個(gè)數(shù)組中,從而獲取到當(dāng)前模塊所有的依賴。我們使用這個(gè)函數(shù)查看下入口文件的依賴:
const fileContent = fs.readFileSync(path).toString();
let result = getDependencies(fileContent);
console.log(result) // ["./action.js","./name.js"]
將所有模塊組成一個(gè)集合
function createGraph(filename) {
let module = fileToModule(filename);
let queue = [module];
for (let module of queue) {
const dirname = path.dirname(module.id);
module.dependencies.forEach((relativePath) => {
const absolutePath = path.join(dirname, relativePath);
const child = fileToModule(absolutePath);
queue.push(child);
});
}
// 上面得到的是一個(gè)數(shù)組。轉(zhuǎn)化成對(duì)象
let modules = {}
queue.forEach((item) => {
modules[item.id] = item.code;
})
return modules;
}
console.log(createGraph("./index.js"));
執(zhí)行模塊的函數(shù)
我們?cè)谏厦娴哪K對(duì)象中獲得了所有模塊信息,接下來(lái)我們執(zhí)行入口文件對(duì)應(yīng)的函數(shù)exec。

從上圖中我們可以看出:當(dāng)我們執(zhí)行入口文件對(duì)應(yīng)的函數(shù)時(shí)exec(index.js),它發(fā)現(xiàn):
存在依賴./action.js,于是調(diào)用exec("./action.js")。這時(shí)候不存在其他依賴了,那么直接返回值。這條線結(jié)束。
存在依賴./name.js,于是調(diào)用exec("./name.js")。又發(fā)現(xiàn)依賴./family-name.js,于是調(diào)用exec("./family-name.js")。這時(shí)候不存在其他依賴了,返回值。這條線結(jié)束。
我們可以發(fā)現(xiàn)其實(shí)這就是一個(gè)遞歸的過(guò)程,不斷查找依賴,然后執(zhí)行對(duì)應(yīng)的函數(shù)。因此,我們可以大致寫出以下這個(gè)函數(shù):
const exec = function(moduleId){
const fn = modules[moduleId]; // 獲取到每個(gè)id對(duì)應(yīng)的函數(shù)
let exports = {};
const require = function(filename){
const dirname = path.dirname(module.id);
const absolutePath = path.join(dirname, filename);
return exec(absolutePath);
}
fn(require, exports);
return exports
}
最終完整的代碼如下:
const fs = require("fs");
const path = require("path");
const {parse} = require("@babel/parser");
const traverse = require("@babel/traverse").default;
// 1.加殼
const fileToModule = function (path) {
const fileContent = fs.readFileSync(path).toString();
return {
id: path,
dependencies: getDependencies(path),
code: `function (require, exports) {
${fileContent};
}`,
};
};
// 2.獲取依賴
function getDependencies(filePath) {
let result = null;
let dependencies = [];
const fileContent = fs.readFileSync(filePath).toString();
// parse
const ast = parse(fileContent, { sourceType: "CommonJs" });
// transform
traverse(ast, {
enter: (item) => {
if (
item.node.type === "CallExpression" &&
item.node.callee.name === "require"
) {
const dirname = path.dirname(filePath);
dependencies.push(path.join(dirname, item.node.arguments[0].value));
console.log("dependencies", dependencies);
}
},
});
return dependencies;
}
// 3. 將所有依賴形成一個(gè)集合
function createGraph(filename) {
let module = fileToModule(filename);
let queue = [module];
for (let module of queue) {
const dirname = path.dirname(module.id);
module.dependencies.forEach((relativePath) => {
const absolutePath = path.join(dirname, relativePath);
console.log("queue:",queue);
console.log("absolutePath:",absolutePath);
const result = queue.every((item) => {
return item.id !== absolutePath;
});
if (result) {
const child = fileToModule(absolutePath);
queue.push(child);
} else {
return false;
}
});
}
let modules = {};
queue.forEach((item) => {
modules[item.id] = item.code;
});
return modules;
}
let modules = createGraph("./index.js");
// 4. 執(zhí)行模塊
const exec = function (moduleId) {
const fn = modules[moduleId];
let exports = {};
const require = function (filename) {
const dirname = path.dirname(module.id);
const absolutePath = path.join(dirname, filename);
return exec(absolutePath);
};
fn(require, exports);
return exports;
};
// exec("./index.js");
// 5. 寫入文件
function createBundle(modules){
let __modules = "";
for (let attr in modules) {
__modules += `"${attr}":${modules[attr]},`;
}
const result = `(function(){
const modules = {${__modules}};
const exec = function (moduleId) {
const fn = modules[moduleId];
let exports = {};
const require = function (filename) {
const dirname = path.dirname(module.id);
const absolutePath = path.join(dirname, filename);
return exec(absolutePath);
};
fn(require, exports);
return exports;
};
exec("./index.js");
})()`;
fs.writeFileSync("./dist/bundle3.js", result);
}
createBundle(modules)