Webpack--动态 import 原理及源码分析

news2024/11/29 11:57:57

前言

在平时的开发中,我们经常使用 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 回调,表示脚本加载失败

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1187614.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

5个WebGIS功能小技巧

我们在《为什么要研发WebGIS系统&#xff1f;》一文中&#xff0c;分享为什么要研发水经微图Web版的WebGIS系统。 这里&#xff0c;我们再为你分享一下水经微图Web版中的几个功能小技巧。 批量修改标注名称 在工具栏中选择“框选”工具&#xff0c;框选需要修改标注的要素。 …

自定义表单模型小程序源码系统 带完整的部署教程

大家好啊&#xff0c;今天源码小编来给大家分享一款自定义表单模型小程序源码系统。在数字化时代&#xff0c;信息收集和处理显得尤为重要。无论是企业还是个人&#xff0c;都需要通过表单来收集、整理、分析各种信息。但是&#xff0c;传统的表单构建方式往往需要编写大量的代…

电脑如何截屏?一起来揭晓答案!

在数字时代&#xff0c;截屏已经成为我们日常生活和工作中的必备技能。无论是为了捕捉有趣的网络瞬间&#xff0c;保存重要信息&#xff0c;还是为了协作和教育&#xff0c;电脑截屏都是一个强大而方便的工具。本文将介绍三种电脑如何截屏的方法&#xff0c;以满足各种需求&…

景联文科技助力金融机构强化身份验证,提供高质量人像采集服务

随着社会的数字化和智能化进程的加速&#xff0c;人像采集在金融机构身份认证领域中发挥重要作用&#xff0c;为人们的生活带来更多便利和安全保障。 金融机构在身份验证上的痛点主要包括以下方面&#xff1a; 身份盗用和欺诈风险&#xff1a;传统身份验证方式可能存在漏洞&am…

IS420ESWBH3A GE 附加配置文件和I/O组件中的单独标签

IS420ESWBH3A GE 附加配置文件和I/O组件中的单独标签 为CompactLogix、MicroLogix和ControlLogix等以太网/IP兼容型PLC用户提供了一种节省自动化机器空间、资金和布线的新方法。ClearLink提供4个运动控制轴、一个串行端口、13个可配置的数字和模拟I/O点以及可扩展的I/O。tek …

Spring源码系列-框架中的设计模式

简单工厂 实现方式&#xff1a; BeanFactory。Spring中的BeanFactory就是简单工厂模式的体现&#xff0c;根据传入一个唯一的标识来获得Bean对象&#xff0c;但是否是在传入参数后创建还是传入参数前创建这个要根据具体情况来定。 实质&#xff1a; 由一个工厂…

Linux常用命令——cancel命令

在线Linux命令查询工具 cancel 取消已存在的打印任务 补充说明 cancel命令用于取消已存在的打印任务。 语法 cancel(选项)(参数)选项 -a&#xff1a;取消所有打印任务&#xff1b; -E&#xff1a;当连接到服务器时强制使用加密&#xff1b; -U&#xff1a;指定连接服务器…

LeetCode |142. 环形链表 II

LeetCode |142. 环形链表 II OJ链接 一个指针从相遇点开始走&#xff0c;一个指针从头开始走&#xff0c;它们会在入口点相遇~~ struct ListNode *detectCycle(struct ListNode *head) {struct ListNode* slow,*fast;slow fast head;while(fast && fast->next…

扬帆起航正当时——远航汽车下线仪式在山西运城成功举办

11月8日&#xff0c;“智赢未来 远航汽车——远航汽车下线仪式”在山西省运城市大运集团新能源生产基地成功举办。运城市委书记丁小强、市长储祥好&#xff0c;以及来自省、市、区各级政府领导&#xff0c;远航汽车供应商代表、客户代表、全国主流媒体&#xff0c;大运集团各级…

eclipse的安装与配置详细教程(包括UML插件 汉化 JDK 代码补全 导入导出等)

