Webpack 是怎樣運行的?(一)

Webpack 是時下最流行的前端打包工具,它打包開發代碼,輸出能在各類瀏覽器運行的代碼,提高了開發至發佈過程的效率。前端

你可能已經知道,這種便捷是由 Webpack 的插件系統帶來的,但咱們今天先把這些概念放在一邊,從簡單的實踐開始,探索 Webpack 打包出的代碼是如何在瀏覽器環境運行的。webpack

簡單配置

配置文件是使用 Webpack 的關鍵,一份配置文件主要包含入口(entry)、輸出文件(output)、模式、Loader、插件(Plugin)等幾個部分,但若是隻須要組織 JS 文件的話,指定入口和輸出文件路徑便可完成一個迷你項目的打包:git

項目目錄:es6

  • build
    • webpack.config.js -- 存放 webpack 配置對象
  • src
    • index.js -- 源文件
  • package.json -- 本文使用 webpack ^4.23.0 做示例

爲了更好地觀察產出的文件,咱們將模式設置爲 development 關閉代碼壓縮,再開啓 source-map 支持原始源代碼調試。github

配置文件 build/webpack.config.jsweb

const path = require('path');
const resolve = relativePath => path.resolve(__dirname, relativePath);

module.exports = {
  mode: 'development',
  devtool: 'source-map',
  entry: resolve('../src/index.js'),
  output: {
    path: resolve('../dist'),
  }
};
複製代碼

源文件 src/index.jsjson

document.writeln('Hello webpack!');
複製代碼

如今咱們運行命令 webpack --config build/webpack.config.js ,打包完成後會多出一個輸出目錄 dist:瀏覽器

  • build
    • webpack.config.js
  • dist
    • main.js
  • src
    • index.js
  • package.json

main 是 webpack 默認設置的輸出文件名,咱們快速瞄一眼這個文件:緩存

dist/main.js閉包

(function(modules){
  // ...
})({
  "./src/index.js": (function(){
    // ...
  })
});
複製代碼

整個文件只含一個 當即執行函數(IIFE),咱們稱它爲 webpackBootstrap,它僅接收一個對象 —— 未加載的 模塊集合(modules),這個 modules 對象的 key 是一個路徑,value 是一個函數。你也許會問,這裏的模塊是什麼?它們又是如何加載的呢?

模塊

彆着急,在細看產出代碼前,咱們先豐富一下源代碼:

項目目錄:

  • build
    • webpack.config.js
  • src
    • utils
      • math.js
    • index.js
  • package.json

新文件 src/utils/math.js

export const plus = (a, b) => {
  return a + b;
};

export const minus = (a, b) => {
  return a - b;
};
複製代碼

src/index.js

import {plus, minus} from './utils/math.js';

document.writeln('Hello webpack!');
document.writeln('1 + 2: ', plus(1, 2));
document.writeln('1 - 2: ', minus(1, 2));
複製代碼

咱們按照 ES 規範的模塊化語法寫了一個簡單的模塊 src/utils/math.js,給 src/index.js 引用。目前,雖然各大瀏覽器開始支持經過 <script type="module"> 的方式支持 ES6 Module,但還需時間覆蓋。Webpack 用本身的方式支持了 ES6 Module 規範,前面提到的 module 就是和 ES6 module 對應的概念。

接下來咱們看一下這些模塊是如何通 ES5 代碼實現的。再次運行命令 webpack --config build/webpack.config.js 後查看輸出文件:

dist/main.js

(function(modules){
  // ...
})({
  "./src/index.js": (function(){
    // ...
  }),
  "./src/utils/math.js": (function() {
    // ...
  })
});
複製代碼

IIFE 傳入的 modules 對象裏多了一個鍵值對,對應着新模塊 src/utils/math.js,這和咱們在源代碼中拆分的模塊互相呼應。然而,有了 modules 只是第一步,這份文件最終達到的效果應該是讓各個模塊按開發者編排的順序運行。

探究 webpackBootstrap

接下來看看 webpackBootstrap 函數中有些什麼:

// webpackBootstrap
(function(modules){

  // 緩存 __webpack_require__ 函數加載過的模塊
  var installedModules = {};
  
  /** * Webpack 加載函數,用來加載 webpack 定義的模塊 * @param {String} moduleId 模塊 ID,通常爲模 塊的源碼路徑,如 "./src/index.js" * @returns {Object} exports 導出對象 */
  function __webpack_require__(moduleId) {
    // ...
  }

  // 在 __webpack_require__ 函數對象上掛載一些變量
  // 及函數 ...

  // 傳入表達式的值爲 "./src/index.js"
  return __webpack_require__(__webpack_require__.s = "./src/index.js");
})(/* modules */);
複製代碼

能夠看到其實主要作了兩件事:

  1. 定義一個模塊加載函數 __webpack_require__
  2. 使用加載函數加載入口模塊 "./src/index.js"

整個 webpackBootstrap 中只出現了入口模塊的影子,那其餘模塊又是如何加載的呢?咱們順着 __webpack_require__("./src/index.js") 細看加載函數的內部邏輯:

// ...

