返回笔记首页

Module Federation 联邦模块(下)- 简历与面试

主题配置

四、简历撰写指南

4.1 项目经验描述模板

项目名称: Module Federation 微前端组件库共享实践

项目时间: 2024.07 - 2024.12

项目描述: 负责公司多个业务系统的组件库统一管理和共享方案落地,采用 Webpack 5 Module Federation 技术实现运行时模块共享。通过将公共组件、工具函数、业务模块抽离为独立的远程应用,实现了跨应用的代码复用,显著降低了包体积和维护成本。

核心职责

  1. 技术方案设计与落地
    • 深入研究 Module Federation 原理:容器初始化、模块加载、依赖共享机制
    • 设计组件库远程暴露方案,将 50+ 通用组件抽离为独立应用
    • 制定共享依赖策略(Vue、Vue Router、Axios 等),实现单例模式加载
    • 规范 exposes/remotes/shared 配置标准
  2. 组件库架构实现
    • 搭建独立的组件库应用(remote-components),暴露 Button、Form、Table 等 50+ 组件
    • 实现按需加载机制,组件级别的懒加载
    • 开发组件文档系统,自动生成 API 文档和使用示例
    • 建立组件版本管理体系,支持多版本并存
  3. 宿主应用改造
    • 改造 8 个业务系统接入 Module Federation
    • 配置 webpack.config.js,设置 remotes 和 shared
    • 实现动态远程模块加载,支持运行时切换组件版本
    • 处理异步加载、错误降级、版本兼容等问题
  4. 性能优化
    • 共享依赖优化:Vue、Vue Router 等公共库只加载一次,包体积减少 40%
    • 并行加载:多个远程模块并行加载,首屏时间降低 35%
    • 缓存策略:利用浏览器缓存和 CDN,二次加载速度提升 80%
    • 预加载:高频组件提前加载,用户体验显著提升
  5. 工程化建设
    • 开发 CLI 工具,一键生成远程应用模板
    • 建立 CI/CD 流程,组件库独立构建部署
    • 实现版本管理和回滚机制
    • 建立监控告警体系,实时监控模块加载情况

技术栈: Webpack 5、Module Federation、Vue3、Babel、PostCSS

项目成果
  • 包体积:各业务系统减少 40%(共享依赖生效)
  • 首屏速度:提升 35%(并行加载 + 缓存优化)
  • 开发效率:组件复用率 80%,新功能开发时间减少 50%
  • 维护成本:组件统一管理,Bug 修复一次所有系统生效,维护成本降低 60%
  • 构建速度:单个系统构建时间从 5 分钟降至 2 分钟
  • 迭代速度:组件库独立发版,不影响业务系统,发布频率提升 3 倍

4.2 SOP 标准回答话术

面试官:详细说说 Module Federation 的原理

回答话术

"好的。Module Federation 是 Webpack 5 的核心特性,它允许多个独立构建的应用在运行时共享代码。我从几个关键概念来解释:

第一,容器(Container)的概念

Module Federation 会把每个应用打包成一个容器,容器对外暴露一个入口文件(通常叫 remoteEntry.js)。这个入口文件包含了容器的元数据和初始化逻辑。

第二,Host 和 Remote 的关系
  • Host 是消费方,相当于主应用,它通过配置 remotes 来引用其他应用的模块
  • Remote 是提供方,相当于子应用或组件库,它通过配置 exposes 来暴露模块

比如我们的项目,有一个组件库应用作为 Remote,8 个业务系统作为 Host。

第三,共享依赖(Shared)机制

这是 Module Federation 最核心的价值。通过配置 shared,可以让多个应用共享同一份依赖。

举个例子,Vue 有 3MB,如果 3 个应用都打包 Vue,就是 9MB。但用 Module Federation 配置 shared 后,Vue 只会加载一次,节省了 6MB。

具体实现是通过 Webpack 的共享作用域(Shared Scope)。启动时,每个应用会把自己的 shared 依赖注册到共享作用域中。如果发现已经有相同版本的依赖,就直接用,不会重复加载。

第四,运行时加载流程

当 Host 应用要使用 Remote 的模块时:

  1. 先加载 Remote 的 remoteEntry.js
  2. 初始化容器,进行共享作用域的协商
  3. 通过容器的 get 方法获取模块
  4. 执行模块代码,返回结果

这个过程是完全异步的,所以我们通常用 defineAsyncComponent 来加载远程组件。

第五,版本管理

Module Federation 支持版本协商。比如 Host 要求 Vue ^3.3.0,Remote 提供的是 3.3.4,版本匹配,就共享。如果 Remote 只有 3.2.0,不匹配,就会加载两份 Vue。

