揭祕 webpack5 模塊打包

上一節中我們初步瞭解了webpack可以利用內置靜態模塊類型 (asset module type) 來處理資源文件,我們所知道的本地服務,資源的壓縮,代碼分割,在webpack構建的工程中有一個比較顯著的特徵是,模塊化,要麼commonjs要麼esModule, 在開發環境我們都是基於這兩種,那麼通過webpack打包後,如何讓其支持瀏覽器能正常的加載兩種不同的模式呢?

接下來我們一起來探討下webpack中打包後代碼的原理

正文開始...

初始化基礎項目

新建一個文件夾webpack-05-module

npm init -y

我們安裝項目一些基礎支持的插件

npm i webpack webpack-cli webpack-dev-server html-webpack-plugin babel-loader @babel
l/core -D

在根目錄新建webpack.config.js,配置相關參數,爲了測試 webpack 打包cjsesModule我在entry寫了兩個入口文件,並且設置mode:developmentdevtool: 'source-map', 設置source-map是爲了更好的查看源代碼

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  entry: {
    cjs: './src/commonjs_index.js',
    esjs: './src/esmodule_index.js'
  },
  devtool: 'source-map',
  output: {
    filename: 'js/[name].js',
    path: path.resolve(__dirname, 'dist'),
    assetModuleFilename: 'images/[name][ext]'
  },
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        options: {
          presets: ['@babel/env']
        }
      },
      {
        test: /\.(png|jpg)$/i,
        type: 'asset/resource'
        // generator: {
        //   // filename: 'images/[name][ext]',
        //   publicPath: '/assets/images/'
        // }
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: './public/index.html'
    })
  ]
};

src目錄下新建commonjs_index.js, esmodule_index.js文件

commonjs_index.js

// commonjs_index.js
const { twoSum } = require('./utils/common.js');
import imgSrc from './assets/images/1.png';
console.log('cm_sum=' + twoSum(1, 2));
const domApp = document.getElementById('app');
var img = new Image();
img.src = imgSrc;
domApp.appendChild(img);

引入的common.js

// utils/common.js
function twoSum(a, b) {
  return a + b;
}
module.exports = {
  twoSum
};

esmodule_index.js

// esmodule_index.js
import twoSumMul from './utils/esmodule.js';
console.log('es_sum=' + twoSumMul(2, 2));

引入的esmodule.js

// utils/esmodule.js
function twoSumMul(a, b) {
  return a * b;
}
// esModule
export default twoSumMul;

當我們運行npm run build命令,會在根目錄dist/js文件夾下打包入口指定的兩個文件

webpack 打包 cjs 最終代碼

我把對應註釋去掉後就是下面這樣的

// cjs.js
(() ={
  var __webpack_modules__ = {
    './src/utils/common.js'(module) ={
      function twoSum(a, b) {
        return a + b;
      }

      module.exports = {
        twoSum: twoSum
      };
    },
    './src/assets/images/1.png'(module, __unused_webpack_exports, __webpack_require__) ={
      'use strict';
      module.exports = __webpack_require__.p + 'images/1.png';
    }
  };

  var __webpack_module_cache__ = {};

  function __webpack_require__(moduleId) {
    var cachedModule = __webpack_module_cache__[moduleId];
    if (cachedModule !== undefined) {
      return cachedModule.exports;
    }
    var module = (__webpack_module_cache__[moduleId] = {
      exports: {}
    });

    __webpack_modules__[moduleId](module, module.exports, __webpack_require__ "moduleId");

    return module.exports;
  }

  (() ={
    __webpack_require__.g = (function () {
      if (typeof globalThis === 'object') return globalThis;
      try {
        return this || new Function('return this')();
      } catch (e) {
        if (typeof window === 'object') return window;
      }
    })();
  })();

  (() ={
    __webpack_require__.r = (exports) ={
      if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
      }
      Object.defineProperty(exports, '__esModule'{ value: true });
    };
  })();

  (() ={
    var scriptUrl;
    if (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + '';
    var document = __webpack_require__.g.document;
    if (!scriptUrl && document) {
      if (document.currentScript) scriptUrl = document.currentScript.src;
      if (!scriptUrl) {
        var scripts = document.getElementsByTagName('script');
        if (scripts.length) scriptUrl = scripts[scripts.length - 1].src;
      }
    }
    if (!scriptUrl) throw new Error('Automatic publicPath is not supported in this browser');
    scriptUrl = scriptUrl
      .replace(/#.*$/, '')
      .replace(/\?.*$/, '')
      .replace(/\/[^\/]+$/, '/');
    __webpack_require__.p = scriptUrl + '../';
  })();

  var __webpack_exports__ = {};
  (() ={
    'use strict';
    __webpack_require__.r(__webpack_exports__);
     var _assets_images_1_png__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./assets/images/1.png */ './src/assets/images/1.png');
    var _require = __webpack_require__(/*! ./utils/common.js */ './src/utils/common.js'),
      twoSum = _require.twoSum;

    console.log('cm_sum=' + twoSum(1, 2));
    var domApp = document.getElementById('app');
    var img = new Image();
    img.src = _assets_images_1_png__WEBPACK_IMPORTED_MODULE_0__;
    domApp.appendChild(img);
  })();
})();