function __webpack_require__(moduleId) {
  // 重複加載則利用緩存
  if (installedModules[moduleId]) {
    return installedModules[moduleId].exports;
  }

  // 若是是第一次加載,則初始化模塊對象,並緩存
  var module = installedModules[moduleId] = {
    i: moduleId,  // 模塊 ID
    l: false,     // 模塊加載標識
    exports: {}   // 模塊導出對象
  };

  /** * 執行模塊 * @param module.exports -- 模塊導出對象引用,改變模塊包裹函數內部的 this 指向 * @param module -- 當前模塊對象引用 * @param module.exports -- 模塊導出對象引用 * @param __webpack_require__ -- 用於在模塊中加載其餘模塊 */
  modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

  // 模塊加載標識置爲已加載
  module.l = true;

  // 返回當前模塊的導出對象引用
  return module.exports;
}

// ...
複製代碼

首先,加載函數使用了閉包變量 installedModules,用來將已加載過的模塊保存在內存中。 接着是初始化模塊對象,並把它掛載到緩存裏。而後是模塊的執行過程,加載入口文件時 modules[moduleId] 其實就是 ./src/index.js 對應的模塊函數。執行模塊函數前傳入了跟模塊相關的幾個實參,讓模塊能夠導出內容,以及加載其餘模塊的導出。最後標識該模塊加載完成,返回模塊的導出內容。

根據 __webpack_require__ 的緩存和導出邏輯,咱們得知在整個 IIFE 運行過程當中,加載已緩存的模塊時,都會直接返回 installedModules[moduleId].exports,換句話說,相同的模塊只有在第一次引用的時候纔會執行模塊自己。

模塊執行函數

__webpack_require__ 中經過 modules[moduleId].call() 運行了模塊執行函數,下面咱們就進入到 webpackBootstrap 的參數部分,看看模塊的執行函數。

// webpackBootstrap
(function(modules){

  // ...

})({

  /*** 入口模塊 ./src/index.js ***/
  "./src/index.js": (function (module, __webpack_exports__, __webpack_require__) {
 "use strict";

    // 用於區分 ES 模塊和其餘模塊規範,不影響理解 demo,戰略跳過。
    __webpack_require__.r(__webpack_exports__);

    // 源模塊代碼中,`import {plus, minus} from './utils/math.js';` 語句被 loader 解析轉化。
    // 加載 "./src/utils/math.js" 模塊,
    /* harmony import */ var _utils_math_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils/math.js */ "./src/utils/math.js");

    document.writeln('Hello webpack!');
    document.writeln('1 + 2: ', Object(_utils_math_js__WEBPACK_IMPORTED_MODULE_0__["plus"])(1, 2));
    document.writeln('1 - 2: ', Object(_utils_math_js__WEBPACK_IMPORTED_MODULE_0__["minus"])(1, 2));
  }),

  /*** 工具模塊 ./src/utils/math.js ***/
  "./src/utils/math.js": (function(module, __webpack_exports__, __webpack_require__) {
 "use strict";

    // 同 "./src/index.js"
    __webpack_require__.r(__webpack_exports__);

    // 源模塊代碼中,`export` 語句被 loader 解析轉化。
    // 導出 __webpack_exports__
    /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "plus", function() { return plus; });
    /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "minus", function() { return minus; });
    const plus = (a, b) => {
      return a + b;
    };

    const minus = (a, b) => {
      return a - b;
    };
  })
});

複製代碼

執行順序是:入口模塊 -> 工具模塊 -> 入口模塊。入口模塊中首先就經過 __webpack_require__("./src/utils/math.js") 拿到了工具模塊的 exports 對象。再看工具模塊,ES 導出語法轉化成了__webpack_require__.d(__webpack_exports__, [key], [getter]),而 __webpack_require__.d 函數的定義在 webpackBootstrap 內:

// ...

  // 定義 exports 對象導出的屬性。
  __webpack_require__.d = function (exports, name, getter) {

    // 若是 exports (不含原型鏈上)沒有 [name] 屬性,定義該屬性的 getter。
    if (!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, {
        enumerable: true,
        get: getter
      });
    }
  };

  // 包裝 Object.prototype.hasOwnProperty 函數。
  __webpack_require__.o = function (object, property) {
    return Object.prototype.hasOwnProperty.call(object, property);
  };

// ...
複製代碼

可見 __webpack_require__.d 其實就是 Object.defineProperty 的簡單包裝(怪不得叫 d 呢)。

回顧一下,__webpack_exports__ 本來在 __webpack_require__ 中建立,初始值爲 {}。這個導出對象一路傳到工具模塊 math.js 中,被添加上 plusminus,而後又在 __webpack_require__ 函數最後導出,爲入口模塊 index.js 的執行函數所用。

exports 的一輩子:

exports 的一輩子

引用工具模塊導出的變量後,入口模塊再執行它剩餘的部分。至此,Webpack 基本的模塊執行過程就結束了。

以上內容可克隆示例代碼庫調試,分支爲 demo1

除了 ES6 Module 規範,Webpack 一樣支持 CommonJS 與 AMD 規範,你能夠替換模塊化規範,從新打包來觀察它們的區別。

小結

好了,咱們用流程圖總結一下 Webpack 模塊的加載思路:

webpack-module-implementation-sync

參考

Webpack 術語表 - Module

相關文章
相關標籤/搜索