Webpack源码分析-打包后的文件分析

Webpack源码解析

// 使用webpack版本

"html-webpack-plugin": "^4.5.0",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12"

打包后文件分析

webpack-code
├─ dist
│  ├─ bundle.js
│  └─ index.html
├─ src
│  ├─ index.html
│  └─ index.js
├─ package.json
├─ webpack.config.js
├─ README.md
└─ yarn.lock
// index.js

console.log('index.js 内容');

module.exports = "入口文件导出内容";
// webpack.config.js

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  entry: "./src/index.js",
  output: {
    filename: "bundle.js",
    path: path.join(__dirname, "dist")
  },
  mode: "development",
  devtool: "none",
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html"
    })  
  ]
}
// dist/bundle.js 删除注释

(function (modules) {
  // modules传入的模块定义:{ moduleId: moduleFunc(module, exports) {} }
  // 模块加载缓存
  var installedModules = {};

  // webpack 自定义的 require 函数,用来加载模块,最终导出加载的模块内容
  function __webpack_require__(moduleId) {
    // 检查缓存中是否存在需要加载的模块
    // 存在就返回exports,exports 既是模块内容
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // 新建一个模块,并存入缓存
    var module = (installedModules[moduleId] = {
      i: moduleId, // 模块ID
      l: false, // 是否已加载过
      exports: {}, // exports 对象,初始化为一个空对象
    });

    // 加载模块对应的函数 moduleFunc(module, exports) {}
    modules[moduleId].call(
      module.exports, // 初始化this为一个空对象,这就是模块具有自身作用域的实现
      module, // 需要加载的模块
      module.exports, // 模块的exports
      __webpack_require__ // webpack的require方法,用来替换CommonJS的require
    );

    // 标注该模块已经加载
    module.l = true;

    // 返回模块的导出
    return module.exports;
  }

  // 暴露模块对象 (__webpack_modules__)
  __webpack_require__.m = modules;

  // 暴露加载缓存
  __webpack_require__.c = installedModules;

  // 为 exports 定义 getter 函数,并添加一个属性
  // 当模块中使用ESModule导出成员时,可以通过这个方法将成员绑定到exports上
  // getter 就是返回这个成员的方法 () => name
  __webpack_require__.d = function (exports, name, getter) {
    if (!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, { enumerable: true, get: getter });
    }
  };

  // 给exports定义__exModule,用来标识是一个ESModule
  // 主要解决ESModule和CommonJS混合使用的问题
  __webpack_require__.r = function (exports) {
    if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
      // Symbol 如果存在即表示是ESModule,添加 Module 标识
      Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
    }

    // 添加__esModule标识
    // 来确认这是一个标准的ESModule
    Object.defineProperty(exports, "__esModule", { value: true });
  };

  // 创建一个虚假的命名空间对象
  // mode & 1: value is a module id, require it
  // mode & 2: merge all properties of value into the ns
  // mode & 4: return value when already ns object
  // mode & 8|1: behave like require
  __webpack_require__.t = function (value, mode) {
    if (mode & 1) value = __webpack_require__(value);
    if (mode & 8) return value;
    if (mode & 4 && typeof value === "object" && value && value.__esModule)
      return value;
    var ns = Object.create(null);
    __webpack_require__.r(ns);
    Object.defineProperty(ns, "default", { enumerable: true, value: value });
    if (mode & 2 && typeof value != "string")
      for (var key in value)
        __webpack_require__.d(
          ns,
          key,
          function (key) {
            return value[key];
          }.bind(null, key)
        );
    return ns;
  };

  // 用于与非和谐模块兼容的 getDefaultExport 函数 CommonJS/AMD/CMD等
  __webpack_require__.n = function (module) {
    var getter =
      module && module.__esModule
        ? function getDefault() {
            // 如果是ESModule的话就返回默认导出
            return module["default"];
          }
          // 不是的话直接返回这个module
        : function getModuleExports() {
            return module;
          };
    __webpack_require__.d(getter, "a", getter);
    return getter;
  };

  // Object.prototype.hasOwnProperty.call
  __webpack_require__.o = function (object, property) {
    return Object.prototype.hasOwnProperty.call(object, property);
  };

  // __webpack_public_path__ webpack打包的公共路径
  __webpack_require__.p = "";

  // 加载模块并返回模块内容
  return __webpack_require__((__webpack_require__.s = "./src/index.js"));
})({
  "./src/index.js": function (module, exports) {
    console.log("index.js 内容");
    module.exports = "入口文件导出内容";
  },
});

