切换主题
发布订阅 - 不一样的设计模式
前言
在工程化环境中分模块开发,一般项目都会拥有组件页面 路由 请求响应拦截器 状态管理仓库四个模块,这些模块之间往往存在大量相互调用。
例如用户登录失效的场景:
- 后端返回 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')
})