Webpack学习之动态import原理及源码分析

 更新时间:2023年04月26日 09:29:44   作者:runnerdancer  
这篇文章主要为大家介绍了Webpack学习之动态import原理及源码分析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

前言

在开始之前,先给我的mini-react打个广告。对react源码感兴趣的朋友,走过路过的朋友点个star

在平时的开发中,我们经常使用 import()实现代码分割和懒加载。在低版本的浏览器中并不支持动态 import(),那 webpack 是如何实现 import() polyfill 的?

原理分析

我们先来看看下面的 demo

function component() {
  const btn = document.createElement("button");
  btn.onclick = () => {
    import("./a.js").then((res) => {
      console.log("动态加载a.js..", res);
    });
  };
  btn.innerHTML = "Button";
  return btn;
}
document.body.appendChild(component());

点击按钮,动态加载 a.js脚本,查看浏览器网络请求可以发现,a.js请求返回的内容如下:

简单看,实际上返回的就是下面这个东西:

(self["webpackChunkwebpack_demo"] =
  self["webpackChunkwebpack_demo"] || []).push([
  ["src_a_js"],
  {
    "./src/a.js": () => {},
  },
]);

从上面可以看出 3 点信息:

  • 1.webpackChunkwebpack_demo 是挂到全局 window 对象上的属性
  • 2.webpackChunkwebpack_demo 是个数组
  • 3.webpackChunkwebpack_demo 有个 push 方法,用于添加动态的模块。当a.js脚本请求成功后,这个方法会自动执行。

再来看看 main.js 返回的内容

仔细观察,动态 import 经过 webpack 编译后,变成了下面的一坨东西:

__webpack_require__.e("src_a_js")
  .then(__webpack_require__.bind(__webpack_require__, "./src/a.js"))
  .then((res) => {
    console.log("动态加载a.js..", res);
  });

上面代码中,__webpack_require__ 用于执行模块,比如上面我们通过webpackChunkwebpack_demo.push添加的模块,里面的./src/a.js函数就是在__webpack_require__里面执行的。

__webpack_require__.e函数就是用来动态加载远程脚本。因此,从上面的代码中我们可以看出:

  • 首先 webpack 将动态 import 编译成 __webpack_require__.e 函数
  • __webpack_require__.e函数加载远程的脚本,加载完成后调用 __webpack_require__ 函数
  • __webpack_require__函数负责调用远程脚本返回来的模块,获取脚本里面导出的对象并返回

源码分析及实现

如何动态加载远程模块

在开始之前,我们先来看下如何使用 script 标签加载远程模块

var inProgress = {};
// url: "http://localhost:8080/src_a_js.main.js"
// done: 加载完成的回调
const loadScript = (url, done) => {
  if (inProgress[url]) {
    inProgress[url].push(done);
    return;
  }
  const script = document.createElement("script");
  script.charset = "utf-8";
  script.src = url;
  inProgress[url] = [done];
  var onScriptComplete = (prev, event) => {
    var doneFns = inProgress[url];
    delete inProgress[url];
    script.parentNode && script.parentNode.removeChild(script);
    doneFns && doneFns.forEach((fn) => fn(event));
    if (prev) return prev(event);
  };
  script.onload = onScriptComplete.bind(null, script.onload);
  document.head.appendChild(script);
};

loadScript(url, done) 函数比较简单,就是通过创建 script 标签加载远程脚本,加载完成后执行 done 回调。inProgress用于避免多次创建 script 标签。比如我们多次调用loadScript('http://localhost:8080/src_a_js.main.js', done)时,应该只创建一次 script 标签,不需要每次都创建。这也是为什么我们调用多次 import('a.js'),浏览器 network 请求只看到家在一次脚本的原因

实际上,这就是 webpack 用于加载远程模块的极简版本。

__webpack_require__.e 函数的实现

首先我们使用installedChunks对象保存动态加载的模块。key 是 chunkId

