標籤:

從 Bundle 文件看 Webpack 模塊機制

我們知道 Webpack 是一個模塊打包工具,但是它打包後的 bundle 文件到底長什麼樣呢?本文將通過一個最簡單的示例,分析 Webpack 打包後為我們生成的代碼,來闡述項目代碼運行時的模塊機制。

示例所用 Webpack 版本為 2.3.0

準備點材料

webpack.config.js

const path = require(path)nnmodule.exports = {n entry: ./a.js,n output: {n path: path.resolve(__dirname, dist),n filename: bundle.jsn }n}n

b.js

console.log(module b runs)nnexport default {n name: bn}n

c.js

import b from ./bnnexport default {n name: cn}n

a.js

import b from ./bnimport c from ./cnnconsole.log(b.name)nconsole.log(c.name)n

簡單來說,就是以 a.js 為入口文件,將 a.js,b.js 和 c.js 打包到 dist/bundle.js 文件中。

完整的示例代碼請戳 how-webpack-modules-work

進入正題前,先思考個問題:b.js 中 的 module b runs 會輸出幾次?

換句話說,a.js 和 c.js 都引用了 b.js, 那麼console.log(module b runs) 到底執行了幾次?

答案是 1 次。為什麼?我們往下看。

模塊初始化函數

在 build.js 中,有一個 modules 變數

// modules 通過 IIFE 的方式傳入n(function (modules) {n ...n})([n (function (module, __webpack_exports__, __webpack_require__) {n ...n }),n (function (module, __webpack_exports__, __webpack_require__) {n ...n }),n (function (module, __webpack_exports__, __webpack_require__) {n ...n })n])n

可以看到 modules 變數存的是一個數組,裡面每一項都是一個函數,我們把這些函數叫作模塊初始化函數。Webpack 在打包的時候,會做以下幾件事:

  • 將每個文件視為一個獨立的模塊
  • 分析模塊之間的依賴關係,將模塊中 import export 相關的內容做一次替換,比如:

    // c.jsnimport b from ./bnnexport default {n name: cn} nn// 最後被轉化為nvar __WEBPACK_IMPORTED_MODULE_0__b__ = __webpack_require__(0)nn// 這裡需要特別注意一點, Webpack 將 a 屬性作為模塊的 default 值n__webpack_exports__["a"] = ({n name: cn})n

  • 給所有模塊外面加一層包裝函數,使其成為模塊初始化函數

  • 把所有模塊初始化函數合成一個數組,賦值給 modules 變數

拿 c.js 做個例子,它最後會被包裝成如下形式:

function (module, __webpack_exports__, __webpack_require__) {n var __WEBPACK_IMPORTED_MODULE_0__b__ = __webpack_require__(0)nn __webpack_exports__["a"] = ({n name: cn })n}n

modules && __webpack_exports__

上面提到的模塊初始化函數中,第一個參數叫 module,第二個參數叫 __webpack_exports__,它們有什麼聯繫和區別呢?

module 可以理解成模塊的「元信息」,裡面不僅存著我們真正用到的模塊內容,也存了一些模塊 id 等信息。

__webpack_exports__ 是我們真正「require」時得到的對象。

這兩個對象之間有如下的關係:module.exports === __webpack_exports__

在模塊初始化函數執行過程中,會對 __webpack_exports__(剛傳入的時候為空對象)賦上新的屬性,這也是為什麼我叫這個函數為模塊初始化函數的原因 -- 整個過程就是為了「構造」出一個新的 __webpack_exports__對象。

Webpack 中還有一個 installedModules 變數,通過它來保存這些已載入過的模塊的引用。

模塊 id

每個模塊有一個唯一的 id,這個 id 從 0 開始,是個整數,modules 和 installedModules 變數通過這個 id 來保存相應的模塊初始化函數和模塊引用。 為了更好的梳理它們三個之間的關係,我們再拿上面的 c.js 做例子:

