我对微前端技术的理解

477 阅读26分钟

背景

在传统的单体前端架构中,随着应用规模的不断扩大,代码库变得庞大且复杂,开发和维护成本急剧上升。不同功能模块之间的耦合度高,导致一个模块的修改可能会影响到整个应用的稳定性。此外,单体架构下团队协作效率低下,多个团队难以并行开发,容易出现代码冲突和资源竞争。技术栈的升级也变得困难重重,因为整个应用依赖于一套统一的技术栈,难以进行局部的升级和优化。这就是传说中的前端巨无霸(Frontend Monolith) ,微前端架构正是为了解决这些问题而提出的。

  1. 什么是微前端

  1. 概念

Micro Frontends 官网定义了微前端概念:

Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently.

构建一个现代 web 应用所需要的技术、策略和方法,具备多个团队独立开发、部署的特性。

微前端这个术语最初来自 2016 年的 ThoughtWorks 技术雷达,它将微服务的概念扩展到了前端领域。

然而,这个概念并不新鲜,过去它叫针对垂直系统的前端一体化或独立系统。不过微前端显然是一个更加友好并且不那么笨重的术语

巨石应用

垂直组织

  1. 核心理念

微前端架构的核心是将前端应用分解为多个独立的模块,这些模块可以是页面、组件或功能模块。每个模块都有自己的代码库、构建流程和部署流程,可以独立地进行开发、测试和部署。在运行时,这些模块通过某种机制被组合在一起,形成一个完整的前端应用。这种架构模式强调模块之间的解耦和独立性,同时也需要考虑模块之间的通信和协作。

微前端架构的关键特性包括:

image.png 2. ## 微前端架构的优势与使用场景

  1. 优势

  • 独立开发、降低代码耦合: 每个子应用由独立的前端团队开发,一般是独立仓库,这使得在代码维护、代码编译方面更加友好,各子应用可独立选择框架(React/Vue/Angular)。
  • 独立部署: 团队可以自行控制负责子应用的研发、编译、测试、部署等,上线后自动更新内容且不影响其他模块。
  • 技术栈无关、增量升级: 对于一个古老的项目来说这是很具有吸引力的,可以对项目进行增量技术迭代
  • 独立运行时: 每个微应用之间状态隔离,运行时状态不共享
  • 按需加载:提升首屏性能和用户体验。
  1. 使用场景

  • 一个非常庞大的项目,越来越大,后续难以维护。
  • 如果有跨部门和跨团队协作开发项目,这样会导致团队效率降低和沟通成本加大,这时我们可以使用微前端,每个团队或者每个部门单独维护自己的项目,我们只需要一个主项目来把分散的子项目汇集到一起即可。
  • 一个非常老旧的项目,开发效率低,但是一时半会又不能全部重构,这时我们就可以新创建一个新技术新项目的基座,把老项目的页面接入到新项目里面,后面新需求都在新项目里面开发就好,不用再动老项目。
  • 想独立部署每一个单页面应用。
  • 改善初始化加载时间,延迟加载代码。
  • 基于多页的子应用缺乏管理,规范/标准不统一,无法统一控制视觉呈现、共享的功能和依赖。造成重复工作。
  1. 实现微前端有哪些方案

单纯根据对概念的理解,很容易想到实现微前端的重要思想就是将应用进行拆解和整合,通常是一个父应用加上一些子应用,那么使用类似Nginx配置不同应用的转发,或是采用iframe来将多个应用整合到一起等等这些其实都属于微前端的实现方案

  1. Nginx路由转发

通过Nginx配置反向代理来实现不同路径映射到不同应用,例如www.abc.com/app1对应app1,www.abc.com/app2对应app2,这种方案本身并不属于前端层面的改造,更多的是运维的配置

image.png

server {
    listen 80;
    server_name www.abc.com;

    location /app1 {
        proxy_pass http://app1-server;
    }

    location /app2 {
        proxy_pass http://app2-server;
    }
}

2. iframe 嵌套

iframe 可以创建一个全新的独立的宿主环境,这意味着前端应用之间可以相互独立运行,仅需要做好应用之间的管理、通信即可,父子通信可采用postMessage或者contentWindow方式

image.png

<div id="app1" style="height: 500px;">
    <iframe src="http://app1-server" style="width: 100%; height: 100%;"></iframe>
