​ 本文针对微前端技术进行调研,主要从微前端背景、主流框架及原理以及当前行业中的一些实践方案三个方面进行介绍,最后给出了一些自己的思考和总结。

微前端背景介绍

是什么?

Mirco-frontends官网对微前端定义是:构建一个现代Web应用所需的技术、策略和方法,它具有多个团队独立开发、部署的特性。通俗来讲,微前端就是一种类似于微服务的架构理念,可以将一个复杂的前端应用拆分为更小、更简单的子应用,使得这些子应用可以由不同的团队进行独立的开发和部署。同时它也可以将不同框架(如React、Vue、Angular等)开发的、运行已久的应用进行合并,以达到降低复用成本、减少项目之间的耦合、提升项目扩展性等目的。一个微前端应用通常由一个基座应用(主应用)和多个子应用构成,通过基座应用来管理子应用的加载和卸载。

image-20240207171854987微前端具有以下几个核心价值(引用自qiankun官网):

  • 技术栈无关:

    主框架不限制接入应用的技术栈,微应用具备完全的自主权

  • 独立开发、独立部署:

    微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新

  • 增量升级:

    在面对各种复杂场景时,我们通常很难对一个已存在的系统做全量的技术升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略

  • 独立运行时:

    每个微应用之间状态隔离,运行时状态不共享

为什么?

​ 项目中为什么要引入微前端方案?在上述核心价值中已有所体现。现代的前端应用发展趋势正在变得越来越富功能化、富交互化,已有被维护的项目会随着时间的推进变得越来越庞大、越来越难以维护。开发人员往往会面临以下几个问题:

  • 不同的团队间需要使用不同的技术栈开发同一个应用;
  • 每个团队都希望独立开发、独立部署,并且独立部署的应用有更新时如何同步更新其它模块;
  • 新项目中还需要老项目中的代码,如何才能在无侵入的情况下复用老项目中的代码;

​ 此时,开发人员可以使用微前端架构理念,将一个应用划分成若干个子应用。当路径切换时加载不同的子应用,这样来实现每个子应用的独立,同时也不用受技术栈的限制,解决期待前端团队协同开发的问题。

何时用?

​ 是否需要使用微前端需要根据具体的业务场景来定,但通常情况下在包括但不限于以下的几种场景下使用微前端能得到一个长久的收益:

  • 零散的活动页面

    ​ 在很多活动场景中,运营需要配置一些重复性的业务,页面间存在雷同性、相似性或由不同的组件拼凑而成的。使用一个配置系统将这些组件进行灵活的管理将大大提高开发的效率,而此时配置系统很适合使用微前端理念来解决。

  • 中台项目

    ​ 面向于公司内部提供一些服务类的产品,会随着功能的丰富和配置系统的增加变得愈发庞大和复杂,使用微前端构建可以得到长久的收益。

  • 大型产品项目

    ​ 便于多个团队协作开发独立部署,减少开发周期。

微前端主流框架

​ 几乎所有的微前端框架都需要解决两大共性问题:一是应用的加载与切换。包括路由的处理、应用加载的处理和应用入口的选择。二是应用的隔离与通信。包括JS的隔离(副作用隔离)、样式的隔离以及父子应用与子子应用之间的通信问题。围绕着这两大问题出现了如下几种常用的微前端框架/方案:

从single-spa到qiankun

​ single-spa是一个很好的微前端框架,而qiankun框架就是基于single-spa来实现的,在single-spa的基础上做了一层封装,同时也解决了single-spa的一些缺陷。

Single-spa

​ 使用single-spa首先需要在基座应用中注册所有APP的路由,single-spa会保存各子应用的路由映射关系,并充当微前端控制器Controler。当URL响应时,匹配子应用路由并加载渲染子应用,如下图所示:

image-20240207173851674

