微前端
- 微前端的概念借鉴自后端的微服务,主要是为了解决大型工程在变更、维护、扩展等方面的困难而提出
- 严格来讲,它们只是用于解决微前端中运行时容器的相关问题。除了运行时容器,一套完整的微前端方案还需要解决版本管理、质量管控、配置下发、线上监控、灰度发布、安全监测等与工程和平台相关的问题,而这些问题中的大部分工作目前仍处于探索阶段
- 在微前端架构中,各个子应用可以基于不同的技术框架
- 各个子应用将一些特定的业务功能封装在一个业务黑箱中,只对外暴露少量生命周期方法;基座应用根据路由地址变化,动态地加载对应的业务黑箱,并将其渲染到指定的占位DOM元素上
- 微前端也可以一次加载多个业务黑箱,这称为多实例模式(类似于vue-router的命名视图)
主流微前端方案
- iframe:技术难度低,隔离性和兼容性很好,但是性能和使用体验比较差,多用于集成第三方系统;
- 基座模式,主要基于路由分发,
qiankun
和single-spa
就是基于这种模式- 主要基于路由分发,即由一个基座应用来监听路由,并按照路由规则来加载不同的应用,以实现应用间解耦;
- 组合式集成,即单独构建组件,按需加载,类似npm包的形式
- EMP,主要基于
Webpack5 Module Federation
- 基于
Webpack5 Module Federation
,一种去中心化的微前端实现方案,它不仅能很好地隔离应用,还可以轻松实现应用间的资源共享和通信;
- 基于
- Web Components
无界 (wujie-micro.github.io)
js
// 这段代码是使用无界前端框架(WujieVue)创建的一个组件,并设置了一些属性和事件。
<!--
<WujieVue
width="100%"
height="100%"
name="xxx"
url="http://192.168.88.128:8001/"
:sync="true"
:fetch="fetch"
:props="props"
:beforeLoad="beforeLoad"
:beforeMount="beforeMount"
:afterMount="afterMount"
:beforeUnmount="beforeUnmount"
:afterUnmount="afterUnmount"
></WujieVue>
-->
- `width` 和 `height` 属性定义了组件的宽度和高度,分别设置为100%。
- `name` 属性定义了组件的名称,设置为"xxx"。
- `url` 属性定义了组件加载的URL地址,设置为"xxx"。
- `sync` 属性定义了组件是否同步加载,设置为true表示同步加载。
- `fetch` 属性定义了组件加载数据的方法,可以自定义实现。
- `props` 属性定义了组件的属性,可以传递给组件的数据。
- `beforeLoad` 、 `beforeMount` 、 `afterMount` 、 `beforeUnmount` 和 `afterUnmount` 是组件的生命周期钩子函数,可以在相应的阶段执行一些操作。
无界
概念
实现方案
- js 沙箱机制
- 将子应用的
js
注入主应用同域的iframe
中运行 iframe
是一个原生的window
沙箱,内部有完整的history
和location
接口- 子应用实例
instance
运行在iframe
中,路由也彻底和主应用解耦,可以直接在业务组件里面启动应用 - css 沙箱机制
- 采用webcomponent来实现页面的样式隔离,
- 无界会创建一个
wujie
自定义元素,然后将子应用的完整结构渲染在内部 - 子应用的实例
instance
在iframe
内运行,dom
在主应用容器下的webcomponent
内,通过代理iframe
的document
到webcomponent
,可以实现两者的互联
- 通信机制 3种
- props 注入
- 子应用通过
$wujie.props
可以轻松拿到主应用注入的数据
- 子应用通过
- window.parent 通信机制
- 子应用
iframe
沙箱和主应用同源,子应用可通过window.parent
通信
- 子应用
- 去中心化的通信机制
- 无界提供了
EventBus
实例,注入到主应用和子应用,所有的应用可以去中心化的进行通信
- 无界提供了
- props 注入
运行模式
单例模式
alive:false
需要进行子应用的 生命周期改造
- 在单例式下,改变 url 子应用的路由会发生跳转到对应路由
ts
// 【生命周期改造】此处以vite为例,更多参看官网
declare global {
interface Window {
// 是否存在无界
__POWERED_BY_WUJIE__?: boolean;
// 子应用mount函数
__WUJIE_MOUNT: () => void;
// 子应用unmount函数
__WUJIE_UNMOUNT: () => void;
// 子应用无界实例
__WUJIE: { mount: () => void };
}
}
if (window.__POWERED_BY_WUJIE__) {
let instance: any;
window.__WUJIE_MOUNT = () => {
const router = createRouter({ history: createWebHistory(), routes });
instance = createApp(App)
instance.use(router);
instance.mount("#app");
};
window.__WUJIE_UNMOUNT = () => {
instance.unmount();
};
/*
由于vite是异步加载,而无界可能采用fiber执行机制
所以mount的调用时机无法确认,框架调用时可能vite
还没有加载回来,这里采用主动调用防止用没有mount
无界mount函数内置标记,不用担心重复mount
*/
window.__WUJIE.mount()
} else {
createApp(App).use(createRouter({ history: createWebHistory(), routes })).mount("#app");
}
保活模式
alive:true
- 改变 url 子应用的路由不会发生变化,需要采用 通信 的方式对子应用路由进行跳转
- 保活状态下
startApp
无法更改子应用路由,不同菜单栏无法跳转到指定子应用路由,推荐单例模式
- 保活状态下
- 子应用实例
instance
和webcomponent
都不会销毁,子应用的状态和路由都不会丢失
重建模式
路由同步
路由同步会将子应用路径的
path+query+hash
通过window.encodeURIComponent
编码后挂载在主应用url
的查询参数上,其中key
值为子应用的 name。确保 刷新浏览器或者将
url
分享出去子应用的路由状态都不会丢失;当一个页面存在多个子应用时无界支持所有子应用路由同步,浏览器刷新、前进、后退子应用路由状态也都不会丢失
- 路由同步 | 短路径: 用于缩短 url 中子应用路径展示
路由跳转
主应用为 history 模式
vue
<!-- 子应用间跳转 -->
<!-- 主应用代码: -->
<template>
<!-- 子应用 A -->
<wujie-vue name="A" url="//hostA.com" :props="{jump}" ></WujieVue>
</template>
<script>
export default {
methods: {
jump(location) {
this.$router.push(location);
}
}
</script>
<!-- 直接打开其他子应用
子应用A 代码; /pathB 为子应用B的路径 -->
window.$wujie?.props.jump({ path: "/pathB" });
<!-- 进入子应用的指定路由
要求:子应用 B 开启路由同步能力
注意这种办法只有在 B 应用未曾激活过才生效?
-->
window.$wujie?.props.jump({ path: "/pathB", query: { B: "/test" } });
<!-- 进入子应用的指定路由
子应用B已经激活过时:
-->
window.$wujie?.bus.$emit("routeChange", "/test"); // 子应用A 执行跳转
window.$wujie?.bus.$on("routeChange", (path) => this.$router.push({ path })); // 子应用 B 监听并跳转
<!--【主应用为 hash 模式 】 子应用间跳转 -->
主应用为hash模式
当主应用为 hash 模式时,主应用路由的 query 参数会挂载到 hash 的值后面,而无界路由同步读取的是 url 的 query 查询参数,所以需要手动的挂载查询参数
vue
<!-- 直接打开其他子应用【同上】
子应用A 代码; /pathB 为子应用B的路径 -->
window.$wujie?.props.jump({ path: "/pathB" });
<!-- 进入子应用的指定路由【子应用未实例化过】 -->
<!-- 1.主应用 的 jump 修改: -->
<template>
<wujie-vue name="A" url="//hostA.com" :props="{jump}" ></wujie-vue>
</template>
<script>
export default {
methods: {
jump(location, query) {
// 跳转到主应用B页面
this.$router.push(location);
const url = new URL(window.location.href);
url.search = query
// 手动的挂载url查询参数
window.history.replaceState(null, "", url.href);
}
}
</script>
<!-- 2.子应用 B 开启路由同步能力 -->
<!-- 3.子应用的点击跳转函数: -->
function handleJump() {
window.$wujie?.props.jump({ path: "/pathB" } , `?B=${window.encodeURIComponent("/test")}`});
}
<!-- 子应用为保活应用,【实例化过】 同上 -->
通信方式
props通信
js
// 主应用·通过props注入数据/方法
<WujieVue name="xxx" url="xxx" :props="{ data: xxx, methods: xxx }"></WujieVue>
// 子应用通过 $wujie 获取
const props = window.$wujie?.props; // {data: xxx, methods: xxx}
window 通信
由于子应用运行的
iframe
的src
和主应用是同域的,所以相互可以直接通信
js
// 主应用调用子应用的全局数据
window.document.querySelector("iframe[name=子应用id]").contentWindow.xxx;
// 子应用调用主应用的全局数据
window.parent.xxx;
eventBus [推荐]
无界提供一套去中心化的通信方案,主应用和子应用、子应用和子应用都可以通过这种方式方便的进行通信, 详见 api
js
// 主应用中
// 如果使用wujie
import { bus } from "wujie";
// 如果使用wujie-vue
import WujieVue from "wujie-vue";
const { bus } = WujieVue;
// 如果使用wujie-react
import WujieReact from "wujie-react";
const { bus } = WujieReact;
// 主应用监听事件
bus.$on("事件名字", function (arg1, arg2, ...) {});
// 主应用发送事件
bus.$emit("事件名字", arg1, arg2, ...);
// 主应用取消事件监听
bus.$off("事件名字", function (arg1, arg2, ...) {});
// 子应用中
// 子应用监听事件
window.$wujie?.bus.$on("事件名字", function (arg1, arg2, ...) {});
// 子应用发送事件
window.$wujie?.bus.$emit("事件名字", arg1, arg2, ...);
// 子应用取消事件监听
window.$wujie?.bus.$off("事件名字", function (arg1, arg2, ...) {});
主应用
js
import { bus, setupApp, preloadApp, startApp, destroyApp } from "wujie";
/* setupApp 设置子应用[可选],由于preloadApp和startApp参数重复,为了避免重复输入,可以通过setupApp来设置默认参数。*/
/* preloadApp 预加载 */
type preOptions {
/** 唯一性用户必须保证 */
name: string;
/** 需要渲染的url */
url: string;
/** 需要渲染的html, 如果用户已有则无需从url请求 */
html?: string;
/** 注入给子应用的数据 */
props?: { [key: string]: any };
/** 自定义运行iframe的属性 */
attrs?: { [key: string]: any };
/** 自定义降级渲染iframe的属性 */
degradeAttrs?: { [key: string]: any };
/** 代码替换钩子 */
replace?: (code: string) => string;
/** 自定义fetch,资源和接口 */
fetch?: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
/** 子应用保活模式,state不会丢失 */
alive?: boolean;
/** 预执行模式 */
exec?: boolean; // exec:true, 开启预加载
/** js采用fiber模式执行 */
fiber?: boolean;
/** 子应用采用降级iframe方案 */
degrade?: boolean;
/** 子应插件 */
plugins: Array<plugin>;
/** 子应用生命周期 */
beforeLoad?: lifecycle;
/** 没有做生命周期改造的子应用不会调用 */
beforeMount?: lifecycle;
afterMount?: lifecycle;
beforeUnmount?: lifecycle;
afterUnmount?: lifecycle;
/** 非保活应用不会调用 */
activated?: lifecycle;
deactivated?: lifecycle;
/** 子应用资源加载失败后调用 */
loadError?: loadErrorHandler
};
/* startApp 启动子应用 */
子应用
ServerComponents
- Next.js