背景

在当前云原生微服务、业务中台、低代码平台等 IT 架构下,不再是传统的烟囱式应用系统建设,而是打破企业业务部门竖井,建立企业级的信息化平台(数据中台、业务中台),那么对业务开发的解耦和聚合将成为关键技术,目前对于系统后端已有成熟的微服务架构,基于 SpringBoot 开发微服务,通过 SpringCloud 或 istio 进行微服务治理。前端也同样有类似的需求,如何支持不同的前端团队开发各自业务的 UI 页面,运行时通过统一的框架集成整合起来,这就是微前端框架出现的主要诉求。

微前端介绍

2016 年底,“Micro frontend”一词首次出现在 ThoughtWorks Technology Radar 上。它将微服务的概念扩展到前端世界。当前的趋势是构建一个功能丰富、功能强大的浏览器应用程序,也就是单页应用程序,它位于微服务架构之上。随着时间的推移,前端层(通常由一个独立的团队开发)不断增长,并且越来越难以维护。这就是我们所说的前端巨石(Frontend Monolith)。

Micro frontend 背后的理念是将网站或网页应用视为独立团队所拥有的功能的组合。每个团队都有自己关心和擅长的独特业务或任务领域。团队是跨功能的,开发从数据库到用户界面的端到端特性。

微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为 多个小型前端应用聚合为一的应用 。各个前端应用还可以 独立运行、独立开发、独立部署

微前端优势

1、复杂度可控: 每一个 UI 业务模块由独立的前端团队开发,避免代码巨无霸,保持开发时的高速编译,保持较低的复杂度,便于维护与开发效率。

2、独立部署: 每一个模块可单独部署,颗粒度可小到单个组件的 UI 独立部署,不对其他模块有任何影响。

3、技术选型灵活: 也是最具吸引力的,在同一项目下可以使用如今市面上所有前端技术栈 vue react angular, 也包括未来的前端技术栈。

4、容错: 单个模块发生错误,不影响全局,就跟后端微服务一样。

5、扩展: 每一个服务可以独立横向扩展以满足业务伸缩性,解决资源的不必要消耗;

微前端框架选型

在选型微前端框架时,调研了市面上实现微前端的框架,可供选择的有 iframe、sigle-spa、qiankun 和 microApp。single-spa 太过于基础,对原有项目的改造过多,成本太高; iframe 在所有微前端方案中是最稳定的、上手难度最低的,但它有一些无法解决的问题,例如性能低、通信复杂、双滚动条、弹窗无法全局覆盖,它的成长性不高,只适合简单的页面渲染。剩下的只有 qiankun 和 microApp 了。

1、京东 MicroApp

MicroApp 是一款基于类 WebComponent 进行渲染的微前端框架,不同于目前流行的开源框架,它从组件化的思维实现微前端,旨在降低上手难度、提升工作效率。它是目前市面上接入微前端成本最低的框架,并且提供了 JS 沙箱、样式隔离、元素隔离、预加载、资源地址补全、插件系统、数据通信等一系列完善的功能。MicroApp 与技术栈无关,也不和业务绑定,可以用于任何前端框架和业务。

MicroApp 的核心功能在 CustomElement 基础上进行构建,CustomElement 用于创建自定义标签,并提供了元素的渲染、卸载、属性修改等钩子函数,我们通过钩子函数获知微应用的渲染时机,并将自定义标签作为容器,微应用的所有元素和样式作用域都无法逃离容器边界,从而形成一个封闭的环境。

2、阿里乾坤 qiankun

qiankun 是一个基于 single-spa微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。qiankun 孵化自蚂蚁金融科技基于微前端架构的云产品统一接入平台,在经过一批线上应用的充分检验及打磨后,将其微前端内核抽取出来并开源,希望能同时帮助社区有类似需求的系统更方便的构建自己的微前端系统,同时也希望通过社区的帮助将 qiankun 打磨的更加成熟完善。目前 qiankun 已在蚂蚁内部服务了超过 200+ 线上应用,在易用性及完备性上,绝对是值得信赖的。

