webpack系列4-按需加载

106 阅读2分钟

前言背景

在我们前端首页页面做优化时候, 代码分割是一个常用的手段,页面只加载需用用到的资源,其他的模块等到需要时候再去加载

import()示例

// a.js
export default function a() {
  console.log("我是模块 a");
}

import("./a").then(({ default: a }) => {
  console.log(a);
});

webpack 解析原理

源码

  • import 字段解析 webpack_require.e 主体方法
(() => {
  __webpack_require__.f = {};
  __webpack_require__.e = (chunkId) => {
    return Promise.all(
      Object.keys(__webpack_require__.f).reduce((promises, key) => {
        __webpack_require__.f[key](chunkId, promises);
        return promises;
      }, [])
    );
  };
})();

/*
- webpack_require.e 对应 import() 方法,即异步加载的主体方法
- 返回一个 promise
*/
  • 主体方法
__webpack_require__.e = function requireEnsure(chunkId) {
  var installedChunkData = installedChunks[chunkId];
  // 0 表示已经加载成功,无需再做任何处理
  if (installedChunkData === 0) {
    return new Promise(function (resolve) {
      resolve();
    });
  }
  if (installedChunkData) {
    return installedChunkData[2];
  }
  var promise = new Promise(function (resolve, reject) {
    installedChunkData = installedChunks[chunkId] = [resolve, reject];
  });
  // 数据结构为
  /*
    {
      id: [resolve, reject]
    }
  */
  installedChunkData[2] = promise;
  // 生成一个 script 标签,用于异步加载 js 文件
  var head = document.getElementsByTagName("head")[0];
  var script = document.createElement("script");
  script.type = "text/javascript";
  script.charset = "utf-8";
  script.async = true;
  script.timeout = 120000;

  if (__webpack_require__.nc) {
    script.setAttribute("nonce", __webpack_require__.nc);
  }
  // __webpack_require__.p 就是 __webpack_public_path__ 对应的地址
  script.src =
    __webpack_require__.p +
    "" +
    ({ 0: "a-async" }[chunkId] || chunkId) +
    ".async.js";
  var timeout = setTimeout(onScriptComplete, 120000);
  script.onerror = script.onload = onScriptComplete;
  function onScriptComplete() {
    // avoid mem leaks in IE.
    script.onerror = script.onload = null;
    clearTimeout(timeout);
    var chunk = installedChunks[chunkId];
    // 如果文件加载成功,chunk就被设置为 0;后面只处理了加载失败的情况
    if (chunk !== 0) {
      if (chunk) {
        chunk[1](new Error("Loading chunk " + chunkId + " failed."));
      }
      installedChunks[chunkId] = undefined;
    }
  }
  head.appendChild(script);

  return promise;
};
  • a 文件
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
  [0],
  {
    "./src/a.js": function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      eval(``);

      /***/
    },
  },
]);
  • webpackJsonp 方法
window["webpackJsonp"] = function webpackJsonpCallback(data) {
  // chunkid
  var chunkIds = data[0];
  // chunkid对应的模块
  var moreModules = data[1];
  var moduleId,
    chunkId,
    i = 0,
    resolves = [];

  for (; i < chunkIds.length; i++) {
    chunkId = chunkIds[i];
    if (
      Object.prototype.hasOwnProperty.call(installedChunks, chunkId) &&
      installedChunks[chunkId]
    ) {
      // 收集chunk对应的resolve方法
      resolves.push(installedChunks[chunkId][0]);
    }
    // 标记该chunk已经加载
    installedChunks[chunkId] = 0;
  }
  for (moduleId in moreModules) {
    if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
      // 添加chunk模块,到全局modules对象中
      modules[moduleId] = moreModules[moduleId];
    }
  }
  if (parentJsonpFunction) parentJsonpFunction(data);

  // 依次执行chunk对应promise的resolve方法
  while (resolves.length) {
    resolves.shift()();
  }
};

图例

原理

  • 主文件
    • import 会被解析为 webpack_require_e
    • chunk 没有被加载过会为这个 chunkId 创建一个 promsie 对象
    • promise的resolve、reject方法放在installchunks[chunkId]中
      • {
          id: [resolve, reject]
        }
        
    • 创建一个script标签来加载js文件
    • 挂载到window上一个webpackJsonp方法
  • 异步文件
    • chunk对应的resolve方法, 放在了installedChunks[chunkId]里
    • 依次执行chunk对应promise的resolve方法