</div>

<div id="app2" style="height: 500px;">
    <iframe src="http://app2-server" style="width: 100%; height: 100%;"></iframe>
</div>

3. 前端微服务化

前端微服务化,是微服务架构在前端的实施,每个前端应用都是完全独立(技术栈、开发、部署、构建独立)、自主运行的,最后通过模块化的方式组合出完成的应用

image.png 4. 前端微件化

微前端的微件化是指将部分业务功能构建成一个独立的chunk代码,使用时只需要远程加载即可。这种开发方式通过构建一个新的构建系统,将特定的业务功能封装成微件(widget),用户可以根据需要动态加载这些微件,从而实现功能的模块化和复用

image.png

  1. Web Components构建

Web Components 是一套不同的技术,允许开发者创建可重用的定制元素(它们的功能封装在代码之外)并且在 Web 应用中使用它们

image.png

  1. webpack5:module federation

webpack5 提供了一个新特性:module federation,基于这个特性,我们可以在一个 JavaScript 应用中动态加载并运行另一个 JavaScript 应用的代码,并实现应用之间的依赖共享

优缺点对比

方案优点缺点|
Nginx路由转发实现简单; 不需要对现有应用进行改造;完全技术栈无关;用户体验不好,每次切换应用时,浏览器都需要重新加载页面;多个子应用无法并存;局限性比较大;子应用之间的通信比较困难; 子应用切换时需要重新登录;
iframe嵌套实现简单;css 和 js 天然隔离,互不干扰; 完全技术栈无关;多个子应用可以并存; 不需要对现有应用进行改造- 用户体验不好,每次切换应用时,浏览器需要重新加载页面;UI 不同步,DOM 结构不共享;全局上下文完全隔离,内存变量不共享,子应用之间通信、数据同步过程比较复杂;对 SEO 不友好;子应用切换时可能需要重新登录,体验不好;
Web Components实现简单; css 和 js 天然隔离,互不干扰;完全技术栈无关;多个子应用可以并存;主要是浏览器 兼容性 问题;开发成本较高
前端微服务化纯前端改造,体验良好,可无缝感知切换,子应用相互隔离。需要设计和开发,由于父子应用处于同一页面运行,需要解决子应用的样式冲突,变量对象污染,通信机制等技术点
前端微件化灵活性和复用性:微件化使得功能模块化,便于复用和维护;性能优化:只加载需要的功能模块,减少页面加载时间,提升用户体验资源管理:需要有效管理多个微件的加载和卸载,避免资源浪费。兼容性:确保不同微件之间的兼容性和交互性。
webpack5 module federation切换应用时,浏览器不用重载页面,提供和单页应用一样的用户体验;相同资源不需要重复加载;应用启动后,无需加载与自己无关的资源构建工具只能使用 webpack5;对老项目不友好,需要对 webpack 进行改造
  1. 主流微前端框架

微前端架构基本需要实现三个部分:

  • 主应用接入子应用,包括子应用的注册、路由的处理、应用的加载和路由的切换。
  • 主应用加载子应用,这部分之所以重要,是因为接入的方式决定了是否可以更高效的解耦。
  • 子应用的容器,这是子应用加载之后面临的问题,包含了JS沙箱、样式隔离和消息机制。

微前端时间线

image.png 微前端关键对比