从打包结果可以看出,webpack将模块打包成一个立即执行(IIFE)函数,入参是一个键值对对象,可以认为是模块定义,键是模块的相对路径,值是一个函数,将来用于加载模块内容,函数体就是模块里面的代码内容。

CommonJS模块打包

以上是单文件打包,__webpack_require__ 上面挂载的属性方法的作用没有体现出来,现在我们在index中引入一个模块:

// log.js

module.exports = (log) => console.log(log)
// index.js

let log = require('./log');

console.log('index.js 内容');

module.exports = "入口文件导出内容";

log('这是index引用的log模块。')

依赖的模块中是以CommonJS的方式导出一个方法,看一下打包结果:

// dist/bundle.js

// 上面函数体没有任何变化

...

// CommonJS模块的导入使用 __webpack_require__

({
  "./src/index.js": function (module, exports, __webpack_require__) {
    let log = __webpack_require__(/*! ./log */ "./src/log.js");
    console.log("index.js 内容");
    module.exports = "入口文件导出内容";
    log("这是index引用的log模块。");
  },
  "./src/log.js": function (module, exports) {
    // 导出也改成了module自己的exports
    module.exports = (log) => console.log(log);
  },
})

由上可以看出,webpack使用CommonJS构建代码,对于CommonJS引用CommonJS,编译的代码并没有什么特别大的改变,那么我们把引用的代码改成ESModule看看:

// log.js

export const name = 'jack'

const age = 14

export default {
  say: () => {
    console.log(`${name} is ${age} years old.`)
  }
}
// index.js

const log = require('./log')

console.log(log.name)

log.default.say()
// dist/bundle.js

// 上面函数体没有任何变化

...

({
  "./src/index.js": function (module, exports, __webpack_require__) {
    // 对于CommonJS的require引用依然只是替换为 __webpack_require__
    const log = __webpack_require__(/*! ./log */ "./src/log.js");

    console.log(log.name);

    log.default.say();
  },

  "./src/log.js":
    /*! exports provided: name, default */
    function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      // 这是一个ESModule,往module的exports对象上添加一个__esModule属性进行标注
      __webpack_require__.r(__webpack_exports__);
      // 这里就是通过 r 方法将ESModule导出的成员绑定至module的exports上
      // 将返回这个成员作为该属性的getter方法
      /* harmony export (binding) */ __webpack_require__.d(
        __webpack_exports__,
        "name",
        function () {
          return name;
        }
      );
      const name = "jack";

      const age = 14;

      // 为module的exports添加default属性
      /* harmony default export */ __webpack_exports__["default"] = {
        say: () => {
          console.log(`${name} is ${age} years old.`);
        },
      };
    },
});

可以看出如果我们的引用模块中使用的全部是ESModule,__webpacl_require__ 上面挂载的 r 方法和 d 方法就派上用场了。

ESModule模块打包

下面我们在主入口文件中使用ESModule的方式引入其它模块,引入的模块分别使用CommonJS和ESModule来导出成员,来看看打包的结果:

1. 依赖模块使用CommonJS导出

// index.js

import log from './log'

console.log(log.name)

log.say()
// log.js

const name = 'jack'
const age = 15

module.exports = {
  name,
  say() {
    console.log(`${name} is ${age} years old.`)
  }
}

log.js中使用CommonJS导出name属性和say方法,index中通过ESModule的方式导入,我们看一下打包结果:

// dist/bundle.js

// 上面函数体无变化

...

