自己實(shí)現(xiàn)一個(gè)簡(jiǎn)易的模塊打包器(干貨)
作者:海因斯坦,原文鏈接:https://juejin.im/post/6893809205183479822
一、寫在前面
在日常的開發(fā)過程中,我們?cè)絹碓蕉嗟厥褂?code style>webpack這種構(gòu)建工具,但是對(duì)于它的使用,我們更多的是停留在去進(jìn)行一些簡(jiǎn)單的配置,比如 loader, plugin 的配置。我們很少?gòu)牧汩_始使用 webpack 去搭建一個(gè)項(xiàng)目(更多地是使用 cli),更加很少地去理解它內(nèi)部的打包原理。為什么它能夠?qū)⑽募D(zhuǎn)化成一個(gè)模塊,為什么能夠?qū)⑺心K打包到一個(gè)文件中?打包后的文件到底是什么樣的(可能很多人很少去看打包后的 build 文件)?這些我們都不太了解,但是當(dāng)我們的構(gòu)建速度雨來越慢的時(shí)候,我們想要去優(yōu)化它卻因?yàn)閷?duì) webpack 了解太少,而無從下手。而且隨著面試越來越多地問到 webpack 底層原理,webpack越來越成為我們的阻礙了。但是對(duì)于很多人來說,去看webpack的源碼,可能很多人都會(huì)頭大,無從下手。如果看別人的源碼講解,又會(huì)陷入各種概念中,什么事件機(jī)制,內(nèi)部鉤子,Tapable插件架構(gòu)和鉤子設(shè)計(jì)。這些都讓人難以理解。相反,我覺得如果我們從結(jié)果出發(fā),看webpack最終打包后的文件是怎么樣的,然后實(shí)現(xiàn)一個(gè)簡(jiǎn)單的跟它相同的打包器,這樣反而能夠讓我們繞開很多高深的東西,更加理解其原理。這時(shí)候我們?cè)偃タ丛创a或者別人的文章可能事半功倍。
二、模塊打包器
2.1 什么是模塊打包器
我們看官網(wǎng)對(duì)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。更加通俗地理解就是:每個(gè)文件就是一個(gè)模塊,一個(gè)文件中又會(huì)引入其他文件的內(nèi)容,我們最終要實(shí)現(xiàn)的就是以某 i 一個(gè)文件為入口:將它所有依賴的文件最終打包成一個(gè)文件,這就是模塊打包器。
2.2 使用 webpack 打包后的文件
我們知道了模塊打包器會(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}`;
console.log(message);
復(fù)制代碼
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";
接下來我們使用 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);
???})();
?})();
上面的代碼看起來還是有點(diǎn)復(fù)雜,我們進(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ù)由三部分組成:
模塊集合 這個(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è)模塊集合中通過模塊id或者模塊名進(jìn)行獲取即可。
??var?modules?=?{
????"./action.js":?(module,?exports)?=>?{
??????let?action?=?"making?webpack";
??????exports.action?=?action;
????},
??};
模塊函數(shù)執(zhí)行 每一個(gè)模塊對(duì)應(yīng)于一個(gè)函數(shù),當(dāng)遇到 require(xxx)的時(shí)候?qū)嶋H上就是去執(zhí)行引入的這個(gè)模塊函數(shù)。
?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;
?}
入口文件立即執(zhí)行(執(zhí)行模塊的函數(shù)) 我們都知道一個(gè)模塊的打包,必須有一個(gè)入口文件,而且這個(gè)文件必須立即執(zhí)行,才能獲取到所有的依賴。其實(shí)入口文件,也是一個(gè)模塊,立即執(zhí)行這個(gè)模塊對(duì)應(yīng)的函數(shù)即可。
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();
好了,到目前為止,我們基本知道了 webpack 模塊打包后生成的文件是什么樣的。如果我們想要實(shí)現(xiàn)同樣的功能,只需要同時(shí)實(shí)現(xiàn):模塊集合,模塊執(zhí)行和入口函數(shù)立即執(zhí)行即可。其中最關(guān)鍵的就是實(shí)現(xiàn)模塊集合和模塊執(zhí)行。
三、具體實(shí)現(xiàn)
從上面的分析中我們可以知道,我們要實(shí)現(xiàn)的主要包括兩個(gè)部分:
將項(xiàng)目中所有的文件生成一個(gè)大的模塊集合 模塊執(zhí)行函數(shù)。遇到引入模塊時(shí),執(zhí)行對(duì)應(yīng)的函數(shù)。
接下來我們就分別實(shí)現(xiàn)這兩個(gè)部分。
3.1 實(shí)現(xiàn)模塊集合
3.1.1 給文件內(nèi)容加殼
我們可以看下 webpack 打包后每個(gè)模塊的具體內(nèi)容:
var?modules?=?{
??"./action.js":?(module,?exports)?=>?{
??????//?文件內(nèi)容
????let?action?=?"making?webpack";
????exports.action?=?action;
??},
};
我們可以發(fā)現(xiàn)每個(gè)模塊實(shí)際上就是在外層套上了一個(gè)函數(shù)的外殼。為什么要把文件內(nèi)容放入到一個(gè)函數(shù)中了,這是因?yàn)槲覀兌贾滥K化最重要的一個(gè)特點(diǎn)就是環(huán)境隔離,各個(gè)模塊之間互不影響。試想一下,如果不對(duì)文件內(nèi)容進(jìn)行隔離處理,而是直接打包到一起,那么各個(gè)模塊之間定義的變量在同一作用域肯定會(huì)互相影響。而函數(shù)常常用來形成一個(gè)單獨(dú)的作用域,用來隔離變量。因此,我們首先給所有文件加殼。
index.js 模塊
function(require,exports){
????let?action?=?require("./action.js").action;
????let?name?=?require("./name.js").name;
????let?message?=?`${name}?is?${action}`;
????console.log(message);
}
action.js 模塊
function(require,exports){
????let?action?=?"making?webpack";
????exports.action?=?action;
}
name.js 模塊
function(require,exports){
????let?familyName?=?require("./family-name.js").name;
????exports.name?=?`${familyName}?阿爾伯特`;
}
family-name.js 模塊。
function(require,exports){
????exports.name?=?"haiyingsitan";
}
然后,我們?yōu)榱藚^(qū)分或者獲取這些模塊,我們需要給每個(gè)模塊一個(gè)模塊 id 或者模塊名稱,這里我們直接使用文件路徑作為每個(gè)模塊的 id。最后將這些模塊組成一個(gè)集合。如下所示:
const?modules?=?{
??"./index.js":function(require,exports){
????let?action?=?require("./action.js").action;
????let?name?=?require("./name.js").name;
????let?message?=?`${name}?is?${action}`;
????console.log(message);
??},
??"./action.js":?function?(require,?exports)?{
????let?action?=?"making?webpack";
????exports.action?=?action;
??},
??"./name.js":?function?(require,?exports)?{
????let?familyName?=?require("./family-name.js").name;
????exports.name?=?`${familyName}?阿爾伯特`;
??},
??"./family-name.js":?function?(require,?exports)?{
????exports.name?=?"haiyingsitan";
??}
};
也就是說,我們最終要實(shí)現(xiàn)的就是這樣的一個(gè)集合。到目前為止,我們要實(shí)現(xiàn)的功能是:
給每個(gè)文件內(nèi)容加殼 每個(gè)模塊以路徑作為模塊 id 將所有的模塊合在一起形成一個(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,然而我們無法獲取到他們的模塊內(nèi)容。因此,我們需要處理require引入的模塊。也就是說要找到當(dāng)前模塊中的所有依賴,然后解析這些依賴將其放入模塊集合中。
3.1.2 獲取當(dāng)前模塊的所有依賴
接下來我們就是要實(shí)現(xiàn)找到一個(gè)模塊中所有的依賴。
//?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)問題,我們暫時(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"]
我們可以順利獲取到入口文件的所有依賴,接下來我們就是要進(jìn)一步去解析這些入口文件的依賴了。因此,我們?cè)谖募D(zhuǎn)化成模塊時(shí),最好把模塊的所有依賴信息也展示出來方便處理。因此,我們修改一下fileToModule這個(gè)函數(shù)。
const?fileToModule?=?function?(path)?{
??const?fileContent?=?fs.readFileSync(path).toString();
??return?{
??????id:path,
??????dependencies:getDependencies(fileContent),???//?新增模塊信息
??????code:`(require,exports)?=>?{
????????????${fileContent.toString()};
??????}`
??}
};
好了,到目前為止我們能夠獲取到每個(gè)模塊的依賴,同時(shí)我們又能夠把每個(gè)依賴轉(zhuǎn)化成一個(gè)對(duì)象,那么接下來就是把所有的對(duì)象放到一個(gè)大的對(duì)象中從而得到項(xiàng)目中所有模塊的集合。
3.1.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);
??????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"));
createGraph就是根據(jù)入口文件,然后依次獲取到所有的依賴,每獲取一個(gè)就將其添加到 queue 數(shù)組中,由于使用 let of 進(jìn)行遍歷,let of 會(huì)繼續(xù)遍歷新添加的元素,而不需要像 for 循環(huán)那樣,需要進(jìn)行處理。我們看下入口文件最終得到的模塊集合:
{
????'./index.js':?'function(require,exports){?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}',
????'action.js':?'function(require,exports){let?action?=?"making?webpack";\r\nexports.action?=?action;;\n??????}',
????'name.js':?'function(require,exports){let?familyName?=?require("./family-name.js").name;\r\nexports.name?=?`${familyName}?阿爾伯特`;;\n}',
????'family-name.js':?'function(require,exports){exports.name?=?"haiyingsitan";;\n?}'
}
3.2 執(zhí)行模塊的函數(shù)
我們?cè)谏厦娴哪K對(duì)象中獲得了所有模塊信息,接下來我們執(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è)遞歸的過程,不斷查找依賴,然后執(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
}
注意:上面的modules[moduleId]如果按照我們之前的數(shù)據(jù)結(jié)構(gòu)獲取到的實(shí)際上是一個(gè)字符串,但是我們需要它作為函數(shù)執(zhí)行。因此,我們?yōu)榱朔奖悴榭次覀冞@里稍微修改一下直接文件轉(zhuǎn)模塊的代碼。
const?fileToModule?=?function?(path)?{
??console.log("path:",path)
??const?fileContent?=?fs.readFileSync(path).toString();
??return?{
????id:?path,
????dependencies:?getDependencies(fileContent),
????code:?function(require,exports)?{
??????eval(fileContent.toString())???//?看這里?里面的內(nèi)容用eval來執(zhí)行。外面是函數(shù)聲明,不是一個(gè)字符串了。
????},
??};
};
我們不方便去執(zhí)行一個(gè)字符串,因此我們考慮把 code 聲明成一個(gè)函數(shù),函數(shù)里面是模塊的內(nèi)容,通過eval去執(zhí)行。但是當(dāng)我們寫入文件時(shí)不需要這樣,這里是為了方便查看。
3.3 將打包后的文件寫入指定文件
好了,到目前為止我們實(shí)現(xiàn)了一個(gè)模塊打包器所需要的最重要的兩個(gè)部分:模塊集合,以及模塊的執(zhí)行函數(shù)。最終完整的代碼如下:
const?fs?=?require("fs");
const?path?=?require("path");
//?將文件轉(zhuǎn)化成模塊對(duì)象
const?fileToModule?=?function?(path)?{
??const?fileContent?=?fs.readFileSync(path).toString();
??return?{
????id:?path,
????dependencies:?getDependencies(fileContent),
????code:?function?(require,?exports)?{
??????eval(fileContent.toString());
????},
??};
};
//?獲取模塊的所有依賴
function?getDependencies(fileContent)?{
??let?reg?=?/require\(['"](.+??"'"")['"]\)/g;
??let?result?=?null;
??let?dependencies?=?[];
??while?((result?=?reg.exec(fileContent)))?{
????dependencies.push(result[1]);
??}
??return?dependencies;
}
//?將所有模塊以及他們的依賴轉(zhuǎn)化成模塊對(duì)象
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);
????});
??}
??let?modules?=?{};
??queue.forEach((item)?=>?{
????modules[item.id]?=?item.code;
??});
??return?modules;
}
let?modules?=?createGraph("./index.js");
//?執(zhí)行模塊函數(shù)
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;
};
我們從入口文件開始打包,看看能不能得到跟 webpack 相同的結(jié)果。
exec("./index.js");??//輸出:haiyingsitan 阿爾伯特 is making webpack
我們可以發(fā)現(xiàn),順利地得到了跟 webpack 相同的結(jié)果,成功地實(shí)現(xiàn)了模塊的打包。接下來我們要實(shí)現(xiàn)的就是把我們的模塊打包后生成到一個(gè)文件中。
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函數(shù)用于將打包后的文件寫入單獨(dú)的文件中。我們可以看下打包后生成的文件如下:
(function?()?{
??//?模塊集合
??const?modules?=?{
????"./index.js":?function?(require,?exports)?{
??????let?action?=?require("./action.js").action;
??????let?name?=?require("./name.js").name;
??????let?message?=?`${name}?is?${action}`;
??????console.log(message);;
????},
????"action.js":?function?(require,?exports)?{
??????let?action?=?"making?webpack";
??????exports.action?=?action;;
????},
????"name.js":?function?(require,?exports)?{
??????let?familyName?=?require("./family-name.js").name;
??????exports.name?=?`${familyName}?阿爾伯特`;;
????},
????"family-name.js":?function?(require,?exports)?{
??????exports.name?=?"haiyingsitan";;
????},
??};
??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;
??};
??//入口函數(shù)執(zhí)行
??exec("./index.js");
})()
我們可以看到打包后的文件跟 webpack 打包后的文件基本相同。(注意:由于目前只支持引入自定義模塊,對(duì)于內(nèi)置的 path 等無法引入,因此如果要測(cè)試打包后的文件能否正常執(zhí)行,請(qǐng)手動(dòng)在文件頂部加上 path 的引入)。
四、進(jìn)一步優(yōu)化
4.1 使用正則匹配 require 存在的問題
到目前為止,我們已經(jīng)能夠?qū)崿F(xiàn)模塊的打包生成,但是這里仍然存在一些問題,我在前面2.2.1 獲取當(dāng)前模塊的所有依賴的實(shí)現(xiàn)中說到,我們使用/require\(['"](.+? "'"")['"]\)/g這個(gè)正則來匹配require的引入。但是,如果文件中存在符合這條正則但是不是用于引入的內(nèi)容了。比如:
const?str?=?`require('隨便寫的')`;
const?str?=?/require\(['"](.+??"'"")['"]\)/g;
console.log(re.exec(str))//??這里也能夠正確匹配
我們發(fā)現(xiàn)上面的字符串也能夠正確匹配我們的正則,但是這個(gè)字符串并不是一個(gè)require引入。但是它會(huì)被當(dāng)作引入進(jìn)行處理,從而導(dǎo)致報(bào)錯(cuò)。可能有人會(huì)說我們可以寫更好的正則,區(qū)分更多的情況,但是再好的正則也無法兼容所有的情況,那么有沒有什么方法能夠完全正確地區(qū)分用于引入的 require 和其他的 require 了。我們可以參考 webpack 它是怎么能夠正確識(shí)別的。關(guān)鍵就是使用babel。
4.2 引入 babel
關(guān)于babel的原理啥的大家可以去找其他文章看。這里大家簡(jiǎn)單地記住 babel 的核心就是解析(parse),轉(zhuǎn)換(transform),**生成(generate)**這三個(gè)步驟,如下圖所示。
通過將代碼解析成抽象語(yǔ)法樹(AST),然后我們就可以對(duì)我們想要的節(jié)點(diǎn)進(jìn)行操作,轉(zhuǎn)換成新的 AST,然后再生成新的代碼。這里可能大家會(huì)覺得復(fù)雜,但是我們不涉及 babel 底層的原理,只是簡(jiǎn)單應(yīng)用它的轉(zhuǎn)換功能,因此不需要深究。我們可以在AST Explore[1]中查看一下如何將代碼轉(zhuǎn)換成 AST。以下面這段代碼為例:
let?action?=?require("./action.js");
我們可以發(fā)現(xiàn),babel將上面代碼轉(zhuǎn)換成 ast 后,我們可以準(zhǔn)確地獲取到require這個(gè)節(jié)點(diǎn)的類型為CallExpression,節(jié)點(diǎn)的name為require,參數(shù)的 value 為./action.js,這樣的話就能夠正確區(qū)分出用于引用的require和作為值或者變量的require了。因此,我們需要修改一下獲取依賴的這個(gè)函數(shù)的實(shí)現(xiàn):修改前:
function?getDependencies(fileContent)?{
??let?reg?=?/require\(['"](.+??"'"")['"]\)/g;
??let?result?=?null;
??let?dependencies?=?[];
??while?((result?=?reg.exec(fileContent)))?{
????dependencies.push(result[1]);
??}
??return?dependencies;
}
修改后:
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;
}
我們通過 babel 的 parse 獲取到 ast 后,然后查找每個(gè)節(jié)點(diǎn)的類型是否是CallExpression,同時(shí)節(jié)點(diǎn)的名字是否是require,如果同時(shí)滿足,說明這個(gè) require 是一個(gè)函數(shù),用于引入模塊的。那么我們就可以把它的參數(shù)作為依賴放入數(shù)組中保存起來。
4.3 解決模塊之間互相依賴的問題
我們知道模塊之間可以互相引用,比如 name.js 模塊引入了 family-name.js 模塊。而在 family.js 模塊中又引入了 name.js 模塊。如下圖所示:name.js 模塊
let?familyName?=?require("./family-name.js").name;????//?引入了family-name.js模塊
exports.name?=?`${familyName}?阿爾伯特`;
family-name.js 模塊
const?name1?=?require("./name.js");????//?引入了family-name.js模塊
exports.name?=?"haiyingsitan";
這時(shí)候會(huì)帶來問題。由于我們?cè)?strong>2.1.3 將所有模塊組成一個(gè)集合中生成模塊對(duì)象。使用 for of 遍歷模塊集合,如果存在依賴就將其轉(zhuǎn)換成模塊添加到模塊集合中,由于互相依賴會(huì)導(dǎo)致一開始把模塊family.js添加到模塊中,然后又把name.js添加到模塊對(duì)象中,然后name.js中又依賴family.js又需要把重復(fù)的family.js模塊添加進(jìn)去,這樣的話會(huì)導(dǎo)致模塊集合無限循環(huán)下去。
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);
???????//?看這里,會(huì)不斷地創(chuàng)建依賴
??????const?child?=?fileToModule(absolutePath);
??????queue.push(child);
????});
??}
????//?上面得到的是一個(gè)數(shù)組。轉(zhuǎn)化成對(duì)象
??let?modules?=?{}
??queue.forEach((item)?=>?{
????modules[item.id]?=?item.code;
??})
??return?modules;
}
如下圖所示:最終會(huì)導(dǎo)致模塊集合不斷地出現(xiàn)重復(fù)的模塊name.js和family.js,導(dǎo)致循環(huán)永遠(yuǎn)不會(huì)終止下去。
我們可以發(fā)現(xiàn):實(shí)際上出現(xiàn)這種問題的根本是不斷地往模塊集合中添加重復(fù)的模塊,因此我們可以在添加之前判斷是否是重復(fù)的模塊,如果是就不往其中進(jìn)行添加,從而避免不斷循環(huán)下去。實(shí)現(xiàn)如下:
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);
??????//?看這里看這里???判斷一下模塊集合中是否已經(jīng)存在這個(gè)模塊
??????const?result?=?queue.every((item)?=>?{
????????return?item.id?!==?absolutePath;
??????});
??????if?(result)?{
??????????//?不存在,直接添加
????????const?child?=?fileToModule(absolutePath);
????????queue.push(child);
??????}?else?{
??????????//?存在終止本次循環(huán)
????????return?false;
??????}
????});
??}
??let?modules?=?{};
??queue.forEach((item)?=>?{
????modules[item.id]?=?item.code;
??});
??return?modules;
}
五、總結(jié)
好了,到目前為止,我們已經(jīng)能夠?qū)崿F(xiàn)一個(gè)簡(jiǎn)易的 webpack 打包器了。最終的代碼如下:
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);
我們可以發(fā)現(xiàn)最終的實(shí)現(xiàn)過程其實(shí)就是:
加殼,將文件轉(zhuǎn)換成模塊 獲取每個(gè)模塊的依賴 將所有模塊形成一個(gè)大的模塊集合 執(zhí)行模塊的函數(shù) 寫入文件
通過上面的分析,從零開始一步一步地去實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 webpack 模塊打包器,遠(yuǎn)比你自己去看 webpack 源碼,更加簡(jiǎn)單而且更加印象深刻。當(dāng)然,目前我們的打包器功能肯定并不完善,比如我們目前不支持內(nèi)置的引入,不支持 ES6 語(yǔ)法的轉(zhuǎn)換,不支持 css 等的引入。但是這些功能我們都可以逐步去實(shí)現(xiàn)。真正重要的是,我們對(duì)類似 webpack 這種打包器的原理不再是完全不理解了(畢竟我們都實(shí)現(xiàn)了跟它相同的功能了),接下來如果想要深入研究,只是在上面添加功能罷了。完結(jié)撒花。
??愛心三連擊 1.看到這里了就點(diǎn)個(gè)在看支持下吧,你的「點(diǎn)贊,在看」是我創(chuàng)作的動(dòng)力。
2.關(guān)注公眾號(hào)
程序員成長(zhǎng)指北,回復(fù)「1」加入Node進(jìn)階交流群!「在這里有好多 Node 開發(fā)者,會(huì)討論 Node 知識(shí),互相學(xué)習(xí)」!3.也可添加微信【ikoala520】,一起成長(zhǎng)。
“在看轉(zhuǎn)發(fā)”是最大的支持
六、參考資料
AST Explore[1] babel-parse[2] 手寫簡(jiǎn)易模塊打包器[3]
參考資料
AST Explore: https://astexplorer.net/
[2]
babel-parse: https://babel.docschina.org/docs/en/babel-parser
[3]手寫簡(jiǎn)易模塊打包器: https://zhuanlan.zhihu.com/p/257046071