维度/框架Single-SPAqiankunmicro-appwujieEMP
实现原理**Single-SPA**是纯路由级生命周期调度器,无内置沙箱或资源控制能力。基于**Single-SPA**扩展路由调度能力,通过动态资源加载和沙箱隔离实现子应用集成。基于 WebComponents 封装 <micro-app> 标签,通过劫持fetch/XHR重写资源请求,结合DOM监听实现渲染控制。利用iframe原生隔离能力,通过DOM代理将子应用内容渲染至主应用容器,消除iframe布局割裂。EMP 方案是基于 webpack 5 module federation 的微前端方案。
沙箱机制需手动实现Proxy/快照沙箱,或依赖SystemJS动态加载模块。JS隔离:Proxy代理全局对象(支持多实例),快照沙箱作为降级方案。 样式隔离:动态加载/卸载样式标签,可选 Shadow DOM 增强。JS隔离:Proxy代理子应用window对象。 样式隔离:Scoped CSS选择器限定作用域,支持Shadow DOM模式。JS/CSS隔离:iframe原生上下文隔离。 DOM操作:代理 iframe 子应用DOM事件到主容器,实现无缝嵌入。无隔离机制,依赖版本控制和接口契约避免冲突。
通信方式完全自定义(事件总线、全局状态管理库等)。Props数据注入 + 事件驱动(主从模式),支持全局状态库扩展。CustomEvent 事件总线 + 标准化API数据传递(props注入与子应用主动触发)。postMessage通信 + 事件代理池优化(复用通道降低性能损耗)。模块直接调用,需严格管理版本兼容性。
Vite支持需插件插件主持原生原生社区方案
优点多框架支持:single-spa支持多种前端框架,包括React、Angular、Vue等。这意味着你可以在同一应用中混合使用不同的前端技术栈,无需重写现有代码。独立部署和开发:每个微前端应用都可以独立部署和开发,使得团队可以独立地迭代和发布他们的部分,而不会影响整个应用。 增量升级:在面对复杂场景时,全量技术栈升级或重构通常难度较大。微前端提供了一种实施渐进式重构的有效策略。 友好的jQuery支持:single-spa对jQuery的支持友好,方便了传统网页转换为微前端的过程。自动分析和加载:qiankun能自动分析html以获取js和css,不需要开发者手动指定如何加载。沙箱机制:基于快照和Proxy的思路实现了JS隔离,基于Shadow Dom和scoped css的思路实现了CSS隔离。 提供通信机制:qiankun提供了acitons全局状态管理的机制和props,用于应用间的通信。自动应用加载和卸载:qiankun能监听路由变化,实现当前路由对应的子应用的自动加载和卸载。简单易用:micro-app的使用方式类似于iframe,上手简单。侵入性低:对原有代码的影响微乎其微。功能丰富:micro-app提供了js沙箱、样式隔离、元素隔离、预加载、数据通信、静态资源补全等丰富的功能。隔离机制类似于qiankun,例如,样式隔离基于scoped css,js沙箱机制基于Proxy。 零依赖:micro-app不依赖于任何第三方库。 支持Vite:支持使用Vite进行快速开发。简洁易用:Wujie采用组件式的使用方式,接入简单,与micro-app有类似的简洁性。彻底隔离:虽然CSS隔离是基于Shadow DOM,但iframe通过proxy的方法将DOM劫持到Shadow DOM上,提供了彻底的隔离。轻量级:Wujie借助iframe和webcomponent来实现沙箱,有效减小了代码量,使得其体积较为轻量。支持Vite:Wujie支持使用Vite进行快速开发。依赖自动管理,可以共享 Host 中的依赖,版本不满足要求时自动 fallback 到 Remote 中依赖;共享模块粒度自由掌控,小到一个单独组件,大到一个完整应用。既实现了组件级别的复用,又实现了微服务的基本功能;共享模块非常灵活,模块中所有组件都可以通过异步加载调用;
缺点复杂性:single-spa作为一个框架,需要额外的学习和配置,可能增加项目的复杂性。性能开销:按需加载和运行多个微前端应用可能会带来一些性能开销,尤其是在初始加载时,需要仔细优化和配置以保证良好的性能表现。无沙箱机制:对于CSS和JavaScript的隔离需要手动实现,例如通过Shadow DOM或者Proxy。无内置通信机制:微应用间的通信需要自己实现,可能需要借助全局状态、自定义事件或其他第三方库。无内置应用生命周期管理:single-spa并未提供应用的加载和卸载机制,需要开发者自行实现。无全局状态管理:single-spa本身并不提供全局状态管理,需要与其他状态管理库配合使用。适配成本高:从生命周期、静态资源路径、路由、webpack配置等方面都需要做一系列的适配工作,可能导致适配成本较高。CSS沙箱隔离不完全:严格模式基于Shadow DOM,虽然形成了天然的隔离,但第三方组件的弹窗默认挂在到body下面,这样弹窗中的自定义样式会失效,需要手动设置挂载节点。实验性模式类似于scoped css模式来隔离,对于样式名冲突时,也可能出现问题。不支持ESM脚本:qiankun官方不支持如vite等ESM脚本的运行。静态资源补全问题:静态资源补全是基于父应用的,而非子应用。这需要开发者自己手动解决。路由状态保持问题*:在多应用激活后,无法保持各子应用的路由状态,刷新后全部丢失。隔离问题:默认css隔离方式,主应用的样式还是会污染到子应用浏览器兼容性:对于不支持webcomponent的浏览器,micro-app没有进行降级处理。内存开销:承载js的iframe是隐藏在主应用的body下面的,相当于是常驻内存,这可能会带来额外的内存开销。基于 Webpack5 Module Federation,需要统一 Webpack5 技术;文档资料,社区不够活跃;
  1. 微前端集成要点