({
  "./src/index.js":
    /*! no exports provided */
    function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      // 首先标注这个模块是一个标准的ESModule
      // Module {__esModule: true, Symbol(Symbol.toStringTag): 'Module'}
      //   Symbol(Symbol.toStringTag):'Module'
      //   __esModule:true
      //  >__proto__:Object
      __webpack_require__.r(__webpack_exports__);
      // import 改为 __webpack_require__
      // 声明一个变量用来承接加载的log内容,命名前缀使用模块名,数字表示加载的第几个模块
      // {name: 'jack', say: ƒ}
      //   name:'jack'
      // > say:ƒ say() {\n          console.log(`${name} is ${age} years old.`);\n        }
      // > __proto__:Object
      /* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_0__ =
        __webpack_require__(/*! ./log */ "./src/log.js");
        // 调用挂载的 n 方法判断是否是ESModule,是的话就加载默认导出,不是的话就加载整个module
        // 加载的内容通过 o 函数挂载到一个 a 属性上
        // ƒ getModuleExports() {\n            return module;\n          }
        // > a (get):ƒ getModuleExports() {\n            return module;\n          }
        //   > :Object
        //       name:'jack'
        //       say:ƒ say() {\n          console.log(`${name} is ${age} years old.`);\n        }
        //       __proto__:Object
        //   arguments:null
        //   caller:null
        //   length:0
        //   name:'getModuleExports'
        // > prototype:{constructor: ƒ}
        //   [[FunctionLocation]]:@ /Users/xueyong/lagou-edu/webpack-code/dist/bundle.js:87
        // > [[Scopes]]:Scopes[3]
        // > __proto__:function () { [native code] }
      /* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_0___default =
        /*#__PURE__*/ __webpack_require__.n(_log__WEBPACK_IMPORTED_MODULE_0__);

      // 调用a就会调用它的get方法,get方法就是 n 函数返回的那个getter,有一个 getModuleExports 函数
      // 调用这个函数就是返回模块中的内容 module
      console.log(_log__WEBPACK_IMPORTED_MODULE_0___default.a.name);

      _log__WEBPACK_IMPORTED_MODULE_0___default.a.say();
    },

  "./src/log.js":
    /*! no static exports found */
    function (module, exports) {
      const name = "jack";
      const age = 15;

      module.exports = {
        name,
        say() {
          console.log(`${name} is ${age} years old.`);
        },
      };
    },
});

由上可见,webpack对CommonJS的导出不做修改,对于ESModule的导入修改较大,首先会使用 r 函数标记这个模块是ESModule,其次导入会修改成 __webpack_require__,导入其它模块时会使用 n 函数标示默认导出并添加 a 属性绑定导出内容。

2. 依赖模块使用ESModule导出

// log.js

export const name = 'jack'

const age = 14

export default {
  say: () => {
    console.log(`${name} is ${age} years old.`)
  }
}
// index.js

import log, { name } from './log'

console.log(name)

log.say()
// dist/bundle

// 上面函数体无变化

...

({
  "./src/index.js":
    /*! no exports provided */
    function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      __webpack_require__.r(__webpack_exports__);
      /* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_0__ =
        __webpack_require__(/*! ./log */ "./src/log.js");

      console.log(_log__WEBPACK_IMPORTED_MODULE_0__["name"]);

      _log__WEBPACK_IMPORTED_MODULE_0__["default"].say();
    },

  "./src/log.js":
    /*! exports provided: name, default */
    function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      __webpack_require__.r(__webpack_exports__);
      /* harmony export (binding) */ __webpack_require__.d(
        __webpack_exports__,
        "name",
        function () {
          return name;
        }
      );
      const name = "jack";

      const age = 14;

      /* harmony default export */ __webpack_exports__["default"] = {
        say: () => {
          console.log(`${name} is ${age} years old.`);
        },
      };
    },
});

当引用模块是ESModule时,导入会变得简单一点,因为引用模块的导出会被webpack修改,所有属性都会绑定到exports上,引用时直接取值即可。

手写功能函数

我们可以自己实现一下函数体中的功能函数,它在每次打包时几乎都是不变得:

// myBubdle.js

/**
 * webpack打包函数
 * @param { 模块定义 } modules
 * 
 * installedModules -> 模块加载缓存
 * __webpack_require__ -> webpack加载模块的方法
 * m -> 暴露modules 
 * c -> 暴露installedModules
 * o -> 判断对象上是否存在自身属性
 * d -> 给对象加上某个属性
 * r -> 标注ESModule模块
 * n -> 获取module导出
 * p -> 公共路径
 */

