Skip to content

发布订阅 - 不一样的设计模式

前言

在工程化环境中分模块开发,一般项目都会拥有组件页面 路由 请求响应拦截器 状态管理仓库四个模块,这些模块之间往往存在大量相互调用。

例如用户登录失效的场景:

  • 后端返回 401 状态码
  • 响应拦截器捕获后调用路由模块跳转至登录页
  • 调用组件库的消息提示,显示身份验证过期
  • 删除状态管理工具中的用户信息(包括用户信息、token、权限字符、菜单列表等)

表面上模块已分离,实际上却高度耦合。当项目复杂度增加时,后端返回的不同错误状态码需要在不同页面进行差异化处理,这导致:

  • 模块间的耦合度越来越高
  • 请求响应拦截器中仅有 10%-20% 代码处理网络请求本身
  • 剩余代码充斥着大量的 if-else 判断逻辑

事件中心

我们可以封装一个事件中心,每个模块只负责自己的事情,处理自己的错误逻辑,然后通过事件中心进行事件分发,事件中心负责监听事件,并执行对应的处理逻辑。

传统模式的问题

1. 高耦合性

传统的处理方式中,各模块之间直接相互调用,形成了复杂的依赖关系网。当一个模块发生变化时,可能会引发连锁反应,影响其他多个模块。

2. 维护困难

随着业务复杂度的提升,模块间的调用关系越来越复杂,代码维护成本急剧上升。特别是在多人协作开发时,修改一个模块可能会影响到其他开发者的功能。

3. 代码复用性差

由于模块间耦合度过高,难以将单个模块独立出来进行复用,降低了开发效率。

发布订阅模式的优势

1. 降低耦合度

通过事件中心,发布者和订阅者之间不再直接依赖,而是通过事件进行通信,大大降低了模块间的耦合度。

2. 提高灵活性

各个模块可以独立开发和测试,只需要遵循统一的事件规范即可,提高了开发效率。

3. 便于扩展

当需要新增功能时,只需添加新的事件订阅者,而不需要修改现有的发布者代码。

实际应用场景

除了上述的错误处理场景,发布订阅模式还适用于以下情况:

1. 用户状态管理

  • 用户登录/登出时,通知各个组件更新状态
  • 权限变更时,刷新菜单和页面权限

2. 数据同步

  • 购物车数量变化时,更新头部购物车图标
  • 消息未读数变化时,更新消息提示

3. UI 状态管理

  • 侧边栏展开/收起时,通知内容区域调整宽度
  • 主题切换时,通知所有组件更新样式

实现方案

简单事件中心实现

javascript
class EventEmitter {
  constructor() {
    this.events = {}
  }

  // 订阅事件,返回取消订阅函数
  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = []
    }
    this.events[event].push(callback)

    // 返回取消订阅函数
    return () => {
      this.off(event, callback)
    }
  }

  // 发布事件
  emit(event, ...args) {
    if (this.events[event]) {
      this.events[event].forEach((callback) => callback(...args))
    }
  }

  // 取消订阅
  off(event, callback) {
    if (this.events[event]) {
      if (callback) {
        this.events[event] = this.events[event].filter((cb) => cb !== callback)
      } else {
        delete this.events[event]
      }
    }
  }
}

使用示例

1. 网络请求响应拦截器模块

javascript
import { eventEmitter } from './eventCenter'

// 请求拦截器
axios.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // 发布401错误事件
      eventEmitter.emit('unauthorized', {
        status: 401,
        message: '身份验证已过期,请重新登录',
        originalRequest: error.config
      })
    } else if (error.response?.status === 403) {
      // 发布403错误事件
      eventEmitter.emit('forbidden', {
        status: 403,
        message: '权限不足',
        originalRequest: error.config
      })
    }
    return Promise.reject(error)
  }
)

2. 路由模块

javascript
import { eventEmitter } from './eventCenter'
import router from './router'

// 监听401错误事件,跳转登录页
const unsubscribe401 = eventEmitter.on('unauthorized', (errorData) => {
  console.log('检测到401错误:', errorData)

  // 跳转到登录页
  router.push('/login')
})