Ethan独立开发 PromptKit ShipOneDay-Next Starter

微前端实战:大型前端应用的拆分与治理

"这个系统太庞大了,每次发布都提心吊胆..." 上个月的技术评审会上,我们团队正面临一个棘手的问题。一个运行了两年的企业级中后台系统,代码量超过 30 万行,构建时间长达 20 分钟,任何小改动都可能引发意想不到的问题。作为技术负责人,我决定是时候引入微前端架构了。

经过一个月的改造,我们成功将这个庞然大物拆分成多个独立应用,构建时间缩短到了 3 分钟,各个团队也能独立开发部署了。今天就来分享这次微前端改造的实战经验。

为什么选择微前端?

说实话,刚开始团队对微前端也有顾虑 - 会不会过度设计?性能会不会受影响?但当我们列出现有问题时,答案就很明显了:

// 原有的单体应用结构
const LegacyApp = {
  modules: {
    crm: {
      size: '12MB JS + 2MB CSS',
      team: 'A团队',
      updateFrequency: '每周2次'
    },
    erp: {
      size: '15MB JS + 3MB CSS',
      team: 'B团队',
      updateFrequency: '每天1次'
    },
    dashboard: {
      size: '8MB JS + 1MB CSS',
      team: 'C团队',
      updateFrequency: '每月2次'
    }
  },
  problems: {
    buildTime: '20min+',
    deployment: '全量发布',
    teamCollaboration: '代码冲突频繁',
    maintenance: '难以局部更新'
  }
}

架构设计与实现

1. 基座应用

首先,我们需要一个轻量级的基座应用来管理子应用:

// 基座应用 - App Shell
import { registerApplication, start } from 'single-spa'

// 注册子应用
const registerMicroApp = (name: string, entry: string) => {
  registerApplication({
    name,
    app: async () => {
      // 动态加载子应用
      const module = await System.import(entry)
      return module.default
    },
    activeWhen: location => {
      // 基于路由匹配激活子应用
      return location.pathname.startsWith(`/${name}`)
    }
  })
}

// 配置子应用
const microApps = [
  {
    name: 'crm',
    entry: '//localhost:3001/main.js',
    container: '#crm-container'
  },
  {
    name: 'erp',
    entry: '//localhost:3002/main.js',
    container: '#erp-container'
  },
  {
    name: 'dashboard',
    entry: '//localhost:3003/main.js',
    container: '#dashboard-container'
  }
]

// 注册所有子应用
microApps.forEach(app => registerMicroApp(app.name, app.entry))

// 启动微前端框架
start()

2. 子应用改造

每个子应用需要暴露生命周期钩子:

// 子应用入口
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import { createStore } from './store'

// 导出生命周期钩子
export async function bootstrap() {
  console.log('CRM 应用启动中...')
}

export async function mount(props) {
  const { container, globalStore } = props
  const store = createStore(globalStore)

  ReactDOM.render(
    <Provider store={store}>
      <App />
    </Provider>,
    container
  )
}

export async function unmount(props) {
  const { container } = props
  ReactDOM.unmountComponentAtNode(container)
}

3. 通信机制

子应用间的通信是个关键问题,我们实现了一个事件总线:

// utils/eventBus.ts
class EventBus {
  private events = new Map<string, Function[]>()

  // 订阅事件
  on(event: string, callback: Function) {
    if (!this.events.has(event)) {
      this.events.set(event, [])
    }
    this.events.get(event)!.push(callback)

    // 返回取消订阅函数
    return () => {
      const callbacks = this.events.get(event)!
      const index = callbacks.indexOf(callback)
      callbacks.splice(index, 1)
    }
  }

  // 发布事件
  emit(event: string, data?: any) {
    if (!this.events.has(event)) return
    this.events.get(event)!.forEach(callback => {
      try {
        callback(data)
      } catch (error) {
        console.error(`Error in event ${event}:`, error)
      }
    })
  }
}

export const eventBus = new EventBus()

// 使用示例
// CRM 子应用
eventBus.emit('orderCreated', { orderId: '123' })

// ERP 子应用
eventBus.on('orderCreated', data => {
  updateInventory(data.orderId)
})

4. 样式隔离

为了避免样式冲突,我们采用了 CSS Modules 和动态 CSS 前缀:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              modules: {
                localIdentName: '[name]__[local]___[hash:base64:5]'
              }
            }
          },
          {
            loader: 'postcss-loader',
            options: {
              plugins: [
                require('postcss-prefix-selector')({
                  prefix: '[data-app="crm"]'
                })
              ]
            }
          }
        ]
      }
    ]
  }
}

性能优化

微前端虽然解决了很多问题,但也带来了新的挑战,比如首屏加载性能。我们通过以下方式进行优化:

  1. 预加载策略:
// 基于路由预测用户行为
const prefetchApps = async () => {
  const nextPossibleApps = predictNextApps()

  // 预加载可能用到的子应用
  nextPossibleApps.forEach(app => {
    const script = document.createElement('link')
    script.rel = 'prefetch'
    script.href = app.entry
    document.head.appendChild(script)
  })
}
  1. 共享依赖:
// webpack.config.js
module.exports = {
  externals: {
    react: 'React',
    'react-dom': 'ReactDOM',
    antd: 'antd'
  },
  // 使用 CDN 加载共享依赖
  scripts: ['https://unpkg.com/react@17/umd/react.production.min.js', 'https://unpkg.com/react-dom@17/umd/react-dom.production.min.js', 'https://unpkg.com/antd@4/dist/antd.min.js']
}

实践心得

这次微前端改造让我深刻体会到:

  1. 架构改造要循序渐进,先从边界清晰的模块开始
  2. 子应用拆分要基于业务边界,而不是技术边界
  3. 通信机制要简单可靠,避免复杂的状态同步
  4. 持续关注性能指标,及时发现和解决问题

最让我欣慰的是,改造后团队的开发效率明显提升,发布也更加灵活可控。正如那句话说的:"合久必分,分久必合。"在前端架构的演进中,找到当下最合适的平衡点才是关键。

写在最后

微前端不是银弹,它更像是一把双刃剑 - 使用得当可以解决很多问题,但也可能引入新的复杂性。关键是要根据团队和业务的实际情况,做出合适的选择。

有什么问题欢迎在评论区讨论,我们一起探讨微前端实践的更多可能!

如果觉得有帮助,别忘了点赞关注,我会继续分享更多架构实战经验~

posted @   技术出海录  阅读(61)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 本地部署 DeepSeek:小白也能轻松搞定!
· 传国玉玺易主,ai.com竟然跳转到国产AI
· 自己如何在本地电脑从零搭建DeepSeek!手把手教学,快来看看! (建议收藏)
· 我们是如何解决abp身上的几个痛点
· 如何基于DeepSeek开展AI项目
Ethan独立开发 PromptKit ShipOneDay-Next Starter
点击右上角即可分享
微信分享提示