Skip to content

Go back

从零实现Webpack的异步导入功能

Published:  at 

文章目录

介绍

Webpack 的异步导入(dynamic import)支持代码分割与懒加载。本文通过分析打包后的代码,来理解 webpack 在运行时如何实现异步模块加载。

打包后的运行时代码

模块系统核心

// 模块存储:将模块 ID 映射到模块定义函数
var modules = {};

// 模块缓存:保存已安装/已加载模块
var cache = {};

// 模块加载函数
function require(moduleId) {
  // 若模块已加载,直接返回缓存
  if (cache[moduleId]) {
    return cache[moduleId];
  }

  // 创建并缓存新模块对象
  var module = (cache[moduleId] = { exports: {} });

  // 执行模块定义函数,填充 exports
  modules[moduleId](module, module.exports, require);

  return module.exports;
}

// 将模块存储和缓存挂载到 require 上
require.m = modules; // 模块定义
require.c = cache; // 已安装模块

模块定义辅助方法

// 将模块标记为 ES module
require.r = exports => {
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
  Object.defineProperty(exports, "__esModule", { value: true });
};

// 为 exports 定义 getter
require.d = (exports, definition) => {
  for (var key in definition) {
    Object.defineProperty(exports, key, {
      enumerable: true,
      get: definition[key],
    });
  }
};

Chunk 加载工具函数

// 加载 chunk 的基础 URL
require.p = "";

// 根据 chunk ID 生成文件名
require.u = chunkId => chunkId + ".main.js";

// 创建并注入 script 标签来加载 chunk
require.l = url => {
  let script = document.createElement("script");
  script.src = url;
  document.head.appendChild(script);
};

异步 Chunk 加载

// 跟踪已安装/下载中的 chunks
// 值为 0 表示已安装,值为 [resolve, reject] 表示加载中
var installedChunks = { main: 0 };

// 用于 chunk 加载的 JSONP 回调注册逻辑
require.f.j = (chunkId, promises) => {
  var installedChunkData;

  // 创建 promise,并保存 resolve/reject
  var promise = new Promise((resolve, reject) => {
    installedChunkData = installedChunks[chunkId] = [resolve, reject];
  });

  // 将 promise 存入数组,供 Promise.all() 汇总
  promises.push((installedChunkData[2] = promise));

  // 发起 chunk 脚本加载
  var url = require.p + require.u(chunkId);
  require.l(url);
};

// 加载 chunk 并返回 promise
require.e = chunkId => {
  let promises = [];
  require.f.j(chunkId, promises);
  return Promise.all(promises);
};

全局 Chunk 加载回调

// 处理已加载 chunk 数据的回调
var webpackJsonCallback = (chunkIds, moreModules) => {
  var resolves = [];

  // 取出并准备执行所有待完成 chunk 的 resolve
  for (var i = 0; i < chunkIds.length; i++) {
    var chunkId = chunkIds[i];
    resolves.push(installedChunks[chunkId][0]); // 获取 resolve 函数
    installedChunks[chunkId] = 0; // 标记为已安装
  }

  // 将新模块合并到模块存储中
  Object.assign(modules, moreModules);
};

// 设置用于 JSONP chunk 加载的全局数组
var chunkLoadingGlobal = (window["webpackChunk_app_bundle"] = []);
chunkLoadingGlobal.push = webpackJsonCallback; // 重写 push 以触发回调

入口使用方式

// 10 秒后异步加载一个 chunk
window.setTimeout(() => {
  require.e("src_video_js").then(require.bind(null, './src/video.js')).then(() => {
    // chunk 加载完成,模块可通过 require() 获取
  });
}, 10000);

懒加载 Chunk 的模块定义

// chunk 模块定义(通过 JSONP 方式加载)
window["webpackChunk_app_bundle"].push([
  ["src_video_js"],
  {
    "./src/video.js": (module, exports, require) => {
      require.r(exports);
      require.d(exports, {
        default: () => DEFAULT_EXPORT,
      });
      var DEFAULT_EXPORT = "video";
    },
  },
]);

webpack异步加载流程总结

  1. 触发:调用 require.e(chunkId)
  2. 创建 Promise:创建带 resolve/reject 的 promise,并存入 installedChunks
  3. 注入脚本:创建带 chunk URL 的 <script> 标签并插入 DOM
  4. 执行 Chunk:浏览器加载并执行 chunk 脚本
  5. JSONP 回调:chunk 调用 window["webpackChunk_app_bundle"].push(),触发 webpackJsonCallback
  6. 模块注册:把新模块合并到 modules 存储
  7. Promise 完成:待处理 promise 被 resolve,加载结束


Previous Post
从零实现一个Babel插件
Next Post
从零实现Vue-Router