Webpack 中 Dynamic Import 打包产物是啥

js 复制代码
// src/index.jsx
const Counter = lazy(() =>
  import(/* webpackChunkName: "lazy_chunk" */ "./features/counter/counter.jsx")
);

先抛代码,相信大家都用过或者熟悉以上懒加载模块的使用,大多数朋友也都懂如何实现加载的,那我们就来确认下打包产物到底是什么,异步加载的包如何被主包消费的。带着问题一起来探索。

dynamic import

源代码

源代码如文章开头代码示例,魔法注释自行查看官网介绍。

webpack output 配置如下,代码简单,没做其他分包处理,打包结果中最终会包含 main.bundle.jslazy_chunk.bundle.js 两个 js 文件和一个 index.html 文件。

js 复制代码
// webpack.config.js
output: {
    path: path.resolve(__dirname, "dist"),
    filename: "[name].bundle.js",
},
plugins: [
    new HTMLPlugin({ template: "./src/index.html" }),
    // new MiniCssExtractPlugin 等...
  ],
};

lazy_chunk 构建产物

chunk.bundle.js
js 复制代码
"use strict";
// 1. 在 self["webpackChunkdemoy"] 中增加一个 item 
(self["webpackChunkdemoy"] = self["webpackChunkdemoy"] || []).push([
  ["lazy_chunk"], // 对应魔法注释中的 chunk name 
  {
     // 模块 path 对应执行函数
    /***/ "./src/features/counter/counter.jsx":
      /***/ (
        __unused_webpack_module,
        __webpack_exports__,
        __webpack_require__
      ) => {
        __webpack_require__.r(__webpack_exports__);
        // 2. 导出 Counter
        __webpack_require__.d(__webpack_exports__, {
          default: () => /* binding */ Counter,
        });
        // 通过 __webpack_require__ 引入依赖
        var MODULE_1__ = __webpack_require__(/*! react */ "dependence path");
        // 还有很多很多 var MODULE_2...3...4...

        // babel-react 等编译后的结果
        function Counter() {
          // react_jsx_runtime  _jsxs、_jsx 
        }
      },
  },
]);
//# sourceMappingURL=lazy_chunk.bundle.js.map

在代码中我们注意到整个 chunk 就执行了一个操作,将构建结果 push 到 self["webpackChunkdemoy"]中,那 push 之后做了什么呢,带着疑问我们到 main.bundle.js 中看看这个变量是什么,又是在什么时机加载 lazy_chunk 的,两者是如何配合并执行的。

如何消费 lazy_chunk

顺着线索一步步去找这些相关的函数是什么,做了哪些事?

self["webpackChunkdemoy"]
js 复制代码
// main.bundle.js
// 1. 全局搜索仅此 2 个 `webpackChunkdemoy` 文本,因此只需观察 chunkLoadingGlobal 
 var chunkLoadingGlobal = (self["webpackChunkdemoy"] = self["webpackChunkdemoy"] || []);
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
// 2. 改写 push,原生 push 入参,并执行 webpackJsonpCallback。补充:二参是上一步 push 传入的 chunk 构建产物
chunkLoadingGlobal.push = webpackJsonpCallback.bind(
    null,
    chunkLoadingGlobal.push.bind(chunkLoadingGlobal) 
);

通过观察 chunkLoadingGlobal.push 的改写,我们又发现了新的线索 webpackJsonpCallback👇🏻

webpackJsonpCallback

查看代码请点击展开

scss 复制代码
var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
    var [chunkIds, moreModules, runtime] = data;
    // add "moreModules" to the modules object,
    // then flag all "chunkIds" as loaded and fire callback
    var moduleId,
      chunkId,
      i = 0;
    // 注意 installedChunks
    if (chunkIds.some((id) => installedChunks[id] !== 0)) {
      for (moduleId in moreModules) {
        if (__webpack_require__.o(moreModules, moduleId)) {
          __webpack_require__.m[moduleId] = moreModules[moduleId];
        }
      }
      if (runtime) var result = runtime(__webpack_require__);
    }
    if (parentChunkLoadingFunction) parentChunkLoadingFunction(data);
}

ok 如你所见,代码折叠了,是的,因为不好看懂。installedChunks 是执行逻辑之前定义的,所以我们回到主线入手,看看 main 中如何加载 lazy_chunk、又是如何消费包内容的。

在整个流程中,会回到 webpackJsonpCallback 中,接下来要多留意 webpackJsonpCallback 中出现的关键词 installedChunks。好,我们正式回到完整流程的开始。

main 中如何加载 lazy_chunk?

调用入口

找到调用 lazy_chunk 的逻辑,不难看出 __webpack_require__.e 将返回携带懒加载模块内容的 Promise,Promise 的生成依赖 keyof __webpack_require__.f(ts 伪着凑合看)。

