Stories

Detail Return Return

Webpack的誕生與問題解決之道 - Stories Detail

背景

在早期的前端技術標準根本沒有預料到前端行業會有今天的發展,在設計上存在很多缺陷,隨着web應用複雜性增加,網頁已經從展示簡單的文案和圖像逐漸演變為功能複雜、交互密集的應用程序,這種變化推動了前端模塊化的發展,以應對以下幾個挑戰:

  1. 依賴管理混亂
  2. 全局作用域污染
  3. 代碼膨脹

舉個例子來説明下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Test</title>
  </head>
  <body></body>
  <script src="module-1.js"></script>
  <script src="module-2.js"></script>
</html>

module-1.js

const add = (a, b) => a + b;

module-2.js

const add = (b, a) => b + a;

毫無疑問,在瀏覽器渲染上面的html代碼時,會有報錯提示add已經被聲明過了,這就是上面提到的全局作用域污染問題

Uncaught SyntaxError: Identifier 'add' has already been declared (at module-2.js:1:1)

來改動一下兩個js文件的代碼,module-2.js文件裏面聲明瞭一個函數用來實現乘法,module-1.js
裏面需要使用,這時如果不調整js文件的引入順序,那麼也會有問題。
module-1.js

const add = (a, b) => a + b;
multi(1,2)

module-2.js

const add = (b, a) => b + a;
const multi = (b, a) => b * a;

因為module-1.js文件裏的js先執行,此時multi函數還沒有聲明,直接使用的話就產生報錯,這就是上面提到的依賴管理問題,當隨着工程的日益增長,文件間的依賴關係變得難以追蹤,引入的文件變得越來越多,如何配置準確的順序來加載js資源變得尤為困難。

Uncaught ReferenceError: multi is not defined at module-1.js:2:9

另外由於在模塊化之前,所有的代碼都集中在幾個js文件內,容易造成代碼量快速膨脹的情況。

JavaScript模塊化

為了解決上面的問題,前端引入了模塊化的概念,通過將各個功能點切分成獨立的小部分,這種方式被稱為模塊化。

  1. 模塊可以聲明對其它模塊的依賴,使得能夠正確加載依賴
  2. 每個模塊有自己的作用域,聲明的變量只能在當前模塊內訪問到,如果需要在其它模塊訪問,需要在當前模塊導入該變量。
  3. 由於按照功能點切分成獨立的小部分,原先所有代碼都擠在幾個文件造成代碼膨脹的問題自然而然就解決了。

JavaScript模塊化經歷過早期AMD(require.js)和CMD(sea.js)的過渡,2015年官方推出了ES Module模塊化標準。當js文件使用ES Module模塊化標準時,可以使用import來引入依賴,

在當時由於大多數瀏覽器都不支持ES Module,所以在引入ES Module模塊化標準後,也引發了新的問題:

  1. ES Module這個模塊化標準本身存在兼容性問題
  2. 模塊化切分過細後形成多個文件,在進入頁面後需要從服務端下載多個CSS和JS資源,瀏覽器對同一域名資源請求的併發量也有限制,會影響頁面的展示效果。

為了解決上面問題,webpack這樣的打包工具應運而生。

Webpack是什麼?

引用Webpack官網的介紹

webpack is a _static module bundler_ for modern JavaScript applications

翻譯下來就是webpack是服務於現代JavaScript應用的靜態模塊打包工具。

Webpack解決了什麼問題?

  1. 首先它能夠藉助loader將ES6代碼轉換成ES5代碼,代碼中的import和export語句會被編譯成其它語句,那麼在引入js文件時就不需要帶上type = "module"來表明這是一個ES Module模塊文件,這樣就解決了ES Module在瀏覽器上的兼容性問題。
  2. 它能夠將細分的模塊再重新打包到一起,因為在生產環境也不需要模塊化,解決了因模塊過細導致頻繁請求資源文件的問題。
  3. 另外需要支持將不同種類的文件,把所有的文件都當成模塊,其中所有的資源文件都通過代碼來控制,那麼代碼和資源文件都有統一的管理方式。