// 存储已经加载和正在加载的chunks,此对象存储的是动态import的chunk,对象的key是chunkId,值为
// 以下几种:
// undefined: chunk not loaded
// null: chunk preloaded/prefetched
// [resolve, reject, Promise]: chunk loading
// 0: chunk loaded
var installedChunks = {
  main: 0,
};

由于 import() 返回的是一个 promise,然后import()经过 webpack 编译后就是一个__webpack_require__.e函数,因此可以得出__webpack_require__.e返回的也是一个 promise,如下所示:

const scriptUrl = document.currentScript.src
  .replace(/#.*$/, "")
  .replace(/\?.*$/, "")
  .replace(/\/[^\/]+$/, "/");
__webpack_require__.e = (chunkId) => {
  return Promise.resolve(ensureChunk(chunkId, promises));
};
const ensureChunk = (chunkId) => {
  var installedChunkData = installedChunks[chunkId];
  if (installedChunkData === 0) return;
  let promise;
  // 1.如果多次调用了__webpack_require__.e函数,即多次调用import('a.js')加载相同的模块,只要第一次的加载还没完成,就直接使用第一次的Promise
  if (installedChunkData) {
    promise = installedChunkData[2];
  } else {
    promise = new Promise((resolve, reject) => {
      // 2.注意,此时的resolve,reject还没执行
      installedChunkData = installedChunks[chunkId] = [resolve, reject];
    });
    installedChunkData[2] = promise; //3. 此时的installedChunkData 为[resolve, reject, promise]
    var url = scriptUrl + chunkId;
    var error = new Error();
    // 4.在script标签加载完成或者加载失败后执行loadingEnded方法
    var loadingEnded = (event) => {
      if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId)) {
        installedChunkData = installedChunks[chunkId];
        if (installedChunkData !== 0) installedChunks[chunkId] = undefined;
        if (installedChunkData) {
          console.log("加载失败.....");
          installedChunkData[1](error); // 5.执行上面的reject,那resolve在哪里执行呢?
        }
      }
    };
    loadScript(url, loadingEnded, "chunk-" + chunkId, chunkId);
  }
  return promise;
};

__webpack_require__.e的主要逻辑在ensureChunk方法中,注意该方法里面的第 1 到第 5 个注释。这个方法创建一个 promise,并调用loadScript方法加载动态模块。需要特别主要的是,返回的 promise 的 resolve 方法并不是在 script 标签加载完成后改变。如果脚本加载错误或者超时,会在 loadingEnded 方法里调用 promise 的 reject 方法。

实际上,promise 的 resolve 方法是在脚本请求完成后,在 self["webpackChunkwebpack_demo"].push()执行的时候调用的

如何执行远程模块?

远程模块是通过self["webpackChunkwebpack_demo"].push()函数执行的

前面我们提到,a.js请求返回的内容是一个self["webpackChunkwebpack_demo"].push()函数。当请求完成,会自动执行这个函数。实际上,这就是一个 jsonp 的回调方式。该方法的实现如下:

var webpackJsonpCallback = (data) => {
  var [chunkIds, moreModules] = data;
  var moduleId,
    chunkId,
    i = 0;
  for (moduleId in moreModules) {
    // 1.__webpack_require__.m存储的是所有的模块,包括静态模块和动态模块
    __webpack_require__.m[moduleId] = moreModules[moduleId];
  }
  for (; i < chunkIds.length; i++) {
    chunkId = chunkIds[i];
    if (installedChunks[chunkId]) {
      // 2.调用ensureChunk方法生成的promise的resolve回调
      installedChunks[chunkId][0]();
    }
    // 3.将该模块标记为0,表示已经加载过
    installedChunks[chunkId] = 0;
  }
};
self["webpackChunkwebpack_demo"] = [];
self["webpackChunkwebpack_demo"].push = webpackJsonpCallback.bind(null);

所有通过import()加载的模块,经过 webpack 编译后,都会被 self["webpackChunkwebpack_demo"].push()包裹。

总结

