微前端:一种由独立交付的多个前端应用组成整体的架构风格。具体的,将前端应用分解成一组能够独立开发、测试、部署的应用,而在用户看来仍然是内聚的单个产品。微前端不是单纯的前端框架,而是整合了技术、策略和方法,可能会以脚手架、辅助插件和规范约束这种生态圈形式展示出来
前端路由:客户端浏览器可以不依赖服务端,根据不同的URL渲染不同的视图页面
基座:负责应用管理注册和渲染,包括公共的登录和统一鉴权、菜单管理等功能的前端运行框架
子应用:负责按业务功能拆分的应用,是一个用子应用脚手架开发,独立部署的SPA应用
背景
伴随着业务的发展,不同团队开发的业务系统增加,会带来如下问题:
-
新应用系统兼容老业务系统,老业务系统升级、变迁等问题
-
不同团队开发的业务系统,在运营和运维上相互独立,给系统管理员和业务管理员增加新的管理成本
-
业务管理系统有很多共性特点,如这类应用组织架构模型、相似的布局风格、统一让认证登录、权限管理完全一致,存在重复建设
-
多团队协作容易不规范,充满杂乱的风格不一致的代码,没有明确的约定或技术愿景
目标
提供一套微前端解决方案,统一不同业务系统入口,保障不同业务系统的独立性和可协作性,统一视觉交互,避免重复建设工作。
方案
我们从实际业务场景出发,结合ngdb后端技术架构设计和qiankun、umi和antd等前端技术,提出如下微前端架构设计:
应用开发
1、是一个提供子应用注册加载渲染的运行框架 2、一套包括规范、工具、开发脚手架的开发框架
开发生态
前端开发生态视图
-
子应用脚手架:接入基座的子应用快速实现应用工程搭建和简单功能示例展示
-
规范:规范前端开发技术体系、视觉体系
-
公共组件、插件管理:所有应用开发可以复用的组件和插件
-
基座工程:提供子应用的运行环境
-
开发框架:开发框架包括拖拽式h5开发、云原生开发
-
按钮权限组件:使用按钮权限组件,可以快速实现子应用的前端按钮权限控制
-
请求插件: 封装了请求网关公共报文信息,方便子应用开发内管应用
应用部署
基座的功能
-
导航路由 + 资源加载框架
-
用户的登录、注册管理
-
系统的统一鉴权管理
-
导航菜单管理
-
路由管理
-
数据管理
-
全局状态管理
-
管理其他子应用:如在何时加载应用、何时卸载应用等
基座权限模型
基座组织架构数据同步
设计原则及理念
-
子应用独立开发、独立部署、独立运行
-
子应用可以自由拆解和组合,子应用组件、插件复用
-
子应用插拔式
原理
应用访问
应用访问视图
路由分发
对于单体前端应用,框架将路由分发到制定的页面或组件,在微前端中,同样需要路由来做分发,通过路由来找到相应的应用。通过路由将不同的业务分发到不同的、独立部署的应用上
路由分发步骤:
-
1、路由分发到应用:当url前缀与注册的子应用路由匹配时,基座会激活对应的应用
-
2、应用分发到自有路由:在应用模块被激活的时候,应用会根据url路由渲染应用具体的页面
应用加载及渲染过程
应用协调
-
统一配置与主题切换,利用 CSS Variables 等方式动态换肤
-
应用的生命周期,规范化子应用的生命周期,并且在不同生命周期中执行不同的操作
-
数据共享,子应用间数据共享
-
服务共享,跨应用数据共享与服务调用
-
组件共享,可能将某个纯界面组件或者业务组件以插件(Plugin)或者部件(Widget)的方式共享出去;提供某个计算能力。
应用间通信
缓存数据共享与权限
考虑是主应用来缓存这些数据,子应用通过启动时创建生命周期钩子,去订阅,主应用在订阅时回调一份缓存数据给子应用,主应用在缓存更新后统一发布数据更新。
-
用户的权限数据通过缓存共享给子应用的数据
-
用户个性化信息设置
-
用户登录状态
-
登录用户信息
-
共享全局状态
Actions
通信
-
场景:
-
修改子应用加载动画效果
-
应用主题色切换
-
应用权限的考虑通过给子应用下发数据时通过过滤器过滤出对应应用的数据
-
-
定义全局状态,并返回通信方法,建议在主应用使用,微应用通过 props 获取通信方法
-
示例
主应用:
import{initGlobalState,MicroAppStateActions}from'qiankun'; // 初始化 state constactions:MicroAppStateActions=initGlobalState(state); actions.onGlobalStateChange((state,prev)=>{ // state: 变更后的状态; prev 变更前的状态 console.log(state,prev); }); actions.setGlobalState(state); actions.offGlobalStateChange();
子应用
// 从生命周期 mount 中获取通信方法,使用方式和 master 一致 export function mount(props) { props.onGlobalStateChange((state, prev) => { // state: 变更后的状态; prev 变更前的状态 console.log(state, prev); }); props.setGlobalState(state); }
公共数据
通过应用通信进行数据共享,共享数据如下:
-
用户数据
-
localstorage
通信方式如下:
应用生命周期
-
应用的生命周期
-
不同生命周期能做的事情和要做的事
关键技术点设计
访问加载动画
首次访问基座时,网络缓慢的情况下,会给用户卡顿感或空白等待时间,影响用户体验
-
在加载静态资源文件时先处理加载动画效果
访问子应用时,需要加载子应用静态资源,会出现空白等待时间,此时展示加载动画效果处理,提升用户体验
登录页
-
展示登录表单
-
表单防重提交
layout布局
-
layout布局组成:
-
顶部导航
-
左侧导航
-
中间内容区
-
-
支持左侧导航和顶部导航切换
-
主题切换
工作台页面
-
查询用户拥有的应用
-
查询快捷导航
-
获取应用信息为子应用渲染做准备
子应用渲染
在MicroApp组件中根据名称入口地址加载手动加载应用
-
子应用菜单渲染
-
根据应用名称查询对应应用的菜单
-
根据菜单动态参数替换菜单变量
-
为每个应用动态生成权限管理公共路由
-
应用首页
-
点击顶部应用名称返回应用首页
-
访问图标回到基座首页
路由
路由机制:基座配置子应用的路由为 subApp: { url: '/subApp/**', entry: './subApp.js' },则当浏览器的地址为 /subApp/abc 时,框架需要先加载 entry 资源,待 entry 资源加载完毕,确保子应用的路由系统注册进主框架之后后,再去由子应用的路由系统接管 url change 事件。同时在子应用路由切出时,主框架需要触发相应的 destroy 事件,子应用在监听到该事件时,调用自己的卸载方法卸载应用
-
动态路由
-
动态菜单生成对应的前端路由
-
-
权限路由控制
-
umi路由匹配为非严格模式
-
路由支持变量
-
支持外链
Umi路由匹配机制的运行逻辑
-
Umi中路由路由匹配默认是非严格模式,只要满足一点就会进入对应路由,存在路由不精确匹配问题,需修改路由匹配模式为严格模式
动态路由权限
通过patchRouter动态添加路由
-
动态路由特点:
-
1、支持约定式和配置式
-
2、动态修改权限
-
3、动态使用菜单
-
4、支持运行时配置 Layout
-
5、支持页面级别配置 Layout
-
6、支持页面级权限
-
7、支持页面级修改权限
-
8、支持不使用 layout
-
参考@umijs/plugin-access-layout实现
路由守卫
在路由守卫函数中判断用户是否有权限访问该路由
-
路由配置如下
routes: [ { path: '/', component: '../pages/index', // 在PrivateRoute.js 中对路由和登录状态进行权限判断 wrappers: ['@/routes/PrivateRoute.js'], }, { path: '/workplace', component: '../pages/workplace/index', wrappers: ['@/routes/PrivateRoute.js'], } ],
路由监听
-
监听路由变化
history.listen(({ pathname, query }) => { // 监听路由及获取路由参数 }
菜单树处理
动态菜单树生成方式
-
通过变量替换处理,支持菜单参数传递
-
暴露方法,前端传递变量之后生成菜单树,菜单树与路由匹配
支持子应用触发菜单树渲染
-
基座暴露api给子应用调用触发基座的菜单树渲染
菜单异步加载策略
-
二级菜单按需异步加载,减少后端查询压力,提升相应速度
运行时配置
-
umi 约定 src 目录下的 app.js 为运行时的配置文件
-
.umirc.js 做编译时的配置
按钮权限组件
基于react antd的Button组件进行封装,用于按钮权限控制
-
特点
-
支持所有antd Button组件的api
-
支持权限控制
-
404页面
-
当用户访问不存在或是无权限页面处理
应用首页千人千面
-
脚手架中定义一个默认首页,业务方在首页中进行权限判断做路由重定向
class Index extends React.Component { componentWillMount() { // 根据用户权限判断展示首页 } componentDidMount() { // 根据用户权限判断展示首页 } render() { // 处理千人前面逻辑 const SubContent = () => { // 项目自定义判断条件 if (false) { return <Workplace />; } return <Home />; }; return ( <div size="large" spinning="false"> {<SubContent />} </div> ); } } export default Index;
umi中的connect的用法
简单来说connect是用来连接前端的ui界面和和前端model的一个嫁接桥梁 ,通过使用connect将model里面定义的state,和dispatch,和histoey方法等传递到前端供前端使用
-
https://my.oschina.net/u/4447095/blog/3160700
问题
浏览器兼容问题
umi插件替换
-
升级 umi-plugin-react 为 @umijs/preset-react
-
https://umijs.org/docs/upgrade-to-umi-3#%E5%8D%87%E7%BA%A7-umi-plugin-react-%E4%B8%BA-umijspreset-react
权限路由控制
-
wrappers Routes的区别
-
Routes umi低版本有效 高版本无效
创建umi命令导致的版本差别
npm create umi npm create @umijs/umi-app
子应用偶尔出现白屏
前两个应用不注册
子应用在未登录时就注册
菜单没有按需加载
参考文档
-
https://zhuanlan.zhihu.com/p/88449415
-
https://zhuanlan.zhihu.com/p/78362028
-
https://zhuanlan.zhihu.com/p/72676123
-
https://zhuanlan.zhihu.com/p/96464401
-
https://www.imooc.com/article/304768
-
https://zhuanlan.zhihu.com/p/275923805
-
https://blog.youkuaiyun.com/qq_34621851/article/details/105341137
-
https://juejin.cn/post/6844904087616487438
-
https://cloud.tencent.com/developer/article/1758334
-
https://www.jianshu.com/p/3484415c8a00
-
https://juejin.cn/post/6885211340999229454
-
https://github.com/satan31415/umi-admin
-
https://v2.umijs.org/zh/guide/runtime-config.html#render https://github.com/liuxx-u/bird-front/blob/master/doc/bird-button.md
-
https://www.imooc.com/article/304768
-
https://github.com/a1029563229/blogs/blob/master/BestPractices/qiankun/Communication.md
-
https://blog.youkuaiyun.com/qq_37262037/article/details/112209213
-
https://blog.youkuaiyun.com/jx950915/article/details/107041903
-
https://www.yuque.com/umijs/umi/subscriptions
-
https://github.com/liuxx-u/bird-front/blob/master/doc/bird-button.md
-
https://blog.youkuaiyun.com/shaoyouiqng/article/details/109219288
-
https://my.oschina.net/xiaohuoni/blog/4416055