初次看,感覺webpack打包cjs的代碼太長了,但是刪除掉註釋後,我們仔細分析發現,並沒有那麼複雜

首先是該模塊採用 IFEE 模式,一個匿名的自定義自行函數內包裹了幾大塊區域

1、初始化定義了 webpack 依賴的模塊

 var __webpack_modules__ = {
    './src/utils/common.js'(module) ={
      function twoSum(a, b) {
        return a + b;
      }
      // 當在執行時,返回這個具體函數體內容
      module.exports = {
        twoSum: twoSum
      };
    },
    './src/assets/images/1.png'(module, __unused_webpack_exports, __webpack_require__) ={
      'use strict';
      // 每一個對應的模塊對應的內容
      module.exports = __webpack_require__.p + 'images/1.png';
    }
  };

我們發現webpack是用模塊引入的路徑當成key, 然後value就是一個函數,函數體內就是引入的具體代碼內容,並且內部傳入了一個形參module, 實際上這個module就是爲{exports: {}}定義的對象,把內部函數twoSum綁定了在對象上

2、調用模塊優先從緩存對象模塊取值

 var __webpack_module_cache__ = {};
 // moduleId 就是引入的路徑
  function __webpack_require__(moduleId) {
    // 根據moduleId優先從緩存中獲取__webpack_modules__中綁定的值 {twoSum: TwoSum}
    var cachedModule = __webpack_module_cache__[moduleId];
    if (cachedModule !== undefined) {
      return cachedModule.exports;
    }
    // 傳入__webpack_modules__內部value的形參 module
    var module = (__webpack_module_cache__[moduleId] = {
      exports: {}
    });
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__ "moduleId");
    // 根據moduleId依次返回 {twoSum: twoSum}、__webpack_require__.p + 'images/1.png‘圖片路徑
    return module.exports;
  }