function (modules) {
  // 01 定义一个缓存对象,用来存放加载过得模块
  var installedModules = {};

  // 02 webpack自己的加载模块方法
  function __webpack_require__ (moduleId) {
    // 判断缓存中是否存在该模块,存在的话就返回其导出
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }

    // 声明一个模块
    var module = {
      i: moduleId,
      l: false,
      exports: {}
    }

    // 调用函数,加载模块内容
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    // 标注加载完毕
    module.l = true;

    // 返回模块内容
    return module.exports;
  }

  // 03 暴露modules
  __webpack_require__.m = modules;

  // 04 暴露加载缓存
  __webpack_require__.c = installedModules;

  // 05 提供函数判断一个对象上是否存在某个属性
  __webpack_require__.o = function(object, property) {
    return Object.prototype.hasOwnProperty.call(object, property);
  }

  // 06 给对象添加某个属性,用于解析ESModule时绑定成员属性至exports上
  // 并给这个属性添加get方法,用来获取导出对象exports
  __webpack_require__.d = function(exports, name, getter) {
    if (!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, { enumerable: true, get: getter });
    }
  }

  // 07 标注一个模块是ESModule,以便加载时将其转换为CommonJS
  // 首先判断当前模块是否是ESModule,是的话就使用Symbol来绑定一个唯一属性,标注为 Module
  // 不问当前是否是ESModule,最终打包ESModule时都会加上 { __esModule: true }
  __webpack_require__.r = function(exports) {
    if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
    }
    Object.defineProperty(exports, "__esModule", { value: true });
  }

  // 08 获取模块的导出
  // 根据module上是否被标记__esModule来判断导出对象
  __webpack_require__.n = function(module) {
    var getter = module && module.__esModule ?
      function getDefault() {
        return module["default"];
      }
      :
      function getModuleExports() {
        return module;
      }
    
    // 给 exports 添加a属性用来绑定返回的模块内容,a属性提供get方法用来访问模块中的成员属性
    __webpack_require__.d(getter, "a", getter);

    return getter;
  }

  // 09 公共路径
  __webpack_require__.p = "";

  // 10 返回加载的模块内容
  return __webpack_require__((__webpack_require__.s = "./src/index.js"));
}

懒加载实现流程

1. 懒加载打包文件分析

我在通过页面上的一个按钮来加载引用的模块:

// index.html

<button id="btn">点击加载</button>
// index.js

const btn = document.getElementById('btn')

btn.addEventListener('click', () => {
  import(/* webpackChunkName: "log" */'./log').then(function(module) {
    console.log(module)
    module.default('log 加载完毕。')
  })
})
// log.js

module.exports = (log) => console.log(log)

点击按钮后可以看到加载的模块内容:

▼ Module {__esModule: true, Symbol(Symbol.toStringTag): "Module", default: ƒ}
   ▼ default: (log) => console.log(log)
       arguments: (...)
       caller: (...)
       length: 1
       name: ""[[FunctionLocation]]: log.bundle.js:10
     ▶ [[Prototype]]: ƒ ()[[Scopes]]: Scopes[1]
   Symbol(Symbol.toStringTag): "Module"
   __esModule: true

我们来看一下打包后的文件:

// dist/bundle.js

// 已经删除前面相同的功能函数代码,只保留了新生成的函数

