前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >多应用聚合实践

多应用聚合实践

作者头像
lonelydawn
发布2023-04-01 11:24:01
1.5K0
发布2023-04-01 11:24:01
举报

iframe

在企业中,各个研发部门往往各自开发自己的应用。当需要把这些应用聚合在一起时。以往的解决方案是在主应用中嵌入 iframe,使用 iframe 加载和切换子应用页面。 这种做法有几个缺点:

  1. iframe 路径状态无法保存。当父应用页面被刷新时,iframe 会丢失跳转的路径状态(你可以将iframe中的页面状态保存在父应用的URL上,然后在刷新页面的时候从URL上读取状态再来修改iframe中的页面地址。不过这会增加父应用和子应用的耦合和通信成本。当子应用数量较多时,维护成本也会很高)。
  2. iframe 中的 DOM 是独立的。好处是 iframe 中的 DOM、CSS、JS 不会影响到父级,但坏处是当你想覆盖整个窗口来展示一个模态框时,它只会展示在 iframe 那一块区域。
  3. iframe 与父级通信困难。你可以使用 window.postMessage 来传递消息,不过你还需要实现一个消息管理中心,它需要集中管理所有的消息类型,并当接收到消息时将此下发到所有订阅它的对象以执行对应的 action,以此来降低消息管理的复杂度。
  4. iframe 中的内容需要等待iframe加载后再开始加载,白屏时间长,体验较差。
  5. iframe 中的内容不会增加主页面的搜索权重,影响 SEO。 那么,如果不使用 iframe,应该如何聚合多个应用呢? 结合前端组件化,我们可以使用动态渲染组件的方式来实现这一效果,不过需要原有项目做一些规范化的改动。

应用库化

以 React 项目为例,我们将组件挂载到DOM上时常会调用 ReactDOM.render 方法,如

代码语言:javascript
复制
ReactDOM.render(<App />, mountNode);

假如把这封装成一个方法并对外暴露,那么在别的项目中调用这个方法并传入一个待绑定的DOM节点,不就可以集成这个项目了吗? 这么做需要把应用库化。

具体实现

第一步,在入口文件导出应用绑定DOM的方法,如下

代码语言:javascript
复制
import ReactDOM from 'react-dom';
import App from './App';

export function render(mountNode) {
	ReactDOM.render(<App />, mountNode);
}

/**
 * 应在父应用的全局注入控制变量以标识所处环境,如下面的 window.__IS_FUSION_PLATFORM__
 * 若子应用未检测到该控制变量,则认为未处在父应用中,可直接初始化以便独立使用
 * 若检测到该控制变量,则认为处在父应用中,等待父应用调用即可
 */
if (!window.__IS_FUSION_PLATFORM__) {
	 render(document.getElementById('root'));
}

第二步,修改webpack配置,告诉webpack在编译时将应用库化,即入口处的导出可被其他应用引用,如下

代码语言:javascript
复制
// 其他配置省略
output: {
    library: : {
        name: 'myApp',
        type: 'umd'
    }
}

library的详细介绍可以参考官方文档,这里只讲一下在此处的作用:

  • library.name 定义库的名称
  • library.type 配置将库暴露的方式,亦即用户该以何种方式引入或使用导出的内容,可选值有"var"、 "module"、 "assign"、 "assign-properties"、 "this"、 "window"、 "self"、 "global"、 "commonjs"、 "commonjs2"、 "commonjs-module"、 "commonjs-static"、 "amd"、 "amd-require"、 "umd"、 "umd2"、 "jsonp" 以及 "system"。 选择"umd"将使导出可以以任何方式被引入或使用,不过这样会增加一些编译产出。如果能确定库在什么环境(浏览器或Node)可用,或者想以什么样的方式被引入,那么可以选定上面的一些值。 选择"umd"、"this"、"window"时,编译后的代码在浏览器环境中会将导出挂在window上面,如下图

之后,我们在其他项目中引入编译产出,即可调用导出的方法。 此外,需要注意页面和接口请求的跨域问题。在子应用中,我们可能把页面和接口放在同一个域下以避免跨域问题;但在将子应用聚合到父应用之后,若父应用和子应用不在同一个域,应将接口代理转发一下。

示例

这一节简单演示一下上述内容,主要是讲发布和引用库的过程。如果对此很了解,可以跳过。 首先使用 create-react-app 初始化hw-library和hw-app两个项目。 在hw-library中,主要做了以下几点修改:

  • 修改入口文件以导出render方法,同上一节
  • 修改webpack配置,将应用打包输出为main.js,并添加output.library配置项
  • 重新定义了App组件,如下
代码语言:javascript
复制
// App.js
export default function App() {
	return (
		<h1>HW Library</h1>
	);
}
代码语言:javascript
复制
/* App.css */
.title {
    margin-top: 200px;
    text-align: center;
}
  • 在package.json中添加module配置,以定义用户在使用esm导入时的默认入口,如下 package.json