js 复制代码
// main.bundle.js
const Counter = /*#__PURE__*/ (0, react__WEBPACK_IMPORTED_MODULE_1__.lazy)(() =>
  // 1. 加载函数 __webpack_require__.e
  __webpack_require__
    .e(/*! import() | lazy_chunk */ "lazy_chunk")
    .then(
      __webpack_require__.bind(
        __webpack_require__,
        /*! ./features/counter/counter.jsx */ "./src/features/counter/counter.jsx"
      )
    )
);

__webpack_require__.e = (chunkId) => {
  return Promise.all(
    Object.keys(__webpack_require__.f).reduce((promises, key) => {
      __webpack_require__.f[key](chunkId, promises);// 2. __webpack_require__.f[key] 处理 promises 并返回
      return promises;
    }, [])
  );
};

通过搜索发现 __webpack_require__.f 下只有一个方法 __webpack_require__.f.j,那就简单了,看看返回的 promises 被 push 了哪些值👇🏻

准备阶段

js 复制代码
// main.bundle.js
// 🔧 判断模块是否已加载
__webpack_require__.o = (obj, prop) =>
  Object.prototype.hasOwnProperty.call(obj, prop);
}

// 1. 已加载过的 chunk
var installedChunks = {
    main: 0, // 0 - 已成功加载状态,因为当前执行的就是 main,所以是 0,加载成功前的值是 [resolve, reject],下边__webpack_require__.f.j 中会讲
};
  
__webpack_require__.f.j = (chunkId, promises) => {
  // JSONP chunk loading for javascript 
  // 2. 已加载过则直接取,不需要再次请求资源
  var installedChunkData = __webpack_require__.o(installedChunks, chunkId)
    ? installedChunks[chunkId] 
    : undefined;
  if (installedChunkData !== 0) {
    // 0 means "already installed".

    // a Promise means "currently loading".
    if (installedChunkData) {
      promises.push(installedChunkData[2]); // 3. 正在请求资源中,不需要重新发起,返回上一次的 promise 即可
    } else {
        // 未加载过的
      if (true) {
        // all chunks have JS
        // setup Promise in chunk cache
        var promise = new Promise(
          (resolve, reject) =>
            (installedChunkData = installedChunks[chunkId] = [resolve, reject])
        );
        promises.push((installedChunkData[2] = promise));
        // 4. 此时的状态 promises = [promise]  
        //    installedChunkData = [resolve, reject, promise]
        //    installedChunkData = { main: 0, lazy_chunk: [resolve, reject]}
        

        // 5. 准备完整的 url __webpack_require__.p 资源目录地址   __webpack_require__.u 根据 webpack config 中 `output.filename` 生成完整名称 
        var url = __webpack_require__.p + __webpack_require__.u(chunkId); // 该例子中为 file:///Users/xxx/Desktop/demo/dist/lazy_chunk.bundle.js
        // create error before stack unwound to get useful stacktrace later
        var error = new Error();
        // 6. 在 script 标签加载完成或者加载失败后执行,【script 与第7步 __webpack_require__.l 有关】
        var loadingEnded = (event) => {
          if (__webpack_require__.o(installedChunks, chunkId)) {
            installedChunkData = installedChunks[chunkId];
            // 7. installedChunkData !== 0 加载失败
            if (installedChunkData !== 0) installedChunks[chunkId] = undefined;
            if (installedChunkData) {
              error.message = 'xxx' // 简化...
              installedChunkData[1](error);
            }
          }
        };
        // 7. 加载函数
        __webpack_require__.l(
          url,
          loadingEnded,
          "chunk-" + chunkId,
          chunkId
        );
      }
    }
  }
};

请求资源

轻松走到正式发起请求的时候了,那么这一步要做的就是 script 标签加载上一步生成的 url,成功则调用 installedChunks[chunkId][0] 中的 resolve,并将 installedChunks[chunkId] 设置为0,失败则会执行 loadingEnded 中,执行 reject