实施微前端的过程并非易事,它需要深入了解和娴熟掌握一系列关键技术,包括路由管理、全局状态管理及通信、性能优化,以及错误隔离与处理等。下面,我们将详细探讨这些关键技术:

  1. 微前端中的路由菜单管理

微前端架构允许微应用独立运行,每个微应用都具有自己的路由菜单导航逻辑。这样就带来了一个问题,如何将所有微应用的菜单整合到主应用中。为解决这一挑战,我们可以采取以下三种策略:

  1. 中心化路由管理(🌟推荐)

    1.     在中心化路由管理模式下,所有微应用的路由被集中管理,由主应用维护全局路由表和菜单,并将微应用的路由信息融入到全局路由表中。在这种模式下,微应用需要通过主应用获取自己的路由信息,并依据该信息进行路由控制和导航。
    2.     优势:
    3. 统一性:由于所有的路由信息都在主应用中进行集中管理,避免了不同微应用的路由冲突,使得路由的维护和管理更为方便。
    4. 可控性:主应用能够集中管理所有路由和权限,提供了更好的控制能力。
    5.     劣势:
    6. 耦合性:微应用需要依赖主应用,这限制了微应用的灵活性。
    7. 集中化:随着微应用数量的增加,主应用的逻辑负担可能过重,可能造成单点故障。
    8.     以 qiankun 框架为例,中心化路由管理的实现关键步骤如下:
    9. 在主应用中定义全局路由表和菜单,将各个微应用的路由信息整合到全局路由表中。
    10. 主应用获取全部路由表,在注册子应用时,通过 props 传递给子应用。
    11. // 在主应用程序中定义获取路由信息的接口
      function getRoutes() {
        // 返回全局路由表
        return globalRoutes;
      }
      
      // 在主应用程序中注册子应用
      qiankun.registerMicroApps([
        {
          name: 'my-app',
          entry: '//localhost:3001',
          container: '#my-app',
          activeRule: '/my-app',
          props: {
            getRoutes, // 将获取路由信息的接口传递给子应用
          },
        },
        // ...
      ]);
      
    12. 在微应用中获取主应用传递的路由信息,并使用 qiankun 框架提供的生命周期函数 "mount" 来实现路由信息的获取。
    13. /**
       * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
       */
      export const mount: (props: any) => void = async props => {
        await render();
        setGlobalData(props.data);
        setLoadGlobalFinish(true);
      };
      
  2. 动态路由管理

    1.     在动态路由管理模式下,主应用和微应用各自维护自己的路由菜单逻辑及控制。微应用向主应用暴露自己的路由菜单数据,主应用在运行时,动态获取微应用的数据来生成自己的路由菜单。
    2.     优势:
    3. 解耦:微应用各自维护自己的路由逻辑,实现了微应用和主应用的完全解耦。
    4. 灵活性:每个应用都可以根据自身的需要定制自己的路由管理策略。
    5.     劣势:
    6. 难以维护:由于每个应用都有自己的路由菜单逻辑,这可能导致维护的复杂性提高。
    7.     在 qiankun 框架中,动态路由管理模式可以通过以下步骤实现:
    8. 微应用暴露出路由菜单数据:微应用需要在自己的入口文件中暴露出自己的路由菜单数据,使用 setGlobalState 方法将数据保存到 qiankun 的全局状态中,供主应用使用。
    9.    import { setGlobalState } from 'qiankun';
      
         const routes = [
           {
             path: '/home',
             name: 'Home',
             component: Home,
           },
           {
             path: '/about',
             name: 'About',
             component: About,
           },
         ];
      
         setGlobalState({
           routes,
         });
      
    10. 主应用获取微应用的路由菜单数据:主应用可以通过 useEffect 钩子函数和 getGlobalState 方法来获取微应用的路由菜单数据,并保存到主应用的状态中。
    11. import { useEffect } from 'react';
      import { getGlobalState } from 'qiankun';
      
      const App = () => {
        const [routes, setRoutes] = useState([]);
      
        useEffect(() => {
          const routes = getGlobalState('routes') || [];
          setRoutes(routes);
        }, []);
      
        return (
          <div>
          <ul>
          {routes.map((route) => (
            <li key={route.path}>
        <Link to={route.path}>{route.name}</Link>
        </li>
      ))}
        </ul>
        <hr />
        <Router>
        {routes.map((route) => (
          <Route key={route.path} path={route.path} component={route.component} />
          ))}
          </Router>
          </div>
        );
      };
      
  3. 混合式路由管理

    1.     混合式路由管理则是中心化路由管理和动态路由管理两者的结合。主应用维护全局的路由表和菜单,微应用则维护自身的路由表和菜单。微应用的路由表和菜单信息会被主应用动态获取和整合。
    2.     优势:
    3. 灵活性:微应用可以根据自身需要定制自己的路由管理策略。
    4. 统一性:主应用能够集中管理所有路由和权限,提供了更好的控制能力。
    5.     劣势:
    6. 复杂性:需要在主应用和微应用之间维护路由信息的同步,可能增加系统的复杂性。
    7.     混合式路由管理的实现主要是结合了中心化路由管理和动态路由管理的实现方式,主应用需要动态地获取微应用的路由信息,并将这些信息整合到全局路由表中。
  1. 微前端的全局状态管理及通信