(function (modules) {
  // webpackBootstrap
  // 块加载的JSONP回调函数
  function webpackJsonpCallback(data) {
    var chunkIds = data[0];
    var moreModules = data[1];

    // add "moreModules" to the modules object,
    // then flag all "chunkIds" as loaded and fire callback
    var moduleId,
      chunkId,
      i = 0,
      resolves = [];
    for (; i < chunkIds.length; i++) {
      chunkId = chunkIds[i];
      if (
        Object.prototype.hasOwnProperty.call(installedChunks, chunkId) &&
        installedChunks[chunkId]
      ) {
        resolves.push(installedChunks[chunkId][0]);
      }
      installedChunks[chunkId] = 0;
    }
    for (moduleId in moreModules) {
      if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
        modules[moduleId] = moreModules[moduleId];
      }
    }
    if (parentJsonpFunction) parentJsonpFunction(data);

    while (resolves.length) {
      resolves.shift()();
    }
  }

  // 一个对象,用来存储加载过得或者正加载中的块
  // undefined = 块未加载, null = 块预加载或预请求
  // Promise = 块加载中, 0 = 块加载完毕
  var installedChunks = {
    main: 0,
  };

  // 拼接 script src
  function jsonpScriptSrc(chunkId) {
    return __webpack_require__.p + "" + chunkId + ".bundle.js";
  }


  // bundle.js只包含加载入口文件模块
  // 下面是额外依赖模块的加载函数
  // 使用promise的形式异步地创建script标签,将异步加载的模块嵌入应用中
  // 通过JSONP加载模块内容
  __webpack_require__.e = function requireEnsure(chunkId) {
    var promises = [];

    // javascript 的 JSONP 块加载

    var installedChunkData = installedChunks[chunkId];
  
    if (installedChunkData !== 0) {
      // 0 表示加载完毕了.

      // 如果是一个Promise,表示正在加载过程中,promises中推入加载块.
      if (installedChunkData) {
        promises.push(installedChunkData[2]);
      } else {
        // 在块缓存中设置 Promise
        var promise = new Promise(function (resolve, reject) {
          installedChunkData = installedChunks[chunkId] = [resolve, reject];
        });
        promises.push((installedChunkData[2] = promise));

        // 开始加载块
        var script = document.createElement("script");
        var onScriptComplete;

        script.charset = "utf-8";
        script.timeout = 120;
        if (__webpack_require__.nc) {
          script.setAttribute("nonce", __webpack_require__.nc);
        }
        script.src = jsonpScriptSrc(chunkId);

        // 在堆栈展开之前创建错误以便稍后获得有用的堆栈跟踪
        // 读取加载块时的错误回调
        var error = new Error();
        onScriptComplete = function (event) {
          // avoid mem leaks in IE.
          script.onerror = script.onload = null;
          clearTimeout(timeout);
          var chunk = installedChunks[chunkId];
          if (chunk !== 0) {
            if (chunk) {
              var errorType =
                event && (event.type === "load" ? "missing" : event.type);
              var realSrc = event && event.target && event.target.src;
              error.message =
                "Loading chunk " +
                chunkId +
                " failed.\n(" +
                errorType +
                ": " +
                realSrc +
                ")";
              error.name = "ChunkLoadError";
              error.type = errorType;
              error.request = realSrc;
              chunk[1](error);
            }
            installedChunks[chunkId] = undefined;
          }
        };
        var timeout = setTimeout(function () {
          onScriptComplete({ type: "timeout", target: script });
        }, 120000);
        script.onerror = script.onload = onScriptComplete;
        document.head.appendChild(script);
      }
    }
    return Promise.all(promises);
  };

  // create a fake namespace object
  // mode & 1: value is a module id, require it
  // mode & 2: merge all properties of value into the ns
  // mode & 4: return value when already ns object
  // mode & 8|1: behave like require
  __webpack_require__.t = function (value, mode) {
    if (mode & 1) value = __webpack_require__(value);
    if (mode & 8) return value;
    if (mode & 4 && typeof value === "object" && value && value.__esModule)
      return value;
    var ns = Object.create(null);
    __webpack_require__.r(ns);
    Object.defineProperty(ns, "default", { enumerable: true, value: value });
    if (mode & 2 && typeof value != "string")
      for (var key in value)
        __webpack_require__.d(
          ns,
          key,
          function (key) {
            return value[key];
          }.bind(null, key)
        );
    return ns;
  };

  // 异步加载的错误回调
  __webpack_require__.oe = function (err) {
    console.error(err);
    throw err;
  };

  var jsonpArray = (window["webpackJsonp"] = window["webpackJsonp"] || []);
  var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
  jsonpArray.push = webpackJsonpCallback;
  jsonpArray = jsonpArray.slice();
  for (var i = 0; i < jsonpArray.length; i++)
    webpackJsonpCallback(jsonpArray[i]);
  var parentJsonpFunction = oldJsonpFunction;

  // Load entry module and return exports
  return __webpack_require__((__webpack_require__.s = "./src/index.js"));
})({
  "./src/index.js": function (module, exports, __webpack_require__) {
    const btn = document.getElementById("btn");

    btn.addEventListener("click", () => {
      __webpack_require__
        .e(/*! import() | log */ "log")
        .then(__webpack_require__.t.bind(null, /*! ./log */ "./src/log.js", 7))
        .then(function (module) {
          console.log(module);
          module.default("log 加载完毕。");
        });
    });
    btn.click()
  },
});

