阶段 4:前端架构设计 - 真正的分水岭

2 阅读5分钟

阶段 4:前端架构设计 - 真正的分水岭

这部分是架构师的核心竞争力。前面三阶段是"能干活",这部分是"能带团队、能设计系统"。


一、系统拆分能力

1.1 拆分的三个维度

维度原则示例
按业务拆高内聚低耦合,独立业务上下文电商:用户域、商品域、订单域、支付域
按模块拆功能边界清晰,可独立开发测试后台管理系统:仪表盘、用户管理、权限管理、日志
按团队拆康威定律:系统结构 = 组织沟通结构每个团队负责 1-2 个业务域,边界由 API 定义

1.2 前端分层架构

┌─────────────────────────────────────────────┐
│                  视图层                      │  UI 组件(无业务逻辑)
├─────────────────────────────────────────────┤
│                容器/页面层                   │  组合组件、连接数据
├─────────────────────────────────────────────┤
│                业务逻辑层                    │  Hooks / Composables / Services
├─────────────────────────────────────────────┤
│                状态管理层                    │  Store(全局/局部)
├─────────────────────────────────────────────┤
│                数据访问层                    │  API 请求、缓存、存储
└─────────────────────────────────────────────┘

实际代码组织示例:

src/
├── pages/                    # 页面层
│   └── order/
│       ├── OrderList.tsx     # 页面组件
│       └── components/       # 页面私有组件
├── features/                 # 业务功能(按业务域)
│   ├── order/
│   │   ├── hooks/            # 业务逻辑
│   │   ├── services/         # 调用接口
│   │   ├── store/            # 状态
│   │   └── types/            # 类型定义
│   └── user/
├── shared/                   # 共享层
│   ├── ui/                   # 通用 UI 组件
│   ├── hooks/                # 通用 Hooks
│   ├── utils/                # 工具函数
│   └── api/                  # HTTP 客户端封装
└── app/                      # 应用配置
    ├── router/
    ├── store/
    └── styles/

二、微前端

2.1 核心概念对比

方案原理优点缺点
qiankunHTML Entry + JS 沙箱(Proxy)框架无关、CSS 隔离、JS 沙箱较大、MPA 模式有局限
Module FederationWebpack 5 插件,运行时加载远程模块原生、轻量、共享依赖仅 Webpack
single-spa底层库,需要自己实现加载/卸载灵活、轻量需要更多配置

2.2 Module Federation 核心原理

概念理解:

多个独立构建的应用,在运行时共享模块。一个应用可以"暴露"自己的模块,另一个应用可以"消费"它。

配置示例:

// 主应用(host)webpack.config.js
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'hostApp',           // 应用名
      remotes: {                 // 消费远程模块
        orderApp: 'orderApp@http://localhost:3001/remoteEntry.js',
        userApp: 'userApp@http://localhost:3002/remoteEntry.js'
      },
      shared: {                  // 共享依赖,避免重复加载
        react: { singleton: true, eager: true },
        'react-dom': { singleton: true },
        'react-router-dom': { singleton: true }
      }
    })
  ]
}

// 子应用(remote)webpack.config.js
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'orderApp',
      filename: 'remoteEntry.js',    // 入口文件
      exposes: {                     // 暴露给其他应用使用的模块
        './OrderList': './src/OrderList',
        './OrderDetail': './src/OrderDetail',
        './store': './src/store'
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true }
      }
    })
  ]
}

运行时使用:

// 主应用中动态加载子应用组件
const OrderList = React.lazy(() => import('orderApp/OrderList'))

function App() {
  return (
    <div>
      <h1>主应用</h1>
      <React.Suspense fallback="Loading Order...">
        <OrderList />
      </React.Suspense>
    </div>
  )
}

版本冲突处理策略:

shared: {
  react: {
    singleton: true,
    requiredVersion: '^18.0.0',
    eager: false,
    version: '18.2.0'
  }
}
// 如果子应用要求 react 17,会自动降级/拒绝加载

2.3 qiankun 核心实现思路

// 1. 主应用注册子应用
import { registerMicroApps, start } from 'qiankun'

registerMicroApps([
  {
    name: 'order-app',
    entry: '//localhost:3001',
    container: '#subapp-container',
    activeRule: '/order',
    props: { token: 'xxx' }
  }
])

start({
  sandbox: {          // JS 沙箱
    strictStyleIsolation: true,  // CSS 隔离(Shadow DOM)
    experimentalStyleIsolation: true  // 运行时样式转换
  }
})

