背景
基础架构组在搭建一套由许多个应用组成的系列应用,共享同一套用户体系(包含鉴权登录、权限管理、企业微信组织架构)。
目前的应用大致有:
- 员工管理平台
- 数据库管理平台
- 运维自动化平台
- 项目发布平台(k8s)
- 微服务、定时任务管理平台
- …
这就涉及到了用户体系的共享方案。
- 做一个登录服务和导航页,统一在
login.xxx.com 下完成登录,并跳转回对应的网址。
- 微前端,主应用完成登录,使用顶部导航菜单在同一个网页来切换微应用,将用户信息下传。
由于这些平台在前端框架选择上比较类似,决定使用 Umi.js,统一风格,并且我们需要可以非常方便地根据登录用户,在导航中插拔可访问的微应用。
最后选择了 Umi.js 自带的微前端(乾坤插件)。
主应用
1. 概述
主应用只需要两个模块:
- 登录(获取到 token)
- 获取用户权限信息(一棵树)
考虑到主应用的轻量,不需要太多框架型的东西存在,最后选择用 CRA + 乾坤 来搭建。
2. 通过接口获取到需要注册的微应用,并挂载
确定一个挂载微应用的 DOM
1 2 3 4 5 6 7 8 9 10
| const LAYOUT_ROUTES: { [key: string]: JSX.Element } = { '/': <Redirect to="/fengchu-admin" />, '/**': ( <Layout> <div id="container" /> </Layout> ), };
|
通过接口获取到的 entries 动态挂载微应用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import { registerMicroApps, start } from 'qiankun';
const entries = await api()
useEffect( () => { const microApps = entries?.children .reduce((acc: AppEntry[], current) => ([...acc, current, ...current.children]), []) .map((item) => ({ name: item.name, entry: `http://${item.domain}`, container: '#container', activeRule: item.activeRule, props: { profile, }, })) || []; registerMicroApps(microApps);
start(); }, [entries], );
|
3. css 优化
这个项目里暂时不打算用 ShadowDom 来挂载微应用,所以不打开乾坤的沙盒模式。主应用的样式采用 css-in-js 的方案来隔离。最后选择了 emotion。
微应用
1. 接入
微应用统一用了 Umi.js 所以接入起来非常方便。
第一步:在 .umirc.js 或 config.js 修改框架配置的地方添加
1 2 3 4 5
| export default defineConfig({ qiankun: { slave: {}, }, })
|
第二步:app.tsx 中添加生命周期钩子
1 2 3 4 5 6 7 8
| export const qiankun = { async bootstrap() {}, async mount() {}, async unmount() {}, };
|
这样基本上就可以联通了,然后做一些界面的更新。
微应用本身也是有顶部菜单栏的,有logo和头像,如果通过主应用打开,那就不需要自身的菜单了。
在 app.tsx 中添加:
1 2 3 4 5 6 7
| export const layout: RunTimeLayoutConfig = ({ initialState }) => { return { headerRender: window.__POWERED_BY_QIANKUN__ ? false : (_, defaultDom) => defaultDom, className: window.__POWERED_BY_QIANKUN__ ? 'qiankun-active' : '', ..., }; };
|
通过 window.__POWERED_BY_QIANKUN__ 来判断是否显示自身的 Header。
最后,微应用是通过二级域名部署的:
- fengchu.baixing.cn (主)
- admin.fengchu.baixing.cn (微)
- cloud.fengchu.baixing.cn (微)
- …
所以通过域名作为入口会跨域,有两种方案解决:
- 在主应用的镜像里用 nginx 反代,然后用 /fengchu-admin-app 作为入口
1 2 3 4 5
| location /fengchu-admin-app { proxy_pass http://admin.fengchu.baixing.cn; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Requested-For $proxy_add_x_forwarded_for; }
|
- 在微应用中的镜像里 nginx 添加返回头,允许主应用域名跨域访问:
1 2 3 4 5 6 7 8 9 10 11
| location / { add_header Access-Control-Allow-Origin http://fengchu.baixing.cn; add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS'; add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
if ($request_method = 'OPTIONS') { return 204; }
try_files $uri $uri/ /index.html; }
|
都行吧,第一个会有路径冲突的风险,第二个需要在每个微应用中都配置。
2. 接收用户和权限信息
主应用中,在注册微应用的时候,通过 props 传入了准备好的用户信息数据 profile:
1 2 3 4 5 6 7 8 9 10 11 12
| registerMicroApps([ { name: item.name, entry: `http://${item.domain}`, container: '#container', activeRule: item.activeRule, props: { profile, }, }, ... ]);
|
profile 来自全局状态管理 profile model(用的哪个工具不重要),所以带有监听,只要有更新此处的 props 就会随即更新。
微应用中,创建一个类用来存放用户信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| import { currentUser } from '@/services/fengchu/api';
interface Profile { id?: number; name?: string; avatar?: string; }
class UserProfile { private user?: Profile;
constructor(user: Profile) { this.user = user; }
public setProfile(user: Profile) { this.user = user; }
public async getUser() { if (!this.user) { const { data } = await currentUser(); this.user = data; return data; } return this.user; } }
const userProfile = new UserProfile({}); export default userProfile;
|
在 getUser() 中,如果事先已经接收到主应用的profile,则直接返回,如果没有(单独打开微应用)则通过 api 获取后存入。
在 bootstrap 钩子里获取父级传参:
1 2 3
| async bootstrap(props: any) { userProfile.setProfile(props?.profile?.user); },
|
总结
其实很多场景下根本不需要用到微前端。(你可能不需要微前端)
但为了尝鲜,也不是,为了可以方便地通过权限来管理可访问应用,并让所有系列应用共享一套用户体系,加上乾坤提供的近乎躺平的落地方案,就采用了。
由于没有用 ShadowDom 去隔离样式,微应用的主题色还能覆盖主应用的,效果还行。