微前端设计方案

5,563 阅读15分钟

基座详细设计方案

术语

微前端:是一种由独立交付的多个前端应用组成整体的架构风格。具体的,将前端应用分解成一组能够独立开发、测试、部署的应用,而在用户看来仍然是内聚的单个产品。微前端不是单纯的前端框架,而是整合了技术、策略和方法,由开发脚手架、辅助插件、组件和规范而形成的前端开发生态。

React:是一个颠覆式的前端框架。在React官方这样介绍的它:一个声明式、高效、灵活的、创建用户界面的JavaScript库,即使React的主要作用是构建UI,但是项目的逐渐成长已经使得react成为前后端通吃的WebApp解决方案。

qiankun:就是一款由蚂蚁金服推出的比较成熟的微前端框架,基于 single-spa 进行二次开发,用于将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。

umi:中文可发音为乌米,是一个可插拔的企业级 react 应用框架。umi 以路由为基础的,支持类 next.js 的约定式路由,以及各种进阶的路由功能,并以此进行功能扩展,比如支持路由级的按需加载。然后配以完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求,目前内外部加起来已有 50+ 的插件。

antd:基于 Ant Design 设计体系的 React UI 组件库,主要用于研发企业级中后台产品。

RBAC:基于角色的访问控制(RBAC)是实施面向企业安全策略的一种有效的访问控制方式。其基本思想是,对系统操作的各种权限不是直接授予具体的用户,而是在用户集合与权限集合之间建立一个角色集合。每一种角色对应一组相应的权限。一旦用户被分配了适当的角色后,该用户就拥有此角色的所有操作权限。

dva:dva 首先是一个基于 reduxredux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-routerfetch,所以也可以理解为一个轻量级的应用框架。

npm: 是 Node.js 官方提供的包管理工具,他已经成了 Node.js 包的标准发布平台,用于 Node.js 包的发布、传播、依赖控制。npm 提供了命令行工具,使你可以方便地下载、安装、升级、删除包,也可以让你作为开发者发布并维护包。

Webpack:是一个前端资源加载/打包工具。它将根据模块的依赖关系进行静态分析,然后将这些模块按照指定的规则生成对应的静态资源。

前端路由:客户端浏览器可以不依赖服务端,根据不同的URL渲染不同的视图页面。

基座:负责应用管理注册和渲染,包括公共的登录和统一鉴权、菜单管理等功能的前端运行框架。

子应用:负责按业务功能拆分的应用,是一个用子应用脚手架开发,独立部署的SPA应用。

生命周期:React组件件从创建到销毁的过程称为React生命周期。

钩子函数:在生命周期当中所暴露出来的函数称为钩子函数。第一阶段,初始化(bootstrap),初始默认数据;第二阶段,组件挂载(Mount),组件挂载显示在UI上;第三阶段,组件的卸载(Unmount),不再调用该组件。

沙箱机制:英文是sandbox,保证程序运行在一个隔离的环境下,不对外界的其他程序造成影响。沙箱主要是一种安全机制,把一些不信任的代码运行在沙箱之内,不能访问沙箱之外的代码。比如在线编辑器、执行第三方js、react服务端渲染等,只要是运行不信任的程序,沙箱隔离就会使用到。

localstorage:在HTML5中,新加入了一个localStorage特性,这个特性主要是用来作为本地存储来使用的,解决了cookie存储空间不足的问题(cookie中每条cookie的存储空间为4k),localStorage中一般浏览器支持的是5M大小,这个在不同的浏览器中localStorage会有所不同。

微服务是面向服务架构(SOA)的一种变体,把应用程序设计成一系列松耦合的细粒度服务,并通过轻量级的通信协议组织起来。具体地,将应用构建成一组小型服务。这些服务都能够独立部署、独立扩展,每个服务都具有稳固的模块边界,甚至允许使用不同的编程语言来编写不同服务,也可以由不同的团队来管理。然而,越来越重的前端工程也面临同样的问题,自然地想到了将微服务思想应用照搬到前端,于是有了「微前端(micro-frontends)」的概念:一种由独立交付的多个前端应用组成整体的架构风格。具体的,将前端应用分解成一些更小、更简单的能够独立开发、测试、部署的小块,而在用户看来仍然是内聚的单个产品。

