微前端方案调研

lxf2023-03-14 09:35:01

什么是微前端?

单个应用,在较长时间跨度内,由于参与团队的增加,已经从一个普通应用演变为一个巨型应用,变得不可维护。微前端架构就是为了解决这种应用。

多团队开发维护现代应用的一套方案,这些团队可以独立开发部署. see Micro Frontends

微前端架构具有以下核心特性:

  • 技术无关:主框架不限制子应用使用的技术栈,子应用有充分的自主权。
  • 独立开发和部署:子应用仓库、开发、部署都是独立的。

微前端基本功能

加载子应用

监听路由加载对应的子应用。

通信

子应用之间、子应用与主应用之间的通信。

沙箱

js/style 隔离: 子应用之间、子应用与主应用之间的样式和 js 不互相影响。

微前端方案

iframe

iframe:浏览器原生,不从体验角度上来看几乎是最可靠的微前端方案了。主应用通过 iframe 来加载子应用,iframe 自带的样式、环境隔离机制使得它具备天然的沙盒机制,但也是由于它的隔离性导致其并不适合作为加载子应用的加载器,iframe 的特性不仅会导致用户体验的下降,也会在研发在日常工作中造成较多困扰。

Baidu:EMP

基于 Webpack5 Module Federation 搭建的微前端解决方案。通过 cdn 加载微应用,可以动态更新微应用,微应用只需要部署一次便可以提供给任何基于 Module Federation 的应用使用。每个微应用间都可以引入其它的微应用,无中心应用的概念。可以选择只加载微应用中需要的部分。每一个应用都可以进行状态共享。

single-spa

什么是 import-maps ?

Import maps allows control over what URLs get fetched by JavaScript import statements and import() expressions.

<script type="importmap"> 
  {
    "imports": {
      "moment": "/node_modules/moment/src/moment.js",
      "lodash": "/node_modules/lodash-es/lodash.js"
    }
  }
</script> 
import moment from "moment"; 
import { partition } from "lodash"; 

// Change to below 
import moment from "/node_modules/moment/src/moment.js"; 
import { partition } from "/node_modules/lodash-es/lodash.js";

import-maps 还没有被所有浏览器支持. see caniuse.com/import-maps. 可以使用 SystemJS or es-module-shims 提供对 import-maps 的支持.

框架特性
  1. 加载子应用:single-spa 是一个顶层的路由器。当一个路由被激活时,它下载并执行该路由的代码

  2. 生命周期:bootstrap, mount, unmount, unload

  3. JS Entry.

  4. 懒加载

    • 注册子应用时支持懒加载

      registerApplication({
        name: 'app1',
        app: () => import('src/app1/main.js'), // lazy loading
        activeWhen: '/app1',
        customProps: { some: 'value' }
      });
      
  5. 通信

    • props: customProps
如何使用?
  1. 怎么创建一个 single-spa 应用?

    • 创建 root config: 包括一个 HTML 页面和一个注册子应用的 js。子应用注册需要提供:  HTML demo
      • name:子应用名称
      • app:指定子应用入口
      • activeRule:判断何时触发子应用
    • 修改子应用入口文件使其导出 bootstrap, mount & unmount hooks
  2. Demos

    • React: react.microfrontends.app
    • Vue: vue.microfrontends.app/rate-doggos

蚂蚁:Qiankun

两个场景
  • 单一实例:在同一时间只显示一个子应用,子应用的切换通常是根据网址的变化来完成的。qiankun v1.x
  • 多个实例:多个子应用可以同时呈现。这种情况下,子应用更像是一个业务组件而不是一个应用。qiankun v2.x
框架特性
  1. 基于 single-spa 封装,提供了更加开箱即用的 API。
  2. 技术栈无关,任意技术栈的应用均可使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
  3. HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
  4. 样式隔离,确保微应用之间样式互相不干扰。
  5. JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
  6. 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
  7. umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统。
如何使用?

主应用

  1. 安装
yarn add qiankun # or npm i qiankun -S
  1. 在主应用中注册子应用
import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'react app', // app name registered
    entry: '//localhost:7100',
    container: '#yourContainer',
    activeRule: '/yourActiveRule',
  }, {
    name: 'vue app',
    entry: { scripts: ['//localhost:7100/main.js'] },
    container: '#yourContainer2',
    activeRule: '/yourActiveRule2',
  },
]);
start();