使用single-spa以Vue脚手架搭建的应用为基座构建微前端项目,实现步骤及关键代码如下:

  • 在main.js中完成基座配置,其核心是通过registerApplication注册子应用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    / main.js
    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    import { registerApplication, start } from 'single-spa'
    Vue.config.productionTip = false
    const mountApp = (url)=>{
    return new Promise((resolve,reject) => {
    const script = document.createElement('script');
    script.src = url;
    script.onload = resolve;
    script.onerror = reject;
    // 通过script标签的方式挂在应用
    const firstScript = document.getElementsByTagName('script')[0];
    // 挂载子应用
    firstScript.parentNode.insertBefore(script, firstScript);
    })
    }
    const loadApp = (appRouter,appName) => {
    // 远程加载子应用
    return async() => {
    // 手动挂在子应用
    await mountApp(appRouter + '/js/chunk-venders.js/');
    await mountApp(appRouter + '/js/app.js');
    // 获取子应用生命周期
    return window[appName]
    }
    }
    // 子应用列表
    const appList = [
    {
    name:'app1', // 子应用名称
    app:loadApp('http://localhost:8083','app1'), // 挂在子应用
    activeWhen:location => location.pathname.startsWith('/app1')// 匹配该子路由的条件
    customProps:{} // 传递给子应用的对象
    },
    {
    name:'app2',
    app:loadApp('http://localhost:8082','app2'),
    activeWhen:location => location.pathname.startWith('/app2'),
    customProps:{}
    }
    ]
    // 注册子应用
    appList.map(item => {
    registerApplication(item)
    })
    // 注册路由并启动基座
    new Vue({
    router,
    mounted(){
    start()
    },
    render:h => h(App)
    }).$mount('#app')
  • 在子应用中配置生命周期挂在与导出方式。配置的核心是使用single-spa-vue生成字路由配置,并抛出其生命周期函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    import singleSpaValue from 'single-spa-vue'
    Vue.config.productionTip = false
    const appOptions = {
    el:'#microApp',
    router,
    render:h => h(App)
    }
    // 如果不是微应用环境,则启动自身挂在的方式
    if(!process.env.isMicro) {
    delete appOption.el
    new Vue(appOptions).$mount('#app')
    }
    // 基于基座应用,导出生命周期
    const appLifecycle = singleSpaVue({
    Vue,
    appOptions
    })
    // 抛出子应用生命周期
    // 启动生命周期函数
    export const bootstrap = (props) => {
    return appLifecycle.bootstrap(()=>{});
    }
    // 挂载生命周期函数
    export const mount = (props) => {
    return appLifecycle.mount(()=>{})
    }
    // 卸载生命周期函数
    export const unmount = (props) => {
    return appLifecycle.unmount(()=>{})
    }
  • 配置子应用为umd打包方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // vue.config.js
    const package = require('./package.json')
    module.export = {
    // 告诉子应用在这个地址加载静态资源,否则会去基座应用的域名下加载
    publicPath:'//localhost:8082',
    // 开发服务器
    devServer:{
    port: 8082
    },
    configureWebpack:{
    // 导出umd格式的包,在全局对象上挂载属性package.name,基座应用需要通过这个全局对象获取一些信息
    // 比如子应用到出的生命周期函数
    output:{
    // library的值在所有子应用中需要唯一
    library:package.name,
    libraryTarget:'umd'
    }
    }
    }
  • 配置子应用环境变量

    1
    2
    3
    4
    // .env.micro
    NODE_ENV = development
    VUE_APP_BASE_URL = /app2
    isMicro = true

​ Single-SPA通过路由劫持和导出生命周期钩子函数的方式很好的解决了路由的加载切换和应用接入的问题,但在应用入口的选择、应用隔离(JS隔离和样式隔离)等方面仍有不足。例如,使用JS Entry的方式需要更改打包配置,将整个微应用打包成一个JS发布到静态资源服务器,然后在主应用中配置文件地址告诉single-spa去哪加载微应用,这将导致打包产物体积膨胀、无法并行加载等问题,且该方式侵入性较强。除此之外,Single-SPA并未提供应用之间的通信方案,它仅在注册应用时给微应用注入一些状态信息,后续通讯需要用户自己去实现。

