Maurice Wu
Published on

微前端框架 MicroApp 调研

微前端的适用场景

首先,微前端肯定不是银弹,并不是万能的,它有其适用的场景,也有不适合的场景。

如果你的产品有下面的需求,那么是可以考虑使用微前端的。

  1. 需要组合不同系统的产品,这些独立的产品的服务边界是明确的,没有强交集的。
  2. 前端处于一个不可控的体系中,也就是说系统的各个部分使用的技术栈,开发的团队都是不一样的。
  3. 需要在升级困难的老系统使用新技术栈开发的项目。

虽然微前端的独立开发独立部署很吸引人,确实可以降低大团队的协作沟通问题(系统越大,参与人员越多,沟通成本越高)。但是如果只是单纯为了应对大应用的可维护性,而简单粗暴地引入微前端的方式,将系统划分为几个更小的应用(可能根本就无法从业务层面划分),势必会增加系统的复杂性。

MicroApp 的实现方式和原理

MicroApp基于CustomElement实现,每个子应用都是在 <micro-app/>这个自定义元素中渲染。通过 CustomElementconnectedCallback``disconnectedCallback等事件来完成对子应用生命周期的管理。

MicroApp的样式隔离的实现方式有两种:

  1. shadowDom
  2. 拦截 标签的创建和插入,对不同子应用的样式文件追加子应用前缀来实现隔离。

MicroApp提供两种 JavaScript 沙箱:

  1. iframe 沙箱
  2. with 沙箱

MicroApp会拦截 标签的创建和插入,将子应用的 JavaScript 代码包装一层。这样代码里面访问到的顶层变量如 window, self,document是不同的对象,达到不同子应用之间逻辑的隔离,不会互相污染。

大体流程

画板

loadSource

  1. 使用 fetch 加载 html entry 文件,然后对其进行处理,将 body``head标签替换成micro-body``micro-head(一个 document 里面只能有一个 head 和 body 标签),同时可以应用 plugins 来自定义对 html entry 进行处理。
  2. micro-head中的link``style``script进行处理。
  3. 串行加载外联 link 标签的样式,然后对其进行 scopedCss 处理,生成style标签插入替换。
  4. 串行加载外联 script 标签的内容,然后用 sandbox 对其进行包装处理,拦截顶层的 window,self,document 访问。
  5. 在 app.onLoad 阶段,执行子应用的 js 代码。

scoped css

在 scopedCss 函数中,会应用 cssParser 对所有的样式添加前缀 micro-app [name=xxx],并且对于空的 style元素进行监听,一旦有新的样式插入,也会执行 scopedCss 。

分为两种情况:

对于子应用初始创建的时候,会依次遍历 head 中的所有 <link rel="style">style元素,然后执行加载样式文件,最后执行 scopedCss函数。

另外一种情况是,MicroApp 会拦截动态创建的linkstyle元素,对动态插入的样式也会应用 scopedCss。

execScript

在 app.onLoad 阶段,对 link 和 script 里面的 javascript 代码进行处理。

按照前面阶段处理的顺序,依次加在 link 的外联 javascript 代码或者是直接 runScript 执行内联代码。

在加载 script 文件完成之后,就已经使用 sanbox 对代码进行包装,拦截顶层的 window,self,document 访问。

keep-alive

如果子应用设置了 keep-alive,那么 <micro-app></micro-app>标签隐藏的时候,对应的字应用会进入 hidden 状态。

此时子应用的 sandbox 和 dom 树都会被保存起来,等待下一次渲染的时候,直接恢复。

prefetch

主动调用 microApp.prefetch 可以提前创建子应用,进行 loadSource 。

虚拟路由系统

如何支持 ESModule

对于 的脚本,必须使用 iframe sandbox

iframe sandbox 会在主应用页面中插入一个 display: none 的 iframe 标签。当需要执行 type=module 或者 inline script 的时候,就会创建一个 script 标签插入到 iframe 中。在 iframe 中的执行的脚本访问到的顶层变量就是当前 iframe 的 window,也就是当前的 js sandbox。

MicroApp 的局限

  1. 无法处理子应用 vw vh rem
  2. 子应用的媒体查询会有问题,如果子应用只是在局部渲染的话
  3. 对 esm 不支持 (使用 iframe 沙箱后,已经可以支持)
  4. 子应用是 ESModule 的时候,无法拦截 location 的操作,需要子应用一开始就是用虚拟路由。
  5. 比较成熟的应用都是后台管理系统,缺少其他类型系统的应用。
  6. 更耗内存(几乎是所有微前端的问题)

实际应用

  1. 主应用设置子应用页面路由,让所有的子应用页面都命中同一个页面组件。
const MICRO_APP_PREFIX = '/ma'
export const routes = [
  {
    path: MICRO_APP_PREFIX,
    component: () => import('@/layout'),
    name: 'MicroAppContainerPage',
    children: [
      {
        path: `${MICRO_APP_PREFIX}*`,
        component: MicroAppPageVue,
        name: 'MicroAppPage',
        meta: {
          title: '',
        },
      },
    ],
  },
]
  1. 创建子应用的时候,传递额外数据,如登陆鉴权,子应用受限菜单等信息
<template>
  <micro-app
    keep-alive
    :name="appName"
    :url="appFullPath"
    :baseroute="baseRoute"
    :data="appData"
  ></micro-app>
</template>

<script>
export default {
  computed: {
    appData() {
      user: {
        token: store.getters['user/token'],
      },
    }
  }
}
</script>
  1. 同步主子应用的 Tab 栏信息

Tab 栏一般用来展示用户已经访问过的页面,如果主应用有 Tab 栏的话,还需要处理以下的情况

  • 主应用打开某个子应用的页面的时候,需要记录到 Tab。
  • 主应用关闭某个子应用页面的时候,需要通知对应的子应用,让其不要缓存该页面,以节省内存。
  • 如果 Tab 栏中某个子应用的所有页面都已经关闭了,应该销毁该子应用,以节省内存。
  • 子应用内部的页面跳转或者关闭的时候,应该通知给主应用,让主应用决定是否展示在 Tab 栏中,或者在 Tab 栏中删掉对应的子应用页面。
  1. 子应用的 401 403 404 的错误信息应交由主应用处理。
  2. 子应用最好不要使用动态路由。是否可以访问子应用的某个页面应该是主应用决定的。