可以通过 singleton: true 强制单例,requiredVersion 指定版本要求,strictVersion 控制是否严格检查。

我们项目的实践

我们把 50 多个通用组件抽离成独立应用,8 个业务系统通过 Module Federation 引用。Vue、Vue Router、Axios 等配置为 shared,实现单例加载。

效果很明显:包体积减少 40%,首屏速度提升 35%。而且组件库可以独立发版,不需要每个业务系统都重新构建。

当然也有一些坑,比如:

  1. 版本兼容问题,需要严格约定版本规范
  2. TypeScript 类型支持不太好,需要额外配置
  3. 调试比较困难,需要同时启动多个应用

但总体来说,对于大型项目的组件共享场景,Module Federation 是目前最优的方案。"

面试官:Module Federation 和 qiankun 有什么区别?
回答话术

"这两个方案的定位完全不同:

Module Federation
  • 粒度是模块级(组件、函数、类)
  • 侧重代码共享和复用
  • 适合组件库、工具库的共享
  • 需要 Webpack 5
  • 构建时优化更好
qiankun
  • 粒度是应用级
  • 侧重应用集成和隔离
  • 适合独立应用的组合
  • 框架无关
  • 运行时灵活性更好

举个例子:

如果你有一个 Button 组件,想在多个系统里用,用 Module Federation 最合适。直接 import 就行,就像用本地组件一样。

但如果你有一个完整的订单系统,想嵌入到主应用里,用 qiankun 更合适。它提供了完整的沙箱隔离、生命周期管理。

我们项目里其实两个都用了:

  • Module Federation 用来共享组件库和工具函数
  • qiankun 用来集成不同的业务系统

它们可以配合使用,不冲突。"

面试官:如何解决 Module Federation 的版本冲突问题?
回答话术

"版本冲突是 Module Federation 最大的挑战,我们有一套完整的解决方案:

第一,制定严格的版本规范

我们建立了一个依赖版本表,所有应用必须遵守:

plain
Vue: ^3.3.0
Vue Router: ^4.2.0
Axios: ^1.5.0

每次升级依赖都要在这个表里更新,确保所有应用同步。

第二,使用 singleton 强制单例
javascript
shared: {
  vue: {
    singleton: true,  // 强制只有一个实例
    requiredVersion: '^3.3.0'
  }
}

如果版本不匹配,Webpack 会报错或警告,我们能及时发现问题。

第三,版本兼容性测试

我们做了一个自动化测试,每次发版前:

  1. 检测所有应用的依赖版本
  2. 生成兼容性矩阵
  3. 模拟不同版本组合进行测试
  4. 发现问题及时修复
第四,降级策略

如果真的遇到版本冲突,我们有几种处理方式:

  1. 加载两份依赖(不推荐,但能保证功能)
  2. 降级到兼容版本
  3. 使用独立构建(放弃共享,独立打包)
第五,监控告警

我们在运行时监控版本冲突:

javascript
if (sharedModule.version !== expectedVersion) {
  reportWarning('版本冲突', {
    expected: expectedVersion,
    actual: sharedModule.version
  })
}

发现问题立即告警,快速响应。

实际效果

通过这套机制,我们基本杜绝了版本冲突导致的线上问题。偶尔有冲突,也能在测试阶段发现。"

4.3 难点与亮点分析

难点1:TypeScript 类型支持

问题描述: Module Federation 动态导入的模块,TypeScript 无法推断类型,开发体验差。

解决方案
typescript
// 方案1:手动声明类型
// types/remote-modules.d.ts
declare module 'remoteApp1/Button' {
  import { DefineComponent } from 'vue'
  const Button: DefineComponent<{
    type?: 'primary' | 'success' | 'warning' | 'danger'
    text?: string
  }>
  export default Button
}

// 使用
import RemoteButton from 'remoteApp1/Button' // 有类型提示

// 方案2:自动生成类型
// 在远程应用构建时生成类型声明文件
// build-types.js
const { exec } = require('child_process')

exec('vue-tsc --declaration --emitDeclarationOnly', (err, stdout) => {
  if (err) {
    console.error('类型生成失败', err)
    return
  }

  // 将生成的 .d.ts 文件发布到 npm 或 CDN
  console.log('类型生成成功')
})

// 宿主应用安装类型包
npm install @types/remote-app1

// 方案3:使用 @module-federation/typescript
// 自动同步远程模块的类型
npm install @module-federation/typescript

// webpack.config.js
const FederatedTypesPlugin = require('@module-federation/typescript')