qiankun

为了解决Single-SPA的一些不足,qiankun在Single-SPA的基础上进行了进一步的扩展,并且保留了Single-SPA中的优秀理念。其主要的扩展如下:

  • HTML Entry:JS Entry的方式是把子应用打包成一个entry script,其中css、图片等资源必须都打包到js bundle中导致bundle体积庞大,并且资源无法并行加载。HTML Entry的方式是主应用fetch子应用的html入口文件,去掉html/head/body后,把子节点插入主应用容器中,具有灵活、低成本接入的优点。

  • JS沙箱:JS 沙箱为每个微应用生成单独的 window proxy 对象,配合 HTML Entry 提供的 JS 脚本执行器 (execScripts) 来实现 JS 隔离,确保微应用之间全局变量/事件不冲突。

  • CSS样式隔离:提供了两种样式隔离的方案:严格的样式隔离和改变选择器名的方式。严格的样式隔离模式,为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响。改变样式名的方式利用css预处理器在编译时生成不冲突的选择器名。

  • 应用间通信:提供了两种通信方法,一种是挂载一个事件总线,在总线上注册监听事件,通过发布订阅模型来实现应用之间的相互通信。另一种基于props,将state 和 onGlobalStateChange通过props传递给子应用,从而实现应用间的通信。

使用qiankun以Vue脚手架搭建主应用、React搭建子应用,实现关键代码如下:

  • 在主应用中注册微应用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const apps = [
    {
    name: "ReactMicroApp", // 微应用名称 - 具有唯一性
    entry: "//localhost:10100", // 微应用入口 - 通过该地址加载微应用
    container: "#frame", // 微应用挂载节点 - 微应用加载完成后将挂载在该节点上
    activeRule: "/react", // 微应用触发的路由规则 - 触发路由规则后将加载该微应用
    },
    ];
    export default apps;
  • 配置主应用菜单

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    export default class App extends Vue {
    menus = [
    {
    key: "Home", // 唯一 Key 值
    title: "主页", // 菜单标题
    path: "/", // 菜单对应的路径
    },
    {
    key: "ReactMicroApp",
    title: "React 主页",
    path: "/react",
    },
    {
    key: "ReactMicroAppList",
    title: "React 列表页",
    path: "/react/list",
    },
    ];
    }
  • 配置微应用,导出qiankun所需的三个生命周期钩子函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    if (window.__POWERED_BY_QIANKUN__) {
    // 动态设置 webpack publicPath,防止资源加载出错
    // eslint-disable-next-line no-undef
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    }
    import React from "react";
    import ReactDOM from "react-dom";
    import "antd/dist/antd.css";
    import "./public-path";
    import App from "./App.jsx";

    /**
    * 渲染函数
    * 两种情况:主应用生命周期钩子中运行 / 微应用单独启动时运行
    */
    function render() {
    ReactDOM.render(<App />, document.getElementById("root"));
    }
    // 独立运行时,直接挂载应用
    if (!window.__POWERED_BY_QIANKUN__) {
    render();
    }
    /**
    * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
    * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
    */
    export async function bootstrap() {
    console.log("ReactMicroApp bootstraped");
    }
    // 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
    export async function mount(props) {
    render(props);
    }
    // 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
    export async function unmount() {
    ReactDOM.unmountComponentAtNode(document.getElementById("root"));
    }
  • 配置路由命名空间,确保主应用能正常加载子应用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // micro-app-react/src/App.jsx
    const BASE_NAME = window.__POWERED_BY_QIANKUN__ ? "/react" : "";
    const App = () => {
    //...
    return (
    // 设置路由命名空间
    <Router basename={BASE_NAME}>{/* ... */}</Router>
    );
    };
  • 修改webpack配置,使生命周期钩子函数能被qiankun识别。由于React脚手架对webpack配置做了隐藏,因此我们可以借助react-app-rewired来实现。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    // 新建 config-overrides.js 文件
    const path = require("path");
    module.exports = {
    webpack: (config) => {
    // 微应用的包名,这里与主应用中注册的微应用名称一致
    config.output.library = `ReactMicroApp`;
    // 将你的 library 暴露为所有的模块定义下都可运行的方式
    config.output.libraryTarget = "umd";
    // 按需加载相关,设置为 webpackJsonp_VueMicroApp 即可
    config.output.jsonpFunction = `webpackJsonp_ReactMicroApp`;
    config.resolve.alias = {
    ...config.resolve.alias,
    "@": path.resolve(__dirname, "src"),
    };
    return config;
    },

    devServer: function (configFunction) {
    return function (proxy, allowedHost) {
    const config = configFunction(proxy, allowedHost);
    // 关闭主机检查,使微应用可以被 fetch
    config.disableHostCheck = true;
    // 配置跨域请求头,解决开发环境的跨域问题
    config.headers = {
    "Access-Control-Allow-Origin": "*",
    };
    // 配置 history 模式
    config.historyApiFallback = true;
    return config;
    };
    },
    };