微前端在新城市后台应用落地

一、基座应用改造方案

微前端框架:microApp

// 安装依赖
npm i @micro-zoe/micro-app --save
// 在入口处引入 main.js
import microApp from '@micro-zoe/micro-app' 
microApp.start()

分配一个路由给子应用。 在router -> active.js中添加子应用路由。 注意: mgSite是用来区分微前端页面和普通页面的,必填。至于值,为了方便理解,可以用将来线上对应子应用文件夹的名字,如果暂时不清楚的话可以先随意命名,后面再统一替换

// 添加子路由
{
        path: '/pages/control',
        name: '商品管理',
        meta: {
            mgSite: 'i2'//必填
        },
        component: ()=>import('../pages/_microApp/index')
 }

// _microApp/index.vue 页面中引入 micro-app 组件

  <div>
    
  </div>

micro-app 组件 在main.js中执行 microApp.start() 方法的时候已经进行了全局注册,无需再局部引用

参数是否必填说明备注
name应用名称,整个基座应用中name不可重复必须字母开头,且不可以带有除中划线和下划线外的特殊符号
url应用地址:子应用地址子应用和基座应用本质上还是在同一个页面,子应用的路由还是基于浏览器地址
baseroute子应用的基础路由如果基座应用是history路由,子应用是hash路由,则不需要设置baseroute。

二、子应用改造方案

  1. 子应用配置跨域
headers: {
  'Access-Control-Allow-Origin': '*'
}

注意:子应用在本地开发的时候 只能用webpack启动项目

  1. 子应用路由改造,新建一个路由文件layout.js
export default {
  path: window.__MICRO_APP_BASE_ROUTE__ || '/pages',
  component: () =&gt; import('@/views/layout.vue'),
  name: 'layout',
  children: [
  // 其他的路由都写到这里
  ],
};
  1. 新建一个父级组件layout.vue
<div style="height: 100%;overflow: hidden" class="light-wrap">
    <div style="height: 100%;overflow: hidden" class="uu-layout-content">
      
    </div>
  </div>

export default {
  name: 'Layout',
};

注意: 新城市后台子应用路由改造完以后会出现页面无权限的情况。这是因为页面添加了baseroute 前缀。有两种方案解决

  1. 在权限系统,将对应页面的路由加上baseroute 前缀。缺点是改造成本太大 每个环境都要改一遍,容易出错
  2. 设置baseroute = pages. 这是原有页面自带的路由前缀,改造成本小。但是主、子两个应用之间的路由不能重复。否则会出现路由冲突风险。

至此,主、子应用 可完成正常的渲染

三、功能配置

  1. keep-alive 怎么配置?

在micro-app组件上添加 keep-alive属性即可

  1. 主应用、子应用之间页面互相跳转

子应用跳主应用配置如下

主应用:将microAppData通过data属性绑定到micro-app 组件

microAppData: {
  pushState: (path) =&gt; {
  removeDomScope()
  this.$router.push(path)
  },
}

注意:如果主应用页面级别被缓存,也就是说整个页面被vue的keep-alive组件包裹,需要监听路由的改变,重新赋值。

watch: {
    $route() {
      this.initAppName()
      this.microAppData = {
        pushState: (path) =&gt; {
          removeDomScope()
          this.$router.push(path)
        },
      }
    },
  },

子应用:修改vueRouter 原型上负责跳转的方法

const originalPush = VueRouter.prototype.push;
VueRouter.prototype.push = function push(location, onResolve, onReject) {
  try {
    window.microApp.getData().pushState(location);
  } catch (e) {
    originalPush.call(this, location, onResolve, onReject);
  }
}

主应用跳转子应用

// 主应用
microApp.setData('子应用name', { path: '/new-path/' })
// 子应用
window.microApp.addDataListener((data) => {
  // 当基座下发跳转指令时进行跳转
  if (data.path) {
    router.push(data.path)
  }
})