微前端作为一种新兴的设计理念,它将大型单体前端应用程序分解成多个独立运行的小型应用。在这种架构下,全局状态管理和通信是一个重要的话题。实现这个目标的方式有很多,以下是几种常见的策略:

  1. 框架自身的状态管理机制: 比如qiankun框架,它提供了官方的actions和无界的去中心化通信方案eventBus等工具来实现状态管理和通信。
  2. Redux或Mobx: 对于使用React的微前端应用,Redux或Mobx是实现全局状态管理的常见选择。在这种情况下,需要在微前端应用之间共享和同步Redux store。
  3. Vuex: 对于使用Vue的微前端应用,Vuex是一个可行的全局状态管理方案。

在大多数基础场景下,微前端框架本身提供的通信方法已经能够满足需求。然而,在面临一些特殊的业务需求时,可能需要自定义通信解决方案。

接下来,我们将以qiankun框架提供的actions为例,详细解析如何实现微前端的通信。

首先,我们在主应用中注册MicroAppStateActions实例,并导出:

// micro-app-main/src/shared/actions.ts
import { initGlobalState, MicroAppStateActions } from "qiankun";

const initialState = {};
const actions: MicroAppStateActions = initGlobalState(initialState);

export default actions;

然后,在注册子应用的列表中,将actions以props的形式传递给子应用:

const apps = [
  {
    name: 'App1', 
    entry: "http://localhost:8000",
    container: "#container",
    activeRule: '/vue',
    props: { actions}   //向子应用传递创建的全局状态
  }
]

接下来,当主应用的某个组件(如ComponentA)的数据发生变化时,我们可以通过调用actions的setGlobalState方法来通知子应用:

<template>
  <div>
    <el-button @click="handleNotification">通知子应用1</el-button>
  </div>
</template>

<script>
  import actions from "@/shared/actions";
  export default {
    name: 'ComponentA',
    methods: {
      handleNotification() {
        actions.setGlobalState({message:'发送消息'})
      }
    }
  }
</script>

在子应用中接收消息,接受前需要先配置子应用的全局状态Actions,并在main.js中注入父应用传过来的acitons实例:

function initMethod(state: unknown) {
  console.warn("state----", state);
}
class Actions {
  // 默认值为空 Action
  actions = {
    onGlobalStateChange: initMethod,
    setGlobalState: initMethod,
  };
  // 设置 actions
  setActions(actions: unknown) {
      this.actions = actions;
  }
        // 映射
  onGlobalStateChange(...args: any) {
    return this.actions.onGlobalStateChange({...args});
  }
        // 映射
  setGlobalState(...args: unknown[]) {
    return this.actions.setGlobalState({...args});
  }
}
const actions = new Actions();
export default actions;

