函数式组件 + Store + Bus + Service 架构总结

3 阅读3分钟

函数式组件 + Store + Bus + Service 架构总结

一、四层职责(先记住这个)

Component  → 显示什么 + 用户做了什么
Store      → 现在是什么状态(事实)
Bus        → 发生了什么事(通知)
Service    → 业务怎么做(逻辑)

一句话版本:

组件不写业务,Service 不写 JSX,Store 不存事件,Bus 不存状态。


二、数据流方向

用户操作
   ↓
Component 调用 Service
   ↓
Service 处理业务逻辑
   ├── store.setState()  → 更新状态(事实)
   └── bus.emit()        → 发出通知(事件)
         ↓
   ├── Component 读 Store 重新渲染
   └── 其他 Service 监听 Bus 响应

三、各层实现

Store(Zustand)

// store.ts
interface EditorState {
  activeShardId: number;
  frameRange: [number, number];
  loadingStatus: 'idle' | 'loading' | 'ready' | 'error';
}

export const useEditorStore = create<EditorState>(() => ({
  activeShardId: 0,
  frameRange: [0, 0],
  loadingStatus: 'idle',
}));

原则:只存有"当前值"的东西。


Bus(事件总线)

// bus.ts
export interface BusEvents {
  'shard:willChange': { fromId: number; toId: number };
  'shard:didChange':  { shardId: number };
  'shard:loadError':  { shardId: number; error: Error };
  'frame:rangeChanged': { start: number; end: number };
  'toast:show': { message: string; type: 'info' | 'error' | 'success' };
}

export const bus = new TypedEventBus<BusEvents>();

原则:只存"发生了什么",没有当前值。


Service(业务逻辑)

// services/shardService.ts
class ShardService extends BaseService {
  private currentShardId = 0;

  init() {
    // 订阅其他模块的事件
    this.on('shard:didChange', ({ shardId }) => {
      this.currentShardId = shardId;
    });
  }

  async switchShard(newShardId: number) {
    // 1. 通知即将发生
    bus.emit('shard:willChange', {
      fromId: this.currentShardId,
      toId: newShardId,
    });

    // 2. 更新 loading 状态
    store.setState({ loadingStatus: 'loading' });

    try {
      // 3. 执行业务逻辑
      await this.loadPcd(newShardId);

      // 4. 更新状态(事实)
      store.setState({
        activeShardId: newShardId,
        loadingStatus: 'ready',
      });

      // 5. 通知完成
      bus.emit('shard:didChange', { shardId: newShardId });

    } catch (error) {
      store.setState({ loadingStatus: 'error' });
      bus.emit('shard:loadError', { shardId: newShardId, error });
    }
  }
}

原则:只处理业务逻辑,不写 JSX,不直接依赖其他 Service。


Editor(统一实例化)

// editor.ts
class Editor {
  shardService = new ShardService();
  cacheService = new CacheService();
  frameService = new FrameService();
  toastService = new ToastService();

  init() {
    this.shardService.init();
    this.cacheService.init();
    this.frameService.init();
    this.toastService.init();
  }

  destroy() {
    this.shardService.destroy();
    this.cacheService.destroy();
    this.frameService.destroy();
    this.toastService.destroy();
  }
}

// 全局单例,应用共享
export const editor = new Editor();

Hook(组件订阅 Bus 的标准方式)

// hooks/useBusEvent.ts
export function useBusEvent<K extends keyof BusEvents>(
  event: K,
  handler: (payload: BusEvents[K]) => void,
) {
  const handlerRef = useRef(handler);
  handlerRef.current = handler;

  useEffect(() => {
    const unsub = bus.on(event, (payload) => {
      handlerRef.current(payload);
    });
    return unsub; // 组件卸载自动取消订阅
  }, [event]);
}

Component(函数式组件)

// components/ShardPanel.tsx
export function ShardPanel() {
  // ✅ 读 Store → 渲染
  const activeShardId = useEditorStore(s => s.activeShardId);
  const loadingStatus = useEditorStore(s => s.loadingStatus);

  // ✅ 监听 Bus → 一次性 UI 副作用
  useBusEvent('shard:loadError', ({ shardId }) => {
    toast.error(`分片 ${shardId} 加载失败`);
  });

  // ✅ 用户操作 → 调用 Service
  const handleShardClick = (id: number) => {
    editor.shardService.switchShard(id);
  };

  return (
    <div>
      {shardList.map(shard => (
        <div
          key={shard.id}
          onClick={() => handleShardClick(shard.id)}
          className={shard.id === activeShardId ? 'active' : ''}
        >
          区域 {shard.id}
          {loadingStatus === 'loading' && shard.id === activeShardId && (
            <span>加载中...</span>
          )}
        </div>
      ))}
    </div>
  );
}

四、应用入口

// App.tsx
import { editor } from './editor';

export function App() {
  useEffect(() => {
    editor.init();
    return () => editor.destroy();
  }, []);

  return (
    <div>
      <ShardPanel />
      <Timeline />
      <RoiPanel />
      <ToastContainer />
    </div>
  );
}

五、判断每件事该放哪层

这个东西有"当前值"吗?
  是 → Store

这件事只在"发生瞬间"有意义吗?
  是 → Bus

这是业务逻辑 / 数据处理吗?
  是 → Service

这是 UI 渲染 / 用户交互吗?
  是 → Component

六、常见错误对照

错误写法问题正确做法
组件里写业务逻辑逻辑散落,难复用移到 Service
Store 里存事件重复消费,状态污染走 Bus
Bus 传递"当前状态"新订阅者拿不到存 Store
组件里 new Service每次渲染重建实例Editor 单例统一管理
Service 直接调用其他 Service网状依赖通过 Bus 解耦

七、完整协作图

┌─────────────────────────────────────┐
│           Component                  │
│  useEditorStore() → 读状态渲染       │
│  useBusEvent()    → 响应一次性副作用 │
│  onClick()        → 调用 Service     │
└──────────┬──────────────────────────┘
           │ 调用
           ▼
┌─────────────────────────────────────┐
│           Service                    │
│  处理业务逻辑                        │
│  store.setState() → 更新状态         │
│  bus.emit()       → 发出通知         │
└──────────┬──────────────────────────┘
           │
     ┌─────┴──────┐
     ▼            ▼
┌─────────┐  ┌─────────┐
│  Store  │  │   Bus   │
│  事实   │  │  通知   │
└────┬────┘  └────┬────┘
     │             │
     ▼             ▼
  Component    Service /
  重新渲染     Component
              响应副作用

八、一句话总结

Store 是系统的记忆,Bus 是系统的神经,Service 是系统的大脑,Component 是系统的脸。
四层各司其职,系统行为可预测、可追踪、可测试。