消息总线 + 可插拔的消息插件管理系统
一个 高度解耦、易于扩展的消息处理系统 。它由两个核心部分组成,各自分工明确,协同工作。 消息总线 负责"找人"(把消息派发给正确的处理器),而 可插拔插件系统 负责"提供人手"(管理和寻找能处理消息的具体逻辑)。两者结合,让系统能够灵活、动态地处理各种类型的消息。
优点:高度解耦、高拓展性
可插拔的消息插件管理系统
- 在全局,也就是在
main.js中注册消息插件
javascript
import { registerBuiltinMessagePlugins } from '@/plugins/messages/registerBuiltinMessagePlugins'
registerBuiltinMessagePlugins()
- 新增一个插件 + 在注册入口挂进去
javascript
import { registerMessagePlugin } from '@/core/message/messageRegistry'
import { textMessagePlugin } from '@/plugins/messages/text'
import { imageMessagePlugin } from '@/plugins/messages/image'
import { codeMessagePlugin } from '@/plugins/messages/code'
let registered = false
export const registerBuiltinMessagePlugins = () => {
if (registered) return
;[
textMessagePlugin,
imageMessagePlugin,
codeMessagePlugin,
aiReplyMessagePlugin,
unsupportedMessagePlugin,
].forEach(registerMessagePlugin)
registered = true
}
- 订阅者注册表 + 处理器工厂:记录谁能处理什么类型的消息,根据消息找到对应的处理逻辑
javascript
// 可插拔的消息插件管理系统
const plugins = new Map()
// 注册插件
export const registerMessagePlugin = (plugin) => {
if (!plugin?.type) {
throw new Error('message plugin must provide a type')
}
plugins.set(plugin.type, plugin)
return plugin
}
// 获取插件
export const getMessagePlugin = (type) => {
return plugins.get(type) || plugins.get('unsupported') || null
}
// 处理插件
export const resolveMessagePlugin = (message) => {
if (!message) return getMessagePlugin('unsupported')
if (message.type && plugins.has(message.type)) {
return plugins.get(message.type)
}
return getMessagePlugin('unsupported')
}
那么这里涉及到插件本身的对象格式,以textMessagePlugin为例
javascript
import TextMessage from '@/plugins/messages/text/TextMessage.vue'
import { MESSAGE_TYPES } from '@/core/message/messageTypes'
export const textMessagePlugin = {
type: MESSAGE_TYPES.TEXT,
component: TextMessage,
match(message) {
return message?.type === MESSAGE_TYPES.TEXT
},
buildSendPayload(payload) {
return {
...payload,
attachment: payload?.attachment || null,
}
},
}
收消息链路
- websocket收到消息后分类消息类型 --> 分发消息处理 --> 消息总线 (统一入口分发)
javascript
const data = JSON.parse(event.data)
console.log('收到消息:', data)
const category = classifyWsMessage(data)
dispatchWsMessage(category, data)
设计这一块时实际上我突然间觉得这个bus有些多余,我明明可以直接在dispatch的时候就处理各种类型的逻辑,我为什么还要加入一个bus?
实际上bus是一种解耦合的处理方式,把 入口 - 处理器 分开,也许现在功能单一的情况下并不能很明显的显示他的作用,但如果我要再添加一些例如:日志、埋点、额外的同步操作,这时这种设计就会显得尤为清晰。
- 消息总线实现,提供事件订阅和发布功能
javascript
const getEventListeners = (eventName) => {
if (!listeners.has(eventName)) {
listeners.set(eventName, new Set())
}
return listeners.get(eventName)
}
- 第一步提到的分发dispatch会触发发布事件到消息总线,返回是否有处理器命中
javascript
export const emitMessageBus = async (eventName, payload) => {
const eventListeners = listeners.get(eventName)
if (!eventListeners?.size) return false
// 在这里之前订阅的事件就会循环触发,解耦合体现
for (const handler of eventListeners) {
await handler(payload)
}
return true
}
- 顺带一提这里的listeners也是一个map
javascript
const listeners = new Map()
- 订阅消息总线事件,返回一个取消订阅的函数
javascript
export const onMessageBus = (eventName, handler) => {
const eventListeners = getEventListeners(eventName)
eventListeners.add(handler)
// 这里的设计很巧妙 -> 利用回调函数来进行注册,同时返回的是一个指定该事件的取消订阅方法
// 例如const unsubscribe = onMessageBus('ws:chat', receiveMessage)
// 那么我执行unsubscribe方法就可以取消该事件的订阅
return () => {
eventListeners.delete(handler)
}
}
渲染问题
消息总线处理完成后,就是页面的渲染问题,那么我在这里是用了封装组件的方式,通过获取type来选择渲染组件。不知道你们还记不记得一开始提到的 textMessagePlugin ,就是在这里定义的组件渲染。
这里就不过多的进行展示了,主要是为了分析这种设计模式。
PS:这是一个我当作学习技术的 IM 项目,如果感兴趣可以私信我。或者访问连接 github.com/LovroMance/... 这仅仅是一个不成熟的学习项目。
观察者模式/发布-订阅模式
想了想还是在这里简单说明一下这种设计模式
观察者模式
那么观察者也就是降级版的发布-订阅模式,或者说发布-订阅是基于观察者模式新增了调度中心的变种。
在这里代码来源于 前端充电宝,本人还是很喜欢里面的分享,很全面。所以这里就直接摘取他的例子来介绍了。
发布者:
javascript
// 定义发布者类
class Publisher {
constructor() {
this.observers = []
console.log('Publisher created')
}
// 增加订阅者
add(observer) {
console.log('Publisher.add invoked')
this.observers.push(observer)
}
// 移除订阅者
remove(observer) {
console.log('Publisher.remove invoked')
this.observers.forEach((item, i) => {
if (item === observer) {
this.observers.splice(i, 1)
}
})
}
// 通知所有订阅者
notify() {
console.log('Publisher.notify invoked')
this.observers.forEach((observer) => {
observer.update(this)
})
}
}
订阅者:
javascript
// 定义订阅者类
class Observer {
constructor() {
console.log('Observer created')
}
update() {
console.log('Observer.update invoked')
}
}
发布 - 订阅模式
那么这个就是我上文提到了,为什么不直接在websocket的分发器中执行收到消息后的逻辑处理而是选择转发到消息总线的理由。也就是通过添加中间的调度中心来进行解耦合。
结语
以上便是我作为一名大三前端学生,在探索时的一些粗浅思考与总结。由于个人水平有限,且技术迭代日新月异,文中难免存在理解偏差或疏漏之处,恳请各位前辈、同仁不吝赐教。
写作的过程也是自我梳理与学习的过程,若有不当之处,还望大家多多包涵并指出。路漫漫其修远兮,吾将上下而求索,愿我们都能在技术的道路上保持好奇,持续精进。感谢阅读!