基于WebComponent的micro-app

与qiankun不同,micro-app并没有沿袭single-spa的思路而是借鉴了WebComponent的思想,通过CustomElement结合自定义的ShadowDom,将微前端封装成一个类WebComponent组件,从而实现微前端的组件化渲染。并且由于自定义ShadowDom的隔离特性,micro-app不需要像single-spa和qiankun一样要求子应用修改渲染逻辑并暴露出方法,也不需要修改webpack配置,是目前接入微前端成本最低的方案。

基础概念

​ Web Components 是一套不同的技术,允许你创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的 web 应用中使用它们。其主要由三个部分组成Custom elements(自定义元素)、Shadow DOM(影子 DOM)和HTML templates(HTML 模板),通过一起使用来创建封装功能的定制元素,且不用担心代码冲突(引用自MDN官网)。详细定义可移步至MDN官网阅读。

image-20240207175123787

  • 使用方便:

​ 可以将所有功能都封装到一个类WebComponent组件中,从而实现在基座应用中嵌入一行代码即可渲染一个微前端应用。同时还提供了js沙箱、样式隔离、元素隔离、预加载、数据通信、静态资源补全等一系列完善的功能。

  • 零依赖:

​ micro-app没有任何依赖,这赋予它小巧的体积和更高的扩展性。

  • 兼容所有框架:

​ 为了保证各个业务之间的独立开发、独立部署的能力,micro-app做了许多兼容,在任何技术框架中都可以正常运行。

微应用配置
  • 基座配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // index.js
    import React from "react"
    import ReactDOM from "react-dom"
    import App from './App'
    import microApp from '@micro-zoe/micro-app'

    const appName = 'my-app'

    // 预加载
    microApp.preFetch([
    { name: appName, url: 'xxx' }
    ])

    // 基座向子应用数据通信
    microApp.setData(appName, { type: '新的数据' })
    // 获取指定子应用数据
    const childData = microApp.getData(appName)

    microApp.start({
    // 公共文件共享
    globalAssets: {
    js: ['js地址1', 'js地址2', ...], // js地址
    css: ['css地址1', 'css地址2', ...], // css地址
    }
    })
  • 子应用配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    // index.js
    import React from "react"
    import ReactDOM from "react-dom"
    import App from './App'
    import microApp from '@micro-zoe/micro-app'

    const appName = 'my-app'

    // 子应用运行时,切换静态资源访问路径
    if (window.__MICRO_APP_ENVIRONMENT__) {
    __webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__
    }
    // 子应用向基座发送数据
    // dispatch只接受对象作为参数
    window.microApp.dispatch({ type: '子应用发送的数据' })
    // 获取基座数据
    const data = window.microApp.getData() // 返回基座下发的data数据

    // 性能优化,umd模式
    // 如果子应用渲染和卸载不频繁,那么使用默认模式即可,如果子应用渲染和卸载非常频繁建议使用umd模式
    // 将渲染操作放入 mount 函数 -- 必填
    export function mount() {
    ReactDOM.render(<App />, document.getElementById("root"))
    }

    // 将卸载操作放入 unmount 函数 -- 必填
    export function unmount() {
    ReactDOM.unmountComponentAtNode(document.getElementById("root"))
    }

    // 微前端环境下,注册mount和unmount方法
    if (window.__MICRO_APP_ENVIRONMENT__) {
    window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount }
    } else {
    // 非微前端环境直接渲染
    mount()
    }
  • 路由配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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>
    )
    }

