webpack是怎麼實現js模塊化的?

前言

博主最近一直在學習算法相關的內容,因此挺長一段時間沒有更新技術文章了,正好最近有個朋友問了我一個問題,webpack是怎麼實現模塊化的?我也就順便把這塊相關的內容寫成一篇掘文,分享給那些對這塊內容不太清楚的同窗。前端

經過本文,你會搞清楚下面這些問題:webpack

  • 1.webpack的模塊化實現
  • 2.import會被webpack編譯成什麼?
  • 3.爲何你可使用import引入commonjs規範的模塊?爲何反向引用也能夠?

前端模塊化

對於前端的模塊化,相信你們都很熟悉。在如今的前端開發中,由於三大前端框架以及webpack等一系列打包工具的普及,模塊化的應用已是屢見不鮮。咱們再也不須要像之前用對象來定義js模塊,或者使用AMDCMDjs規範。如今在瀏覽器端,使用模塊的方法就一個,import。隨着時代發展,如今已經有不少瀏覽器原生支持了import語法,可是爲了兼容性,咱們仍是須要經過webpack來處理import語法。web

PS:前不久尤大的vite2.0已經正式發佈了,構建速度真是快到飛起,相信這也是將來的主流打包構建方式。算法

import會被編譯成什麼

咱們先來寫個最簡單的例子,來讓webpack編譯一下。本文的例子使用的webpack5編譯,部分命名可能跟webpack4有些許差別,可是模塊化的思想是一致的。瀏覽器

// index.js
import { read } from './a';
import run from './b';
read();
run();

// a.js
export const read = () => {
  console.log('閱讀');
};

// b.js
export default run = () => {
  console.log('跑步');
};
複製代碼

