背景

基础架构组在搭建一套由许多个应用组成的系列应用,共享同一套用户体系(包含鉴权登录、权限管理、企业微信组织架构)。

目前的应用大致有:

  • 员工管理平台
  • 数据库管理平台
  • 运维自动化平台
  • 项目发布平台(k8s)
  • 微服务、定时任务管理平台

这就涉及到了用户体系的共享方案。

  1. 做一个登录服务和导航页,统一在 login.xxx.com 下完成登录,并跳转回对应的网址。
  2. 微前端,主应用完成登录,使用顶部导航菜单在同一个网页来切换微应用,将用户信息下传。

由于这些平台在前端框架选择上比较类似,决定使用 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';

// entries 是获取到的一棵权限树,在人员管理平台里配置,通过这个信息来注册登录者能看到的微应用
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

css css build css build

微应用

1. 接入

微应用统一用了 Umi.js 所以接入起来非常方便。

第一步:在 .umirc.jsconfig.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() {},
// 应用 render 之前触发
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 (微)

所以通过域名作为入口会跨域,有两种方案解决:

  1. 在主应用的镜像里用 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;
    }
  2. 在微应用中的镜像里 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
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);
},

总结

其实很多场景下根本不需要用到微前端。(你可能不需要微前端

但为了尝鲜,也不是,为了可以方便地通过权限来管理可访问应用,并让所有系列应用共享一套用户体系,加上乾坤提供的近乎躺平的落地方案,就采用了。

cloud微应用

由于没有用 ShadowDom 去隔离样式,微应用的主题色还能覆盖主应用的,效果还行。