无界微前端是如何渲染子应用的?(上)

本文涉及的产品
云解析 DNS,旗舰版 1个月
.cn 域名,1个 12个月
全局流量管理 GTM,标准版 1个月
简介: 无界微前端是如何渲染子应用的?(上)

经过我们团队的调研,我们选择了无界作为微前端的技术栈。目前的使用效果非常好,不仅性能表现出色,而且使用体验也不错。

尽管在使用的过程中,我们也遇到了一些问题,但这些问题往往源于我们对框架实现的不熟悉。我们深入研究了无界技术的源码,并将在本文中与大家分享。本文将重点探讨无界微前端如何渲染子应用的


无界渲染子应用的步骤


无界与其他微前端框架(例如qiankun)的主要区别在于其独特的 JS 沙箱机制。无界使用 iframe 来实现 JS 沙箱,由于这个设计,无界在以下方面表现得更加出色:

  • 应用切换没有清理成本
  • 允许一个页面同时激活多个子应用
  • 性能相对更优

无界渲染子应用,主要分为以下几个步骤:

  • 创建子应用 iframe
  • 解析入口 HTML
  • 创建 webComponent,并挂载 HTML
  • 运行 JS 渲染 UI

创建子应用 iframe


要在 iframe 中运行 JS,首先得有一个 iframe。


