Vite微前端实践,实现⼀个组件化的⽅案
什么是微前端
微前端是⼀种多个团队通过独⽴发布功能的⽅式来共同构建现代化 web 应⽤的技术⼿段及⽅法策略。
微前端借鉴了微服务的架构理念,将⼀个庞⼤的前端应⽤拆分为多个独⽴灵活的⼩型应⽤,每个应⽤都可以独⽴开发、独⽴运⾏、独⽴部署,再将这些⼩型应⽤联合为⼀个完整的应⽤。微前端既可以将多个项⽬融合为⼀,⼜可以减少项⽬之间的耦合,提升项⽬扩展性,相⽐⼀整块的前端仓库,微前端架构下的前端仓库倾向于更⼩更灵活。
特性
技术栈⽆关 主框架不限制接⼊应⽤的技术栈,⼦应⽤可⾃主选择技术栈
独⽴开发/部署 各个团队之间仓库独⽴,单独部署,互不依赖
增量升级 当⼀个应⽤庞⼤之后,技术升级或重构相当⿇烦,⽽微应⽤具备渐进式升级的特性
独⽴运⾏时 微应⽤之间运⾏时互不依赖,有独⽴的状态管理
提升效率 应⽤越庞⼤,越难以维护,协作效率越低下。微应⽤可以很好拆分,提升效率
⽬前可⽤的微前端⽅案
微前端的⽅案⽬前有以下⼏种类型:
基于 iframe 完全隔离的⽅案
作为前端开发,我们对 iframe 已经⾮常熟悉了,在⼀个应⽤中可以独⽴运⾏另⼀个应⽤。它具有显著的优点:
1. ⾮常简单,⽆需任何改造
2. 完美隔离,JS、CSS 都是独⽴的运⾏环境
3. 不限制使⽤,页⾯上可以放多个 iframe 来组合业务
当然,缺点也⾮常突出:
1. ⽆法保持路由状态,刷新后路由状态就丢失
2. 完全的隔离导致与⼦应⽤的交互变得极其困难
3. iframe 中的弹窗⽆法突破其本⾝
4. 整个应⽤全量资源加载,加载太慢
这些显著的缺点也催⽣了其他⽅案的产⽣。
基于 single-spa 路由劫持⽅案
single-spa 通过劫持路由的⽅式来做⼦应⽤之间的切换,但接⼊⽅式需要融合⾃⾝的路由,有⼀定的局限性。
qiankun 孵化⾃蚂蚁⾦融科技基于微前端架构的云产品统⼀接⼊平台。它对 single-spa 做了⼀层封装。主要解决了 single-spa 的⼀些痛点和不⾜。通过 import-html-entry 包解析 HTML 获取资源路径,然后对资源进⾏解析、加载。
通过对执⾏环境的修改,它实现了 JS 沙箱、样式隔离 等特性。
京东 micro-app ⽅案
京东 micro-app 并没有沿袭 single-spa 的思路,⽽是借鉴了 WebComponent 的思想,通过 CustomEl
ement 结合⾃定义的 ShadowDom,将微前端封装成⼀个类 webComponents 组件,从⽽实现微前端的组件化渲染。
在 Vite 上使⽤微前端
我们从 我们从 UmiJS 迁移到了 Vite 之后,微前端也成为了势在必⾏,当时也调研了很多⽅案。
为什么没⽤ qiankun
qiankun 是⽬前是社区主流微前端⽅案。它虽然很完善、流⾏,但最⼤的问题就是不⽀持 Vite。它基于 import-html-entry 解析 HTML 来获取资源,由于 qiankun 是通过 eval 来执⾏这些 js 的内容,⽽ Vite 中的 script 标签类型是 type="module",⾥⾯包含 import/export 等模块代码, 所以会报错:不允许在⾮ type="module" 的 script ⾥⾯使⽤ import。
退⼀步实现,我们采⽤了 single-spa 的⽅式,并使⽤ systemjs 的⽅式进⾏了微前端加载⽅案,也踩了不少的坑。single-spa 没有⼀个友好的教程来接⼊,⽂档虽然多,但⼤多都在讲概念,当时让⼈觉得有⼀种深奥的感觉。
后来看了它的源码发现,这都是些什么……⾥⾯⼤部分代码都是围绕路由劫持⽽展开的,根本没有⽂档上那种⾼⼤上的感觉。⽽我们⼜⽤不到它路由劫持的功能,那我们为什么要⽤它?
从组件化的层⾯来说 single-spa 这种⽅式实现得⼀点都不优雅。
1. 它劫持了路由,与 react-router 和组件化的思维格格不⼊
2. 接⼊⽅式⼀⼤堆繁杂的配置
3. 单实例的⽅案,即同⼀时刻,只有⼀个⼦应⽤被展⽰
后来琢磨着 single-spa 的缺点,我们可以⾃⼰实现⼀个组件化的微前端⽅案。
如何实现⼀个简单、透明、组件化的⽅案
通过组件化思维实现⼀个微应⽤⾮常简单:⼦应⽤导出⼀个⽅法,主应⽤加载⼦应⽤并调⽤该⽅法,并传⼊⼀个 Element 节点参数,⼦应⽤得到该 Element 节点,将本⾝的组件 appendChild 到 Element 节点上。
类型约定
在此之前我们需要约定⼀个主应⽤与⼦应⽤之间的⼀个交互⽅式。主要通过三个钩⼦来保证应⽤的正确执⾏、更新、和卸载。类型定义:
export interface AppConfig {
// 挂载
mount?: (props: unknown) => void;
// 更新
render?: (props: unknown) => ReactNode | void;
// 卸载
unmount?: () => void;
}
⼦应⽤导出
通过类型的约定,我们可以将⼦应⽤导出:mount、render、unmount 为主要钩⼦。
React ⼦应⽤实现:
export default (container: HTMLElement) => {
let handleRender: (props: AppProps) => void;
// 包裹⼀个新的组件,⽤作更新处理
function Main(props: AppProps) {
const [state, setState] = React.useState(props);
// 将 setState ⽅法提取给 render 函数调⽤,保持⽗⼦应⽤触发更新
handleRender = setState;
return <App {...state} />;
}
return {
mount(props: AppProps) {
},
render(props: AppProps) {
handleRender?.(props);
},
unmount() {
ReactDOM.unmountComponentAtNode(container);
},
};
};
Vue ⼦应⽤实现:
react组件之间通信import { createApp } from 'vue';
import App from './App.vue';
export default (container: HTMLElement) => {
// 创建
const app = createApp(App);
return {
mount() {
// 装载
},
unmount() {
// 卸载
app.unmount();
},
};
};
主应⽤实现
React 实现
其核⼼代码仅⼗余⾏,主要处理与⼦应⽤交互 (为了易读性,隐藏了错误处理代码):
export function MicroApp({ entry, ...props }: MicroAppProps) {
/
/ 传递给⼦应⽤的节点
const containerRef = useRef<HTMLDivElement>(null);
// ⼦应⽤配置
const configRef = useRef<AppConfig>();
useLayoutEffect(() => {
import(/* @vite-ignore */ entry).then((res) => {
// 将 div 传给⼦应⽤渲染
const config = res.default(containerRef.current);
// 调⽤⼦应⽤的装载⽅法
configRef.current = config;
});
return () => {
// 调⽤⼦应⽤的卸载⽅法
configRef.current?.unmount?.();
configRef.current = undefined;
};
}, [entry]);
return <div ref={containerRef}>{configRef.current?.render?.(props)}</div>;
}
完成,现在已经实现了主应⽤与⼦应⽤的装载、更新、卸载的操作。现在,它是⼀个组件,可以同时渲染出多个不同的⼦应⽤,这点就⽐single-spa 优雅很多。
entry ⼦应⽤地址,当然真实情况会根据 dev 和 prod 模式给出不同的地址:
<MicroApp className="micro-app" entry="//localhost:3002/src/main.tsx" />
Vue 实现
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
const { entry, ...props } = defineProps<{ entry: string }>();
const container = ref<HTMLDivElement | null>(null);
const config = ref();
onMounted(() => {
const element = container.value;
import(/* @vite-ignore */ entry).then((res) => {
// 将 div 传给⼦应⽤渲染
const config = res.default(element);
// 调⽤⼦应⽤的装载⽅法
config.value = config;
});
});
onUnmounted(() => {
// 调⽤⼦应⽤的卸载⽅法
config.value?.unmount?.();
});
</script>
<template>
<div ref="container"></div>
</template>

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。