UniApp 事件管理器重构实践:从混乱到模块化管理

发布于:2025-09-03 ⋅ 阅读:(19) ⋅ 点赞:(0)

在大型 UniApp 项目中,组件间通信通常依赖 uni.$onuni.$emit
随着业务增长,我们遇到了越来越多的事件管理问题。本文分享事件管理器重构的整个过程,包括遇到的问题、优化思路和最终实现。


一、问题起点:事件监听器混乱

1. 独立注册,缺乏统一管理

原始代码中,每个组件都直接注册和触发事件:

uni.$on('initPageIndex', () => getRewards())
uni.$on('isRongCloudAndSocketInit', () => preLoadComponent())
uni.$emit('isRongCloudAndSocketInit')

问题:

  1. 事件名硬编码

    • 拼写错误可能导致运行时异常
    • 难以追踪哪些组件监听了哪些事件
  2. 缺乏类型检查

    • 编译时无法检测事件名或参数类型
    • 运行时错误难以调试
  3. 内存管理混乱

    • 组件卸载时没有统一清理 $off
    • 易造成内存泄漏

这让我们意识到:必须先统一管理监听器,才能避免混乱。


二、第一步优化:统一事件枚举

为了减少硬编码,我们最初将所有事件放在一个枚举里:

export enum EventTypes {
  INIT_PAGE_INDEX = 'initPageIndex',
  RONGCLOUD_AND_SOCKET_INIT = 'isRongCloudAndSocketInit',
  FOLLOW_ANCHOR = 'FOLLOW_ANCHOR',
  BLOCK_ANCHOR = 'BLOCK_ANCHOR',
  SOCKET_CONNECT = 'SOCKET_CONNECT',
  SOCKET_DISCONNECT = 'SOCKET_DISCONNECT',
}

遇到的问题

  1. 平铺枚举,不直观

    • 所有事件混在一起,难以判断事件所属模块
    • 开发者需要记住前缀如 BROADCASTER_SOCKET_
  2. 维护成本高

    • 项目增长时枚举快速臃肿
    • 新增或删除事件容易出错
  3. IDE 自动补全差

    • 输入 EventTypes. 会显示所有事件
    • 查找特定模块事件不方便

平铺枚举解决了硬编码问题,但结构和可维护性仍然不足


三、最终优化:多层模块化事件结构

1. 核心设计理念

  • 直观性:一眼看出事件属于哪个模块
  • 类型安全:保持完整 TypeScript 类型支持
  • 可维护性:便于添加、删除、重构事件
  • 向后兼容:平滑迁移,不破坏现有代码

2. 多层结构实现

export const EventTypes = {
  // 初始化事件
  init: {
    PAGE_INDEX: 'initPageIndex',
    RONGCLOUD_AND_SOCKET_INIT: 'isRongCloudAndSocketInit',
  },

  // 主播事件
  broadcaster: {
    FOLLOW_ANCHOR: 'FOLLOW_ANCHOR',
    BLOCK_ANCHOR: 'BLOCK_ANCHOR',
    MG_USER_LEFT: 'mgUserLeft',
  },

  // Socket事件
  socket: {
    CONNECT: 'SOCKET_CONNECT',
    DISCONNECT: 'SOCKET_DISCONNECT',
    MESSAGE_EVENT: 'messageEvent',
  }
} as const

优势

  • 结构清晰,IDE 自动补全友好
  • 易于维护:新增事件直接加到对应模块
  • 减少拼写错误和逻辑混乱

3. 类型安全支持

export type EventType = 
  | typeof EventTypes.init[keyof typeof EventTypes.init]
  | typeof EventTypes.broadcaster[keyof typeof EventTypes.broadcaster]
  | typeof EventTypes.socket[keyof typeof EventTypes.socket]

export type EventData = {
  [EventTypes.init.PAGE_INDEX]: null
  [EventTypes.broadcaster.FOLLOW_ANCHOR]: { userId: string; type: 'follow' | 'unfollow' }
  [EventTypes.socket.CONNECT]: null
}
  • 联合类型 + 映射类型约束事件名和参数
  • 编译期自动检查 $on/$emit
  • IDE 补全提高开发体验

四、封装事件管理器

/**
 * 统一事件管理器类
 * 通过泛型参数区分不同类型的事件:Init、Broadcaster、Socket
 */
class EventManager {
  private listeners: Map<string, Set<(...args: any[]) => void>> = new Map()