代码语言:javascript
复制
{
	"module": "build/main.js",
}
  • 将hw-library发布到NPM。 然后,在hw-app的入口文件中,安装并使用该库,如下:
代码语言:javascript
复制
import {render} from 'hw-library/build/main';
import 'hw-library/build/main.css';

render(
    document.getElementById('root')
);

最后启动项目,就可以看到hw-library应用被渲染到了hw-app的节点上了,如下

弊端

这种通过引入JS来聚合应用的方式,被称为JS ENTRY,它是微前端框架single-spa的主要思想,这种方式存在一些弊端:

  • JS ENTRY需要明确知道要引入子应用的哪些JS和CSS文件,就像你在加载antd、swiper等库的一些组件库时,非常定制化。若想设计的通用一些,则需要将子应用打包成一个整体输出,这将导致子应用失去按需加载、资源缓存等优势。
  • 在将子应用的资源文件引入父应用之后,其中定义的全局变量和样式会影响父应用中的其它内容。 qiankun是另一款微前端框架,它在此single-spa的基础上使用HTML ENTRY代替了JS ENTRY,解决了这些弊端。

qiankun

微前端

在微前端的架构中,页面并不是作为一个整体开发的,而是由各个独立维护的组件拼接而成的,这些组件可以复用于任何页面,而一个页面也完全可以由不同的组件异构出多样化的呈现。 single-spa的文档中有对微前端概念的一些介绍,如下:

  • 微前端是指存在于浏览器中的微服务。
  • 微前端作为用户界面的一部分,通常由许多组件组成,并使用类似于React、Vue和Angular等框架来渲染组件。每个微前端可以由不同的团队进行管理,并可以自主选择框架。虽然在迁移或测试时可以添加额外的框架,出于实用性考虑,建议只使用一种框架。
  • 每个微前端都拥有独立的git仓库、package.json和构建工具配置。因此,每个微前端都拥有独立的构建进程和独立的部署/CI。这通常意味着,每个仓库能快速构建。

HTML ENTRY

与JS ENTRY思想不同的是,qiankun使用HTML ENTRY,也就是你需要将webpack打包编译出来的HTML给到qiankun而不是JS。qiankun将使用一系列的正则表达式将里面的HTML、CSS、JS全部匹配出来,这个功能主要依赖于第三方库import-html-entry的importHTML方法,如下:

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>test</title>
	<link href="https://unpkg.com/antd@3.13.6/dist/antd.min.css" rel="stylesheet">
	<link href="https://unpkg.com/bootstrap@4.3.1/dist/css/bootstrap-grid.min.css" rel="stylesheet">
</head>
<body>
	<script src="./a.js"></script>
	<script ignore>alert(1)</script>
	<script src="./b.js"></script>
	<script src="https://unpkg.com/mobx@5.0.3/lib/mobx.umd.js"></script>
	<script src="https://unpkg.com/react@16.4.2/umd/react.production.min.js"></script>
</body>
</html>

在被导入的HTML中,我们引入了antd和bootstrap两个外部样式文件、a.js和b.js两个本地外部文件、mobx和react两个外部JS文件。

代码语言:javascript
复制
// index.js
import importHTML from 'import-html-entry';

importHTML('./index.html').then(
	result => {
		console.log('template: ', result.template);

		result.execScripts().then(exports => {
			console.log('entry exports: ', exports);
		});

		result.getExternalScripts().then(exports => {
			console.log('external scripts: ', exports);
		});

		result.getExternalStyleSheets().then(exports => {
			console.log('external styles: ', exports);
		});
	}
)

在导入HTML文件的代码中,我们将importHTML的解析结果打印出来,如下:

这样,我们就可以就可以将每个子应用的CSS和JS分离出来了。为了避免多个应用挂载的CSS和JS互相影响或冲突,qiankun 对其分别做了处理。

隔离CSS

隔离CSS有两种模式,一种为shadowDOM,另一种为scoped CSS。

shadowDOM

你可以理解shadowDOM为DOM中DOM,他对内部的DOM和CSS做了封装,也就是shadowDOM中的CSS只会影响其挂载节点内的DOM样式,不会影响外部的样式。shadowDOM并不存在于DOM树上,但通过一些内置组件还是能够看到他们的存在,比如video、audio的播放控制栏。 我们可以使用attachShadow给指定元素挂在一个shadow dom,并返回对shadow root的引用,如下:

代码语言:javascript
复制
// Element.attachShadow
const shadowroot = document.getEelementById('target').attachShadow({mode: 'open'});
shadowroot.innerHTML = `
	<div>
		<style></style>
		<div>test</div>
	</div>
`;

这样就可以解决子应用节点下的CSS隔离问题了。

scoped CSS