plugins: [
  new FederatedTypesPlugin({
    remotes: {
      remoteApp1: 'http://localhost:3001'
    }
  })
]

难点2:调试困难

问题描述: 远程模块报错时,堆栈信息不清晰,难以定位问题。

解决方案
javascript
// 1. 使用 Source Map
// webpack.config.js
module.exports = {
  mode: 'development',
  devtool: 'source-map', // 开启 source map

  output: {
    devtoolModuleFilenameTemplate: '[absolute-resource-path]'
  }
}

// 2. 错误边界
// ErrorBoundary.vue
<template>
  <div v-if="error" class="error-boundary">
    <h2>模块加载失败</h2>
    <p>{{ error.message }}</p>
    <details>
      <summary>详细信息</summary>
      <pre>{{ error.stack }}</pre>
      <p>模块: {{ moduleName }}</p>
      <p>来源: {{ remoteUrl }}</p>
    </details>
    <button @click="retry">重试</button>
  </div>
  <slot v-else></slot>
</template>

<script setup>
import { ref, onErrorCaptured } from 'vue'

const props = defineProps({
  moduleName: String,
  remoteUrl: String
})

const error = ref(null)

onErrorCaptured((err) => {
  error.value = err

  // 上报错误
  reportError({
    type: 'module-federation-error',
    module: props.moduleName,
    remote: props.remoteUrl,
    error: err.message,
    stack: err.stack
  })

  return false
})

const retry = () => {
  error.value = null
  window.location.reload()
}
</script>

// 3. 加载监控
class ModuleFederationMonitor {
  constructor() {
    this.loadTimes = new Map()
  }

  startLoad(moduleName) {
    this.loadTimes.set(moduleName, performance.now())
  }

  endLoad(moduleName, success = true) {
    const startTime = this.loadTimes.get(moduleName)
    if (!startTime) return

    const duration = performance.now() - startTime

    // 上报数据
    this.report({
      module: moduleName,
      duration,
      success,
      timestamp: Date.now()
    })

    this.loadTimes.delete(moduleName)
  }

  report(data) {
    console.log('[MF Monitor]', data)

    // 发送到监控平台
    fetch('/api/monitor/mf', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    })
  }
}

const monitor = new ModuleFederationMonitor()

// 使用
monitor.startLoad('remoteApp1/Button')
import('remoteApp1/Button')
  .then(() => monitor.endLoad('remoteApp1/Button', true))
  .catch(() => monitor.endLoad('remoteApp1/Button', false))

难点3:首屏加载性能

问题描述: 远程模块需要额外的网络请求,影响首屏加载速度。

解决方案
javascript
// 1. 预加载关键模块
// webpack.config.js
const PreloadPlugin = require('@vue/preload-webpack-plugin')

plugins: [
  new PreloadPlugin({
    rel: 'prefetch',
    include: 'allAssets',
    fileWhitelist: [/remoteEntry\.js$/]
  })
]

// 2. 并行加载
// 同时加载多个远程模块
Promise.all([
  import('remoteApp1/Button'),
  import('remoteApp1/Header'),
  import('remoteApp2/Table')
]).then(([Button, Header, Table]) => {
  // 所有模块加载完成
})

// 3. 缓存策略
// Service Worker 缓存远程模块
self.addEventListener('fetch', (event) => {
  if (event.request.url.includes('remoteEntry.js')) {
    event.respondWith(
      caches.match(event.request).then((response) => {
        if (response) {
          // 从缓存返回,并在后台更新
          fetch(event.request).then((freshResponse) => {
            caches.open('mf-cache').then((cache) => {
              cache.put(event.request, freshResponse)
            })
          })
          return response
        }

        // 缓存未命中,从网络获取
        return fetch(event.request).then((response) => {
          return caches.open('mf-cache').then((cache) => {
            cache.put(event.request, response.clone())
            return response
          })
        })
      })
    )
  }
})

// 4. CDN 加速
// 将远程模块部署到 CDN
remotes: {
  remoteApp1: 'remoteApp1@https://cdn.example.com/remoteApp1/remoteEntry.js'
}

// 5. 骨架屏
<template>
  <Suspense>
    <template #default>
      <RemoteButton />
    </template>
    <template #fallback>
      <div class="skeleton-button"></div>
    </template>
  </Suspense>
</template>

亮点1:动态远程应用

实现了运行时动态添加远程应用,不需要重新构建:

javascript
class DynamicRemoteManager {
  constructor() {
    this.remotes = new Map()
  }

