找回密码
 立即注册
首页 业界区 业界 微前端实战:大型前端应用的拆分与治理 ...

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

撇瞥 3 天前
"这个系统太庞大了,每次发布都提心吊胆..." 上个月的技术评审会上,我们团队正面临一个棘手的问题。一个运行了两年的企业级中后台系统,代码量超过 30 万行,构建时间长达 20 分钟,任何小改动都可能引发意想不到的问题。作为技术负责人,我决定是时候引入微前端架构了。
经过一个月的改造,我们成功将这个庞然大物拆分成多个独立应用,构建时间缩短到了 3 分钟,各个团队也能独立开发部署了。今天就来分享这次微前端改造的实战经验。
为什么选择微前端?

说实话,刚开始团队对微前端也有顾虑 - 会不会过度设计?性能会不会受影响?但当我们列出现有问题时,答案就很明显了:
  1. // 原有的单体应用结构
  2. const LegacyApp = {
  3.   modules: {
  4.     crm: {
  5.       size: '12MB JS + 2MB CSS',
  6.       team: 'A团队',
  7.       updateFrequency: '每周2次'
  8.     },
  9.     erp: {
  10.       size: '15MB JS + 3MB CSS',
  11.       team: 'B团队',
  12.       updateFrequency: '每天1次'
  13.     },
  14.     dashboard: {
  15.       size: '8MB JS + 1MB CSS',
  16.       team: 'C团队',
  17.       updateFrequency: '每月2次'
  18.     }
  19.   },
  20.   problems: {
  21.     buildTime: '20min+',
  22.     deployment: '全量发布',
  23.     teamCollaboration: '代码冲突频繁',
  24.     maintenance: '难以局部更新'
  25.   }
  26. }
复制代码
架构设计与实现

1. 基座应用

首先,我们需要一个轻量级的基座应用来管理子应用:
  1. // 基座应用 - App Shell
  2. import { registerApplication, start } from 'single-spa'
  3. // 注册子应用
  4. const registerMicroApp = (name: string, entry: string) => {
  5.   registerApplication({
  6.     name,
  7.     app: async () => {
  8.       // 动态加载子应用
  9.       const module = await System.import(entry)
  10.       return module.default
  11.     },
  12.     activeWhen: location => {
  13.       // 基于路由匹配激活子应用
  14.       return location.pathname.startsWith(`/${name}`)
  15.     }
  16.   })
  17. }
  18. // 配置子应用
  19. const microApps = [
  20.   {
  21.     name: 'crm',
  22.     entry: '//localhost:3001/main.js',
  23.     container: '#crm-container'
  24.   },
  25.   {
  26.     name: 'erp',
  27.     entry: '//localhost:3002/main.js',
  28.     container: '#erp-container'
  29.   },
  30.   {
  31.     name: 'dashboard',
  32.     entry: '//localhost:3003/main.js',
  33.     container: '#dashboard-container'
  34.   }
  35. ]
  36. // 注册所有子应用
  37. microApps.forEach(app => registerMicroApp(app.name, app.entry))
  38. // 启动微前端框架
  39. start()
复制代码
2. 子应用改造

每个子应用需要暴露生命周期钩子:
  1. // 子应用入口
  2. import React from 'react'
  3. import ReactDOM from 'react-dom'
  4. import App from './App'
  5. import { createStore } from './store'
  6. // 导出生命周期钩子
  7. export async function bootstrap() {
  8.   console.log('CRM 应用启动中...')
  9. }
  10. export async function mount(props) {
  11.   const { container, globalStore } = props
  12.   const store = createStore(globalStore)
  13.   ReactDOM.render(
  14.     <Provider store={store}>
  15.       
  16.     </Provider>,
  17.     container
  18.   )
  19. }
  20. export async function unmount(props) {
  21.   const { container } = props
  22.   ReactDOM.unmountComponentAtNode(container)
  23. }
复制代码
3. 通信机制

