published on

webpack 按需加载原理

定义 jack.js

export default () => {
  console.log('fafa');
};

mark.js

export const val = 'sd';

export default () => {
  console.log('jaja');
};

index.js

const name = 'jack';
import('./mark').then(mark => mark());
import('./jack').then(jack => jack());

export default name;

使用 webpack 打包

(function(modules) {
  // webpackBootstrap
  // install a JSONP callback for chunk loading
  function webpackJsonpCallback(data) {}

  // The module cache
  var installedModules = {};

  // 用来存储加载过和加载中的 chunk
  // 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';
  }
  // The require function
  function __webpack_require__(moduleId) {}
  // This file contains only the entry chunk.
  // The chunk loading function for additional chunks
  __webpack_require__.e = function requireEnsure(chunkId) {};

  // expose the modules object (__webpack_modules__)
  __webpack_require__.m = modules;

  // expose the module cache
  __webpack_require__.c = installedModules;

  // define getter function for harmony exports
  __webpack_require__.d = function(exports, name, getter) {
    if (!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, { enumerable: true, get: getter });
    }
  };

  // define __esModule on exports
  __webpack_require__.r = function(exports) {
    if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
    }
    Object.defineProperty(exports, '__esModule', { value: true });
  };

  __webpack_require__.t = function(value, mode) {};

  // getDefaultExport function for compatibility with non-harmony modules
  __webpack_require__.n = function(module) {};

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

  // __webpack_public_path__
  __webpack_require__.p = '';

  // on error function for async loading
  __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':
      /*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
      /*! exports provided: default */
      /***/ function(module, __webpack_exports__, __webpack_require__) {
        'use strict';
        eval(
          '__webpack_require__.r(__webpack_exports__);\nconst name = \'jack\';\n__webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./mark */ "./src/mark.js")).then(mark => mark());\n\n/* harmony default export */ __webpack_exports__["default"] = (name);\n\n\n//# sourceURL=webpack:///./src/index.js?',
        );

        /***/
      },
  },
);

jack.js 打包出的内容

(window['webpackJsonp'] = window['webpackJsonp'] || []).push([
  [1],
  {
    /***/ './src/mark.js':
      /*!*********************!*\
  !*** ./src/mark.js ***!
  \*********************/
      /*! exports provided: val, default */
      /***/ function(module, __webpack_exports__, __webpack_require__) {
        'use strict';
        eval(
          '__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "val", function() { return val; });\nconst val = \'sd\'\n\n/* harmony default export */ __webpack_exports__["default"] = (() => {\n  console.log(\'jaja\')\n});\n\n\n//# sourceURL=webpack:///./src/mark.js?',
        );

        /***/
      },
  },
]);

取全局的 webpackJsonp 数组,如果没有,定义为空数组,并调用 push 方法,参数一个数组,第一项对应异步模块的 chunkId,第二项就是模块的内容

总体流程

首先,通过立即执行函数,跳过一系列的方法定义,到下面地方开始运行

// ... some code
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;
return __webpack_require__((__webpack_require__.s = './src/index.js'));

可以看到,jsonpArray 是全局的 webpackJsonp数组,并对这个数组的 push 方法等于 webpackJsonpCallback, 这样根据上面异步 chunk 里的代码,可以看出模块加载了以后,就会执行 webpackJsonpCallback 方法.

加载 index.js. 看下 index.js 对应的 eval 函数内部的内容.

__webpack_require__.r(__webpack_exports__);
const name = 'jack';
__webpack_require__
  .e(/*! import() */ 1)
  .then(__webpack_require__.bind(null, /*! ./mark */ './src/mark.js'))
  .then(mark => mark());
__webpack_require__
  .e(/*! import() */ 0)
  .then(__webpack_require__.bind(null, /*! ./jack */ './src/jack.js'))
  .then(jack => jack());
/* harmony default export */

__webpack_exports__['default'] = name;

可以看到 import().then() 被转换为了 __webpack_require__.e. 这个函数的第一个参数就是按需加载对应 chunk 的 chunkId。 接下来看看 __webpack_require__.e 的具体实现

__webpack_require__.e = function requireEnsure(chunkId) {
  var promises = [];

  // JSONP chunk loading for javascript

  var installedChunkData = installedChunks[chunkId];
  if (installedChunkData !== 0) {
    // 0 表示该chunk已加载过了

    // 如果 installedChunkData 是 promise,即为真,则表示模块正在加载中
    // push installedChunkData[2] 到 promises 数组
    if (installedChunkData) {
      promises.push(installedChunkData[2]);
    } else {
      // installedChunks[chunkId] 赋值为一个数组,前两项是 promise 的 resolve reject
      // installedChunkData = installedChunks[chunkId]
      var promise = new Promise(function(resolve, reject) {
        installedChunkData = installedChunks[chunkId] = [resolve, reject];
      });
      // installedChunkData[2]赋值为上面创建的promise
      promises.push((installedChunkData[2] = promise));

      // 通过创建 script 标签形式下载 chunk script
      var script = document.createElement('script');
      var onScriptComplete;

      script.charset = 'utf-8';
      script.timeout = 120;
      if (__webpack_require__.nc) {
        script.setAttribute('nonce', __webpack_require__.nc);
      }
      // jsonpScriptSrc 拼接 script 地址,webpack public path + chunkId + .bundle.js
      script.src = jsonpScriptSrc(chunkId);

      // create error before stack unwound to get useful stacktrace later
      var error = new Error();
      onScriptComplete = function(event) {
        // avoid mem leaks in IE.
        script.onerror = script.onload = null;
        clearTimeout(timeout);
        var chunk = installedChunks[chunkId];
        // 不为 0 表示下载出错
        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);
    }
  }
  // 使用 Promise.all 处理
  return Promise.all(promises);
};

通过创建 script 标签加载对应 chunkId 的 chunk,加载完以后,执行对应 chunk 内的内容. 也就是上面提到的 webpackJsonpCallback

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 (installedChunks[chunkId]) {
      // installedChunks[chunkId][0] 之前是 __webpack_reuqire__.e 里赋值为 promise 的 resolve
      // 放入 resolves
      resolves.push(installedChunks[chunkId][0]);
    }
    // 赋值为 0 表示加载完成
    installedChunks[chunkId] = 0;
  }
  for (moduleId in moreModules) {
    if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
      // 同步到 modules 中
      modules[moduleId] = moreModules[moduleId];
    }
  }
  if (parentJsonpFunction) parentJsonpFunction(data);

  while (resolves.length) {
    resolves.shift()(); // => promise resolve()
  }
}

通过执行 promise 的 resolve(),这样我们的Promise.all(promises); 全部 resolve 之后,就可以执行 .then 后面的方法了.

整理一下

  1. 通过__webpack_require__ 加载入口文件
  2. 遇到异步模块(import()),转换为使用 __webpack_require__.e 加载
  3. __webpack_require__.e 使用创建 script 标签的方式加载异步模块,并将每个异步的 chunk 创建 promise,并放入 installedChunk
  4. 模块被加载后执行 webpackJsonpCallback 方法,改变 nstalledChunk 对应的 chunkId 加载状态为 0,即表示加载过,然后同步到 modules(所有模块存放的缓存)中,执行之前创建的 promise 的 resolve()
  5. then 里面就可以使用 __webpack_require使用异步模块