子应用

  1. 子应用入口文件处导出生命周期

    子应用需要在自己的入口文件中导出 bootstrapmountunmount 三个生命周期钩子,供主应用程序在适当的时候调用

    export async function bootstrap() {
      console.log('react app bootstraped');
    }
    export async function mount(props) {
      ReactDOM.render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
    }
    export async function unmount(props) {
      ReactDOM.unmountComponentAtNode(props.container ? props.container.querySelector('#root') : document.getElementById('root')); 
    }
    export async function update(props) {
      console.log('update props', props);
    }
    
  2. 子应用打包配置

    为了使主应用能够正确识别子应用暴露的一些信息,子应用需要添加以下打包配置:

    // webpack
    const packageName = require('./package.json').name;
    module.exports = {
      output: {
        library: `${packageName}-[name]`,
        libraryTarget: 'umd',
        jsonpFunction: `webpackJsonp_${packageName}`
      }
    };
    

Demo: http://localhost:7099/react16

字节:Garfish

框架特性
  1. 技术栈无关
  2. 独立开发和部署
  3. 预加载
  4. 支持共享依赖
  5. 支持多实例
  6. 支持 HTML entry & JS entry
核心模块
  1. Loader:加载 html entry and js entry。
  2. Router:自动挂载/卸载子应用。
  3. Sandbox:js & css 隔离。
  4. Store:通信。

微前端方案调研

如何使用?

主应用

  1. 安装
npm install garfish --save
  1. 注册子应用并启动
// index.js(主应用入口处)
import Garfish from 'garfish';
Garfish.run({
  basename: '/',
  domGetter: '#subApp',
  apps: [{
    name: 'react',
    activeWhen: '/react',
    entry: 'http://localhost:3000', // html入口
  }, {
    name: 'vue',
    activeWhen: '/vue',
    entry: 'http://localhost:8080/index.js', // js入口
  }]
});

子应用

  1. 更新打包配置
// webpack.config.js
const webpack = require('webpack');
const isDevelopment = process.env.NODE_ENV !== 'production';
module.exports = {
  output: {
    // 开发环境设置 true 将会导致热更新失效
    clean: isDevelopment ? false : true,
    filename: '[name].[contenthash].js',
    chunkFilename: '[name].[contenthash].js',
    // 需要配置成 umd 规范
    libraryTarget: 'umd',
    // 修改不规范的代码格式,避免逃逸沙箱
    globalObject: 'window',
    // webpack5 使用 chunkLoadingGlobal 代替,或不填保证 package.json name 唯一即可
    jsonpFunction: 'garfish-demo-react',
    // 保证子应用的资源路径变为绝对路径     
    publicPath: 'http://localhost:8080',
  },
  plugin: [
    // 保证错误堆栈信息及 sourcemap 行列信息正确     
    new webpack.BannerPlugin({
      banner: 'Micro front-end',
    })
  ],
  devServer: {
    // 保证在开发模式下应用端口不一样
    port: '8000',
    headers: {       
      // 保证子应用的资源支持跨域,在上线后需要保证子应用的资源在主应用的环境中加载不会存在跨域问题(**也需要限制范围注意安全问题**)
      'Access-Control-Allow-Origin': '*',
    },
  },
};
  1. 导出生命周期
import { reactBridge } from '@garfish/bridge-react';

export const provider = reactBridge({
  el: '#root',
  rootComponent: RootComponent,
  errorBoundary: () => <Error />
});
  1. 设置路由
// src/component/rootComponent
import React from "react";
import { BrowserRouter } from "react-router-dom";

const RootComponent = ({ basename }) => {
  return (
    <BrowserRouter basename={basename}>
      <Routes>
        <Route path="/" element={<App />}>
          <Route path="/home" element={<Home />} />
          <Route path="*" element={<PageNotFound />} />
        </Route>
      </Routes>
    </BrowserRouter>
  )
}

demo: stackblitz.com/edit/garfis…

京东:MicroApp

