本文使用webpack ^4.30.0作示例.为了更好地观察产出的文件,我们将模式设置为development关闭代码压缩,再开启source-map支持原始源代码调试。

新建文件src/utils/math.js:

  1. export const plus = (a, b) => {
  2. return a + b;
  3. };

新建文件src/index.js

  1. import { plus } from './utils/math.js';
  2. console.log('Hello webpack!');
  3. console.log('1 + 2: ', plus(1, 2));

我们按照ES规范的模块化语法写了一个简单的模块src/utils/math.js,给src/index.js引用。Webpack用自己的方式支持了ES6 Module规范。在Webpack中,默认只能处理一部分ES6的新语法,一些更高级的ES6语法或者ES7语法,Webpack是处理不了的;这时候就需要借助于第三方的loader来帮助Webpack处理这些高级的语法,当第三方loader把高级语法转为低级的语法之后,会把结果交给Webpack去打包到main.js中。

新建webpack配置文件webpack.config.js

  1. const path = require('path');
  2. module.exports = {
  3. mode: 'development',
  4. devtool: 'source-map',
  5. entry: './src/index.js',
  6. output: {
  7. path: path.resolve(__dirname, 'dist')
  8. }
  9. };

现在我们运行命令webpack —-config webpack.config.js,打包完成后会多出一个输出目录dist:dist/main.jsmainwebpack默认设置的输出文件名,我们快速瞄一眼这个文件:

  1. (function(modules){
  2. // ...
  3. })({
  4. "./src/index.js": (function(){
  5. // ...
  6. }),
  7. "./src/utils/math.js": (function() {
  8. // ...
  9. })
  10. });

整个文件只含一个立即执行函数(**IIFE**),我们称它为**webpackBootstrap**,它仅接收一个对象 —— 未加载的模块集合(**modules**),这个**modules**对象的**key**是一个路径,**value**是一个函数

上面例子中,IIFE传入的modules对象里包含了两个键值对,一个对应这模块src/index.js,一个对应着模块src/utils/math.js,这和我们在源代码中拆分的模块互相呼应。然而,有了modules只是第一步,这份文件最终达到的效果应该是让各个模块按开发者编排的顺序运行。

探究 webpackBootstrap

接下来看看webpackBootstrap函数中有些什么:

  1. // webpackBootstrap
  2. (function(modules){
  3. // 缓存 __webpack_require__ 函数加载过的模块
  4. var installedModules = {};
  5. /**
  6. * Webpack 加载函数,用来加载 webpack 定义的模块
  7. * @param {String} moduleId 模块 ID,一般为模块的源码路径,如 "./src/index.js"
  8. * @returns {Object} exports 导出对象
  9. */
  10. function __webpack_require__(moduleId) {
  11. // ...
  12. }
  13. // 在 __webpack_require__ 函数对象上挂载一些变量及函数 ...
  14. // 传入表达式的值为 "./src/index.js"
  15. return __webpack_require__(__webpack_require__.s = "./src/index.js");
  16. })(/* modules */);

可以看到,其实主要做了三件事:

  1. 定义一个闭包变量installedModules用来存储加载过的模块;
  2. 定义一个模块加载函数__webpack_require__
  3. 使用加载函数加载入口模块./src/index.js

整个webpackBootstrap中只出现了入口模块的影子,那其他模块又是如何加载的呢?我们顺着__webpack_require__("./src/index.js")细看加载函数的内部逻辑:

  1. function __webpack_require__(moduleId) {
  2. // 重复加载则利用缓存
  3. if (installedModules[moduleId]) {
  4. return installedModules[moduleId].exports;
  5. }
  6. // 如果是第一次加载,则初始化模块对象,并缓存
  7. var module = installedModules[moduleId] = {
  8. i: moduleId, // 模块 ID
  9. l: false, // 模块加载标识
  10. exports: {} // 模块导出对象
  11. };
  12. /**
  13. * 执行模块
  14. * @param module.exports -- 模块导出对象引用,改变模块包裹函数内部的 this 指向
  15. * @param module -- 当前模块对象引用
  16. * @param module.exports -- 模块导出对象引用
  17. * @param __webpack_require__ -- 用于在模块中加载其他模块
  18. */
  19. modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
  20. // 模块加载标识置为已加载
  21. module.l = true;
  22. // 返回当前模块的导出对象引用
  23. return module.exports;
  24. }