代碼很簡單,如今咱們來看下,webpack編譯出來的代碼是什麼樣的。`(去掉了不少註釋)緩存

(() => {
  "use strict";
  var __webpack_modules__ = ({
    "./a.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
      eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"read\": () => (/* binding */ read)\n/* harmony export */ });\nconst read = () => {\r\n console.log('閱讀');\r\n};\n\n//# sourceURL=webpack://my-leetcode/./a.js?");
    }),
    "./b.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
      eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (run = () => {\r\n console.log('跑步');\r\n});\n\n//# sourceURL=webpack://my-leetcode/./b.js?");
    }),
    "./index.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
      eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a */ \"./a.js\");\n/* harmony import */ var _b__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./b */ \"./b.js\");\n\r\n\r\n(0,_a__WEBPACK_IMPORTED_MODULE_0__.read)();\r\n(0,_b__WEBPACK_IMPORTED_MODULE_1__.default)();\n\n//# sourceURL=webpack://my-leetcode/./index.js?");
    })
  });
  
  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__);
    return module.exports;
  }
 	
  (() => {
    __webpack_require__.d = (exports, definition) => {
      for(var key in definition) {
        if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
          Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
        }
      }
    };
  })();
 	
  (() => {
    __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
  })();
 	
  (() => {
    __webpack_require__.r = (exports) => {
      if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
      }
      Object.defineProperty(exports, '__esModule', { value: true });
    };
  })();

  // 執行入口的index.js
  var __webpack_exports__ = __webpack_require__("./index.js");
 })();
複製代碼

首先編譯出來的這個代碼就是一個自執行函數,裏面的內容能夠分爲三部分。性能優化

  • 1.modules對象
  • 2.__webpack_require__方法以及子方法的定義
  • 3.經過__webpack_require__方法運行入口的index.js文件

modules對象

這個對象裏存放了全部你代碼裏寫的做爲一個個模塊的js,它以js的文件路徑做爲key,值爲一個可執行的函數。前端框架

__webpack_require__方法以及子方法的定義

__webpack_require__是一個關鍵的方法,負責實際的模塊加載並執行這些模塊內容,返回執行結果。它的子方法都是用來幫助模塊的加載和執行。markdown

運行index.js文件

經過__webpack_require__方法運行入口文件index.jsapp

webpack模塊化實現

咱們如今從入口index.js開始,一步步跟隨代碼。

__webpack_require__("./index.js");
複製代碼

咱們先來看看__webpack_require__方法

// 模塊緩存
var __webpack_module_cache__ = {};

// 傳入引用模塊的路徑
function __webpack_require__(moduleId) {
  // 若是引用的模塊存在緩存,直接返回緩存內容
  if(__webpack_module_cache__[moduleId]) {
      return __webpack_module_cache__[moduleId].exports;
  }
  // 定義一個module對象,再給它初始化一個exports對象
  var module = __webpack_module_cache__[moduleId] = {
      exports: {}
  };
  // 運行__webpack_modules__裏的相關模塊,傳入相關參數
  __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
  return module.exports;
}
複製代碼

__webpack_require__方法其實就是運行__webpack_modules__裏的相關模塊。咱們如今來看看index.js模塊的可執行函數。

"./index.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
  eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a */ \"./a.js\");\n/* harmony import */ var _b__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./b */ \"./b.js\");\n\r\n\r\n(0,_a__WEBPACK_IMPORTED_MODULE_0__.read)();\r\n(0,_b__WEBPACK_IMPORTED_MODULE_1__.default)();\n\n//# sourceURL=webpack://my-leetcode/./index.js?");
})
// 把eval裏的代碼提取出來,等價於
(__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
  __webpack_require__.r(__webpack_exports__);
  // 定義一個變量,經過__webpack_require__加載a.js文件
  var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./a.js");
  // 定義一個變量,經過__webpack_require__加載b.js文件
  var _b__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("./b.js");
  // 經過以前定義的變量,來運行相關的方法
  (0,_a__WEBPACK_IMPORTED_MODULE_0__.read)();
  (0,_b__WEBPACK_IMPORTED_MODULE_1__.default)();
}
複製代碼

裏面的方法其實很簡單,就是經過__webpack_require__加載a.jsb.js,經過返回值來運行a.jsb.js模塊裏的方法。

咱們如今來看看,__webpack_require__是怎麼加載a.jsb.js模塊,並把它們內部的方法返回出來使用的。咱們先從eval中提取出相關函數。

// a.js
(__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
   __webpack_require__.r(__webpack_exports__);
   __webpack_require__.d(__webpack_exports__, { "read": () => read });
   const read = () => { console.log('閱讀'); };
}

// b.js
(__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
      __webpack_require__.r(__webpack_exports__);
      __webpack_require__.d(__webpack_exports__, { "default": () =>__WEBPACK_DEFAULT_EXPORT__ });
      const __WEBPACK_DEFAULT_EXPORT__ = (run = () => { console.log('跑步') });
    }
複製代碼

由於一個是read方法是export導出的,run方法是export default導出的,可是二者除了在命名上稍微有所區別,其餘都一致。

首先,函數裏,都存在咱們寫在模塊裏的業務代碼,readrun。而後咱們先重點來看下__webpack_require__.d方法。

__webpack_require__.d = (exports, definition) => {
  for(var key in definition) {
    if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
      Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
    }
  }
};
//這裏的重點其實就是一句話,把key的內容,定義到exports的get方法中
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
複製代碼

關於Object.defineProperty的內容不在本文討論範圍內,若是你不清楚這個方法,請先去了解一下它的使用。

咱們再把a.js__webpack_require__.d結合一下。

// a.js
(__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
   __webpack_require__.r(__webpack_exports__);
   // 這裏的__webpack_exports__其實就是__webpack_require__裏定義的module.exports。
   // 這裏就是把read方法定義到module.exports.read上
   Object.defineProperty(__webpack_exports__, "read", { enumerable: true, get: read });
   const read = () => { console.log('閱讀'); };
}
複製代碼

這樣定義以後

// index.js
// 這裏__webpack_require__返回出來的module.exports.read上就定義了一個read方法
var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./a.js");
// 後面天然就可使用a.js裏定義的read方法了。b.js也是相同的道理
_a__WEBPACK_IMPORTED_MODULE_0__.read()
複製代碼

其實就是至關於,webpack將每個模塊暴露出來的方法,都定義在了各自的module.exports對象上,而後返回出來,給其餘的模塊使用。經過這種方法,webpack就實現了js的模塊化。

這不但跟Commonjs的導出方法命名同樣,實現上也是相似。Commonjs中,每一個js文件一建立,也會生成一個 var exports = module.exports = {}, 開發者定義的方法,都會定義到exports或者module.exports

import懶加載實現

懶加載是前端很是經常使用的一種性能優化手段,使用上也很簡單,只要import('xxx.js')就行,如今咱們來看下webpack是怎麼實現懶加載的。咱們稍微改下以前的代碼,而後再從新編譯一下。

// index.js
import('./a.js').then(res => {
  res.read();
})

// a.js
export const read = () => {
  console.log('閱讀');
};
複製代碼

編譯以後,咱們會發現除了主的js文件以外,還會生成一個懶加載的時候須要加載的js文件。 主文件步驟跟以前一致,仍是經過__webpack_require__加載index.js文件。

這裏的代碼量比較大,詳細的流程,我也不在這裏貼代碼了,總的來講,當用戶觸發其加載的動做時,會經過__webpack_require__.l方法動態的在head標籤中建立一個script標籤,而後加載模塊,經過script標籤的onloadonerror事件監聽模塊加載狀態,若是完成,自動執行其中的代碼。

commonjs的文件加載

接下來,咱們看下commonjs規範的文件會被webpack編譯成什麼樣,改造一下代碼

// index.js
const a = require('./a');
const run = require('./b');
a.read();
run();
// a.js
exports.read = () => {
  console.log('閱讀');
};
// b.js
module.exports = run = () => {
  console.log('跑步');
};
複製代碼

別的代碼都一致,主要就來看下__webpack_modules__對象中各個模塊的key對應的函數

// index.js
(__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
  const a = __webpack_require__("./a.js");
  a.read();
  const run = __webpack_require__("./b.js");
  run();
}
// a.js
(__unused_webpack_module, exports) => {
  exports.read = () => { console.log('閱讀') };
}
// b.js
(module) => {
  module.exports = run = () => { console.log('跑步'); };
},
複製代碼

編譯以後的index.js文件跟原來的文件,只是把require換成了__webpack_require__,其餘沒有變化。而a.jsb.js跟原來的代碼是如出一轍的。可是這裏的exportsmodule__webpack_require__調用時候傳入的。至關於,a.jsb.js都直接在__webpack_require__module.exports上定義了相關的方法。那index.js天然也就能夠調用到這些方法了。

這也說明了,爲何可使用import引入commonjs規範的模塊,反向引用也能夠。

總結

webpack的模塊化主要是經過__webpack_require__方法,將各個模塊裏定義的方法,esm定義的方法使用Object.definePropertycommonjs定義的方法直接定義,最終都會統一加到本身定義的module.exports對象上,而後返回出來,給其餘的模塊引用。

import進來的文件通過webpack打包之後會存放在一個對象裏,key爲模塊路徑,value爲模塊的可執行函數。import懶加載會單獨打成一個包,在須要加載的時候,動態進行加載。

由於webpack會把import的方法都會轉換成__webpack_require__方法,使用相似commonjs規範的方式,獲取其餘模塊裏的方法。因此可使用import引入commonjs規範的模塊, 反向引用也能夠。

感謝

本文若是對你有所幫助,請幫忙點個贊,感謝。

相關文章
相關標籤/搜索