读一读 Webpack 中的模块化产物

1,112 阅读4分钟

前言

随着 ECMA Script 的发展, 浏览器以及Node.js 对于 ES Module(以下简称 ESM) 的原生支持,不管是在 Node.js 服务端编写接口 APi,还是在浏览器端编写脚本,我们都可以愉快地使用 ES Module, 人们再也不用在模块语法上做来回切换了。 ES Module 语法已经逐渐替换 CommonJS(以下简称 CJS) 成为大家在开发中默认的开发规范,但有时候会疑惑, Webpack 内部是如何兼容不同的模块化文件的,生成的模块化产物又是怎样的,让我们来读一读编译产物

案例分析

此次案例的依赖环境

webpack 版本: 5.50.0
Node.js 版本: 14.5.0

场景模拟

这里设置一个简单的 demo 场景,我们有一个主入口 main.js ,分别加载了一个 CJS 语法文件、ESM 语法文件、还有一个动态加载的 ESM 语法文件。

源文件夹看起来就长这样

main.js
modules/cjs.js
modules/esm.js
async.js

main.js 代码

// main.js
import cjs from './modules/cjs'
import esm, { count, increment } from './modules/esm'

setTimeout(() =>{
  import('./async')
},0)

因为一个复杂一点的项目中编译出来的文件都会自动分包,这里模拟一个分包场景,在 webpack.config.js 中做一个 splitChunks 的优化,把 modules 下的文件设置成单独分包。

// webpack.config.js
optimization: {
    splitChunks: {
      minSize: 0,
      chunks: 'all',
      cacheGroups: {
        modules: {
          test: /[\/]modules[\/]/,
          priority: 1
        },
      }
    }
  }

async.js 因为是动态加载的,所以默认会单独打一个包。

这样通过 Webpack 编译出来产物大概长这样:

index.html
main.bundle.js
modules-src_webpack_modules_cjs_js-src_webpack_modules_esm_js.bundle.js
src_webpack_async_js.js

整体运行流程图

产物分析

先大概解释一下打包产物中的名词解释

module: 项目中使用的每个文件都是一个模块
chunk: 打包规则下多个模块合并成出来的产物

它们的关系是:chunk 包含 module

大致解释一下产物中对象跟方法的用处

webpack__require

webpack 内部 require 模块的实现,通过 webpack_module_cache 实现缓存功能

webpack_modules

所有模块加载完成后的映射,key 为文件路径,value 为编译后的源代码。
类似

{
    "./src/mian.js": (module, exports, requrie) => {
    ...
  }
}

其中 webpack__require.mwebpack_modules

webpack__require.O

处理运行时加载的逻辑,第一次执行把传入 undefined 、依赖的 chunk、 当前入口模块的 require 函数, 初始化 deffered 对象之后,执行第二次加载. 执行前面初始化的 deferred 对象中的 fn 方法,初始化入口模块的runtime。

var __webpack_exports__ = __webpack_require__.O(undefined, 
    ["modules-src_webpack_modules_cjs_js-src_webpack_modules_esm_js"], 
    () => (__webpack_require__("./src/webpack/main.js")))
  __webpack_exports__ = __webpack_require__.O(__webpack_exports__);
webpack__require.O.j

判断传入的 chunk 是否已经加载完成

webpack__require.e

执行异步加载的入口逻辑

webpack__require.u

根据 chunkId 拼接 .js 文件后缀

webpack_require.g

取不同环境的下 global 对象,返回 globalThis 对象,默认为 window

webpack__require.o

通过 Object.prototype.hasOwnProperty 查找是否自身属性

webpack__require.l

根据 url、回调、chunk,生成 script 标签请求的逻辑

webpack__require.r

定义为 ES Module 的标志,添加 __esModule 属性以及 Symoble.toStringTag == 'Module' 的逻辑。 Symoble.toStringTag 用于 Object.prototype.toString() 返回的类型

installedChunks

当前 chunk 中包含的 module,默认值是入口 module

webpack__require.f.j

负责异步模块加载中的逻辑,根据 Promise 生成一个 [Promise.resolve, Promise.reject, promise] 的数组形式的加载对象,加载完成后会执行 resolve
在 webpack__require.e 中调用

webpackJsonpCallback

用于更新 installedChunkds 以及 __webpack__modules ,chunk 初始化或者异步代码加载完成后执行

chunkLoadingGlobal

全局对象,通过 SplitChunk 优化或者异步加载分包出来的的 chunk ,加载完成时会 push 到这个数组中

webpack_require.d

给 exports 上的属性定义 getter,用于模拟 es6 中 Living binding 变量

模块

webpack 会把所有模块默认作为 CJS 语法处理,当遇到 import 、export 语法才会调用 webpack__require.r 把它当作 ESM 语法生成。

缓存

通过 webpack_require 时,会先判断缓存对象 webpack_module_cache,没有便会在缓存对象进行初始化,

var module = __webpack_module_cache__[moduleId] = {
  // no module.id needed
  // no module.loaded needed
  exports: {}
};

Living Binding

CJS 与 ESM 除了语法上的区别大致有以下:

  • ESM 是静态分析,导入的模块不能用变量来进行计算,也就是固定的。
  • ESM 中多了一个 default 导出(默认导出)方式,所以我们在使用 require 语法导入 ESM module 时,如果该模块是有默许导出属性,需要额外调用一个 default 属性。eg: require('/xxx').default
  • ESM 导出的对象是引用绑定,也就是说当你修改了导入的对象或者方法后,原有的 ESM 模块中的对象或者方法也会相应修改,而 CJS 是值导出。修改导出后的属性不会影响原模块。

Webpack 通过 webpack_require.d 方法来定义 getter 方法,从而使你的修改会应用到原模块上。

/**
 * 把 definition 上的值赋给 exports 
 * @param {*} exports 
 * @param {*} definition 
*/
__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] });
    }
  }
};

依赖的 chunk 文件

__webpack_require__.d(__webpack_exports__, {
  "count": () => (/* binding */ count),
});
var count = 1;

结语

从打包产物看 Webpack 生成的产物逻辑还是比较隐晦的,有些内容比较绕,暂时就先略过了。比如 priority 属性对比的逻辑,循环引用的处理逻辑。待下次可再进行场景的模拟,以及相应的分析。