export function iframeGenerator(
  sandbox: WuJie,
  attrs: { [key: string]: any },
  mainHostPath: string,
  appHostPath: string,
  appRoutePath: string
): HTMLIFrameElement {
  // 创建 iframe 的 DOM
  const iframe = window.document.createElement("iframe");
  // 设置 iframe 的 attr
  setAttrsToElement(iframe, { 
      // iframe 的 url 设置为主应用的域名
      src: mainHostPath, 
      style: "display: none", 
      ...attrs, 
      name: sandbox.id, 
      [WUJIE_DATA_FLAG]: "" 
  });
  // 将 iframe 插入到 document 中
  window.document.body.appendChild(iframe);
  const iframeWindow = iframe.contentWindow;
  // 停止 iframe 的加载
  sandbox.iframeReady = stopIframeLoading(iframeWindow).then(() => {
      // 省略其他内容
  }
  // 注入无界的变量到 iframeWindow,例如 __WUJIE
  patchIframeVariable(iframeWindow, sandbox, appHostPath);                                                           
  // 省略其他内容
  return iframe;
}

创建 iframe 主要有以下流程:

  1. 创建 iframe 的 DOM,并设置属性
  2. 将 iframe 插入到 document 中(此时 iframe 会立即访问 src)
  3. 停止 iframe 的加载(stopIframeLoading)

为什么要停止 iframe 的加载?

因为要创建一个纯净的 iframe,防止 iframe 被污染,假如该 url 的 JS 代码,声明了一些全局变量、函数,就可能影响到子应用的运行(假如子应用也有同名的变量、函数)

为什么 iframe 的 src 要设置为主应用的域名

为了实现应用间(iframe 间)通讯,无界子应用 iframe 的 url 会设置为主应用的域名(同域)

  • 主应用域名为 a.com
  • 子应用域名为 b.com,但它对应的 iframe 域名为 a.com,所以要设置 b.com 的资源能够允许跨域访问

因此 iframe 的 location.href 并不是子应用的 url。


解析入口 HTML


iframe 中运行 js,首先要知道要运行哪些 js

我们可以通过解析入口 HTML 来确定需要运行的 JS 内容

假设有以下HTML


<!DOCTYPE html>
<html lang="en">
<head>
    <script defer="defer" src="./static/js/main.4000cadb.js"></script>
    <link href="./static/css/main.7d8ad73e.css" rel="stylesheet">
</head>
<body>
  <div id="root"></div>
</body>
</html>

经过 importHTML 处理后,结果如下:

  • template 模板部分,去掉了所有的 script 和 style


<!DOCTYPE html>
<html lang="en">
<head>
    <!-- defer script https://wujie-micro.github.io/demo-react16/static/js/main.4000cadb.js replaced by wujie -->
    <!--  link https://wujie-micro.github.io/demo-react16/static/css/main.7d8ad73e.css replaced by wujie -->
</head>
</head>
<body>
  <div id="root"></div>
</body>
</html>
  • getExternalScripts,获取所有内联和外部的 script


[
    {
      async: false,
      defer: true,
      src: 'https://wujie-micro.github.io/demo-react16/static/js/main.4000cadb.js',
      module: false,
      crossorigin: false,
      crossoriginType: '',
      ignore: false,
      contentPromise: // 获取 script 内容字符串的 Promise
  }
]
  • getExternalStyleSheets,获取所有内联和外部的 style


[
    {
        src: "https://wujie-micro.github.io/demo-react16/static/css/main.7d8ad73e.css",
        ignore: false,
        contentPromise: // 获取 style 内容字符串的 Promise
    }
]

为什么要将 script 和 style 从 HTML 中分离?

  • HTML 要作为 webComponent 的内容,挂载到微前端挂载点上
  • 因为无界有插件机制,需要单独对 js/style 进行处理,再插入到 webComponent 中
  • script 除了需要经过插件处理外,还需要放到 iframe 沙箱中执行,因此也要单独分离出来

external 是外部的意思,为什么 getExternalScripts 拿到的却是所有的 script,而不是外部的非内联 script?

external 是相对于解析后的 HTML 模板来说的,由于解析后的 HTML 不带有任何的 js 和 css,所以这里的 external,就是指模板外的所有 JS

无界与 qiankun 的在解析 HTML 上区别?

无界和 qiankun 都是以 HTML 为入口的微前端框架。qiankun 基于 import-html-entry 解析 HTML,而无界则是借鉴 import-html-entry  代码,实现了自己的 HTML 的解析,因此两者在解析 HTML 上的不同,主要是在importHTML 的实现上。

由于无界支持执行 esModule script,需要在分析的结果中,保留更多的信息


[
    {
      async: false,
      defer: true,
      src: 'https://wujie-micro.github.io/demo-react16/static/js/main.4000cadb.js',
      module: false,
      crossorigin: false,
      crossoriginType: '',
      ignore: false,
      contentPromise: // 获取 script 内容字符串的 Promise
  }
]

而  import-html-entry  的分析结果中,只有 script 的 js 内容字符串。

无界是如何获取 HTML 的外部的 script、style 内容的?

分析 HTML,可以拿到外部 scriptstyle 的 url,fetch 发起 ajax 就可以获取到 scriptstyle 的内容

但是 fetch 相对于原来 HTML script 标签,有一个坏处,就是 ajax 不能跨域,因此在使用无界的时候必须要给请求的资源设置允许跨域


处理 CSS 并重新嵌入 HTML


单独将 CSS 分离出来,是为了让无界插件能够对 对 CSS 代码进行修改,下面是一个 CSS loader 插件:


const plugins = [
  {
    // 对 css 脚本动态的进行替换
    // code 为样式代码、url为样式的地址(内联样式为'')、base为子应用当前的地址
    cssLoader: (code, url, base) => {
      console.log("css-loader", url, code.slice(0, 50) + "...");
      // do something
      return code;
    },
  },
];

无界会用以下代码遍历插件修改 CSS


// 将所有 plugin 的 CSSLoader 函数,合成一个 css-loader 处理函数
const composeCssLoader = compose(sandbox.plugins.map((plugin) => plugin.cssLoader));
const processedCssList: StyleResultList = getExternalStyleSheets().map(({ 
    src, 
    contentPromise 
}) => {
  return {
    src,
    // 传入 CSS 文本处理处理函数
    contentPromise: contentPromise.then((content) => composeCssLoader(content, src, curUrl)),
  };
});

修改后的 CSS,会存储在 processedCssList 数组中,需要遍历该数组的内容,将 CSS 重新嵌入到 HTML 中


举个例子,这是我们之前的 HTML


<!DOCTYPE html>
<html lang="en">
<head>
    <!-- defer script https://wujie-micro.github.io/demo-react16/static/js/main.4000cadb.js replaced by wujie -->
    <!--  link https://wujie-micro.github.io/demo-react16/static/css/main.7d8ad73e.css replaced by wujie -->
</head>
</head>
<body>
  <div id="root"></div>
</body>
</html>

嵌入 CSS 之后的 HTML 是这样子的


<!DOCTYPE html>
<html lang="en">
<head>
    <!-- defer script https://wujie-micro.github.io/demo-react16/static/js/main.4000cadb.js replaced by wujie -->
-   <!--  link https://wujie-micro.github.io/demo-react16/static/css/main.7d8ad73e.css replaced by wujie -->
+   <style>
+       /* https://wujie-micro.github.io/demo-react16/static/css/main.7d8ad73e.css */.
+     省略内容
+   <style/>
</head>
</head>
<body>
  <div id="root"></div>
</body>
</html>

将原来的 Link 标签替换成 style 标签,并写入 CSS 。

创建 webComponent 并挂载 HTML


在执行 JS 前,需要先把 HTML 的内容渲染出来。

无界子应用是挂载在 webComponent 中的,其定义如下:


class WujieApp extends HTMLElement {
  //  首次被插入文档 DOM 时调用
  connectedCallback(): void {
    if (this.shadowRoot) return;
    // 创建 shadowDOM
    const shadowRoot = this.attachShadow({ mode: "open" });
    // 通过 webComponent 的标签 WUJIE_DATA_ID,拿到子应用 id,再通过 id 拿到无界实例对象
    const sandbox = getWujieById(this.getAttribute(WUJIE_DATA_ID));
    // 保存 shadowDOM
    sandbox.shadowRoot = shadowRoot;
  }
  // 从文档 DOM 中删除时,被调用
  disconnectedCallback(): void {
    const sandbox = getWujieById(this.getAttribute(WUJIE_DATA_ID));
    sandbox?.unmount();
  }
}
customElements?.define("wujie-app", WujieApp);

于是就可以这样创建 webComponent


export function createWujieWebComponent(id: string): HTMLElement {
  const contentElement = window.document.createElement("wujie-app");
  // 设置 WUJIE_DATA_ID 标签,为子应用的 id‘
  contentElement.setAttribute(WUJIE_DATA_ID, id);
  return contentElement;
}

然后HTML 创建 DOM,这个非常简单


let html = document.createElement("html");
html.innerHTML = template;  // template 为解析处理后的 HTML

直接用 innerHTML 设置 html 的内容即可

然后再插入 CSS(上一小节的内容)


// processCssLoaderForTemplate 返回注入 CSS 的 html DOM 对象
const processedHtml = await processCssLoaderForTemplate(iframeWindow.__WUJIE, html)
最后挂载到 shadowDOM


shadowRoot.appendChild(processedHtml);

这样就完成了 HTML 和 CSS 的挂载了,CSS 由于在 shadowDOM 内,样式也不会影响到外部,也不会受外部样式影响。


目录
相关文章
|
5天前
|
存储 人工智能 前端开发
前端大模型应用笔记(三):Vue3+Antdv+transformers+本地模型实现浏览器端侧增强搜索
本文介绍了一个纯前端实现的增强列表搜索应用,通过使用Transformer模型,实现了更智能的搜索功能,如使用“番茄”可以搜索到“西红柿”。项目基于Vue3和Ant Design Vue,使用了Xenova的bge-base-zh-v1.5模型。文章详细介绍了从环境搭建、数据准备到具体实现的全过程,并展示了实际效果和待改进点。
|
5天前
|
前端开发 机器人 API
前端大模型入门(一):用 js+langchain 构建基于 LLM 的应用
本文介绍了大语言模型(LLM)的HTTP API流式调用机制及其在前端的实现方法。通过流式调用,服务器可以逐步发送生成的文本内容,前端则实时处理并展示这些数据块,从而提升用户体验和实时性。文章详细讲解了如何使用`fetch`发起流式请求、处理响应流数据、逐步更新界面、处理中断和错误,以及优化用户交互。流式调用特别适用于聊天机器人、搜索建议等应用场景,能够显著减少用户的等待时间,增强交互性。
|
7天前
|
前端开发
深入解析React Hooks:构建高效且可维护的前端应用
本文将带你走进React Hooks的世界,探索这一革新特性如何改变我们构建React组件的方式。通过分析Hooks的核心概念、使用方法和最佳实践,文章旨在帮助你充分利用Hooks来提高开发效率,编写更简洁、更可维护的前端代码。我们将通过实际代码示例,深入了解useState、useEffect等常用Hooks的内部工作原理,并探讨如何自定义Hooks以复用逻辑。
|
4天前
|
存储 监控 前端开发
掌握微前端架构:构建未来前端应用的基石
【10月更文挑战第12天】随着前端技术的发展,传统的单体应用架构已无法满足现代应用的需求。微前端架构通过将大型应用拆分为独立的小模块,提供了更高的灵活性、可维护性和快速迭代能力。本文介绍了微前端架构的概念、核心优势及实施步骤,并探讨了其在复杂应用中的应用及实战技巧。
|
5天前
|
人工智能 自然语言处理 运维
前端大模型应用笔记(一):两个指令反过来说大模型就理解不了啦?或许该让第三者插足啦 -通过引入中间LLM预处理用户输入以提高多任务处理能力
本文探讨了在多任务处理场景下,自然语言指令解析的困境及解决方案。通过增加一个LLM解析层,将复杂的指令拆解为多个明确的步骤,明确操作类型与对象识别,处理任务依赖关系,并将自然语言转化为具体的工具命令,从而提高指令解析的准确性和执行效率。
|
5天前
|
前端开发 JavaScript API
深度剖析:前端如何驾驭海量数据,实现流畅渲染的多种途径
深度剖析:前端如何驾驭海量数据,实现流畅渲染的多种途径
30 3
|
5天前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
|
5天前
|
机器学习/深度学习 弹性计算 自然语言处理
前端大模型应用笔记(二):最新llama3.2小参数版本1B的古董机测试 - 支持128K上下文,表现优异,和移动端更配
llama3.1支持128K上下文,6万字+输入,适用于多种场景。模型能力超出预期,但处理中文时需加中英翻译。测试显示,其英文支持较好,中文则需改进。llama3.2 1B参数量小,适合移动端和资源受限环境,可在阿里云2vCPU和4G ECS上运行。
|
5天前
|
前端开发 算法 测试技术
前端大模型应用笔记(五):大模型基础能力大比拼-计数篇-通义千文 vs 文心一言 vs 智谱 vs 讯飞vsGPT
本文对比测试了通义千文、文心一言、智谱和讯飞等多个国产大模型在处理基础计数问题上的表现,特别是通过链式推理(COT)提示的效果。结果显示,GPTo1-mini、文心一言3.5和讯飞4.0Ultra在首轮测试中表现优秀,而其他模型在COT提示后也能显著提升正确率,唯有讯飞4.0-Lite表现不佳。测试强调了COT在提升模型逻辑推理能力中的重要性,并指出免费版本中智谱GLM较为可靠。
前端大模型应用笔记(五):大模型基础能力大比拼-计数篇-通义千文 vs 文心一言 vs 智谱 vs 讯飞vsGPT
|
4天前
|
JSON 前端开发 JavaScript
构建现代前端应用的基石
【10月更文挑战第13天】构建现代前端应用的基石