​ 综上所述,Web Components是有能力以组件加载的方式将微应用整合在一起作为微前端的一种手段,但不幸的是,Web Components是浏览器的新特性,所以它的兼容性不是很好,如果有兼容性要求的项目还是无法使用,具体请查看can i use

基于Webpack5的ModuleFederation

基础概念

​ Module Federation是webpack5提出的概念,用于解决多个应用之间的代码共享的问题,让使用者更加优雅的实现跨应用的代码共享。其解决问题的中心思想与微前端的思想类似,即把一个应用拆分成多个应用,每个应用可独立开发,独立部署,一个应用可动态的加载并运行另一个应用的代码,并实现应用之间的依赖共享。

为了实现这些功能,Module Federation在设计上提出了以下几个概念:

  • Container:被ModuleFederationPlugin打包出来的模块被称为Container。通俗来讲就是,如果项目中一个应用适用了ModuleFederationPlugin构建,那么它就一个Container模块,它可以加载其他的Container,也可以被其他的Container所加载。

  • Host&Remote:

    • 以消费者和生产者的角度看,Container又可被称作Host和Remote。
    • Host:消费方,它可以动态的加载并运行其他的Container的代码。
    • Remote:提供方,用于暴露属性(如组件、方法等)供Host使用。

​ 对于一个container来说,Host和Remote是相对的,一个Container既可以作为Host,又可以作为Remote使用。

  • Shared:一个 Container 可以 与其他 Container 可以共享的第三方依赖,使你的代码中不用重复加载同一份依赖,也就是共享依赖。
微应用配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 配置webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

module.exports = {
// 其他webpack配置...
plugins: [
new ModuleFederationPlugin({
name: 'empBase',
library: { type: 'var', name: 'empBase' },
filename: 'emp.js', // 入口文件名称
remotes: { // 如果把这一模块当作基座模块的话,这里应该配置其他子应用模块的入口文件
app_two: "app_two_remote",
app_three: "app_three_remote"
},
exposes: { //暴露可访问的组件
'./Component1': 'src/components/Component1',
'./Component2': 'src/components/Component2',
},
//共享依赖,其他模块不需要再次下载,便可使用
shared: ["react", "react-dom","react-router-dom"]
})
]
}

其构建过程如下:

  • 首先,Module Federation会让webpack以filename作为文件名生成文件。
  • 其次,文件中以var的形式暴露了一个名为name的全局变量,其中包含了expose以及shared中配置的内容。
  • 最后,作为host时,先通过remote的init方法将自身shared写入remote中,再通过get获取remote中expose的组件,而作为remote时,判断host中是否有可用的共享依赖,若有,则加载host的这部分依赖,若无,则加载自身依赖。

当前行业中的实践方案

X音乐PaaS微前端方案

image-20240207175435482

​ 云音乐微前端平台针对于云音乐的CMS场景,提出了可配置式的微前端平台化设计,包括用于管理主子应用的 PaaS 管理平台,可对主子应用的引用关系以及主子应用的路由、权限等配置进行管理。以及基于 qiankun 实现的根据下发配置渲染微应用与菜单的外框架(主应用)与微应用(子应用)。

​ 在云音乐 CMS 中,对微前端做了以下约定:

  • 以页面为粒度拆分子应用,主应用是多个子应用部分页面的组合;
  • 主应用对页面路由、权限进行管理;

