webpack模块异步加载原理解析

大多数状况下,咱们并不关心 webpack 是怎么作异步加载的,可是做为前端开发工程师咱们须要对异步加载有必定的了解。javascript

原文连接css

在讲解以前,先让咱们搭建一个简单的webpack工程。html

1、工程搭建

// package.json文件
{
  "name": "webpack-study",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "author": "",
  "license": "ISC",
  "scripts": {
    "dev": "cross-env NODE_ENV=development webpack",
    "build": "cross-env NODE_ENV=production webpack"
  },
  "dependencies": {
    "cross-env": "^6.0.3",
    "css-loader": "^3.2.0",
    "rimraf": "^3.0.0",
    "webpack": "^4.41.2"
  },
  "devDependencies": {
    "webpack-chain": "^6.0.0",
    "webpack-cli": "^3.3.10"
  }
}
复制代码

这里我使用了webpack-chain的方式配置 webpack。有兴趣的朋友能够去了解一下。 webpack-chain 经常使用配置前端

//webpack.config.js
const path = require("path");
const rimraf = require("rimraf");
const Config = require("webpack-chain");
const config = new Config();
const resolve = src => {
  return path.join(process.cwd(), src);
};

// 删除 dist 目录
rimraf.sync("dist");

config
  // 入口
  .entry("src/index")
  .add(resolve("src/index.js"))
  .end()
  // 模式
  // .mode(process.env.NODE_ENV) 等价下面
  .set("mode", process.env.NODE_ENV)
  // 出口
  .output.path(resolve("dist"))
  .filename("[name].bundle.js");

config.module
  .rule("css")
  .test(/\.css$/)
  .use("css")
  .loader("css-loader");

module.exports = config.toConfig();
复制代码

而后在src目录下新增两个文件java

// index.js

const css = import("./index.css");
const css2 = import("./index2.css");
复制代码
/* index.css和index2.css同样 */
body {
  width: 100%;
  height: 100%;
  background-color: red;
}
复制代码

2、原理解析

在讲解以前让咱们容许一下,yarn dev,对没错,这时您能够在dist目录下查看到生成了 3 个文件。 其中0.bundle.js1.bundle.js分别对应index.css和index2.css。异步加载的模块会产生一个单独的模块。node

dist
 ┣ src
 ┃ ┗ index.bundle.js
 ┣ 0.bundle.js
 ┗ 1.bundle.js
复制代码

查看index.bundle.js源码,好像不少代码,其实精简下来就是一个自执行函数.webpack

(function(modules) {
  // 模拟 require 语句
  function __webpack_require__() {}
  // 执行存放全部模块数组中的第0个模块
  __webpack_require__((__webpack_require__.s = 0));
})([
  /*存放全部模块的数组*/
]);
复制代码

(一) chunk.bundle.js 初识

异步加载的 js,打包时会额外的打包成一个 js 文件,好比0.bundle.jsgit

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
  [0],
  {
    "./node_modules/css-loader/dist/runtime/api.js": function( module, exports, __webpack_require__ ) {
 "use strict";
      eval("...忽略其中的代码");
    },
    // 执行具体的模块代码
    "./src/index.css": function(module, exports, __webpack_require__) {
      eval("...忽略其中的代码");
    }
  }
]);
复制代码

经过分析0.bundle.js咱们了解到:github

  • 异步加载的代码,会保存在一个全局的webpackJsonp
  • webpackJsonppush 的的值,两个参数分别为
    • 异步加载的文件中存放的须要安装的模块对应的 Chunk ID
    • 异步加载的文件中存放的须要安装的模块列表
  • 知足某种状况下,会执行具体模块中的代码,那么在何时执行,请查看下面的分析

(二)初识 bundle.js

  • bundle 是一个当即执行函数,是入口文件。
  • webpack 将全部模块打包成了 bundle 的依赖,经过一个对象注入

jsonpScriptSrc

jsonpScriptSrc的主要做用是经过publicPath+chunkId的方式获取到异步加载模块的url地址。web

function jsonpScriptSrc(chunkId) {
  return __webpack_require__.p + "" + ({}[chunkId] || chunkId) + ".bundle.js";
}
复制代码

webpack_require

__webpack_require__webpack的核心,webpack经过__webpack_require__引入模块。 __webpack_require__require包裹了一层,主要功能是加载 js 文件。

function __webpack_require__(moduleId) {
  //若是须要加载的模块已经被加载过,就直接从内存缓存中返回
  if (installedModules[moduleId]) {
    return installedModules[moduleId].exports;
  }
  //若是缓存中不存在须要加载的模块,就新建一个模块,并把它存在缓存中
  var module = (installedModules[moduleId] = {
    i: moduleId, // 模块在数组中的 index
    l: false, // 该模块是否已经加载完毕
    exports: {} // 该模块的导出值
  });

  // 从 modules 中获取 index 为 moduleId 的模块对应的函数
  // 再调用这个函数,同时把函数须要的参数传入
  modules[moduleId].call(
    module.exports,
    module,
    module.exports,
    __webpack_require__
  );

  // 把这个模块标记为已加载
  module.l = true;

  // Return the exports of the module
  return module.exports;
}
复制代码