Eclipse安装与配置详细教程 1.Eclipse安装与配置 1.将JDK与Eclipse这两个软件安装包放在一个文件夹下&#xff0c;方便之后安装使用。 2.安装JDK 在D&#xff1a;LeStoreDownload\Java文件夹下另外新建三个文件夹分别命名为java、jdk和eclipse&#xff08;分别用于Java、jdk…

必看:一组WhatsApp2023年数据合集,助你深入洞察WS营销

作为一款遍布全球的超级应用&#xff0c;WhatsApp以高人气、广覆盖和高效便利收获了几十亿用户&#xff0c;也无数次连接了用户与企业。对于WhatsApp运营布局&#xff0c;客观数据能为企业提供多方面的依据和判断。本文将从多个维度展示WhatsApp2023年数据&#xff0c;希望能为…

2023年最佳键盘:打字和游戏的顶级键盘,总共十款,总有一款适合你

只有最好的键盘才能真正提供舒适无缝的打字体验。虽然亚马逊的廉价键盘可以帮助你满足日常打字需求,但它们不会像顶级键盘那样快速和灵敏。更重要的是,他们不会优先考虑人体工程学。 任何普通的键盘都可以作为输入设备正常工作。然而,高质量的选项更准确、更快、反应更灵敏…

数据结构——二叉树(2)

接上一篇文章http://t.csdnimg.cn/nsKsW&#xff0c;本次我们接着讲解关于二叉树的相关知识。 一、二叉树的相关性质&#xff1a; 1. 若规定根节点的层数为 1 &#xff0c;则一棵非空二叉树的 第 i 层上最多有 2^(i-1) 个结点. 2. 若规定根节点的层数为 1 &#xff0c;则 深度…

10.Form表单中Input输入框设置autoComplete=“off“ 不生效

一、问题的描述 form表单的 password框 有时候我们并不需要chrome自动填充记住的密码这个效果&#xff0c;如下图 二 、正常的预期是什么&#xff1f; 输入框获取焦点时&#xff0c;不展示chrome的默认行为。 三、问题产生的原因分析 发现antd的Input组件的 autocomplete“o…

学习伦敦银交易经验的好方法:亏损

要掌握伦敦银交易的技巧&#xff0c;除了看书学习以外&#xff0c;实践的经验也是很重要的&#xff0c;而这些实践的经验中&#xff0c;从亏损中学习会让经验会更加立体和深刻。下面我们就来讨论一下亏损这个学习伦敦银交易技巧的方法。 首先我们需要了解&#xff0c;不论是伦敦…

语音芯片故障的原因简述

语音芯片在语音设备或者相关产品中应用时会出现故障情况&#xff0c;常见的故障情况更多的是无法发出声音或者声音不连贯&#xff0c;还有声音播报不完整或者混乱等情况。下面让我们来探究芯片本身内部的故障问题&#xff0c;以及外部的原因。 芯片内部自身的故障&#xff1a;…

MySQL binlog 日志解析后的exec_time导致表示什么时间?

1. exec_time 到底表示什么时间&#xff1f; MySQL binlog日志解析后&#xff0c;我们能看到会有 exec_time &#xff0c;从字面意思理解这个记录的是执行时间&#xff0c;那这个记录的到底是单条sql的执行时间&#xff1f;还是事务的执行时间&#xff1f;下面通过测试来解读一…

docker 1.13存储路径修改

由于老版本docker还没有data-root配置&#xff0c;特记录一下老版本修改配置。 新版本配置修改参考&#xff1a;https://blog.csdn.net/tootsy_you/article/details/126933702 修改步骤 编辑docker.service服务文件 vim /usr/lib/systemd/system/docker.service在EXStart添加…

图像二值化阈值调整——Triangle算法,Maxentropy方法

一. Triangle方法 算法描述&#xff1a;三角法求分割阈值最早见于Zack的论文《Automatic measurement of sister chromatid exchange frequency》主要是用于染色体的研究&#xff0c;该方法是使用直方图数据&#xff0c;基于纯几何方法来寻找最佳阈值&#xff0c;它的成立条件…

基于ssm的线上旅行信息管理系统(有报告)。Javaee项目,ssm项目。

演示视频&#xff1a; 基于ssm的线上旅行信息管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;ssm项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff0…