详解无界微前端是如何渲染子应用的demo解析

正文

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

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

无界渲染子应用的步骤

无界与其他微前端框架(例如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 主要有以下流程:

  • 创建 iframe 的 DOM,并设置属性
  • 将 iframe 插入到 document 中(此时 iframe 会立即访问 src)
  • 停止 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




    
    


	

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

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



    
    



	
  • 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




    
    



	

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




    
-   
+