伴随着银行业务的发展,不同团队开发的业务系统的增加,会带来如下问题:

新应用系统兼容老业务系统,老业务系统升级、变迁困难等问题。

不同团队开发的业务系统,在运营和运维上相互独立,给系统管理员和业务管理员增加新的管理成本。

业务管理系统有很多共性特点,如这类应用组织架构模型、相似的布局风格、统一的认证登录、权限管理完全一致,存在重复建设。

多团队协作容易不规范,充满杂乱的风格不一致的代码,没有明确的约定或技术愿景。

目标

微前端技术,可以很好的解决以上问题,在银行多业务线多团队的背景下,提供出一套多功能全方位的管理系统,统一不同业务系统入口,保障不同业务系统拥有权限可控性、复用性、统一性、简易性和强适应性。我们提供的微前端基座解决方案可以达到的目标具体如下:

权限可控性:基座的子应用使用了基于角色的访问控制([RBAC](https://baike.baidu.com/item/RBAC)),管理员可以控制操作员的权限,让基座更具有可控性。

复用性:微前端基座的子应用共用相同的公共组件,保证组件的复用,减少重复的组件建设。

统一性:微前端基座的子应用拥有统一的,规范的代码风格和依赖,保证技术框架的统一性,部署运维和监控一体化,保证了管理的统一性。

简易性:减少开发人员的低码率,前端开发使用托拉拽的形式进行组件的布局,让开发变得更加简易,并且利用云效和k8s部署,让子应用的上线和版本的升级更加简易。

强适应性:微前端基座可以将组件、团队、版本进行解耦,实现敏捷交付、敏捷管理,敏捷设计,敏捷开发和敏捷测试,保证我们的系统可以快速的适应不确定的因素,重新定义前端协作开发。

方案

我们从实际业务场景出发,结合bsin-paas后端技术架构设计和qiankun、umi和antd等前端技术,提出如下微前端架构设计:

应用开发

1、是一个提供子应用注册加载渲染的运行框架 2、一套包括规范、工具、开发脚手架的开发框架

开发生态

前端开发生态视图

基座采用的是微前端架构设计,由独立交付的多个前端应用组成一个整体。即将前端应用拆分成多个能够独立开发、测试、部署的应用,而在用户看来仍然是内聚的单个产品。

微前端是一种由独立交付的多个前端应用组成整体的架构风格。具体的,将前端应用分解成一组能够独立开发、测试、部署的应用,而在用户看来仍然是内聚的单个产品。微前端不是单纯的前端框架,而是整合了技术、策略和方法,以脚手架、辅助插件、组件和规范而形成的前端开发生态。 微前端开发生态主要分两部分:一是运行框架即基座,二是开发框架。 1、 基座是子应用的运行环境,负责子应用的管理注册、渲染,包括公共的登录和统一鉴权、菜单管理等功能 2、 开发框架即提供一套子应用可视化搭建的框架,通过该框架,开发人员可以实现零代码或低代码开发应用,开发框架包括拖拽式h5开发、云原生开发 3、 子应用脚手架是接入基座的子应用快速实现应用工程搭建及简单功能示例展示 4、 微前端提供一套开发规范,规范前端开发技术体系、视觉体系 5、 前端统一建设公共组件、插件,方便所有应用开发复用

按钮权限组件:使用按钮权限组件,可以快速实现子应用的前端按钮权限控制

请求插件: 封装了请求网关公共报文信息,方便子应用开发内管应用

应用部署

基座的功能

导航路由 + 资源加载框架

用户的登录、注册管理

系统的统一鉴权管理

导航菜单管理

路由管理

数据管理

全局状态管理

管理其他子应用:如在何时加载应用、何时卸载应用等

默认进入第一个应用

设计原则及理念

子应用独立开发、独立部署、独立运行

子应用可以自由拆解和组合,子应用组件、插件复用

子应用插拔式

前端工程化:模块化、组件化、规范化、自动化

原理

应用访问

应用访问视图

应用加载及路由分发

对于单体前端应用,框架将路由分发到制定的页面或组件,在微前端中,同样需要路由来做分发,通过路由来找到相应的应用。通过路由将不同的业务分发到不同的、独立部署的应用上

路由分发步骤:

1、路由分发到应用:当url前缀与注册的子应用路由匹配时,基座会激活对应的应用

2、应用分发到自有路由:在应用模块被激活的时候,应用会根据url路由渲染应用具体的页面

应用加载及渲染过程

1、获取子应用配置 2、注册子应用信息到qiankun 3、当url与子应用路由匹配时,fetch子应用静态资源文件,将子应用得html挂载到基座的应用渲染区

应用协调

统一配置与主题切换,利用 CSS Variables 等方式动态换肤

应用的生命周期,规范化子应用的生命周期,并且在不同生命周期中执行不同的操作

数据共享,子应用间数据共享

服务共享,跨应用数据共享与服务调用

组件共享,可能将某个纯界面组件或者业务组件以插件(Plugin)或者部件(Widget)的方式共享出去;提供某个计算能力。

应用间通信

--javascript

子应接入到基座中,会存在如下场景:
1、子应用需要获取登录基座的用户信息
2、子应用触发基座的菜单树渲染
3、子应用间信息共享
4、基座改变子应用主题色渲染效果
针对以上应用场景,基座实现了应用间通信
方式一:
• 通过注册子应用时的props属性。将主应用需要传递给微应用的数据传递出去。
方式二:
• 定义全局状态,并暴露通信api方法给应用调用。
不同的方式用于解决不同的场景问题。

基座给子应用下方方法调用

<https://www.jianshu.com/p/7844076c7d15>

缓存数据共享与权限

考虑是主应用来缓存这些数据,子应用通过启动时创建生命周期钩子,去订阅,主应用在订阅时回调一份缓存数据给子应用,主应用在缓存更新后统一发布数据更新。

用户的权限数据通过缓存共享给子应用的数据

    用户个性化信息设置

    用户登录状态

    登录用户信息

    当前应用列表数据

共享全局状态

Actions 通信

场景:
    修改子应用加载动画效果

    应用主题色切换

    应用权限的考虑通过给子应用下发数据时通过过滤器过滤出对应应用的数据

*

定义全局状态,并返回通信方法,建议在主应用使用,微应用通过 props 获取通信方法

*

示例

主应用:

\--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

    import { initGlobalState, MicroAppStateActions } from 'qiankun';
    // 初始化 state
    const actions: MicroAppStateActions = initGlobalState(state);
    actions.onGlobalStateChange((state, prev) => {
      // state: 变更后的状态; prev 变更前的状态
      console.log(state, prev);
    });
    actions.setGlobalState(state);
    actions.offGlobalStateChange();

子应用

\--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

    // 从生命周期 mount 中获取通信方法,使用方式和 master 一致
    export function mount(props) {
      props.onGlobalStateChange((state, prev) => {
        // state: 变更后的状态; prev 变更前的状态
        console.log(state, prev);
      });
      props.setGlobalState(state);
    }
  • 子应用 /src/shared/action.js

--javascript

function emptyAction() {
  // 警告:提示当前使用的是空 Action
  console.warn("Current execute action is empty!");
}
 
class Actions {
  // 默认值为空 Action
  actions = {
    onGlobalStateChange: emptyAction,
    setGlobalState: emptyAction
  };
   
  /**
   * 设置 actions
   */
  setActions(actions) {
    this.actions = actions;
  }
 
  /**
   * 映射
   */
  onGlobalStateChange(...args) {
    return this.actions.onGlobalStateChange(...args);
  }
 
  /**
   * 映射
   */
  setGlobalState(...args) {
    return this.actions.setGlobalState(...args);
  }
}
 
const actions = new Actions();
export default actions;

注入真实 Actions

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

export const qiankun = {
  ...
  async mount(props) {
    // // 注入 actions 实例
    actions.setActions(props);
    ...
  }
  ....
};

使用action

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

// 引入 actions 实例
import actions from "@/shared/actions";

communication = () => {
    //  子应用触发全局状态变化,其他应用可以监听到数据变化
    const sta = {
      menuTreeNo: '1',
    };
    actions.setGlobalState(sta);
};

公共数据

通过应用通信进行数据共享,共享数据如下:

用户数据

localstorage

sessionstorage 主题数据

theme:{

}

通过生命周期函数通信

通过注册子应用时的props属性。将主应用需要传递给微应用的数据传递出去

// 初始化子应用 async bootstrap(props) { // 子应用单独运行时 props 为空 // 子应用通过该生命周期函数获取基座信息 console.log('app1 bootstrap', props); },

技术点设计

访问加载动画

首次访问基座时,网络缓慢的情况下,会给用户卡顿感或空白等待时间,影响用户体验

  • 在加载静态资源文件时先处理加载动画效果

访问子应用时,需要加载子应用静态资源,会出现空白等待时间,此时展示加载动画效果处理,提升用户体验

具体实现

  • 在layout布局组件中监听子应用加载状态

  • 子应用的挂载由子应用的生命周期函数

--javascript


// 基座layout
componentWillMount() {
    // 监听404基座404页面对状态的修改
    var that = this;
    this.actions.onGlobalStateChange((state, prev) => {
      // state: new state; prev old state
      console.log(state, prev);
      that.setState(state);
    });
}

// 子应用app.ts
async mount(props) {
    console.log('app1 mount', props);
    let state = {
      microAppLoading: false,
    };
    // 通过修改全局状态修改子应用加载动画
    mainAppLoadingStateChange(mainAppLoadingStateChangeCb, props, state);
  },

用户登录

登录有两种方式登录:

统一认证登录:通过网关调用统一认证接口登录

本地账号密码登录:通过网关调用基座的RBAC后台登录 用户登录成功之后,获取到返回的token及用户信息,前端将token存放到cookie中,将用户信息存放到sessionStorage

注: token存cookie比较好。 localstorage有xss风险 ,cookie有csrf 和xss风险。 这里存cookie,不是普通的cookie直接与服务器交互。后端不通过cookie取值,前端请求时从cookie取出来 放到请求头里 可以避免csrf。 并对cookie设置httponly 减少xss攻击

子应用渲染

在MicroApp组件中根据名称入口地址加载手动加载应用

子应用菜单渲染

    根据应用名称查询对应应用的菜单

    根据菜单动态参数替换菜单变量

    为每个应用动态生成权限管理公共路由

在app.ts中监听子应用加载状态

--javascript

/**
 * 添加全局的未捕获异常处理器
 * 1、捕获子应用加载状态
 */
addGlobalUncaughtErrorHandler((event: Event | string) => {
  console.log(event)
  if(event && event.reason.message === 'Failed to fetch'){
    // 子应用加载失败会触发三次,只有第一次要提示,其他略过
    console.log(cookie.getCookie('microAppMount'));
    if(cookie.getCookie('microAppMount') == undefined){
      cookie.setCookie('microAppMount', 'yes', 3);
      message.error('子应用加载失败,请检查应用是否可正常访问');
      history.push("/")
    }
  }
  const { message: msg } = event as any;
  // 加载失败时提示
  if (msg && msg.includes('died in status LOADING_SOURCE_CODE')) {
    message.error('子应用加载失败,请检查应用是否可运行');
  }
});

项目开源地址: gitee.com/s11e-DAO/bs…