js 复制代码
var inProgress = {};// 1. 存储正在加载的资源
var dataWebpackPrefix = "demoy:";
__webpack_require__.l = (url, done, key, chunkId) => { // done - loadingEnded
  // 已经在加载中的,绑定 done
  if (inProgress[url]) {
    inProgress[url].push(done);
    return;
  }
  var script, needAttach;
  // 避免重复添加 script 加载
  if (key !== undefined) {
    var scripts = document.getElementsByTagName("script");
    for (var i = 0; i < scripts.length; i++) {
      var s = scripts[i];
      if (
        s.getAttribute("src") == url ||
        s.getAttribute("data-webpack") == dataWebpackPrefix + key
      ) {
        script = s;
        break;
      }
    }
  }
  // 1. 创建 script
  if (!script) {
    needAttach = true;
    script = document.createElement("script");

    script.charset = "utf-8";
    script.timeout = 120;
    if (__webpack_require__.nc) {
      script.setAttribute("nonce", __webpack_require__.nc);
    }
    script.setAttribute("data-webpack", dataWebpackPrefix + key);

    script.src = url;
  }
  // 2. 绑定 done
  inProgress[url] = [done];
  // 3. script 绑定回调
  var onScriptComplete = (prev, event) => {
    // avoid mem leaks in IE.
    script.onerror = script.onload = null;
    clearTimeout(timeout);
    var doneFns = inProgress[url];
    delete inProgress[url];
    script.parentNode && script.parentNode.removeChild(script);// 5. 移除 script 
    doneFns && doneFns.forEach((fn) => fn(event)); // ⭐️ 6. 执行 done
    if (prev) return prev(event);
  };
  var timeout = setTimeout(
    onScriptComplete.bind(null, undefined, {
      type: "timeout",
      target: script,
    }),
    120000
  );
  script.onerror = onScriptComplete.bind(null, script.onerror);
  script.onload = onScriptComplete.bind(null, script.onload);
  // ⭐️ 4. 插入执行,如果成功,此时已经立即执行该 js,也就是 lazy_chunk 中的 push - main 中的 webpackJsonpCallback
  needAttach && document.head.appendChild(script);
};

我们看到上边这一步主要做了 2 件事,创建 script 标签执行js,即执行 webpackJsonpCallback执行 loadingEnded

后者我们已经在代码中注释过了,主要处理加载失败,执行installedChunks[chunkId][1](),即 __webpack_require__.e 中返回的 promise 的reject 函数,不记得的回去看一眼。

前者也就是我们从 lazy_chunk 推不过去的卡点 webpackJsonpCallback,那么完成这一步整个链路就通了,话不多说👇🏻

执行并保存

回到 webpackJsonpCallback
js 复制代码
__webpack_require__.m = __webpack_modules__;
// parentChunkLoadingFunction push       data: [[chunkId], { moduleId: module }]
var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
  var [chunkIds, moreModules, runtime] = data;
  var moduleId,
    chunkId,
    i = 0;
    // 1. 之前没有加载过,则以 moduleId 为颗粒度存储在 __webpack_modules__ 中
  if (chunkIds.some((id) => installedChunks[id] !== 0)) {
    for (moduleId in moreModules) {
      if (__webpack_require__.o(moreModules, moduleId)) {
        __webpack_require__.m[moduleId] = moreModules[moduleId];
      }
    }
    if (runtime) var result = runtime(__webpack_require__);
  }
  if (parentChunkLoadingFunction) parentChunkLoadingFunction(data);// 2. push 存储至 self["webpackChunkdemoy"]
  // 3. resolve() 改变状态并将已加载 chunk 状态修改为 installedChunks[chunkId] = 0
  for (; i < chunkIds.length; i++) {
    chunkId = chunkIds[i];
    if (
      __webpack_require__.o(installedChunks, chunkId) &&
      installedChunks[chunkId]
    ) {
      installedChunks[chunkId][0]();
    }
    installedChunks[chunkId] = 0;
  }
};

webpack_modules 是什么,存储所有已加载模块信息的的变量。如果不清楚 module、chunk、asset 是啥关系,webpack.run 回调函数中取下 stats 参数查看。想查看每个 webpack 构建产物如何执行的,随便构建一个只有一行代码的项目,跑完方便观察模块的存取缓存如何设计的。

总结

看了这么多,抛开细节,主线剧情只有:

  • __webpack_require__:取 by moduleId
  • __webpack_mudules__:存 moduleId:module function
  • __webpack_require__.e:异步取,返回 promise
  • 全文讲了啥script 加载 chunk 返回,通过 webpackJsonpCallback 将 chunk 中的 modules 存 __webpack_mudules__,通过 resolve 确定 promise 状态,promise.then 通过 __webpack_require____webpack_mudules__ 中取。

看图,虽然画很差劲。。。

最后,vscode 调试工具一如既往好用。

相关推荐
木子七几秒前
vue2-vuex
前端·vue
麻辣_水煮鱼4 分钟前
vue数据变化但页面不变
前端·javascript·vue.js
BY—-组态10 分钟前
web组态软件
前端·物联网·工业互联网·web组态·组态
一条晒干的咸魚13 分钟前
【Web前端】实现基于 Promise 的 API:alarm API
开发语言·前端·javascript·api·promise
WilliamLuo1 小时前
MP4结构初识-第一篇
前端·javascript·音视频开发
Beekeeper&&P...1 小时前
web钩子什么意思
前端·网络
过期的H2O21 小时前
【H2O2|全栈】JS进阶知识(七)ES6(3)
开发语言·javascript·es6
啵咿傲1 小时前
重绘&重排、CSS树&DOM树&渲染树、动画加速 ✅
前端·css
前端Hardy1 小时前
HTML&CSS:数据卡片可以这样设计
前端·javascript·css·3d·html
流烟默1 小时前
CSS中Flex布局应用实践总结
前端·css·flex布局