在 webpack 构建编译阶段,import()会被编译成类似__webpack_require__.e("src_a_js").then(__webpack_require__.bind(__webpack_require__, "./src/a.js"))的调用方式

__webpack_require__
  .e("src_a_js")
  .then(__webpack_require__.bind(__webpack_require__, "./src/a.js"))
  .then((res) => {
    console.log("动态加载a.js..", res);
  });

__webpack_require__.e()方法会创建一个 script 标签用于请求脚本,方法执行完返回一个 promise,此时的 promise 状态还没改变。

script 标签被添加到 document.head 后,触发浏览器网络请求。请求成功后,动态的脚本会自动执行,此时self["webpackChunkwebpack_demo"].push()方法执行,将动态的模块添加到__webpack_require__.m属性中。同时调用 promise 的 resolve 方法改变状态,模块加载完成。

脚本执行完成后,最后执行 script 标签的 onload 回调。onload 回调主要是用于处理脚本加载失败或者超时的场景,并调用 promise 的 reject 回调,表示脚本加载失败

以上就是Webpack学习之动态import原理及源码分析的详细内容,更多关于Webpack动态import的资料请关注脚本之家其它相关文章!

相关文章

  • Element-plus安装及基础组件使用详解

    Element-plus安装及基础组件使用详解

    ElementPlus是一个基于Vue3的UI组件库,旨在提供丰富的HTML元素封装,以简化前端开发,主要特点包括预定义样式、事件处理、易用性等,为开发者提供了一致且美观的用户界面,同时支持按需导入,提高项目效率,感兴趣的朋友一起看看吧
    2024-09-09
  • 利用JavaScript实现新闻滚动效果(实例代码)

    利用JavaScript实现新闻滚动效果(实例代码)

    这篇文章主要是对利用JavaScript实现新闻滚动效果的两种方法进行了介绍,需要的朋友可以过来参考下,希望对大家有所帮助
    2013-11-11
  • javascript时间差插件分享

    javascript时间差插件分享

    这篇文章主要为大家分享了javascript时间差插件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-07-07
  • 总结Javascript中的隐式类型转换

    总结Javascript中的隐式类型转换

    这篇文章谈谈JavaScript的隐式类型转换,我们知道在JavaScript中声明变量不需指定类型, 对变量赋值也没有类型检查,同时JavaScript允许隐式类型转换。这些特征说明JavaScript属于弱类型的语言。
    2016-08-08
  • js中document.referrer实现移动端返回上一页

    js中document.referrer实现移动端返回上一页

    本文主要介绍了document.referrer实现移动端返回上一页的方法,具有很好的参考价值,下面跟着小编一起来看下吧
    2017-02-02
  • 微信小程序地图(map)组件点击(tap)获取经纬度的方法

    微信小程序地图(map)组件点击(tap)获取经纬度的方法

    这篇文章主要介绍了微信小程序地图(map)组件点击(tap)获取经纬度的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-01-01
  • JavaScript对象详解之对象属性的添加

    JavaScript对象详解之对象属性的添加

    这篇文章主要给大家介绍了关于JavaScript对象详解之js对象属性的添加的相关资料,在JavaScript中对象是通过键值对来存储数据的一种数据类型,可以通过直接给对象添加属性的方式来增加对象的属性,需要的朋友可以参考下
    2023-07-07
  • js常用排序实现代码

    js常用排序实现代码

    js常用排序,整理了,常用的数组互换。
    2010-12-12
  • JS生成随机字符串的多种方法

    JS生成随机字符串的多种方法

    这篇文章主要介绍了JS生成随机字符串的方法,需要的朋友可以参考下
    2014-06-06
  • ionic2 tabs使用 Modal底部tab弹出框

    ionic2 tabs使用 Modal底部tab弹出框

    最近做了一个项目,其中要实现这样的功能,在首页底部中间的tab弹出一个页面,而且是没有底部栏的,而不是像平常滑动的一个子页面。其实实现方法很简单,下面通过本文给大家介绍下
    2016-12-12

最新评论