子应用间的通信是个关键问题,我们实现了一个事件总线:
  1. // utils/eventBus.ts
  2. class EventBus {
  3.   private events = new Map<string, Function[]>()
  4.   // 订阅事件
  5.   on(event: string, callback: Function) {
  6.     if (!this.events.has(event)) {
  7.       this.events.set(event, [])
  8.     }
  9.     this.events.get(event)!.push(callback)
  10.     // 返回取消订阅函数
  11.     return () => {
  12.       const callbacks = this.events.get(event)!
  13.       const index = callbacks.indexOf(callback)
  14.       callbacks.splice(index, 1)
  15.     }
  16.   }
  17.   // 发布事件
  18.   emit(event: string, data?: any) {
  19.     if (!this.events.has(event)) return
  20.     this.events.get(event)!.forEach(callback => {
  21.       try {
  22.         callback(data)
  23.       } catch (error) {
  24.         console.error(`Error in event ${event}:`, error)
  25.       }
  26.     })
  27.   }
  28. }
  29. export const eventBus = new EventBus()
  30. // 使用示例
  31. // CRM 子应用
  32. eventBus.emit('orderCreated', { orderId: '123' })
  33. // ERP 子应用
  34. eventBus.on('orderCreated', data => {
  35.   updateInventory(data.orderId)
  36. })
复制代码
4. 样式隔离

为了避免样式冲突,我们采用了 CSS Modules 和动态 CSS 前缀:
  1. // webpack.config.js
  2. module.exports = {
  3.   module: {
  4.     rules: [
  5.       {
  6.         test: /\.css$/,
  7.         use: [
  8.           'style-loader',
  9.           {
  10.             loader: 'css-loader',
  11.             options: {
  12.               modules: {
  13.                 localIdentName: '[name]__[local]___[hash:base64:5]'
  14.               }
  15.             }
  16.           },
  17.           {
  18.             loader: 'postcss-loader',
  19.             options: {
  20.               plugins: [
  21.                 require('postcss-prefix-selector')({
  22.                   prefix: '[data-app="crm"]'
  23.                 })
  24.               ]
  25.             }
  26.           }
  27.         ]
  28.       }
  29.     ]
  30.   }
  31. }
复制代码
性能优化

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

  • 预加载策略:
  1. // 基于路由预测用户行为
  2. const prefetchApps = async () => {
  3.   const nextPossibleApps = predictNextApps()
  4.   // 预加载可能用到的子应用
  5.   nextPossibleApps.forEach(app => {
  6.     const script = document.createElement('link')
  7.     script.rel = 'prefetch'
  8.     script.href = app.entry
  9.     document.head.appendChild(script)
  10.   })
  11. }
复制代码

  • 共享依赖:
  1. // webpack.config.js
  2. module.exports = {
  3.   externals: {
  4.     react: 'React',
  5.     'react-dom': 'ReactDOM',
  6.     antd: 'antd'
  7.   },
  8.   // 使用 CDN 加载共享依赖
  9.   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']
  10. }
复制代码
实践心得

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

  • 架构改造要循序渐进,先从边界清晰的模块开始
  • 子应用拆分要基于业务边界,而不是技术边界
  • 通信机制要简单可靠,避免复杂的状态同步
  • 持续关注性能指标,及时发现和解决问题
最让我欣慰的是,改造后团队的开发效率明显提升,发布也更加灵活可控。正如那句话说的:"合久必分,分久必合。"在前端架构的演进中,找到当下最合适的平衡点才是关键。
写在最后

微前端不是银弹,它更像是一把双刃剑 - 使用得当可以解决很多问题,但也可能引入新的复杂性。关键是要根据团队和业务的实际情况,做出合适的选择。
有什么问题欢迎在评论区讨论,我们一起探讨微前端实践的更多可能!
如果觉得有帮助,别忘了点赞关注,我会继续分享更多架构实战经验~

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册