// dist/log.bundle.js

/**
 * window上挂载一个全局对象 webpackJsonp
 * 将模块名和模块加载函数push到这个全局对象中
 * push方法已经在加载bundle.js被重写,也就是JSONP回调
 */

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
  ["log"],
  {
    "./src/log.js": function (module, exports) {
      module.exports = (log) => console.log(log);
    },
  },
]);

由上可见懒加载主要通过JSONP结合Promise来实现模块的动态加载。

2. t 方法分析及实现

// 创建一个命名空间对象,主要是为了返回模块内容
  // mode & 1: value是一个模块id加载这个模块
  // mode & 2: value是一个对象,合并对象的所有属性至ns中
  // mode & 4: 此时ns已经被标注__esmodule,并且属性合并完毕,可以返回
  // mode & 8|1: 标准的CommonJS模块,直接返回模块内容
  __webpack_require__.t = function (value, mode) {
    if (mode & 1) value = __webpack_require__(value);
    if (mode & 8) return value;
    if (mode & 4 && typeof value === "object" && value && value.__esModule)
      return value;
    // 当它不满足CommonJS和ESModule的时候,声明一个对象用来合并成员属性
    var ns = Object.create(null);
    // 标注其为ESModule
    __webpack_require__.r(ns);
    // 绑定默认导出,导出内容为value
    Object.defineProperty(ns, "default", { enumerable: true, value: value });
    // 当value是一个对象,且不是string类型时将其属性合并至ns
    if (mode & 2 && typeof value != "string")
      for (var key in value)
        __webpack_require__.d(
          ns,
          key,
          function (key) {
            return value[key];
          }.bind(null, key)
        );
    // 返回这个ns对象 
    return ns;
  };

按位与运算说明:

操作数被转换为32位整数,并由一系列位(0和1)表示。 超过32位的数字将丢弃其最高有效位。 例如,以下大于32位的整数将被转换为32位整数:

Before: 11100110111110100000000000000110000000000001
After:              10100000000000000110000000000001

第一个操作数中的每个位都与第二个操作数中的相应位配对:第一位到第一位,第二位到第二位,依此类推。

将运算符应用于每对位,然后按位构造结果。

与运算的真值表:

aba AND b
000
010
100
111

只有当两位都为真时才为真,其余全部为假。

代码拆解于实现:

// myBundle.js

// 09 创建一个对象用来合并模块的成员属性,并返回
__webpack_require__.t = function(value, mode) {
  if (mode & 1) {
    // 当 mode & 1 为真时,value是moduleId,此时就加载模块内容
    value = __webpack_require__(value);
  }
  if (mode & 8) {
    // 当 mode & 8|1 为真时,说明表示为require,是CommonJS,直接返回加载的value
    return value;
  }
  if (mode & 4 && typeof value === "object" && value && value.__esModule) {
    // 当 mode & 4 为真时,且value是一个对象,value值存在,并且具备__esModule为true的条件
    // 说明ns已经被标注为ESModule,且成员属性合并完毕,直接返回value
    return value;
  }
  // 以上一切都不满足时说明其不是标准的CommonJS,且未被标注为ESModule,未完成对象合并
  // 此时就声明一个空对象用来合并模块的成员
  const ns = Object.create(null);
  // 标注其为ESModule
  __webpack_require__.r(ns);
  // 设置默认导出,值为value
  Object.defineProperty(ns, "default", { enumerable: true, value: value });
  // 如果 mode & 2 为真时,说明value是一个对象,那么就进行对象合并
  if (mode & 2 && typeof value !== "string") {
    for (var key in value) {
      __webpack_require__.d(
        ns,
        key,
        // 提供一个get方法获取属性值,重置this为null
        function(key) {
          return value[key];
        }.bind(null, key)
      )
    }
  }
  // 返回ns
  return ns
}