  async addRemote(name, url) {
    if (this.remotes.has(name)) {
      console.warn(`Remote ${name} already exists`)
      return
    }

    // 加载远程入口
    const script = document.createElement('script')
    script.src = url

    await new Promise((resolve, reject) => {
      script.onload = resolve
      script.onerror = reject
      document.head.appendChild(script)
    })

    // 初始化容器
    const container = window[name]
    await container.init(__webpack_share_scopes__.default)

    this.remotes.set(name, {
      name,
      url,
      container
    })

    console.log(`Remote ${name} loaded successfully`)
  }

  async getModule(remoteName, moduleName) {
    const remote = this.remotes.get(remoteName)
    if (!remote) {
      throw new Error(`Remote ${remoteName} not found`)
    }

    const factory = await remote.container.get(moduleName)
    return factory()
  }

  removeRemote(name) {
    this.remotes.delete(name)
  }
}

const remoteManager = new DynamicRemoteManager()

// 运行时添加远程应用
await remoteManager.addRemote(
  'remoteApp3',
  'https://cdn.example.com/remoteApp3/remoteEntry.js'
)

// 使用模块
const Button = await remoteManager.getModule('remoteApp3', './Button')

亮点2:智能版本协商

开发了智能版本协商系统,自动处理版本冲突:

javascript
class VersionNegotiator {
  constructor() {
    this.versionMap = new Map()
  }

  register(packageName, version, provider) {
    if (!this.versionMap.has(packageName)) {
      this.versionMap.set(packageName, [])
    }

    this.versionMap.get(packageName).push({
      version,
      provider,
      timestamp: Date.now()
    })
  }

  negotiate(packageName, requiredVersion) {
    const versions = this.versionMap.get(packageName)
    if (!versions) {
      throw new Error(`Package ${packageName} not found`)
    }

    // 找到满足要求的版本
    const compatible = versions.filter(v =>
      this.isCompatible(v.version, requiredVersion)
    )

    if (compatible.length === 0) {
      console.warn(`No compatible version found for ${packageName}@${requiredVersion}`)
      // 返回最新版本
      return versions[versions.length - 1]
    }

    // 返回最高的兼容版本
    return compatible.reduce((prev, curr) =>
      this.compareVersions(curr.version, prev.version) > 0 ? curr : prev
    )
  }

  isCompatible(version, required) {
    // 简化的 semver 兼容性检查
    const [vMajor, vMinor] = version.split('.').map(Number)
    const [rMajor, rMinor] = required.replace(/[^0-9.]/g, '').split('.').map(Number)

    if (required.startsWith('^')) {
      return vMajor === rMajor && vMinor >= rMinor
    }

    if (required.startsWith('~')) {
      return vMajor === rMajor && vMinor === rMinor
    }

    return version === required
  }

  compareVersions(a, b) {
    const aParts = a.split('.').map(Number)
    const bParts = b.split('.').map(Number)

    for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
      const aPart = aParts[i] || 0
      const bPart = bParts[i] || 0

      if (aPart > bPart) return 1
      if (aPart < bPart) return -1
    }

    return 0
  }
}

const negotiator = new VersionNegotiator()

// 注册版本
negotiator.register('vue', '3.3.4', 'hostApp')
negotiator.register('vue', '3.3.2', 'remoteApp1')

// 协商版本
const result = negotiator.negotiate('vue', '^3.3.0')
console.log('使用版本:', result.version, '来自:', result.provider)

亮点3:组件热更新

实现了远程组件的热更新,无需刷新页面:

javascript
class RemoteModuleHotReload {
  constructor() {
    this.modules = new Map()
    this.listeners = new Map()
  }

  register(moduleName, module, version) {
    this.modules.set(moduleName, {
      module,
      version,
      timestamp: Date.now()
    })
  }

  async checkUpdate(moduleName) {
    // 请求最新版本信息
    const response = await fetch(`/api/modules/${moduleName}/latest`)
    const { version } = await response.json()

    const current = this.modules.get(moduleName)

    if (!current || current.version !== version) {
      console.log(`New version available for ${moduleName}: ${version}`)
      return true
    }

    return false
  }

  async update(moduleName) {
    // 重新加载模块
    const module = await import(`${moduleName}?t=${Date.now()}`)

    const newVersion = module.version || Date.now()
    this.modules.set(moduleName, {
      module,
      version: newVersion,
      timestamp: Date.now()
    })

    // 通知监听器
    const listeners = this.listeners.get(moduleName) || []
    listeners.forEach(listener => listener(module))

    console.log(`Module ${moduleName} updated to version ${newVersion}`)
  }