// 2. 子应用需要暴露生命周期
export async function bootstrap() {
  console.log('子应用初始化')
}
export async function mount(props) {
  render(props.container)
}
export async function unmount() {
  ReactDOM.unmountComponentAtNode(container)
}

三、设计模式在前端的应用

3.1 发布订阅 vs 观察者模式

模式关系耦合度前端应用
观察者观察者直接注册到目标松耦合(双方都知道对方)Vue 响应式、MutationObserver
发布订阅通过事件中心转发完全解耦Event Bus、Redux、消息队列
// 观察者模式(直接通信)
class Subject {
  observers = []
  attach(observer) { this.observers.push(observer) }
  notify(data) { this.observers.forEach(obs => obs.update(data)) }
}

class Observer {
  update(data) { console.log('收到:', data) }
}

// 发布订阅(通过事件中心)
class EventBus {
  events = {}
  on(name, fn) { this.events[name] = this.events[name] || []; this.events[name].push(fn) }
  emit(name, data) { (this.events[name] || []).forEach(fn => fn(data)) }
}

3.2 工厂模式 — 组件动态渲染

// 表单组件工厂
interface FormComponent {
  render(): JSX.Element
  getValue(): any
  validate(): boolean
}

class InputComponent implements FormComponent {
  constructor(private schema: any) {}
  render() { return <input type={this.schema.type} /> }
  getValue() { /* ... */ }
  validate() { /* ... */ }
}

class SelectComponent implements FormComponent {
  constructor(private schema: any) {}
  render() { return <select>{this.schema.options.map(...)}</select> }
  getValue() { /* ... */ }
  validate() { /* ... */ }
}

class FormComponentFactory {
  static create(schema: any): FormComponent {
    const type = schema.type
    if (type === 'input') return new InputComponent(schema)
    if (type === 'select') return new SelectComponent(schema)
    if (type === 'datepicker') return new DatePickerComponent(schema)
    throw new Error(`Unknown component type: ${type}`)
  }
}

// 使用
const schema = { type: 'input', placeholder: '请输入用户名' }
const component = FormComponentFactory.create(schema)

3.3 单例模式 — 全局状态/请求

// 全局请求去重单例
class RequestDeduplicator {
  private static instance: RequestDeduplicator
  private pending = new Map<string, Promise<any>>()
  
  static getInstance() {
    if (!RequestDeduplicator.instance) {
      RequestDeduplicator.instance = new RequestDeduplicator()
    }
    return RequestDeduplicator.instance
  }
  
  async request<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
    if (this.pending.has(key)) {
      return this.pending.get(key) as Promise<T>
    }
    const promise = fetcher().finally(() => {
      this.pending.delete(key)
    })
    this.pending.set(key, promise)
    return promise
  }
}

// 使用
const deduper = RequestDeduplicator.getInstance()
const data = await deduper.request('/api/user', () => fetch('/api/user'))

四、插件化架构(高级)

4.1 核心设计

插件系统三要素:

  1. 注册机制 - 如何发现和注册插件
  2. 生命周期 - 插件在不同阶段的执行点
  3. 扩展点 - 插件能"插入"的钩子位置
// 插件接口定义
interface Plugin {
  name: string
  version: string
  
  // 生命周期钩子
  onRegister?(context: PluginContext): void
  onLoad?(context: PluginContext): void
  onUnload?(): void
  
  // 扩展点实现
  hooks?: {
    [hookName: string]: (...args: any[]) => any
  }
  
  // 配置
  config?: Record<string, any>
}

interface PluginContext {
  api: {
    registerHook(name: string, handler: Function): void
    emitHook(name: string, ...args: any[]): Promise<any[]>
    getConfig(key: string): any
    logger: Console
  }
}

4.2 完整实现

// 插件管理器
class PluginManager {
  private plugins = new Map<string, Plugin>()
  private hooks = new Map<string, Function[]>()
  private context: PluginContext
  
  constructor() {
    this.context = this.createContext()
  }
  
  private createContext(): PluginContext {
    return {
      api: {
        registerHook: (name: string, handler: Function) => {
          if (!this.hooks.has(name)) {
            this.hooks.set(name, [])
          }
          this.hooks.get(name)!.push(handler)
        },
        emitHook: async (name: string, ...args: any[]) => {
          const handlers = this.hooks.get(name) || []
          const results = []
          for (const handler of handlers) {
            results.push(await handler(...args))
          }
          return results
        },
        getConfig: (key: string) => {
          // 从全局配置获取
          return (window as any).__PLUGIN_CONFIG__?.[key]
        },
        logger: console
      }
    }
  }
  