在main.js的mount生命周期接收父应用传递的actions,并注入actions实例

export async function mount(props) {
  if (props) {
    // 注入 actions 实例
    actions.setActions(props)
  }
  render(props);
}

最后,我们在子应用的pageA.js中监听全局状态的变化:

<script>
  import actions from "./actions";
  export default {
    name: 'ComponentA',
    mounted(){
      actions.onGlobalStateChange((state, prevState)=>{
        const { message } = state
        console.info('接收消息:',message)
      })
    }
  }
</script>

至此,我们已经完成了使用qiankun进行微前端通信的全部步骤。这种方法的优点在于,它提供了一种跨应用的通信机制,实现了状态的共享与同步。然而,它也有一些缺点,例如在复杂的业务场景下可能需要定制化解决方案,以及可能存在的性能问题等。

总的来说,选择合适的微前端通信方案,需要根据具体的业务场景和技术栈来考量。既要考虑易用性和可维护性,也要兼顾性能和稳定性。

  1. 微前端的性能优化

在微前端架构中,性能优化是一个不能忽视的重要环节。特别是在同时运行多个微应用程序的情况下,需要从多个角度对性能问题进行深入分析。以下是一些常见的性能瓶颈以及相应的优化策略:

  1. 首屏加载速度

    1.   在微前端架构中,多个独立的微应用程序可能会对首屏加载速度产生负面影响。
    2.   优化策略:
    3. 代码拆分:将应用的代码拆分成多个小的代码块,然后按需加载。这样可以降低首屏需要加载的代码量。例如,在React中可以利用React.lazy和Suspense进行代码拆分,而在Vue中,可以使用异步组件对代码进行拆分。
    4. 预加载:在浏览器空闲时提前加载下一屏所需的资源,从而提高下一屏的加载速度。可以利用标签或Webpack的预取功能来实现。
    5. 资源压缩和优化:使用Webpack等构建工具对代码进行压缩和优化,包括JS、CSS和图片等资源。此外,也可以使用CDN服务来加速资源的加载速度。
  2. 微应用切换性能

    1.   由于微前端架构涉及多个独立的微应用,因此应用间的切换可能对性能产生影响。
    2.   优化策略:
    3. 生命周期管理:当应用不再需要时,应及时销毁并回收资源。例如,可以在应用卸载时销毁相关实例,并回收事件监听器、定时器等资源。
    4. 缓存:对已加载的微前端应用进行缓存,以便下次快速加载。例如,可以在应用加载后将应用实例保存到内存中,下次需要时直接从内存中获取。
  3. 全局状态管理

    1.   微前端架构可能会使得全局状态的管理变得复杂,从而影响性能。
    2.   优化策略:
    3. 避免过度的全局状态管理:全局状态的管理应该保持简洁,避免无必要的全局状态。过多的全局状态可能会使状态同步变得复杂,同时也可能影响性能。
    4. 优化全局状态的同步:全局状态的同步应该尽可能高效,避免无必要的同步操作。例如,可以采用观察者模式或发布-订阅模式来进行状态同步。
  4. 共享依赖管理

    1.   尽管微前端模块是独立开发和部署的,但它们之间可能会有一些共享的依赖库,如 React、Vue 等。为了避免重复加载这些共享依赖,可以通过一些技术手段进行优化
    2.   优化策略:
    3. 依赖提取:可以将共享依赖提取出来,作为单独的资源进行加载,或者使用 Webpack 的 externals 配置将共享依赖排除在微前端模块的构建包之外,从而减少重复代码的加载。
  1. 案例分享(以后台管理系统为例

最外层是基座, 基座是微前端应用集成的一个重要平台。同时也肩负着管理公共资源、依赖、规范的责任。主要有以下职责:

  1. 子应用集成,给子应用提供渲染容器
  2. 权限管理
  3. 路由、菜单管理
  4. 主题管理
  5. 共享依赖
  6. 多语言管理(最重要的一点)等

content里面可以任意放不同技术的子应用,我们只需要开发一个主应用(主应用也可以自由选择语言,目前支持react、vue、vite、angular、next.js、nuxt.js),将一些分散的应用接进来,主应用还可以通过控制权限,让不同的账号看到的菜单不一样,即看到不同系统的页面,通过同一个地址访问到不同的子应用。

image.png

主应用:react 18+

子应用:vite + vue3

子应用:react 18+

安装

  1. 主应用
yarn add qiankun # 或者 npm i qiankun -S

2. 子应用(如果你的子应用不是vite构建的,你无需安装任何插件)

npm i vite-plugin-qiankun

搭建

在主应用中注册子应用

import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'react app', // 子应用的名称
    entry: '//localhost:7100', // 子应用运行的url和port
    container: '#yourContainer', // 用于放置子应用显示的载体
    activeRule: '/sub-react', // 匹配的路由
  },
  {
    name: 'vue app',
   entry: '//localhost:3000',
    container: '#yourContainer',
    activeRule: '/sub-vue',
  },
]);