webpack_require.e 异步加载核心

异步加载的核心实际上是使用类jsonp的方式,经过动态建立script的方式实现异步加载。

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

  // 判断当前chunk是否已经安装,若是已经使用

  var installedChunkData = installedChunks[chunkId];
  // installedChunkData为0表示已经加载了
  if (installedChunkData !== 0) {
    //installedChunkData 不为空且不为0表示该 Chunk 正在网络加载中
    if (installedChunkData) {
      promises.push(installedChunkData[2]);
    } else {
      //installedChunkData 为空,表示该 Chunk 尚未加载过,去加载该 Chunk 对应的文件
      var promise = new Promise(function(resolve, reject) {
        installedChunkData = installedChunks[chunkId] = [resolve, reject];
      });
      promises.push((installedChunkData[2] = promise));

      // 经过 DOM 操做,往 HTML head 中插入一个 script 标签去异步加载 Chunk 对应的 JavaScript 文件
      var script = document.createElement("script");
      var onScriptComplete;

      script.charset = "utf-8";
      script.timeout = 120;
      if (__webpack_require__.nc) {
        script.setAttribute("nonce", __webpack_require__.nc);
      }
      // 文件的路径为配置的 publicPath、chunkId 拼接而成
      script.src = jsonpScriptSrc(chunkId);

      // create error before stack unwound to get useful stacktrace later
      var error = new Error();
      // 当脚本加载完成,执行对应回调
      onScriptComplete = function(event) {
        // 避免IE的内存泄漏
        script.onerror = script.onload = null;
        clearTimeout(timeout);
        // 去检查 chunkId 对应的 Chunk 是否安装成功,安装成功时才会存在于 installedChunks 中
        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 加载和执行完成时回调
      script.onerror = script.onload = onScriptComplete;
      document.head.appendChild(script);
    }
  }
  return Promise.all(promises);
};
复制代码

webpackJsonpCallback

webpackJsonpCallback的主要做用是每一个异步模块加载并安装。 webpack 会安装对应的 webpackJsonp 文件。

var jsonpArray = (window["webpackJsonp"] = window["webpackJsonp"] || []);
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
// 重写数组 push 方法,重写以后,每当webpackJsonp.push的时候,就会执行webpackJsonpCallback代码
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
复制代码
function webpackJsonpCallback(data) {
  //chunkIds 异步加载的文件中存放的须要安装的模块对应的 Chunk ID
  // moreModules 异步加载的文件中存放的须要安装的模块列表
  var chunkIds = data[0];
  var moreModules = data[1];

  //循环去判断对应的chunk是否已经被安装,若是,没有被安装就吧对应的chunk标记为安装。
  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的是在__webpack_require__.e 异步加载中的 installedChunks[chunkId] = [resolve, reject];的resolve
      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) {
    // 执行异步加载的全部 promise 的 resolve 函数
    resolves.shift()();
  }
}
复制代码

3、总结

原理很简单,就是利用的 jsonp 的实现原理加载模块,只是在这里并非从 server 拿数据而是从其余模块中。 总体的流程为:

  1. 加载入口 js 文件,__webpack_require__(__webpack_require__.s = 0)
  2. 执行入口 js 文件:modules[moduleId].call(module.exports, module, module.exports, webpack_require);
  • 具体执行的代码为:
(function(module, exports, __webpack_require__) {
  eval(
    'module.exports = __webpack_require__(/*! D:\\webpack\\src\\index.js */"./src/index.js");\n\n\n//# sourceURL=webpack:///multi_./src/index.js?'
  );

  /***/
});

//和
eval(
  '\r\nconst css = __webpack_require__.e(/*! import() */ 0).then(__webpack_require__.t.bind(null, /*! ./index.css */ "./src/index.css", 7))\r\nconst css2 = __webpack_require__.e(/*! import() */ 1).then(__webpack_require__.t.bind(null, /*! ./index2.css */ "./src/index2.css", 7))\r\n\n\n//# sourceURL=webpack:///./src/index.js?'
);
复制代码
  1. 因为上述代码分别__webpack_require__.e0 和 1,分别使用类jsonp的方式异步加载对应 chunk,并缓存到 promise 的 resolve 中,并标记对应 chunk 已经加载**

  2. 调用对应 chunk 模块时会在 window 上注册一个 webpackJsonp 数组,window['webpackJsonp'] = window['webpackJsonp'] || []。而且执行push操做。因为push操做是使用webpackJsonpCallback进行重写的,因此每当执行push的时候就会触发webpackJsonpCallback. webpackJsonpCallback 标记对应 chunk 已经加载并执行代码。

while (resolves.length) {
  // 执行异步加载的全部 promise 的 resolve 函数
  resolves.shift()();
}
复制代码
  1. 完成各个模块的加载

原文连接

相关文章
相关标签/搜索