3、綁定全局對象,引入圖片的資源路徑,主要是__webpack_require__.p圖片地址

  (() ={
    __webpack_require__.g = (function () {
      if (typeof globalThis === 'object') return globalThis;
      try {
        return this || new Function('return this')();
      } catch (e) {
        if (typeof window === 'object') return window;
      }
    })();
  })();
  
   (() ={
    var scriptUrl;
    if (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + '';
    var document = __webpack_require__.g.document;
    if (!scriptUrl && document) {
      if (document.currentScript) scriptUrl = document.currentScript.src;
      if (!scriptUrl) {
        var scripts = document.getElementsByTagName('script');
        if (scripts.length) scriptUrl = scripts[scripts.length - 1].src;
      }
    }
    if (!scriptUrl) throw new Error('Automatic publicPath is not supported in this browser');
    scriptUrl = scriptUrl
      .replace(/#.*$/, '')
      .replace(/\?.*$/, '')
      .replace(/\/[^\/]+$/, '/');
      // 獲取圖片路徑
    __webpack_require__.p = scriptUrl + '../';
  })();

4、將esModule轉換,用Object.defineProperty攔截exports(module.exports) 對象添加__esModule屬性

  (() ={
    __webpack_require__.r = (exports) ={
      if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
      }
      Object.defineProperty(exports, '__esModule'{ value: true });
    };
  })();

5、__webpack_require__(moduleId)執行獲取對應的內容

  var __webpack_exports__ = {};
  (() ={
    'use strict';
    // 在步驟4中做對象攔截,添加__esMoules屬性
    __webpack_require__.r(__webpack_exports__);
    //根據路徑獲取對應module.exports的內容也就是__webpack_require__中的module.exports對象的數據
    var _assets_images_1_png__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./assets/images/1.png */ './src/assets/images/1.png');
    var _require = __webpack_require__(/*! ./utils/common.js */ './src/utils/common.js'),
      twoSum = _require.twoSum;
    console.log('cm_sum=' + twoSum(1, 2));
    var domApp = document.getElementById('app');
    var img = new Image();
    img.src = _assets_images_1_png__WEBPACK_IMPORTED_MODULE_0__;
    domApp.appendChild(img);
  })();
})();

webpack 打包 esModule 最終代碼

我們看下具體代碼

// esjs.js
(() ={
  // webpackBootstrap
  'use strict';
  var __webpack_modules__ = {
    './src/utils/esmodule.js'(__unused_webpack_module, __webpack_exports__, __webpack_require__) ={
      __webpack_require__.r(__webpack_exports__);
      function twoSumMul(a, b) {
        return a * b;
      }
      const __WEBPACK_DEFAULT_EXPORT__ = twoSumMul;
      __webpack_require__.d(__webpack_exports__, {
        default: () => __WEBPACK_DEFAULT_EXPORT__
      });
     
    }
  };

  // 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__ "moduleId");

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

  (() ={
    // 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__ = {};
  (() ={
    __webpack_require__.r(__webpack_exports__);
    var _utils_esmodule_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils/esmodule.js */ './src/utils/esmodule.js');

    console.log('es_sum=' + (0, _utils_esmodule_js__WEBPACK_IMPORTED_MODULE_0__['default'])(2, 2));
  })();
})();

看着代碼似乎與cjs大體差不多,事實上有些不一樣

當我們執行_utils_esmodule_js__WEBPACK_IMPORTED_MODULE_0__這個方法時,實際會在__webpack_modules__方法會根據moduleId執行 value 值的函數體,而函數體會被__webpack_require__.d這個方法進行攔截,會執行 Object.definePropertyget方法,返回綁定在__webpack_exports__對象的值上

主要看以下兩段代碼

  var __webpack_modules__ = {
    './src/utils/esmodule.js'(__unused_webpack_module, __webpack_exports__, __webpack_require__) ={
    // 這裏定義模塊時就已經先進行了攔截,這裏與cjs有很大的區別
      __webpack_require__.r(__webpack_exports__);
      function twoSumMul(a, b) {
        return a * b;
      }
      const __WEBPACK_DEFAULT_EXPORT__ = twoSumMul;
      __webpack_require__.d(__webpack_exports__, {
        default: () => __WEBPACK_DEFAULT_EXPORT__
      });
    }
  };
  ...
    (() ={
    // 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 轉換esModule代碼中, 同樣會是有優先從緩存對象中獲取,通過調用 __webpack_modules__[moduleId](module, module.exports, __webpack_require__ "moduleId"); 這個方法,改變module.exports根據moduleId獲取函數體內的值twoSumMul函數

最後畫了一張簡易的圖,文字理解還是有點多,紙上得來終學淺,絕知此事要躬行,還是得寫個簡單的demo自己深深體會下,具體參考文末的code example

總結

參考資料

[1]

code example: https://github.com/maicFir/lessonNote/tree/master/webpack/webpack-05-module

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/4kD3xQKtskItj2c2SVUIiA