  // 注册插件
  register(plugin: Plugin) {
    if (this.plugins.has(plugin.name)) {
      console.warn(`插件 ${plugin.name} 已存在,将被覆盖`)
    }
    
    // 调用 onRegister
    plugin.onRegister?.(this.context)
    
    // 注册扩展点
    if (plugin.hooks) {
      Object.entries(plugin.hooks).forEach(([name, handler]) => {
        this.context.api.registerHook(name, handler)
      })
    }
    
    this.plugins.set(plugin.name, plugin)
    console.log(`插件 ${plugin.name} v${plugin.version} 注册成功`)
  }
  
  // 加载所有插件
  async load() {
    const loadPromises = Array.from(this.plugins.values()).map(async (plugin) => {
      try {
        await plugin.onLoad?.(this.context)
        console.log(`插件 ${plugin.name} 加载完成`)
      } catch (error) {
        console.error(`插件 ${plugin.name} 加载失败:`, error)
        this.unregister(plugin.name)
      }
    })
    await Promise.all(loadPromises)
  }
  
  // 卸载插件
  unregister(name: string) {
    const plugin = this.plugins.get(name)
    if (!plugin) return
    
    plugin.onUnload?.()
    this.plugins.delete(name)
    console.log(`插件 ${name} 已卸载`)
  }
  
  // 获取所有插件
  getPlugins() {
    return Array.from(this.plugins.values())
  }
}

4.3 实际应用示例

// 1. 定义编辑器插件
const MarkdownPlugin: Plugin = {
  name: '@editor/markdown',
  version: '1.0.0',
  
  onRegister(ctx) {
    console.log('Markdown 插件正在注册')
  },
  
  onLoad(ctx) {
    // 注册工具栏按钮
    ctx.api.registerHook('toolbar:buttons', () => ({
      id: 'markdown',
      icon: '📝',
      onClick: () => console.log('切换 Markdown 模式')
    }))
    
    // 注册解析器
    ctx.api.registerHook('parser:transform', (content: string) => {
      // 将 Markdown 转为 HTML
      return content.replace(/^# (.*)$/gm, '<h1>$1</h1>')
    })
    
    // 注册快捷键
    ctx.api.registerHook('shortcuts', () => ({
      key: 'ctrl+m',
      handler: () => console.log('Markdown 快捷操作')
    }))
  }
}

// 2. 动态加载远程插件(Module Federation 结合)
async function loadRemotePlugin(remoteUrl: string) {
  const { default: plugin } = await import(/* @vite-ignore */ remoteUrl)
  pluginManager.register(plugin)
}

// 3. 使用插件扩展的核心功能
class Editor {
  private pluginManager = new PluginManager()
  
  async init(plugins: Plugin[]) {
    // 注册内置插件
    plugins.forEach(p => this.pluginManager.register(p))
    await this.pluginManager.load()
  }
  
  renderToolbar() {
    // 调用插件注册的钩子
    const buttons = this.pluginManager.context.api.emitHook('toolbar:buttons')
    return buttons.map(btn => /* render button */)
  }
  
  transformContent(raw: string) {
    return this.pluginManager.context.api.emitHook('parser:transform', raw)
  }
}

4.4 插件化架构的关键设计决策

决策点推荐方案原因
插件发现配置文件声明 + 动态 import按需加载、减少体积
依赖管理显式声明 peerDependencies + 版本检查避免运行时冲突
作用域隔离每个插件独立 iframe 或 Shadow DOM样式/脚本不污染
通信方式通过核心 API 代理,不直接通信可控、可追踪
版本升级语义化版本 + 兼容层(Adapter)平滑迁移

五、架构设计面试总结

场景题:设计一个"可插拔的图表库"

回答框架:

  1. 核心层:渲染引擎(ECharts/Highcharts 封装)
  2. 扩展点:主题、交互、数据处理、导出
  3. 插件机制:registerPlugin + 生命周期(beforeRender、afterRender)
  4. 按需加载:动态导入插件代码
  5. 配置化:声明式启用插件

常见追问

追问回答要点
"插件间有依赖怎么办?"在 register 时声明,管理器按拓扑顺序加载
"如何保证插件不拖慢主应用?"Web Worker 执行、空闲时加载、性能监控上报
"插件热更新怎么做?"监听文件变化 → 重新注册 → 卸载旧实例 → 通知使用方刷新
"微前端和插件化区别?"微前端解决"整页整合",插件化解决"功能扩展"