3. 单文件懒加载源码解析

全局安装 http-server,启动一个本地服务器:

// http://127.0.0.1:8080/

Index of /
(drwxr-xr-x)		.vscode/
(drwxr-xr-x)		dist/
(drwxr-xr-x)		node_modules/
(drwxr-xr-x)		src/
(-rw-r--r--)	213B	package.json
(-rw-r--r--)	229B	README.md
(-rw-r--r--)	350B	webpack.config.js
(-rw-r--r--)	129.6k	yarn.lock

Node.js v15.3.0/ http-server server running @ 127.0.0.1:8080

添加调试配置:

// launch.json
{
  // 使用 IntelliSense 了解相关属性。 
  // 悬停以查看现有属性的描述。
  // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "chrome",
      "request": "launch",
      "name": "Launch Chrome",
      "url": "http://localhost:8080",
      "webRoot": "${workspaceFolder}"
    }
  ]
}

bundle.js 打上断点,点击浏览器中的dist目录就可以进入调试,一步步查看代码执行结果。

// log.bundle.js

// 此时的push方法已经被改写了,是JSONP的加载回调
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
  ["log"],
  {
    "./src/log.js": function (module, exports) {
      module.exports = (log) => console.log(log);
    },
  },
]);
// bundle.js

// 省略了其它代码,只保留了懒加载的核心代码

