主应用地址: cloudbase-100032138116.coding.net/p/applet-fe...
创建子应用脚手架:cloudbase-100032138116.coding.net/p/applet-fe...
微前端架构设计:www.garfishjs.org/blog
微前端框架对比
框架 | 介绍 | JS 沙箱 | 样式隔离 | 预加载 | 路由同步 | 数据通信 | 优点 | 缺点 | 背景 |
---|---|---|---|---|---|---|---|---|---|
single-spa | 最早的微前端框架 | 支持 | 支持 | 不支持 | 不支持 | 不支持 | - 需要自己去加载子应用- 需要自己设置js隔离和css隔离- 无法预加载 | ||
Garfish | 基于 SPA 的微前端架构 | 支持 | 支持 | 支持 | 支持 | 支持 | 和 qiankun 的 api 相似,改造成本低。没有基于 single-spa,而是重新实现的,项目的灵活性高,解决了 qiankun 的一些痛点。 | 字节 | |
qiankun | 基于 single-spa 的微前端实现库 | 支持 | 支持 | 支持 | 支持 | 支持 | 使用人数最多,沉淀了很多踩坑经验。 | - 无法支持 vite 等 ESM 脚本运行- 样式隔离不够完美 - strictStyleIsolation: true1. 原理是利用 webComponent 的 shadowDOM 实现 可能会影响React事件、弹框样式丢失 - experimentalStyleIsolation: true1. 原理类似于 vue 的 scope-css 1. 目前性能较差,处于实验阶段 |
阿里 |
无界 | 基于 WebComponent 容器 + iframe 沙箱 | 支持 | 支持 | 支持 | 支持 | 支持 | - 社区不太活跃,解决 bug 的速度较慢。- 调查了一下,看起来坑也挺多的,例子- 对有富文本的项目不友好。复杂的 iframe 到 WebComponent 的代理机制,导致市面上大部分富文本编辑器都无法在无界中完好运行。- 长期维护性一般 | 腾讯 | |
MicroApp | 基于 WebComponent 容器 + iframe 沙箱 | 支持 | 支持 | 支持 | 支持 | 支持 | 内置了两种 JS 沙箱。1. with 代理沙箱 1. 类 qiankun 的 with 代理沙箱,据说相比 qiankun 性能高点1. iframe 沙箱 1. 用于兼容 vite 场景 | - 如果是 iframe 沙箱 + WebComponent,痛点和无界类似。 | 京东 |
EMP | 基于Rspack、 Module Federation | 支持 | 支持 | 支持 | 支持 | 支持 | - 目前 Rspack 和 Module Federation 的社区沉淀较少 | 欢聚时代 |
总结
每个框架都存在一些问题。我们之前的框架是用的 qiankun,结合我们的业务场景,选了 Garfish。
Garfish 的开发团队,是 Web Infra(字节跳动的网络基础设施团队),该团队在前端开源中做了不少贡献,还是有一定影响力的。
有兴趣的可以看下 Module Federation,也是一种微前端架构(类似于服务端的微服务),感觉以后会成为微前端的主流方案。
Module Federation 的作者是 Webpack 的团队成员,也是 Web Infra 组织和 Rspack的团队成员。Vite 的下一个大版本也会和 web-infra 团队合作,共建 Module Federation。
Garfish
快速开始
主应用
javascript
// main.tsx
import Garfish from 'garfish'
import microFeApps from '@/config/micro-fe-apps'
import { GarfishCssScope } from '@garfish/css-scope'
Garfish.run({
basename: '/',
plugins: [GarfishCssScope({ fixBodyGetter: true })],
props: {
msg: 'hello',
},
apps: microFeApps,
beforeLoad(appInfo) {
console.log('子应用开始加载', appInfo.name)
},
afterLoad(appInfo) {
console.log('子应用加载完成', appInfo.name)
},
})
createRoot(document.getElementById('root')!).render(<App />)
// src/config/micro-fe-apps.ts
const microFeApps: MicroFeApps = [
{
name: 'mp-sub-react-demo/*',
activeWhen: '/mp-sub-react-demo',
domGetter: '#mp-sub-react-demo',
// 子应用开发环境
entry: 'http://localhost:8081',
// 子应用生产环境
// entry: 'https://res-sit.wandacm.com.cn/qianfan-static/demo/20240408164058/',
cache: true,
},
]
// src/config/routes.tsx
const routes = [
{
path: '/',
element: <Layout />,
errorElement: <ErrorPage />,
children: [
{
path: 'mp-sub-react-demo/*',
element: <div id="mp-sub-react-demo" className="garfish-sub" />,
name: 'mp-sub-react-demo',
},
],
},
]
子应用
javascript
// src/main.tsx
import RootComponent from '@/Root'
import { reactBridge } from '@garfish/bridge-react-v18'
export const provider = reactBridge({
el: '#root',
rootComponent: RootComponent,
})
/** 这能够让子应用独立运行起来,以保证后续子应用能脱离主应用独立运行,方便调试、开发 **/
if (!window.__GARFISH__) {
createRoot(document.getElementById('root')!).render(
// @ts-expect-error
<RootComponent basename="/" />,
)
}
// src/Root.tsx
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import { type PropsInfo } from '@garfish/bridge-react-v18'
function RootComponent(appInfo: PropsInfo) {
return (
<RouterProvider
router={createBrowserRouter(routes, {
basename: appInfo.basename,
})}
/>
)
}
// src/config/routes.tsx
const routes = [
{
path: '/',
element: <Layout />,
errorElement: <ErrorPage />,
children: [
{
index: true,
element: <Home />,
},
{
path: 'test',
element: <Test />,
},
],
}
]
Webpack 配置
arduino
const isDev = process.env.NODE_ENV === 'development'
const port = 8081
export default {
mode: process.env.NODE_ENV,
entry: path.join(srcDir, 'main.tsx'),
output: {
clean: !isDev,
// 需要配置成 umd 规范
libraryTarget: 'umd',
// 修改不规范的代码格式,避免逃逸沙箱
globalObject: 'window',
// 保证子应用的资源路径变为绝对路径
publicPath: isDev
? `http://localhost:${port}/`
: `//res-sit.wandacm.com.cn/qianfan-static/demo/20240408164058/`,
},
devServer: {
open: true,
host: 'localhost',
port,
historyApiFallback: true,
headers: {
// 保证子应用的资源支持跨域,在上线后需要保证子应用的资源在主应用的环境中加载不会存在跨域问题(**也需要限制范围注意安全问题**)
'Access-Control-Allow-Origin': '*',
},
},
// 生产环境下,将 react 和 react-dom 作为外部依赖,由主应用提供
externals: isDev
? undefined
: {
react: 'React',
'react-dom': 'ReactDOM',
},
}
如何接入一个子应用
- 创建子应用
lua
pnpm create mp-sub
- 在主应用中注册子应用
arduino
// src/config/micro-fe-apps.ts
const microFeApps: MicroFeApps = [
// ...
{
name: 'mp-sub-test',
activeWhen: '/mp-sub-test',
domGetter: '#mp-sub-test',
// 子应用开发环境
entry: 'http://localhost:8081',
// 子应用生产环境
// entry: 'https://res-sit.wandacm.com.cn/qianfan-static/demo/20240408164058/',
cache: true,
},
]
- 在主应用的路由中添加子应用的根路由
javascript
// src/config/routes.tsx
const routes = [
// ...
{
path: 'mp-sub-test/*',
element: <div id="mp-sub-test" className="garfish-sub" />,
name: 'mp-sub-test',
},
]
使用过程中遇到的问题
Vite 子应用开发环境报错 TypeError: Cannot read properties of undefined (reading 'GARFISH_EXPORTS')
TL;DR:当子应用为 ESModule 并且开启 vm 沙箱时 会出现错误。解决办法:子应用最好用 Webpack。
因为 Vite 开发环境会进行依赖预构建,Vite 的开发服务器将所有代码视为原生 ES 模块。而 Garfish 支持 esModule时,需要关掉 vm 沙箱或者为快照沙箱时,才能够使用。关闭 vm 沙箱后,不支持主子应用隔离。
如果需要在 vm 沙箱下开启 ESModule 的能力,可以使用 @garfish/es-module
插件。但会带来严重的首屏性能问题。