在HTML ENTRY这一节,我们讲过可以使用import-html-entry将所有style标签解析出来、对于外部link标签中的样式也可以另外用fetch请求到。这样我们就可以将子应用的所有样式代码拿到了。 scoped CSS隔离CSS代码需要对子应用的代码进行特殊处理,也就是将所有CSS选择器前面加一个父级元素,如下

代码语言:javascript
复制
/* 原来为span,加上父级后为 */
div[data-app-name=myApp] span {
	/* ... */
}

这样,子应用的样式代码就只会作用在data-app-name=myApp的div下面了。

隔离JS

在隔离JS时,qiankun使用了沙箱模式,分为三种:SanpshotSandbox、LegacySandbox、ProxySandbox。

SanpshotSandbox

SanpshotSandbox(快照沙箱)的原理是将主应用的window对象做浅拷贝,将 window 的键值对存成一个 Hash Map。之后无论微应用对 window 做任何改动,当要在恢复环境时,把这个 Hash Map 又应用到 window 上就可以了。 过程如下:

  • 微应用 mount 时
    • 先把上一次记录的变更 modifyPropsMap 应用到微应用的全局 window,没有则跳过
    • 浅复制主应用的 window key-value 快照,用于下次恢复全局环境
  • 微应用 unmount 时
    • 将当前微应用 window 的 key-value 和 快照 的 key-value 进行 diff,diff 出来的结果用于下次恢复微应用环境的依据
    • 将上次快照的 key-value 拷贝到主应用的 window 上,以此恢复环境 Q:为什么只要拷贝和还原window对象就能实现环境切换?在函数作用域中声明的变量也能被还原吗? A:函数作用域链。
LegacySandbox

LegacySandbox 是基于 SanpshotSandbox 的一种优化模式。SanpshotSandbox 在子应用每次unmount时,都需要对window上的每个属性值进行一次diff,不是那么优雅。 LegacySandbox的想法则是 通过监听对 window 的修改来直接记录 diff 内容,因为只要对 window 属性进行设置,那么就会有两种情况:

  • 如果是新增属性,那么存到 addedMap 里
  • 如果是更新属性,那么把原来的键值存到 prevMap,把新的键值存到 newMap 通过 addedMap、prevMap、newMap 就能推断出微应用和原有环境的变化,qiankun也能以此来恢复环境。 Q:怎么实现对window上的属性变化的监听? A:Proxy
ProxySandbox

SanpshotSandbox和LegacySandbox都是单例模式下使用的沙箱,即父应用中只同时展示一个子应用,无论set和get都是直接操作window对象。如果想在父应用中同时展示多个子应用,这两种模式依然会有环境污染的问题。 为了避免真实的 window 被污染,qiankun 实现了 ProxySandbox。它的想法是:

  • 把 window 上的原生属性(如document,location)拷贝出来,单独放在一个对象上,这个对象称为 fakeWindow
  • 给每一个子应用分配一个 fakeWindow
  • 当子应用修改全局变量时
    • 如果是原生属性,则修改全局 window
    • 如果是非原生属性,则修改 fakeWindow
  • 微应用获取全局变量时
    • 如果是原生属性,则从 window 里拿
    • 如果不是原生属性,则优先从 fakeWindow 里获取 Q:qiankun是怎么为每一个子应用分配 fakeWindow 的呢? A:回到HTML ENTRY这一节,我们可以拿到所有的CSS代码,同样也可以拿到所有JS代码。那么,只需要在挂载这些JS代码时,稍微处理一下就好,如下:
代码语言:javascript
复制
// scriptText 为子应用的JS代码
const executableScript = `
  ;(function(window, self, globalThis){
    ;${scriptText}${sourceUrl}
  }).bind(window.proxy)(window.proxy, window.proxy, window.proxy);
`
eval(executableScript)

可以发现这里的代码做了三件事:

  • 把要执行 JS 代码放在一个立即执行函数中,且函数入参有 window, self, globalThis
  • 给这个函数 绑定上下文 window.proxy
  • 执行这个函数,并 把上面提到的沙箱对象 window.proxy 作为入参分别传入 因此,当我们在 JS 文件里有 window.a = 1 时,实际上会变成:
代码语言:javascript
复制
function fn(window, self, globalThis) {
  window.a = 1;
}
const bindedFn = fn.bind(window.proxy);
bindedFn(window.proxy, window.proxy, window.proxy);

那么,在子应用中修改的是window.a,到父应用中修改的则是 window.proxy.a了。这样就实现了为子应用分配fakeWindow。 qiankun的具体用法可以参考官方文档

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-04-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • iframe
  • 应用库化
    • 具体实现
      • 示例
        • 弊端
        • qiankun
          • 微前端
            • HTML ENTRY
              • 隔离CSS
                • shadowDOM
                • scoped CSS
              • 隔离JS
                • SanpshotSandbox
                • LegacySandbox
                • ProxySandbox
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档