​ 云音乐CMS的主要场景是需要对现有存量的基于umi 2、umi 3以及regluar搭建的前端应用进行接入管理,其核心价值在于可管理微应用与编排菜单的 PaaS 平台。因此,运行时的微前端实现方案主要以umi为构建框架,在基于umi官方提供的qiankun插件基础上,编写并封装适配于云音乐CMS场景的umi3和regular插件,其插件主要解决了如下几个问题:

  • 扩展插件功能。为应用默认导出在runtime.js中qiankun需要的方法,并将主应用传递给子应用的参数写入子应用的全局变量中,便于需要时调用。此外,在构建完成后生成菜单并上报平台的功能。
  • 处理base路由问题。在微前端场景下,多个子应用之间可能存在路由重合的场景。因此,需要对子应用路由的 base path进行改写,并拼接上主子应用的 base path。
  • 解决路由反复横跳问题。将初始化路由监听由bootstrap 阶段调整到mount 阶段,并在 history 的 push() 方法上做拦截。

使用方法:X音乐CMS文档:https://p.fn.netease.com/#/use

欢聚时代EMP微前端方案

image-20240207175542935

​ 在中台开发过程中,有许多的配置系统需要去开发和维护, 这些配置系统之间有很多公用模块需要公用的。对于这些公用的模块,为了节约第二次开发和第三次开发的人力资源,需要考虑去把这些共享组件抽取出来作为公共的资源。解决该问题的常用方法是将业务子模块抽取为npm包,但对于该种方式来说,其具有以下痛点:

  • 更新流程繁琐。 当公用模块迭代的业务系统较多时,每次更新npm包版本后都需要更新多个应用的npm包的版本。
  • 构建速度慢。 一个应用系统中可能应用到了多个以npm包形式引入的业务子模块, 随着npm包越来越多的情况下, 同步构建的体积会越来越大、构建时间变长甚至发布的流程也会越来越慢。
  • 应用迭代麻烦。例如,对于中台业务场景通常需要自定义一个统一UI风格的骨架,在实际项目中引用并填充业务内容。但当模版更新某些功能后,已有的项目无法自动的更新模版需要手动更新,并且可能需要做一些冲突处理。

​ 由于该业务场景主要是希望对业务中的共享模块做到独立开发、独立部署、一键更新等特性,对于qiankun和single-spa来说,同一个技术栈具有状态不共享、部分模块可能需要改造才能调用,这些会增加部署、维护、改造的成本,因此采用模块联邦的方式实现微前端更贴近其业务。

基本方案

image-20240207175636517

EMP是采用Webpack5的Module Federation实现的微前端方案,其生态总体框架吧如上所示。

  • 基于webpack 5、Module Federation、TypeScript搭建EMP脚手架,仓库地址:https://github.com/efoxTeam/emp。
  • 使用脚手架搭建项目,实现构建、打包、webpack配置、Medule Federation配置等一系列配置。
  • 搭建一个基站base(可以理解成一个主应用项目), 用于放置公用组件如ui组件、业务组件等。
  • 搭建具体的业务应用,如App1、App2等,这些业务应用就可以共享base中的公用业务组件,同时App1和App2之间也可以相互共享业务组件,并且当组件更新后会同步到相应的应用。

使用方法:

​ emp-cli接入文档:https://github.com/efoxTeam/emp/blob/main/packages/emp-cli/README-zh_CN.md

XX金服ALPHA微前端方案

业务背景

为了满足大型中台项目的开发,要解决如下问题:

  • 将不同的业务子系统集中到一个大平台上统一对外开放;
  • 给不同的用户赋予不同的权限让其能够访问平台的特定业务模块,同时禁止其访问物权限的业务模块;
  • 快速接入新的子系统,并对子系统进行版本管理,保证功能同步;
  • 针对于老系统如何实现从Backbone技术栈到React技术栈或Vue技术栈的平滑升级;