前面兩點不需要Webpack也能完成,使用gulp配合插件可以完成文件的編譯、壓縮以及文件合併。但是第三點無法支持。

Webpack的打包結果是怎樣的?

對上面兩個js文件的代碼加入模塊的導出
module-1.js

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

module-2.js

export const multi = (b, a) => b * a;

為了儘可能地觀察清楚Webpack的打包結果,我們把配置文件中的mode配置項設置為none,這樣Webpack在打包的時候就不會開啓任何優化項。
對前面的代碼來進行打包試試看,Webpack的打包結果究竟是怎樣的?

(() => {
  // webpackBootstrap
  "use strict";
  var __webpack_modules__ = [
    ,
    /* 0 */ /* 1 */
    (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
      __webpack_require__.r(__webpack_exports__);
      /* harmony export */ __webpack_require__.d(__webpack_exports__, {
        /* harmony export */ add: () => /* binding */ add,
        /* harmony export */
      });
      const add = (a, b) => {
        return a + b;
      };
    },
    /* 2 */
    (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
      __webpack_require__.r(__webpack_exports__);
      /* harmony export */ __webpack_require__.d(__webpack_exports__, {
        /* harmony export */ multiple: () => /* binding */ multiple,
        /* harmony export */
      });
      const multiple = (a, b) => {
        return a * b;
      };
    },
  ];
  // The module cache
  var __webpack_module_cache__ = {};

  // The require function
  function __webpack_require__(moduleId) {
    // Check if module is in cache
    var cachedModule = __webpack_module_cache__[moduleId];
    if (cachedModule !== undefined) {
      return cachedModule.exports;
    }
    // Create a new module (and put it into the cache)
    var module = (__webpack_module_cache__[moduleId] = {
      // no module.id needed
      // no module.loaded needed
      exports: {},
    });

    // Execute the module function
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

    // Return the exports of the module
    return module.exports;
  }

  /* webpack/runtime/define property getters */
  (() => {
    // define getter functions for harmony 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/runtime/hasOwnProperty shorthand */
  (() => {
    __webpack_require__.o = (obj, prop) =>
      Object.prototype.hasOwnProperty.call(obj, prop);
  })();

  /* webpack/runtime/make namespace object */
  (() => {
    // define __esModule on exports
    __webpack_require__.r = (exports) => {
      if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
      }
      Object.defineProperty(exports, "__esModule", { value: true });
    };
  })();

  var __webpack_exports__ = {};
  // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
  (() => {
    __webpack_require__.r(__webpack_exports__);
    /* harmony import */ var _module_1__WEBPACK_IMPORTED_MODULE_0__ =
      __webpack_require__(1);
    /* harmony import */ var _module_2__WEBPACK_IMPORTED_MODULE_1__ =
      __webpack_require__(2);

    console.log(
      (0, _module_2__WEBPACK_IMPORTED_MODULE_1__.multiple)(
        (0, _module_1__WEBPACK_IMPORTED_MODULE_0__.add)(1, 2),
        3
      )
    );
  })();
})();

最外層是一個立即執行函數,使用__webpack_module__數組來存儲模塊,因為有module-1.js和module-2.js這兩個文件,在webpack的世界裏,一個文件就是一個模塊,所以__webpack_module__數組的長度為2。打包過後的代碼通過使用__webpack_module__數組索引來獲取對應模塊,每個模塊其實都是一個立即執行函數,每個立即執行都創建了自己的作用域,外層作用域是無法直接獲取到內部的變量,這也解釋了為何模塊外部無法訪問模塊內部變量,只有在使用export的方式導出後,外部才能訪問到。

上方的打包代碼導出的對象是module.exports,這個和CommonJS裏面的module.exports不是一回事,只是名稱一樣

使用export導出的函數,上面的打包代碼,都以屬性的方式掛載在立即執行函數module.exports對象上。

Add a new Comments

Some HTML is okay.