(function (modules) {
  // webpackBootstrap
  // 加载模块的JSONP回调,在动态加载依赖模块时会调用window["webpackJsonp"]的push方法
  // 该方法在加载主文件的过程中已经被改写成webpackJsonpCallback方法
  function webpackJsonpCallback(data) {
    // 第一项是依赖的模块ID结合
    var chunkIds = data[0];
    // 第二项是一个对象,键为模块ID,值是模块的加载函数
    var moreModules = data[1];

    // resolves用来存放依赖模块的promise resolve函数,就是通过 __webapck_require__e 函数加载模块
    var moduleId,
      chunkId,
      i = 0,
      resolves = [];
    // 遍历模块ids
    for (; i < chunkIds.length; i++) {
      chunkId = chunkIds[i];
      // 判断该模块正在加载中且是一个promise对象
      if (
        Object.prototype.hasOwnProperty.call(installedChunks, chunkId) &&
        installedChunks[chunkId]
      ) {
        // 缓存它的resolve方法
        resolves.push(installedChunks[chunkId][0]);
      }
      // 修改其加载状态为0,也就是加载完毕
      installedChunks[chunkId] = 0;
    }
    // 遍历所有模块加载函数,和当前主入口的模块定义 modules 进行合并
    for (moduleId in moreModules) {
      if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
        modules[moduleId] = moreModules[moduleId];
      }
    }
    // parentJsonpFunction作用:使异步加载的模块在多个不同的bundle内同步
    if (parentJsonpFunction) parentJsonpFunction(data);

    while (resolves.length) {
      resolves.shift()();
    }
  }

  // object to store loaded and loading chunks
  // undefined = chunk not loaded, null = chunk preloaded/prefetched
  // Promise = chunk loading, 0 = chunk loaded
  var installedChunks = {
    main: 0,
  };

  // script path function
  function jsonpScriptSrc(chunkId) {
    return __webpack_require__.p + "" + chunkId + ".bundle.js";
  }

  // 该bundle.js文件只加载主入口文件
  // 其它动态加载的模块通过这个方法加载
  __webpack_require__.e = function requireEnsure(chunkId) {
    // 加载模块的promise集合
    var promises = [];

    // 从加载缓存中取出该模块
    var installedChunkData = installedChunks[chunkId];
    // 为0表示已经加载完毕,此处为undefined
    if (installedChunkData !== 0) {

      // 如果是一个promise表示正在加载中,将这个加载模块的promise推送至promises集合中
      if (installedChunkData) {
        promises.push(installedChunkData[2]);
      } else {
        // 声明一个promise缓存resolve和reject函数
        var promise = new Promise(function (resolve, reject) {
          // 将resolve绑定至installedChunkData
          installedChunkData = installedChunks[chunkId] = [resolve, reject];
        });
        // 将这个promise压入promises集合中,并将promise绑定至installedChunkData
        promises.push((installedChunkData[2] = promise));

        // 使用JSONP完成模块加载
        var script = document.createElement("script");
        var onScriptComplete;

        script.charset = "utf-8";
        script.timeout = 120;
        // 判断是否启用了内联脚本的安全策略,如果启用的话就设置nonce为设置的值
        if (__webpack_require__.nc) {
          script.setAttribute("nonce", __webpack_require__.nc);
        }
        // 设置脚本src
        script.src = jsonpScriptSrc(chunkId);

        // 加载失败的错误信息
        var error = new Error();
        // 加载完毕的函数,加载失败或成功都会调用
        onScriptComplete = function (event) {
          // 清空数据避免IE中内存泄漏
          script.onerror = script.onload = null;
          clearTimeout(timeout);
          var chunk = installedChunks[chunkId];
          if (chunk !== 0) {
            if (chunk) {
              var errorType =
                event && (event.type === "load" ? "missing" : event.type);
              var realSrc = event && event.target && event.target.src;
              error.message =
                "Loading chunk " +
                chunkId +
                " failed.\n(" +
                errorType +
                ": " +
                realSrc +
                ")";
              error.name = "ChunkLoadError";
              error.type = errorType;
              error.request = realSrc;
              chunk[1](error);
            }
            installedChunks[chunkId] = undefined;
          }
        };
        // 绑定加载完毕回调
        var timeout = setTimeout(function () {
          onScriptComplete({ type: "timeout", target: script });
        }, 120000);
        script.onerror = script.onload = onScriptComplete;
        document.head.appendChild(script);
      }
    }
    return Promise.all(promises);
  };

  // create a fake namespace object
  // mode & 1: value is a module id, require it
  // mode & 2: merge all properties of value into the ns
  // mode & 4: return value when already ns object
  // mode & 8|1: behave like require
  __webpack_require__.t = function (value, mode) {
    if (mode & 1) value = __webpack_require__(value);
    if (mode & 8) return value;
    if (mode & 4 && typeof value === "object" && value && value.__esModule)
      return value;
    var ns = Object.create(null);
    __webpack_require__.r(ns);
    Object.defineProperty(ns, "default", { enumerable: true, value: value });
    if (mode & 2 && typeof value != "string")
      for (var key in value)
        __webpack_require__.d(
          ns,
          key,
          function (key) {
            return value[key];
          }.bind(null, key)
        );
    return ns;
  };

  // on error function for async loading
  __webpack_require__.oe = function (err) {
    console.error(err);
    throw err;
  };

  // 定义一个 jsonp加载数组,初始加载时 window["webpackJsonp"] 空的,就默认设置为 []
  var jsonpArray = (window["webpackJsonp"] = window["webpackJsonp"] || []);
  // 缓存jsonpArray的原生push方法
  var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
  // 重写jsonpArray方法为jsonp的回调
  jsonpArray.push = webpackJsonpCallback;
  // 浅复制jsonArray
  jsonpArray = jsonpArray.slice();
  // 当jsonpArray不为空时就加载jsonpArray里面的模块
  for (var i = 0; i < jsonpArray.length; i++)
    webpackJsonpCallback(jsonpArray[i]);
  // 将jsonpArray的原生push方法赋值给parentJsonpFunction
  var parentJsonpFunction = oldJsonpFunction;

  // Load entry module and return exports
  return __webpack_require__((__webpack_require__.s = "./src/index.js"));
})({
  "./src/index.js": function (module, exports, __webpack_require__) {
    const btn = document.getElementById("btn");

    btn.addEventListener("click", () => {
      __webpack_require__
        .e(/*! import() | log */ "log")
        .then(__webpack_require__.t.bind(null, /*! ./log */ "./src/log.js", 7))
        .then(function (module) {
          console.log(module);
          module.default("log 加载完毕。");
        });
    });
  },
});

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值