  onUpdate(moduleName, listener) {
    if (!this.listeners.has(moduleName)) {
      this.listeners.set(moduleName, [])
    }
    this.listeners.get(moduleName).push(listener)
  }

  startPolling(moduleName, interval = 30000) {
    setInterval(async () => {
      const hasUpdate = await this.checkUpdate(moduleName)
      if (hasUpdate) {
        await this.update(moduleName)
      }
    }, interval)
  }
}

const hotReload = new RemoteModuleHotReload()

// 注册模块
hotReload.register('remoteApp1/Button', RemoteButton, '1.0.0')

// 监听更新
hotReload.onUpdate('remoteApp1/Button', (newModule) => {
  console.log('Button 组件已更新')
  // 可以在这里触发组件重新渲染
})

// 开始轮询
hotReload.startPolling('remoteApp1/Button')

4.4 完整简历示例

plain
【Module Federation 微前端组件库共享实践】2024.07 - 2024.12

项目背景:
公司有 8 个业务系统,使用大量相同的 UI 组件和工具函数,导致代码重复、维护困难、包体积大。需要统一管理公共代码,实现跨应用复用。

技术选型:
经过对比 npm 包、Monorepo、qiankun、Module Federation 等方案,最终选择 Module Federation,原因:
- 模块级粒度,比应用级更灵活
- 运行时共享依赖,包体积最小
- Webpack 5 原生支持,无需额外框架
- 支持动态加载和版本管理

核心工作:

1. 组件库架构设计
   - 将 50+ 通用组件抽离为独立应用(remote-components)
   - 配置 exposes 暴露组件:Button、Form、Table、Modal 等
   - 设计组件 API,保持一致性和易用性
   - 开发组件文档系统,自动生成使用示例

2. 共享依赖优化
   - 配置 shared:Vue、Vue Router、Axios 等设置为单例模式
   - 实现版本协商机制,自动处理版本冲突
   - 制定依赖版本规范,确保各应用版本一致
   - 包体积从平均 2.5MB 降至 1.5MB(减少 40%)

3. 宿主应用改造
   - 改造 8 个业务系统接入 Module Federation
   - 配置 webpack.config.js 的 remotes 和 shared
   - 使用 defineAsyncComponent 实现组件懒加载
   - 处理异步加载、错误降级、加载状态等

4. 性能优化实践
   - 并行加载:多个远程模块同时加载,首屏时间降低 35%
   - CDN 加速:远程模块部署到 CDN,加载速度提升 60%
   - 缓存策略:利用 Service Worker 缓存,二次访问提升 80%
   - 预加载:高频组件提前加载,用户无感知

5. 工程化建设
   - 开发 CLI 工具,快速创建符合规范的远程应用
   - 建立 CI/CD 流程,组件库独立构建部署
   - 实现版本管理:支持多版本并存、灰度发布、快速回滚
   - 建立监控体系:实时监控模块加载性能和成功率

6. 技术难点攻克
   - TypeScript 类型支持:自动生成类型声明文件,发布到 npm
   - 调试困难:开发 Source Map 工具链、错误边界、加载监控
   - 版本冲突:实现智能版本协商系统,自动选择最优版本
   - 热更新:实现远程组件热更新,无需刷新页面

技术栈:
Webpack 5、Module Federation、Vue3、Babel、PostCSS、TypeScript

项目成果:
- 包体积:各系统减少 40%(1MB),首屏速度提升 35%
- 组件复用:复用率 80%,新功能开发时间减少 50%
- 维护成本:组件统一管理,Bug 修复一次全部生效,降低 60%
- 构建速度:单系统构建从 5 分钟降至 2 分钟(提升 60%)
- 发布频率:组件库独立发版,发布频率提升 3 倍
- 用户体验:二次加载速度提升 80%(缓存生效)
- 团队效率:代码复用率从 30% 提升到 80%,节省开发人力 40%

五、总结

Module Federation 是 Webpack 5 最强大的特性,非常适合:

  • 组件库、工具库的跨应用共享
  • 大型项目的依赖优化
  • 需要细粒度模块复用的场景

核心优势

  1. 模块级粒度,最灵活
  2. 原生支持依赖共享,性能最优
  3. 构建时优化,包体积最小
  4. 支持动态加载和版本管理

适用场景

  • 多个项目共享组件库
  • Design System 的实施
  • 大型单体应用的拆分
  • 需要精细化依赖管理的项目
关键要点
  1. 制定统一的版本规范
  2. 合理配置 shared 和 exposes
  3. 处理好异步加载和错误降级
  4. 建立完善的监控体系
  5. 做好 TypeScript 类型支持