編寫一個(gè)簡(jiǎn)單的 webpack 模塊打包器

早期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í)行即可。


image.png

也就是說(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。


image.png

從上圖中我們可以看出:當(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)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容