提交 021d9f7f 作者: 方治民

feat: 添加 Stomp/WebSocket 实现

上级 e3bd9246
......@@ -17,6 +17,7 @@ export function configAutoImportPlugin(): Plugin {
'@/config/app': ['$app'],
'@/api/services': ['defs'],
'@/api/services/mods': ['API'],
'@/utils/stomp': [['default', 'Stomp']],
},
],
})
......
......@@ -58,6 +58,7 @@
"@iconify/iconify": "^2.2.1",
"@logicflow/core": "^1.1.26",
"@logicflow/extension": "^1.1.26",
"@stomp/stompjs": "^6.1.2",
"@vue/runtime-core": "^3.2.37",
"@vue/shared": "^3.2.37",
"@vueuse/core": "^8.9.4",
......@@ -82,7 +83,9 @@
"qs": "^6.11.0",
"resize-observer-polyfill": "^1.5.1",
"showdown": "^2.1.0",
"sockjs-client": "^1.6.1",
"sortablejs": "^1.15.0",
"stompjs": "^2.3.3",
"tinymce": "^5.10.5",
"vditor": "^3.8.17",
"vue": "^3.2.37",
......@@ -109,7 +112,9 @@
"@types/qrcode": "^1.5.0",
"@types/qs": "^6.9.7",
"@types/showdown": "^1.9.4",
"@types/sockjs-client": "^1.5.1",
"@types/sortablejs": "^1.13.0",
"@types/stompjs": "^2.3.5",
"@typescript-eslint/eslint-plugin": "^5.36.0",
"@typescript-eslint/parser": "^5.36.0",
"@vitejs/plugin-legacy": "^2.0.1",
......
......@@ -255,8 +255,28 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) {
)
}
// =============================================================================
// 生产环境
// const host = 'https://beta.app.yiring.com'
// let apiUrl = `${host}`
// 开发环境
const host = globSetting.apiUrl
let apiUrl = `${host}`
// 检查生产环境,使用对应 env 中的配置,保留原有方式方便开发中调试
apiUrl = import.meta.env.MODE !== 'development' ? globSetting.apiUrl : apiUrl
// 如果使用代理访问网站,则默认使用代理 API 地址
// apiUrl = isProxy(window.location.host) ? `http://proxy.yiring.com` : apiUrl
export { apiUrl }
// =============================================================================
// TODO: 实际项目所需的请求配置,可自定义扩展
export const defHttp = createAxios()
export const defHttp = createAxios({
requestOptions: {
apiUrl,
},
})
// other api url
// export const otherHttp = createAxios({
......
import type { Client, Frame, Message } from 'stompjs'
import SockJS from 'sockjs-client/dist/sockjs.min.js'
import Stomp from 'stompjs'
import { getToken } from '/@/utils/auth'
import { useGlobSetting } from '/@/hooks/setting'
import { useMessage } from '/@/hooks/web/useMessage'
import { apiUrl } from '/@/utils/http/axios'
const { createMessage } = useMessage()
/**
* WebSocket Stomp 实例
*/
class StompInstance {
// 连接地址
private url: string
// 是否开启调试模式
private debug: boolean
// Stomp 实例
private client: Client
// 订阅集合
private subscribes: Recordable
// 重连延时标记
private reconnectId: NodeJS.Timeout
// 重连间隔
private reconnectInterval: number
// 检查订阅延时标记
private checkSubscribeId: NodeJS.Timeout
// 检查订阅的时间间隔
private checkSubscribeInterval = 3000
// 缓存 token
token: string
// 缓存用户信息
info: Recordable
constructor({
url,
debug = false,
reconnectInterval = 10000,
}: {
url: string
debug?: boolean
reconnectInterval?: number
}) {
// 重连间隔
this.reconnectInterval = reconnectInterval
// 是否打印debug信息
this.debug = debug
// ws地址
this.url = url
// stomp实例
this.client = null
// 重连事件编号
this.reconnectId = null
// 订阅集合
this.subscribes = {}
}
/**
* 创建连接
*/
connect() {
// 如已存在连接则不创建
if (this.client && this.client.connected) {
return
}
// 从缓存中获取token
const token = getToken() as string
const options = token ? { token } : {}
// 创建 Stomp 实例
this.client = Stomp.over(new SockJS(this.url))
// 控制是否打印debug信息
if (!this.debug) {
this.client.debug = () => {}
}
// 连接
this.client.connect(
options,
(frame: Frame) => {
// 缓存连接用户信息
this.token = token
this.info = JSON.parse(frame.body)
if (this.debug) {
console.log(`[Stomp] connected, user: ${this.info.user}`)
}
// 检查消息订阅
this.loopCheckSubscribe()
// 订阅全局通知
this.client.subscribe('/user/topic/notice', (frame: Recordable) => {
if (this.debug) {
console.debug('STOMP Notice: ', frame.body)
}
const body = JSON.parse(frame.body)
if (body.status === 400) {
createMessage.warn(body.message)
} else if (body.status === 401) {
// TODO: 退出登录
}
// TODO 根据实际情况再行补充
})
// 尝试登录
this.send('/app/login', { token })
},
(_: Frame) => {
// 重连
this.reconnectId = setTimeout(() => {
this.reconnect()
}, this.reconnectInterval)
},
)
}
/**
* 重新连接
*/
reconnect() {
// 订阅状态置false
Object.keys(this.subscribes).forEach((key) => {
this.subscribes[key]['subscribed'] = false
})
// 停止重连事件
if (this.reconnectId) {
clearTimeout(this.reconnectId)
this.reconnectId = null
}
// 停止订阅状态检查
if (this.checkSubscribeId) {
clearTimeout(this.checkSubscribeId)
this.checkSubscribeId = null
}
// 连接
this.connect()
}
/**
* 断开连接
*/
disconnect() {
// 断开连接
if (this.client) {
this.client.disconnect(() => {})
}
// 停止重连事件
if (this.reconnectId) {
clearTimeout(this.reconnectId)
this.reconnectId = null
}
// 停止订阅状态检查
if (this.checkSubscribeId) {
clearTimeout(this.checkSubscribeId)
this.checkSubscribeId = null
}
// 清空所有除订阅缓存
this.subscribes = {}
}
/**
* 订阅
* @param {string} destination 主题
* @param {Function} callback 回调
*/
subscribe(destination: string, callback: (message: Message) => any) {
if (this.subscribes[destination] && this.subscribes[destination]['subscribed']) {
// 已订阅
return
} else if (this.client && this.client.connected) {
// 已连接:调用订阅,缓存订阅信息
const subscribe = this.client.subscribe(destination, (res: Message) => callback(res))
this.subscribes[destination] = { callback: callback, subscribed: true, subscribe: subscribe }
} else {
// 未连接:缓存订阅信息
this.subscribes[destination] = { callback: callback, subscribed: false }
}
}
/**
* 取消订阅
* @param {string} destination 主题
*/
unsubscribe(destination: string) {
if (this.subscribes[destination]) {
// 取消订阅
this.subscribes[destination].subscribe.unsubscribe()
// 删除订阅缓存
delete this.subscribes[destination]
}
}
/**
* 轮询检查订阅状态
*/
loopCheckSubscribe() {
this.checkSubscribeId = setInterval(() => {
Object.keys(this.subscribes).forEach((key) => {
this.subscribe(key, this.subscribes[key].callback)
})
}, this.checkSubscribeInterval)
}
/**
* 向服务器发送消息
* @param {string} destination 主题
* @param {Object | string} message 消息内容
* @param {Object} headers 消息头
*/
send(destination: string, message: Object | string, headers?: Object) {
if (this.client) {
this.client.send(destination, headers, typeof message === 'object' ? JSON.stringify(message) : message)
}
}
}
const globSetting = useGlobSetting()
const baseUrl = apiUrl + globSetting.urlPrefix
const instance = new StompInstance({
url: `${baseUrl}/stomp/sock-js`,
debug: false,
})
export default instance
......@@ -3,3 +3,5 @@ import type { ComputedRef, Ref } from 'vue'
export type DynamicProps<T> = {
[P in keyof T]: Ref<T[P]> | T[P] | ComputedRef<T[P]>
}
declare module 'sockjs-client/dist/sockjs.min.js'
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论