// 假設 c.js 打包後的模塊 id 為 1n// 那麼對應的 modules 和 installedModules 如下nn// 存的是一個函數nmodules[1] = function (module, __webpack_exports__, __webpack_require__) {n var __WEBPACK_IMPORTED_MODULE_0__b__ = __webpack_require__(0)nn __webpack_exports__["a"] = ({n name: cn })n}nn// 存的是一個對象ninstalledModules[1] = {n moduleId: 1,n l: true,n exports: {n a: {n name: cn }n }n}n

可能有小夥伴會問:為什麼不直接用文件名來作為模塊 id,而是使用從 0 開始的整數?

原因如下:

  • 模塊之間的文件名可能會重複,比如 components 和 containers 目錄下都有個文件叫 menu.js,這樣模塊 id 就會有衝突
  • 相比用文件名做 id,使用數字最後打包的體積更小

require 函數

function __webpack_require__(moduleId) {nn // 檢查 installedModules 中是否存在對應的 modulen // 如果存在就返回 module.exportsn if (installedModules[moduleId])n return installedModules[moduleId].exports;nn // 創建一個新的 module 對象,用於下面函數的調用n var module = installedModules[moduleId] = {n i: moduleId,n l: false,n exports: {}n };nn // 從 modules 中找到對應的模塊初始化函數並執行n modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);nn // 標識 module 已被載入過n module.l = true;nn return module.exports;n}n

__webpack_require__ 做了以下幾件事:

  1. 根據 moduleId 查看 installedModules 中是否存在相應的 module ,如果存在就返回對應的 module.exports
  2. 如果 module 不存在,就創建一個新的 module 對象,並且使 installedModules[moduleId] 指向新建的 module 對象
  3. 根據 moduleId 從 modules 對象中找到對應的模塊初始化函數並執行,依次傳入 module,module.exports,__webpack_require__。可以看到,__webpack_require__ 被當作參數傳入,使得所有模塊內部都可以通過調用該函數來引入其他模塊
  4. 最後一步,返回 module.exports

最後,我們來改造一下 bundle.js

(function (modules) {n // 存放模塊初始化函數n const installedModules = {}n n function require(moduleId) {n // 檢查 installedModules 中是否存在該 modulen // 如果存在就返回 module.exportsn if (installedModules[moduleId])n return installedModules[moduleId].exportsnn // 創建一個新的 module 對象,用於下面函數的調用n var module = installedModules[moduleId] = {n i: moduleId,n l: false,n exports: {}n }nn // 從 modules 中找到對應的模塊初始化函數並執行n modules[moduleId].call(module.exports, module, module.exports, require)nn // 標識 module 已被載入過n module.l = true;nn return module.exportsn }nn // 執行入口模塊,a.js 的 moduleId 為 2n require(2)nn})(n [ /* b.js, moduleId = 0 */n (function (module, exports, require) {n console.log(module b runs)nn exports[a] = ({n name: bn })n }),n /* c.js, moduleId = 1 */n (function (module, exports, require) {n const module_b = require(0)n n exports[a] = ({n name: cn })n }),n /* a.js, moduleId = 2 */n (function (module, exports, require) {n const module_b = require(0)n const module_c = require(1)nn console.log(module_b[a].name)n console.log(module_c[a].name)n })n ]n)n

總結

通過上面的分析,可以看到,一個簡單的模塊機制由這幾個部分構成:

  • 一個數組用於保存所有的模塊初始化函數 -- modules
  • 一個對象用於保存載入過的模塊 -- installedModules
  • 一個模塊載入函數 -- require

了解這些「黑盒」,有助於我們更好的理解模塊化。在此之上,還可以進一步去研究加了 Code Splitting 之後的代碼的樣子,以及思考如何生成這樣一個 bundle 文件。這些內容也非常豐富,值得大家去探索。


推薦閱讀:

用webpack,輸出多個出口文件,每個出口文件對應一個頁面,重複引用jquery?
深入Webpack-編寫Loader
重溫 Webpack, Babel 和 React
webpack 多頁面設置

TAG:webpack |