start();

子应用配置

  • vite子应用
import { createApp } from 'vue'
import App from './App.vue'
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper';
import { router, abstractRouter } from './router';
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

let app;
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
  createApp(App).use(router).use(ElementPlus).mount('#app');
} else {
  renderWithQiankun({
    // 子应用挂载
    mount(props) {
      let routerInstance = null;
      console.log('props', props?.path);
      if (props?.path) {
        routerInstance = abstractRouter;
      } else {
        routerInstance = router;
      }
      app = createApp(App);
      // 使用 provide 将 props 传递给所有后代组件
      app.provide('qiankunProps', props); 
      app.use(routerInstance).use(ElementPlus);
      app.mount(props.container.querySelector('#app'));
      if (props?.path) {
        routerInstance.push(props.path)
      }
    },
    // 只有子应用第一次加载会触发
    bootstrap() {
      console.log('vue app bootstrap');
    },
    // 更新
    update() {
      console.log('vue app update');
    },
    // 卸载
    unmount() {
      console.log('vue app unmount');
      app?.unmount();
    }
  });
}
  • react子应用
import React from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter, MemoryRouter } from "react-router-dom";
import { QiankunContext } from "./QiankunContext.jsx";
import "./index.css";
import App from "./App";
import "./public-path";  // webpack子应用需要新增一个这样的文件,下方有说明

let root;
function render(props) {
  const { container,path } = props;
  const RouterWrapper = props?.path ? MemoryRouter : BrowserRouter;
  const dom = container
    ? container.querySelector("#root")
    : document.getElementById("root");
  root = createRoot(dom);
  root.render(
    <RouterWrapper basename="/sub-react" initialEntries={path ? [path] : ["/"]}>
      <QiankunContext.Provider value={props}>
        <App mianProps={props} />
      </QiankunContext.Provider>
    </RouterWrapper>
  );
}

// 判断是否在qiankun环境下,非qiankun环境下独立运行
if (!window.__POWERED_BY_QIANKUN__) {
  render({});
}

// 各个生命周期
// bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
export async function bootstrap() {
  console.log("react app bootstraped");
}

// 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
export async function mount(props) {
  console.log("props from main framework", props);
  render(props);
}

// 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
export async function unmount(props) {
  root.unmount();
}

basename="/sub-react" 这个和你在主应用注册子应用中的activeRule要保持一直

webpack构建的子应用需要新增下面的文件,并在入口文件中进行导入

src 目录新增 public-path.js

if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

子应用配置文件修改

  • webpack构建的应用
const { name } = require('./package');
module.exports = {
  devServer: {
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd', // 把微应用打包成 umd 库格式
      jsonpFunction: `webpackJsonp_${name}`, // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
    },
  },
};
  • vite构建的应用
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import qiankun from 'vite-plugin-qiankun';
export default defineConfig({
  base: '/sub-vue', // 和基座中配置的activeRule一致
  server: {
    port: 3000,
    cors: true,
    origin: 'http://localhost:3000' //你的实际运行地址
  },
  plugins: [
    vue(),
    qiankun('sub-vue', { // 配置qiankun插件
      useDevMode: true
    }),
  ]
})
  output: {
    library: `${name}-[name]`,
    libraryTarget: 'umd', // 把微应用打包成 umd 库格式
    jsonpFunction: `webpackJsonp_${name}`, // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
  },

参考文档

  1. Micro Frontends - Martin Fowler
  2. Micro Frontends
  3. single-spa 官方文档
  4. qiankun 官方文档
  5. Micro-app官方文档
  6. Wujie官方文档
  7. Web Components 介绍