首先,加载函数使用了闭包变量installedModules,用来将已加载过的模块保存在内存中。接着是初始化模块对象,并把它挂载到缓存里。然后是模块的执行过程,加载入口文件时modules[moduleId]其实就是./src/index.js对应的模块函数。执行模块函数前传入了跟模块相关的几个实参,让模块可以导出内容,以及加载其他模块的导出。最后标识该模块加载完成,返回模块的导出内容。

根据__webpack_require__的缓存和导出逻辑,我们得知在整个IIFE运行过程中,加载已缓存的模块时,都会直接返回installedModules[moduleId].exports,换句话说,相同的模块只有在第一次引用的时候才会执行模块本身

模块执行函数

__webpack_require__中通过modules[moduleId].call() 运行了模块执行函数,下面我们就进入到webpackBootstrap的参数部分,看看模块的执行函数。

  1. /*** 入口模块 ./src/index.js ***/
  2. "./src/index.js": (function (module, __webpack_exports__, __webpack_require__) {
  3. "use strict";
  4. // 用于区分 ES 模块和其他模块规范,不影响理解 demo,战略跳过。
  5. __webpack_require__.r(__webpack_exports__);
  6. /* harmony import */
  7. // 源模块代码中,`import {plus} from './utils/math.js';` 语句被 webpack 解析转化。
  8. // 加载 "./src/utils/math.js" 模块,
  9. var _utils_math_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/utils/math.js");
  10. console.log('Hello webpack!');
  11. console.log('1 + 2: ', Object(_utils_math_js__WEBPACK_IMPORTED_MODULE_0__["plus"])(1, 2));
  12. }),
  13. "./src/utils/math.js": (function (module, __webpack_exports__, __webpack_require__) {
  14. "use strict";
  15. __webpack_require__.r(__webpack_exports__);
  16. /* harmony export (binding) */
  17. // 源模块代码中,`export` 语句被 webpack 解析转化。
  18. __webpack_require__.d(__webpack_exports__, "plus", function () {
  19. return plus;
  20. });
  21. const plus = (a, b) => {
  22. return a + b;
  23. };
  24. })

执行顺序是:入口模块 -> 工具模块 -> 入口模块。

入口模块中首先就通过__webpack_require__("./src/utils/math.js")拿到了工具模块的exports对象。再看工具模块,ES导出语法转化成了__webpack_require__.d(__webpack_exports__, [key], [getter]),而__webpack_require__.d函数的定义在webpackBootstrap内:

  1. // 定义 exports 对象导出的属性。
  2. __webpack_require__.d = function (exports, name, getter) {
  3. // 如果 exports (不含原型链上)没有 [name] 属性,定义该属性的 getter。
  4. if (!__webpack_require__.o(exports, name)) {
  5. Object.defineProperty(exports, name, {
  6. enumerable: true,
  7. get: getter
  8. });
  9. }
  10. };
  11. // 包装 Object.prototype.hasOwnProperty 函数。
  12. __webpack_require__.o = function (object, property) {
  13. return Object.prototype.hasOwnProperty.call(object, property);
  14. };

可见__webpack_require__.d其实就是利用Object.defineProperty方法,在module.exports对象上面增加一个需要导出的属性,然后用getter修饰器进行拦截。当访问导出的属性时,就会执行getter对应的方法,返回计算结果。

引用工具模块导出的变量后,入口模块再执行它剩余的部分。至此,Webpack基本的模块执行过程就结束了。

总结:

  1. 所有引入的模块文件都会被打包成一个模块执行函数;
  2. import或者require语句,都会被转换为__webpack_require__函数来执行模块函数;
  3. export语句,则会被转换__webpack_require__.d函数来执行,实质就是给module.exports上新加一个属性,并用getter修饰器拦截。

image.png