Skip to content
风起
风起

前端微前端之模块联邦

概要

  • 前端微前端架构的核心是对资源作用域的控制,我调研了无界、乾坤、micro-app、iframe、模块联邦等方案,发现没有一个能完美解决问题的,相对来说利用 模块联邦+iframe的方案更好些,调研结果见《微前端调研.xlsx》

使用场景

  1. 应用级集成 - 将多个独立的应用集成在一起,构建微前端架构

    • 实现应用之间的无缝切换
    • 支持不同团队独立开发、部署应用组件
    • 提升大型前端项目的可维护性
  2. 动态特性 - 实现区别于npm的动态包管理器

    • 安装包不需要重新编译
    • 在运行时可直接使用
    • 支持动态加载资源,提高应用性能

集成选型

项目类型推荐技术方案优势
基于vite/vue3的项目模块联邦高性能、交互灵活
基于vue-cli的vue3项目模块联邦
(需先改成vite打包)
高性能、交互灵活
其他技术栈iframe无技术栈限制

应用级集成

注意事项

  1. 部署结构

    • 为兼容iframe方式,将各子应用打包后放在同一域名端口下,以应用目录区分
    • 各子应用需配置正确的baseUrl以确保资源路径正确
  2. 路由配置

    • 使用模块联邦进行应用级集成时,路由需选择history模式
    • 配置正确的路由前缀,避免多应用间路由冲突
    • iframe和模块联邦路由统一约定为:
      • 子应用独立运行时路由为(/app1/*)
      • 集成到主应用后路由为(/main/app1/*)
  3. 资源隔离

    • iframe方式集成

      • 由于各应用处于同一域名端口下,localStorage/sessionStorage的key需带应用前缀,避免冲突
      • 示例:const storageKey = ${APP_PREFIX}:user-settings;
    • 模块联邦方式集成

      • CSS隔离:避免修改html、body等DOM节点的全局样式
      • 组件内CSS应带scoped属性:<style scoped>
      • 全局样式包含在根节点作用域下
      css
      /* 推荐做法 */
      .app-container .global-style { /* 样式定义 */ }
      
      /* 避免使用 */
      body, html { /* 全局样式 */ }
      • JS隔离:避免将变量直接挂在window上
      • 推荐将变量provide在app实例上或使用带应用前缀的命名
      js
      // 推荐做法
      app.provide('appState', state);
      // 或
      window['APP_PREFIX_variable'] = value;
      • 浏览器API:考虑对其他应用的影响,如localStorage/sessionStorage使用前缀
  4. 应用生命周期

    • 集成结构对比

      • iframe集成:主应用→页面→iframe→子应用→页面
      • 模块联邦集成:主应用→页面→子应用→页面
    • 职责划分

      • 主应用:负责应用级的加载、卸载、保活等生命周期
      • 子应用:负责组件级的生命周期管理
    • 事件通信

      • eventbus需兼容两种集成结构,提供生命周期相关事件
      js
      // 事件总线示例
      const eventBus = {
        events: {},
        on(event, callback) {
          if (!this.events[event]) this.events[event] = [];
          this.events[event].push(callback);
        },
        emit(event, data) {
          if (this.events[event]) {
            this.events[event].forEach(cb => cb(data));
          }
        }
      };
      
      // 生命周期事件
      eventBus.emit('app:mounted', { appName: 'childApp1' });
  5. 交互体验

    • iframe集成的挑战

      • 路由同步:浏览器刷新、前进、后退可能出现非预期效果
      • DOM隔离:某些场景交互体验割裂
      • 加载性能:应用加载较慢
      • 解决方案:可考虑实现路由同步机制,或对小型组件使用模块联邦
    • 模块联邦集成的优势

      • 无路由同步问题
      • 无DOM隔离限制
      • 加载性能更好
      • 注意点:需重点关注资源隔离

组件级集成

与应用集成的区别

  1. 路由

    • 子应用携带自己的路由,需要统一规划避免相互冲突,而远程组件不带路由,所以不会引起路由冲突。
  2. 应用实例

    • 子应用有自己的应用实例,和主应用的实例隔离,注册在不同实例上的各模块不通,需要事件通信。
    • 远程组件没有自己的应用实例,一般直接挂载到主应用上,与原主应用组件使用无区别。
  3. 服务结构

    • 区别于中心化的应用集成,组件级集成是去中心化的网状结构,一个服务既可以是远程组件的消费者也可以是提供者。

注意事项

  1. 全局资源

    • 组件依赖的全局资源,要么share主应用的,要么改为组件内import,否则找不到资源
    • 组件内部也要避免全局变量及样式的定义,可以带前缀定义自己的作用域
  2. 共享范围

    • 不局限于共享组件,还可共享函数、类等模块

模块联邦配置

Vite配置示例

js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { federation } from '@module-federation/vite'

export default defineConfig({
  plugins: [
    vue(),
    federation({
      name: 'remoteApp1',
      filename: 'remoteEntry.js',
      // 暴露远程模块
      exposes: {
        './RApp1Dialog': './src/views/Page1.vue', // 暴露组件
        './RApp1Table': './src/views/Page2.vue',
        './RApp1App': './src/export-app.ts', // 暴露应用
      },
      // 注册远程模块
      remotes: {
        remoteApp2: {
          type: 'module',
          name: 'remoteApp2',
          entry:
            env.NODE_ENV === 'production'
              ? 'http://localhost/app2/remoteEntry.js'
              : 'http://localhost:8668/app2/remoteEntry.js',
        },
      },
      // 共享全局依赖
      shared: [
        'vue',
        'vue-router',
        'pinia',
        'vuex',
        'element-plus',
        '@xmagital/hsf-common-ui',
        '@xmagital/hsf-event-bus',
      ],
    }),
  ],
})

远程模块使用示例

js
// 在路由中使用远程应用
const RApp1App = createRemoteComponent({
  loader: () => import('remoteApp1/RApp1App'),
  basename: `${import.meta.env.BASE_URL}/app1`,
})

{
  path: '/app1/:pathMatch(.*)*',
  name: 'app1',
  component: RApp1App,
}

// 在页面中使用远程组件
<template>
  <h2>远程组件集合</h2>
  <RApp1Dialog />
  <RApp1Table />
</template>

<script setup lang="ts">
import RApp1Dialog from 'remoteApp1/RApp1Dialog'
import RApp1Table from 'remoteApp1/RApp1Table'
</script>

动态注册远程模块

  • 对门户应用来说,更好的方式是利用模块联邦的运行时,动态注册其他远程模块
  • 注意:模块联邦的运行时要求生产者用到 manifestgetPublicPath 两个配置
  • 参考:Module Federation Runtime

参考资料