​ 使用微前端方案可以很好的解决以上问题,但对于qiankun和Single-Spa框架存在以下问题:

  • single-spa中所有的子项目都必须存在与同一域下的仓库中;
  • 对于single-spa和qiankun来说,他们存在的一个共同的问题是都有一套自己的路由机制,因此如果旧项目中使用的是react-router就必须对旧的项目中的router进行重构;

​ 因此,ALPHA系统基于single-spa的思想,重构了一套微前端方案,使得子系统发布不受约束、独立部署、同步更新的同时,非侵入式的整合使用React、Angular、Vue等前端框架搭建的中后台项目。

image-20240207175743647

针对于以上需求,APLHA前端方案通过如下过程实现:

  • 封装了一套自己的脚手架,在脚手架中设定适合自身的打包配置;
  • 使用UC系统管理应用的权限,创建项目前需要在UC中创建对应的项目并设置权限;
  • 以js文件为子应用入口文件,将子应用打包生成 ${sourceKey}.js 文件与**${sourceKey}.css文件,并同时使用 webpackManifestPlugin生成自述mapping.json**文件;
  • 在APLHA系统中加入项目的相关配置,当子应用被加载时,通过http请求的方式获取mapping.json自述文件,通过layout中的方法组合并进行模版渲染;
  • 使用消息订阅与发布的方式实现layout与子系统之间的通信;

image-20240207175831323

通过该方法,ALPLHA具有较高的可配置性,可以实现三种不同场景需求:

  • 不使用ALPHA提供的layout和vendor,完全独立渲染;
  • 使用ALPHA提供的layout和vendor,子项目仅渲染内容区;
  • 使用ALPHA提供的layout,子项目有自己的vendor.js;

总结思考

综上所述,现有的几种微前端实现方法与框架可总结如下:

  • iframe
  • Single-Spa
  • qiankun
  • mirco-app(Web Components)
  • EMP(Module Federation)

其中,这些解决方案各有利弊:

  • iframe:可以直接加载其他应用,但无法做到单页导致许多功能无法正常在主应用中展示。
  • Single-Spa:很好的解决了路由的加载切换和应用接入的问题,但在应用入口的选择、应用隔离(JS隔离和样式隔离)等方面仍有不足。并且框架没有提供应用之间的通信方案,需要用户手动实现。
  • qiankun:结合了Single-Spa的优点并弥补了其缺陷。基本上可以称为单页版的iframe,具有沙箱隔离及资源预加载的特点,几乎无可挑剔。
  • mirco-app(Web Components):Web Components是浏览器提供给开发者的能力,能在单页中实现微前端,但是考虑到时浏览器的新特性,故存在兼容性问题,微前端方面的探索也不成熟,只能作为面向未来的微前端手段。
  • EMP(Module Federation):在实现微前端的基础上,扩充了跨应用状态共享、跨框架组件调用、远程拉取ts声明文件、动态更新微应用等能力。同时,第三方依赖的共享,使代码尽可能地重复利用,减少加载的内容。但不足之处在于目前无法覆盖所有前端框架。

​ 对比三个实践场景,我的感受是:没有最好的微前端框架,只有最适用的业务场景。云音乐CMS微前端方案直接利用现成的qiankun插件进行改造,既满足了对存量应用的接入、新应用的独立开发部署等需求,又极大的节约了改造成本。相比于欢聚时代EMP方案,他们的需求是希望在能够独立开发部署的同时,降低共享组件的维护成本及构建、更新效率,因此选择Module Federation无疑更加贴近需求。而对于贝壳金服的APLHA方案,需求主要关注于老应用的平滑升级及非侵入式的路由接入,因此重构了一套适用于自己的微前端方案。所以,针对于主要业务场景选择最合适的微前端方案最为重要,对于后续的细微不足可以慢慢的弥补。例如,云音乐CMS在后续的改造过程中,对于页面初始化加载速度慢的问题,通过提供sdk接入主应用的形式得到了解决,而对于共享组件复用的问题可以以构建subtree的形式得到解决。

参考