  /**
   * 添加事件监听器,保证同一个 listener 不重复注册
   */
  $on<T extends EventType>(eventType: T, listener: EventListeners[T]) {
    if (!listener) return
    if (!this.listeners.has(eventType)) {
      this.listeners.set(eventType, new Set())
    }
    const listenerSet = this.listeners.get(eventType)!
    if (!listenerSet.has(listener)) {
      listenerSet.add(listener)
      try {
        uni.$on(eventType, listener)
      } catch (e) {
        console.warn(`uni.$on failed for event ${eventType}:`, e)
      }
    }
  }

  /**
   * 派发事件,统一通过 uni.$emit
   */
  $emit<T extends EventType>(eventType: T, data?: EventData[T]) {
    try {
      if (data !== undefined) {
        uni.$emit(eventType, data)
      } else {
        uni.$emit(eventType)
      }
    } catch (e) {
      console.warn(`uni.$emit failed for event ${eventType}:`, e)
    }
  }

  /**
   * 移除指定事件监听器
   */
  $off<T extends EventType>(eventType: T, listener: EventListeners[T]) {
    const listenerSet = this.listeners.get(eventType)
    if (listenerSet) {
      listenerSet.delete(listener)
    }
    try {
      uni.$off(eventType, listener)
    } catch (e) {
      console.warn(`uni.$off failed for event ${eventType}:`, e)
    }
  }

  /**
   * 移除指定事件类型的所有监听器
   */
  $offAll(eventType: EventType) {
    const listenerSet = this.listeners.get(eventType)
    if (listenerSet) {
      listenerSet.forEach(listener => {
        try {
          uni.$off(eventType, listener)
        } catch (e) {
          console.warn(`uni.$off failed for event ${eventType}:`, e)
        }
      })
      listenerSet.clear()
    }
  }

  /**
   * 按事件组清理监听器
   */
  $clearGroup(group: keyof typeof EventGroups) {
    const groupEvents = Object.values(EventGroups[group])
    groupEvents.forEach(eventType => this.$offAll(eventType))
  }

  /**
   * 清理所有事件监听器
   */
  $clear() {
    this.listeners.forEach((listenerSet, eventType) => {
      listenerSet.forEach(listener => {
        try {
          uni.$off(eventType, listener)
        } catch (e) {
          console.warn(`uni.$off failed for event ${eventType}:`, e)
        }
      })
    })
    this.listeners.clear()
  }

  /**
   * 获取指定事件类型的监听器数量
   */
  getListenerCount(eventType: EventType): number {
    return this.listeners.get(eventType)?.size || 0
  }

  /**
   * 检查是否有指定事件类型的监听器
   */
  hasListeners(eventType: EventType): boolean {
    return this.getListenerCount(eventType) > 0
  }
}




// 创建单例实例
const eventManager = new EventManager()

export default eventManager

优化点

  • 统一管理:所有事件集中操作
  • 内存安全:组件卸载可清理监听器
  • 类型安全:事件名和参数均可编译检查

五、使用示例

重构前

uni.$on('FOLLOW_ANCHOR', (data) => {
  console.log('关注主播', data)
})

重构后

eventManager.$on(EventTypes.broadcaster.FOLLOW_ANCHOR, (data) => {
  console.log('关注主播', data)
})

eventManager.$emit(EventTypes.broadcaster.FOLLOW_ANCHOR, { userId: '123', type: 'follow' })

onUnmounted(() => {
  eventManager.$offAll(EventTypes.broadcaster.FOLLOW_ANCHOR)
})

优势

  • 模块化结构一目了然
  • IDE 自动补全
  • 编译期类型检查
  • 内存清理机制完善

六、工程价值总结

核心优化点

  1. 从混乱到统一:解决监听器混乱和硬编码问题
  2. 从平铺到模块化:枚举分层,直观可维护
  3. 类型安全:TS 编译期检查,减少运行时错误
  4. 开发效率提升:IDE 自动补全清晰
  5. 内存安全:统一清理机制

设计原则

  • 直观优于简洁:一眼看出事件所属模块
  • 结构优于平铺:便于维护和扩展
  • 类型安全:充分利用 TypeScript
  • 向后兼容:渐进式迁移,不破坏原有代码

小封装,大价值。通过这次优化,事件管理器不仅解决了原有混乱问题,也为项目长期维护和扩展打下了坚实基础。


网站公告

今日签到

点亮在社区的每一天
去签到