业务侧-消息管理器-IMLoader

224 阅读5分钟

设计思路:

前期经过1~2周的预研工作,试了socket相关的方案,因架构组大佬要求比较高,要实现一个大而全的整体方案

比如支持TCP/HTTP/WS等协议的。套件就定位到了MQTT

优先需要搞一些TCP协议的,研究相关技术,发现前端都是根据scoket的方式,采用WS或者WSS协议,服务端在进行转换TCP进行传输,因安全性问题,前端不支持。
为什么非要搞TCP呢?
因为华为云还是什么防火墙不支持scoket透传,这个就有点麻烦了?
经过几次沟通,最后服务端搞定了这个事情,然后我们又回归了WS方式。

image.png

设计动机:

系统业务层具有实时互动场景的需求,比如上课后,在线课堂上老师和学生需要进行相关主题互动,比如投票、抢答什么的。

设计规则:

  • MQTT 前端只需要支持订阅消息获取,不支持发送消息。
  • 发送消息采用HTTP接口方式,服务端业务侧会调用框架层来做消息传递,其实也就是MQTT的主题消息订阅和发布,只不过做了服务端的框架层,不需要业务层做过多的思考。
  • 会话级别的订阅,就是用户登录后,会根据当前的会话标记进行客户端注册和默认主题订阅,这样消息接收会默认都会传送到这个会话消息中,然后根据里面的系统标记和业务标记来区分,当前信令的业务含义。

业务消息体规则:

title:消息标题,用于弹窗/鼠标上浮时展示
content:消息内容,用于保存通用消息通知内容字符串
subscriber:订阅方,用于前端判断哪个业务页面需要订阅此消息。
publisher:发布方,后台自定义,前端可以不关注,也可以做校验,只有特定发布方才处理。
cmdType:指令类型,字符串,提供给前端业务页面做判断需要执行的事件
uniqueKey:唯一键,用于前端业务页面判断,只处理自己这个业务数据的场景,
如:教室课堂页面,应该只处理自己这个教室课堂ID的消息
data:业务数据

以上消息中,业务接收到消息后,主要是根据subscriber来区分业务类型,比如是课堂交互还是作业通知类,这种大业务类上的区分,主要支持在线实时互动和消息的简单通知

其他的字段cmdType、data 可以根据特点的业务场景来进行分类

API

API含义
register系统进入初始化IM逻辑
createConnect创建新得MQTT连接,支持特定场景创建
IMConfig配置获取

代码实践

/*
 * @Description: IM管理器-消息中心
 * @Author: daerduo
 */
import { BaseStore, Config, IM } from "@basic-library";
import { SocketEmitter,cache } from '@basic-utils'
/**
 * IM-消息中心
 * 1、通信连接-配置信息
 * 2、消息连接、会话订阅、消息通知
 */

const getURL = () => {
    const { protocol, hostname, port, path, proxy = "" } = Config.BSConfig?.MQTT_USER_CONFIG

    let portStr = port ? `:${Number(port)}` : ''
    const host = hostname || window.location.host

    return `${protocol}://${host}${portStr}${proxy}${path}`
}

const IMConfig = {
    getURL: () => getURL(),
    options: Config.BSConfig?.MQTT_USER_CONFIG
}

/**
 * @description: 接收信息-内部消息中心执行
 * @param {*} payload
 * @return {*}
 */
const subscriberEmit = (payload) => {
    payload.subscriber && SocketEmitter.emit(payload.subscriber, payload)
}

const register = () => {
    const { userInfo = {}, loginStatus } = BaseStore.app
    const { openIM = false, options } = Config.BSConfig?.MQTT_USER_CONFIG

    if (openIM && loginStatus) {
        try {
            const token = cache.getCache('token','local')
            const clientId = `topic_web_session_${token}` + '_' + Date.now()
            const defaultTopic = `topic_web_session_${token}`
    
            IM.connect(getURL(), {
                clientId,
                defaultTopic,
                ...options
            }).then(() => {
                // 开始订阅当前会话
                IM.subscribe(defaultTopic)
                // 开始监听消息--会话消息【通知】
                /**
                    * title:消息标题,用于弹窗/鼠标上浮时展示
                    * content:消息内容,用于保存通用消息通知内容字符串 
                    * subscriber:订阅方,用于前端判断哪个业务页面需要订阅此消息。
                    * publisher:发布方,后台自定义,前端可以不关注,也可以做校验,只有特定发布方才处理。
                    * cmdType:指令类型,字符串,提供给前端业务页面做判断需要执行的事件
                    * uniqueKey:唯一键,用于前端业务页面判断,只处理自己这个业务数据的场景,如:教室课堂页面,应该只处理自己这个教室课堂ID的消息
                    * data:业务数据
                */
                IM.receiveSession((payload) => payload && subscriberEmit(payload))
            })
        } catch (error) {
            console.error(error)
        }
    }
}

/**
 * @description: 创建新连接
 * @param {*} params
 * @return {*}
 */
const createConnect = (params) => {
    const { options } = Config.BSConfig?.MQTT_USER_CONFIG
    try {
        const token = cache.getCache('token','local')
        const clientId = `topic_web_session_${token}` + '_' + Date.now()
        const defaultTopic = `_${token}`
        const oo = {...options,...{
            will:{
                topic:'error',
                payload:'Connection Closed abnormally...!',
                retain: false,
                qos: 0
            }
        }}

        IM.connect(getURL(), {
            clientId,
            defaultTopic,
            ...oo
        }).then(() => {
            // 开始订阅当前会话
            IM.subscribe(defaultTopic)
            // 开始监听消息--会话消息【通知】
            IM.receiveSession((payload) => payload && subscriberEmit(payload))
        })
    } catch (error) {
        console.error(error)
    }
}

export default {
    register,
    createConnect,
    IMConfig
};

核心解读:

1、IM配置这个一般很少用,很多配置都在系统级的配置里面,通过 Config.BSConfig?.MQTT_USER_CONFIG 就可以获取到
2、createConnect 创建新连接,这个需要看业务场景,因为目前系统就一个会话级客户端,进行不同消息类型传递。
3、IM初始化
从配置中获取IM配置
BaseStore.app,判断是否开启 MQTT客户端ID clientId ,创建主题

const token = cache.getCache('token','local')
const clientId = `topic_web_session_${token}` + '_' + Date.now()
const defaultTopic = `topic_web_session_${token}`

连接成功后,开始订阅默认主题会话

IM.connect
IM.subscribe(defaultTopic)

接收到消息后,推送给前端的事件管理器

/**
 * @description: 接收信息-内部消息中心执行
 * @param {*} payload
 * @return {*}
 */
const subscriberEmit = (payload) => {
    payload.subscriber && SocketEmitter.emit(payload.subscriber, payload)
}


IM.receiveSession((payload) => payload && subscriberEmit(payload))

SocketEmitter.emit()

订阅使用的是 payload.subscriber

4、业务订阅使用


import { SocketEmitter } from '@basic-utils'

const subscriber = '业务类别' // 需要与服务端进行约定业务类别
SocketEmitter.on(subscriber,()=>{
  ...
})

设计完毕,期望你的指点....

针对Loader管理器的一系列,请看下方:

业务侧-Loader设计
业务侧-系统应用加载器-AppLoader
业务侧-登录管理器-LoginLoader
业务侧-异常拦截管理器-ErrorLoader
业务侧-路由管理器-RouterLoader