likes
comments
collection
share

Webpack入门教程(三):打包文件分析

作者站长头像
站长
· 阅读数 21

前言

webpack 官网上有这么一句话:webpackimportexport 语句提供了开箱即用般的支持,事实上 webpack 还能够很好地支持多种其他模块语法。不知道大家有没有对 webpack 在背后是如何 转译 的好奇?它是如何提供开箱即用的支持的?

本章我们就从 webpcak 打包文件后的文件开始分析,来深入的理解一下 webpack 的模块化原理。

1. 纯 CJS 模块

1.1 测试代码

// index.js
const { sayHello } = require("./utils");

sayHello();

/// utils.js
const sayHello = () => {
  console.log("hello world");
};

module.exports = {
  sayHello,
};

CommonJS 模块的代码打包后的文件比较简单,我们直接用下面一张图来演示:

Webpack入门教程(三):打包文件分析

1.2 代码分析

我们将上图的代码分为五块。因为上面四块都是对象函数的定义,所以直接来到第五块,

  1. 这是一个自执行的函数,里面会调用 __webpack_require__ 函数,而这个 __webpack_require__ 函数实际上就是 require 函数的实现了。
  2. 进入 __webpack_require__,首先会去判断当前 require 的模块是否已经缓存过,缓存就直接获取并返回, __webpack_module_chche__ 这个变量就是用于缓存已经加载过的模块。
  3. 没缓存过时就会去 __webpack_modules__ 中获取,而 __webpack_modules__ 以key为模块的路径名称,value为模块这样键值对的形式存储了所有的模块。
  4. 最终获取到了 ./src/utils.js 模块中的 sayHello 方法并成功调用

1.3 总结

  1. webpack 会将运行的入口模块 index.js 中的 require 关键词转化成 __webpack_require__ 放到最后自执行函数中执行
  2. __webpack_require__ 函数中有两个很重要的全局变量就是 __webpack_module_cache____webpack_modules__
    1. __webpack_module_cache__:用于缓存调用过得模块。
    2. __webpack_modules__ 用于存储所有引入的模块,存储的 key 为模块文件的路径,value 为拥有一个参数为 module 的函数,这个函数最终的导出都会赋值到 module.exports 对象上,在 __webpack_require__ 函数中最终返回的也就是这个对象。

2. 纯 ESM 模块

2.1 测试代码

// index.js
import { sayHello } from "./utils";

sayHello();

// utils.js
const sayHello = () => {
  console.log("hello world");
};

export {
  sayHello
}

打包后的结果

图示如下:

Webpack入门教程(三):打包文件分析

代码如下:

(() => {
  "use strict";
  var __webpack_modules__ = {
    "./src/utils.js": (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
      __webpack_require__.r(__webpack_exports__);
      __webpack_require__.d(__webpack_exports__, {
        sayHello: () => sayHello,
      });
      const sayHello = () => {
        console.log("hello world");
      };
    },
  };
  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__);

    return module.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_require__.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
  })();

  (() => {
    __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__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils */ "./src/utils.js");

    (0, _utils__WEBPACK_IMPORTED_MODULE_0__.sayHello)();
  })();
})();
//# sourceMappingURL=main.bundle.js.map

2.1 代码分析

  1. 首先会执行 __webpack_require__.r(__webpack_exports__),这个函数是在第 44 行定义的
  (() => {
    __webpack_require__.r = (exports) => {
      if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
      }
      Object.defineProperty(exports, "__esModule", { value: true });
    };
  })();

他的作用是在 __webpack_exports__ 对象上定义了两个属性 Symbol.toStringTag__esModule,最终的 __webpack_exports__ 对象如下图所示:

Webpack入门教程(三):打包文件分析

  1. 之后就会调用 __webpack_require 函数去加载 utils 模块,__webpack_require 函数与 Commonjs 里的是一模一样的,只不过 __webpack_modules__ 这个对象有些不同:

Webpack入门教程(三):打包文件分析

__webpack_modules__ 对象中,或先后调用 r 函数和 d 函数, r 函数的作用上面已经讲过了不再复述,d 函数的作用其实就是将第二个参数对象的属性拷贝到第一个参数上。

  1. 最终返回获取到的 utils模块执行:
(0, _utils__WEBPACK_IMPORTED_MODULE_0__.sayHello)();

这里的代码其实就是调用的 sayHello 函数

