<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          自己實(shí)現(xiàn)一個(gè)簡(jiǎn)易的模塊打包器(干貨)

          共 20957字,需瀏覽 42分鐘

           ·

          2020-12-17 12:09

          作者:海因斯坦,原文鏈接: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.jsname.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ù)由三部分組成:

          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è)模塊集合中通過模塊id或者模塊名進(jìn)行獲取即可。
          ??var?modules?=?{
          ????"./action.js":?(module,?exports)?=>?{
          ??????let?action?=?"making?webpack";
          ??????exports.action?=?action;
          ????},
          ??};
          1. 模塊函數(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;
          ?}
          1.  入口文件立即執(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è)部分:

          1. 將項(xiàng)目中所有的文件生成一個(gè)大的模塊集合
          2. 模塊執(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)的功能是:

          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,然而我們無法獲取到他們的模塊內(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)的namerequire,參數(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.jsfamily.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ā)”是最大的支持

          六、參考資料

          1. AST Explore[1]
          2. babel-parse[2]
          3. 手寫簡(jiǎn)易模塊打包器[3]

          參考資料

          [1]

          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

          瀏覽 26
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  天天躁夜夜躁av天天爽 | 超碰在线青青草 | 约操美乳 | 九一免费观看网站 | 91啪啪视频 |