四、常见问题

  1. 子应用的权限怎么控制?

子应用理论上只是通过配置的URL去获取html、css、js等静态资源,可以把子应用看成一个主应用封装的webComponent组件。所以权限、token和基座应用一样,可以直接通过sessionstore、localstore读取,权限的获取和校验子应用需要和主应用保持一致

  1. 项目中的tag切换失效

micro-app name冲突导致。注意: 在基座应用中micro的name值必须保持唯一

  1. 页面初始化的时候出现短暂空白

在引入micro-app的页面中加上全局的loading,页面渲染完成以后loading消失

注意:需要在页面级别的生命周期mounted函数中 改变loading的状态。 micro-app组件也有一套自己的生命周期,但是在加上了keep-alive属性以后,mounted函数不执行。

  1. 打开子应用时有时会进入404、非法进入页面

子应用在跳转主应用的时候,如果主应用中的页面被keep-alive缓存组件包裹,就会出现跳转404的情况。解决办法如下:

watch: {
    $route() {
      this.microAppData = {
        pushState: (path) =&gt; {
          removeDomScope()
          this.$router.push(path)
        },
      }
    }

在watch中监听路由的改变,重新给microAppData赋值

  1. 子应用命名冲突问题

多个子应用页面切花的时候可能会出现命名冲突的问题,解决办法: 在watch 中监听 $route,然后路由发生改变的时候 调用initAppName 改变micrp-app 的name

watch: {
    $route() {
      this.initAppName()
    },
  },
methods: {
    initAppName() {
      const { hash } = location
      if (hash.split('#')[1] &amp;&amp; this.appName !== hash.split('#')[1]) {
        this.appName = hash.split('#')[1]
      }
    }
}
  1. 子应用 router.push 需要重写。解决跳转的问题

通过重写路由push方法,使得我们页面之间在跳转的时候仍然可以使用this.$router这种方式

const originalPush = VueRouter.prototype.push;
VueRouter.prototype.push = function push(location, onResolve, onReject) {
  try {
    window.microApp.getData().pushState(location);
  } catch (e) {
    originalPush.call(this, location, onResolve, onReject);
  }
}
  1. 主应用的iconfont 会影响子应用的iconfont

font-family 名字冲突所致,换个名字就好了 #423

  1. 父应用的全局样式会影响子应用

通过添加css前缀或者css modules 的方式解决。 遗憾的是在element-ui 和 iview2框架中没有提供全局修改css 前缀的功能。最后通过社区的两个webpack插件(vite对应插件没找到)解决了这个问题。

yarn add change-prefix-loader postcss-change-css-prefix --s

vue.config.js中添加以下代码

<!-- vue.config.js -->

module.exports = {
    chainWebpack: config =&gt; {
        config.module
            .rule('change-prefix')
            .test(/\.js$/)
            .include.add(path.resolve(__dirname, './node_modules/element-ui/lib'))
            .end()
            .use('change-prefix')
            .loader('change-prefix-loader')
            .options({
                prefix: 'ivu-',
                replace: 'gp-'
            })
            .end()
    },
}

在postcss.config.js文件中添加以下代码 (如果没有就新建一个)

<!-- postcss.config.js -->
const addCssPrefix = require('postcss-change-css-prefix')
module.exports = {
  plugins: [
    addCssPrefix({
      prefix: 'ivu-',
      replace: 'gp-',
    }),
  ],
}

插件总归是插件,会有个别样式前缀没有添加成功。这种情况下自己可以手动加下。切记 手动加的前缀要和postcss.config.js中定义的保持一致

总结

经过这次微前端改造,项目由一个应用 拆分出了 三个子应用,起到了极大的瘦身效果,目前线上运行稳定。本次微前端的实际落地为我们后续的技术升级提供了经验方案,也打破了前端后台长期以来受制于各种UI框架的桎梏,可谓是意义重大。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注