框架特性
  1. 没有沿袭single-spa的思路,而是借鉴了 WebComponent 的思想,通过 CustomElement 结合自定义的 ShadowDom,将微前端封装成一个类 WebComponent 组件,从而实现微前端的组件化渲染。
  2. 样式隔离
  3. js 沙箱
  4. 预加载
  5. 通信

微前端方案调研

如何使用?

主应用

  1. 安装
npm i @micro-zoe/micro-app --save
  1. 入口文件处启动 microApp
// index.js
import microApp from '@micro-zoe/micro-app'
microApp.start()
  1. 为子应用配置一个路由
// router.js
import { BrowserRouter, Switch, Route } from 'react-router-dom'
import MyPage from './my-page'

export default function AppRoute () {
  return (
    <BrowserRouter>
      <Switch>
        // 非严格匹配,/my-page/* 都指向 MyPage 页面
        <Route path='/my-page'>
          <MyPage />
        </Route>
      </Switch>
    </BrowserRouter>
  )
}
  1. MyPage 中嵌入子应用
// my-page.js 
export function MyPage () {
  return (
    <div>
      <h1>子应用</h1>
      // name(必传):应用名称
      // url(必传):应用地址,会被自动补全为http://localhost:3000/index.html
      // baseroute(可选):基座应用分配给子应用的基础路由,就是上面的 `/my-page`
      <micro-app name='app1' url='http://localhost:3000/' baseroute='/my-page'></micro-app>
    </div>
  )
}

子应用

  1. 设置基础路由
// router.js
import { BrowserRouter, Switch, Route } from 'react-router-dom'
export default function AppRoute () {
  return (
    // 设置基础路由,子应用可以通过window.__MICRO_APP_BASE_ROUTE__获取基座下发的baseroute
    // 如果没有设置baseroute属性,则此值默认为空字符串
    <BrowserRouter basename={window.__MICRO_APP_BASE_ROUTE__ || '/'}> ... </BrowserRouter>
  )
}
  1. 更新打包配置
devServer: {
  headers: {
    'Access-Control-Allow-Origin': '*',
  }
}

Demo

  • Vue: www.micro-zoe.com/main-vue2/

微前端特性方案对比

微前端方案调研

总结

single-spa: 是一个纯粹的顶层路由器,对于实现微前端给出了参考的解决方案,但是自身没有实现这些功能的,比如:样式隔离/js隔离等。接入成本高。

Qiankun: 基于 single-spa,从框架层面解决了样式隔离、js隔离等问题。同时提供了预加载、应用通信等功能。对于接入的子应用的改动相对中等。社区比较活跃,解决 issue 的速度也比较快,相对稳定。总的来说算是目前比较优选的微前端框架。

Garfish: 路由参考了 single-spa,沙箱参考了 Qiankun,提供了插件机制去扩展框架的功能,框架的基础能力也是通过插件机制。实现的功能、接入成本和 Qiankun 差不多,比较新,使用没有 Qiankun 多。

MicroApp: 基于类WebComponent,实现了JS沙箱、样式隔离、元素隔离、预加载、资源地址补全、插件系统、数据通信等一系列完善的功能。接入改动最小。MicroApp 依赖 CustomElements 和 Proxy 两个较新的 API, 对于不支持 CustomElements 的浏览器,可以通过引入 polyfill 进行兼容,详情可参考:webcomponents/polyfills。但是 Proxy 暂时没有做兼容,所以对于不支持 Proxy 的浏览器无法运行 MicroApp。

对比single-spaQiankunGarfishMicroApp
技术栈无关✔️✔️✔️✔️
支持多实例✖️✔️✔️✔️
预加载✖️✔️✔️✔️
JS 隔离✖️✔️✔️✔️
样式隔离✖️✔️✔️✔️
共享依赖✖️✖️✔️✔️
通信✔️✔️✔️✔️
接入成本highestmiddlemiddlelow

参考

What is micro-frontends

Qiankun

蚂蚁 Qiankun 官网
GitHub Qiankun
如何设计实现微前端框架 - qiankun

Garfish

字节 Garfish 官网
Github garfish
字节跳动是如何落地微前端的 - 知乎
PMP

MicroApp

京东 MicroApp 官网
GitHub MicroApp
极致简洁的 MicroApp 开源了