2.3 总结

  1. 整体的流程和纯 CJS 模块类似,都是通过 __webpack_require__ 请求模块,通过 __webpack_module_cache__ 作缓存, 通过 __webpack_modules__ 存储所有导入模块,不同的是新增了一些辅助函数,比如
    1. __webpack_require__.r:标记为纯ES模块
    2. __webpack_require__.d:将模块中导出的变量、函数定义到 module.exports
    3. __webpack_require__.o:封装了 hasOwnProperty 方法,判断一个属性是否是对象的自有属性(不是继承来的)

3. require 导入 ESM 模块

3.1 测试代码


// index.js
const { sayHello } = require("./utils");

sayHello();


// utils.js
const sayHello = () => {
  console.log("hello world");
};

export { sayHello };

将以上测试代码打包后,对比纯 ESM 模块的打包文件,发现只有下图所示的区别。

Webpack入门教程(三):打包文件分析

代码如下:

(() => {
  var __webpack_modules__ = {
    "./src/utils.js": (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
      "use strict";
      __webpack_require__.r(__webpack_exports__);
      __webpack_require__.d(__webpack_exports__, {
        sayHello: () => sayHello,
      });
      const sayHello = () => {
        console.log("hello world");
      };
    },
  };
  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__);

    return module.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_require__.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
  })();

  (() => {
    __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__ = {};
  (() => {
    const { sayHello } = __webpack_require__(/*! ./utils */ "./src/utils.js");

    sayHello();
  })();
})();
//# sourceMappingURL=main.bundle.js.map

3.2 代码分析

因为这里的代码整体和 纯 ESM 模块 打包后的代码相同,就不做分析了。

3.3 总结

require 导入 ESM 模块 的方式打包后的代码整体与 纯 ESM 模块 打包后的打包相同,仅一处不同,纯 ESM 模块 会将当前模块(index.js)的导出 __webpack_exports__ 标记为 es模块

4. import 导入 CJS 模块

4.1 测试代码

// index.js
const sayHello = () => {
  console.log("hello world");
};

module.exports = {
  sayHello
}

// utils.js
const sayHello = () => {
  console.log("hello world");
};

module.exports = {
  sayHello
}

打包后的文件

(() => {
  var __webpack_modules__ = {
    "./src/utils.js": (module) => {
      const sayHello = () => {
        console.log("hello world");
      };

      module.exports = { sayHello };
    },
  };
  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__);

    return module.exports;
  }

  (() => {
    __webpack_require__.n = (module) => {
      var getter = module && module.__esModule ? () => module["default"] : () => module;
      __webpack_require__.d(getter, { a: getter });
      return getter;
    };
  })();

  (() => {
    __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_require__.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
  })();

  (() => {
    __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__ = {};
  (() => {
    "use strict";
    __webpack_require__.r(__webpack_exports__);
    var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/utils.js");
    var _utils__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(_utils__WEBPACK_IMPORTED_MODULE_0__);

    (0, _utils__WEBPACK_IMPORTED_MODULE_0__.sayHello)();
  })();
})();
//# sourceMappingURL=main.bundle.js.map

4.2 代码分析

  1. 执行代码 61 行将 __webpack_exports__ 变量标记为 es模块
  2. 执行代码 62__webpack_require__ 方法获取对应模块
  3. 执行代码 63 行,这里有一个新的方法 __webpack_require__.n

Webpack入门教程(三):打包文件分析

这个函数接受一个 module 作为参数,他会首先判断module是否是es模块,如果是,就会导出module['default'],如果不是则导出 module__webpack_require__.d 函数负责使用 getter函数的值定义到 getter 对象上的属性 a 上。最后返回 getter 函数。

  1. 最终执行 sayHello 函数

4.3 总结

import 导入 CJS 模块 相比 纯 ESM 模块,多了一个 __webpack_require__.d 函数,这个函数是用于获取es模块的默认导出的,因为在我们这个例子中utils.js 模块没有默认导出的对象。所以最终的 _utils__WEBPACK_IMPORTED_MODULE_0___default 就没有被使用到了。

5. 总结

我们一共分析了

  1. 纯 CJS 模块的打包文件
  2. 纯 ESM 模块的打包文件
  3. require 导入 ESM 模块的打包文件
  4. import 导入 CJS 模块的打包文件

四种情况,无论是哪一种方法,webpack 转译 的整体逻辑都是一样的。即:

  1. 通过 __webpack_require__ 方法函数请求模块
  2. 通过 __webpack_module_cache__全局对象缓存模块
  3. 通过 __webpack_modules__全局对象存放模块
  4. 另外会通过一些辅助函数边缘情况的处理,比如 r 标识 es模块,d 函数赋值属性,o 函数。因为我们的场景比较简单,实际这些辅助函数还会更多更复杂,比如存在加载图片、css、异步加载的情况。