提交 192edf94 作者: 方治民

feat: 新增 websocket(stomp)、统一网络/代理配置

上级 d19af0d4
# API 接口地址
VITE_GLOB_API_URL=http://192.168.0.156:18181
VITE_GLOB_API_URL=http://192.168.0.156:17181
# API 接口地址前缀
VITE_GLOB_API_URL_PREFIX=/api
......@@ -25,6 +25,7 @@
- [x] 项目构建,文档编写
- [x] [changelog](https://www.cnblogs.com/mengfangui/p/12634845.html)
- [x] 完善网络请求相关配置
- [x] 集成 [Stomp WebSocket](https://github.com/jmesnil/stomp-websocket)
- [x] 集成 [Pont](https://github.com/alibaba/pont)
- [x] 集成 [Pinia](https://pinia.vuejs.org/)
- [x] 集成 [vue-i18n](https://github.com/intlify/vue-i18n-next)
......
......@@ -22,6 +22,7 @@ export function configAutoImportPlugin(): Plugin {
'@/api/services': ['defs'],
'@/api/services/mods': ['API'],
'@/common': ['Message'],
'@/utils/stomp': [['default', 'Stomp']],
},
],
})
......
......@@ -78,13 +78,14 @@
"@dcloudio/uni-quickapp-webview": "^3.0.0-alpha-3060820221027004",
"@dcloudio/uni-ui": "^1.4.23",
"@vue/runtime-core": "3.2.41",
"@vueuse/core": "^8.9.4",
"@vueuse/core": "^9.5.0",
"axios": "^0.26.1",
"dayjs": "^1.11.6",
"lodash-es": "^4.17.21",
"mockjs": "^1.1.0",
"pinia": "^2.0.23",
"qs": "~6.9.7",
"stompjs": "^2.3.3",
"vue": "3.2.41",
"vue-i18n": "^9.2.2",
"vue-types": "^4.2.1"
......@@ -101,6 +102,7 @@
"@types/mockjs": "^1.0.7",
"@types/prettier": "^2.7.1",
"@types/qs": "^6.9.7",
"@types/stompjs": "^2.3.5",
"@typescript-eslint/eslint-plugin": "^5.42.1",
"@typescript-eslint/parser": "^5.42.1",
"commitizen": "^4.2.5",
......@@ -120,7 +122,7 @@
"npm-run-all": "^4.1.5",
"picocolors": "^1.0.0",
"pont-engine": "^1.5.2",
"postcss": "^8.4.18",
"postcss": "^8.4.19",
"postcss-html": "^1.5.0",
"postcss-less": "^6.0.0",
"prettier": "^2.7.1",
......
const Message = {
info: (msg: string) => {
uni.showToast({
title: msg,
icon: 'none',
})
},
warn: (msg: string) => {
uni.showToast({
title: msg,
icon: 'none',
position: 'bottom',
})
},
success: (msg: string) => {
uni.showToast({
title: msg,
......@@ -8,7 +21,7 @@ const Message = {
error: (msg: string) => {
uni.showToast({
title: msg,
icon: 'error',
icon: 'none',
})
},
}
......
......@@ -16,6 +16,8 @@ import { setObjToUrlParams, deepMerge } from '/@/utils'
import { joinTimestamp, formatRequestDate } from './helper'
import { AxiosRetry } from '/@/utils/http/axios/axiosRetry'
import * as HTTP from '/@/api/types'
import { API_URL, API_URL_PREFIX } from '/@/utils/net'
import { handleResponseResource } from '/@/utils/proxy'
const globSetting = useGlobSetting()
const urlPrefix = globSetting.urlPrefix
......@@ -52,7 +54,7 @@ const transform: AxiosTransform = {
// 这里逻辑可以根据项目进行修改
const hasSuccess = data && Reflect.has(data, 'status') && status === HTTP.Status.OK
if (hasSuccess) {
return body
return handleResponseResource(body, options)
}
// 在此处根据自己项目的实际情况对不同的code执行不同的操作
......@@ -248,7 +250,12 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) {
}
// TODO: 实际项目所需的请求配置,可自定义扩展
export const defHttp = createAxios()
export const defHttp = createAxios({
requestOptions: {
apiUrl: API_URL,
urlPrefix: API_URL_PREFIX,
},
})
// other api url
// export const otherHttp = createAxios({
......
import { useGlobSetting } from '/@/hooks/setting'
let HOST = 'localhost'
let API_URL = `${HOST}`
const globSetting = useGlobSetting()
const API_URL_PREFIX = globSetting.urlPrefix
// =============================================================================
// ✨ 生产环境
// HOST = 'https://beta.app.yiring.com'
// API_URL = `${HOST}`
// 📖 开发环境
HOST = globSetting.apiUrl
API_URL = `${HOST}`
// 🔦 检查生产环境,使用对应 env 中的配置,保留原有方式方便开发中调试
API_URL = import.meta.env.MODE !== 'development' ? globSetting.apiUrl : API_URL
// 📢 如果使用代理访问网站,则默认使用代理 API 地址
// API_URL = isProxy(window.location.host) ? `http://proxy.yiring.com` : API_URL
// =============================================================================
export {
/**
* API URL
* eg: https://beta.app.yiring.com
*/
API_URL,
/**
* API URL Prefix
* eg: /api
*/
API_URL_PREFIX,
}
/**
* FIXD: 固定说明,此文件内不允许使用 import.env 之类的环境变量,否则会导致地图模块异常
*/
const PROXY_LIST = [
// eg: 开发环境
// ['http://192.168.1.100:18100', `http://proxy.yiring.com:41180`],
// eg: 测试环境
// ['http://10.111.117.15:18100', `http://proxy.yiring.com:42180`],
// eg: 生产环境
// ['http://192.168.0.100:18100', `https://oss.app.yiring.com`],
]
/**
* 将内部地址(内网)转换成代理/预览地址(公网)
* @param url 内网地址
*/
export const getExtranetUrl = (url: string): string => {
let uri = url
PROXY_LIST.forEach((proxy) => {
uri = uri.replace(new RegExp(proxy[0], 'gi'), proxy[1])
})
return uri
}
/**
* 将代理/预览地址(公网)转换成内部地址(内网)
* eg: 主要用于解决内网文件上传后预览,但是实际上传存储时还是内网地址的问题
* @param url 代理/公网地址
*/
export const getIntranetUrl = (url: string): string => {
let uri = url
PROXY_LIST.forEach((proxy) => {
uri = uri.replace(new RegExp(proxy[1], 'gi'), proxy[0])
})
return uri
}
/**
* 替换处理返回的数据中本地链接
* @param body 接口返回的内容
* @returns 替换本地链接后的数据结果
*/
export const handleResponseResource = <T>(body: T, options: Recordable) => {
if (options.apiUrl.includes('proxy.yiring.com') && typeof body === 'object') {
let text = JSON.stringify(body)
// 处理内网地址的预览问题
text = getExtranetUrl(text)
return JSON.parse(text) as T
}
return body
}
import type { Client, Message, Frame } from 'stompjs'
import Stomp from './uni-stomp'
import UniWebSocket from './uni-websocket'
import { getToken } from '/@/utils/auth'
import { useMessage } from '/@/hooks/app/useMessage'
import { API_URL, API_URL_PREFIX } from '/@/utils/net'
const { createMessage } = useMessage()
// 自定义实现 WebSocket 类
Stomp.WebSocketClass = UniWebSocket
/**
* 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地址
if (url.indexOf('http://') === 0) {
this.url = url.replace('http://', 'ws://')
} else if (url.indexOf('https://') === 0) {
this.url = url.replace('https://', 'wss://')
} else {
this.url = url
}
// stomp实例
this.client = null
// 重连事件编号
this.reconnectId = null
// 订阅集合
this.subscribes = {}
}
/**
* 创建连接
*/
connect() {
// 如已存在连接则不创建
if (this.client && this.client.connected) {
return
}
// 从缓存中获取token
const token = getToken()
const options = token ? { token } : {}
// TODO: 检查 Token 是否有效
// 创建 Stomp 实例
this.client = Stomp.client(this.url) as Client
// 控制是否打印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) => {
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 })
},
(_) => {
// 重连
this.reconnectId = setTimeout(() => {
this.reconnect()
}, this.reconnectInterval)
},
)
}
/**
* 重新连接
*/
reconnect() {
// 停止重连事件
if (this.reconnectId) {
clearTimeout(this.reconnectId)
this.reconnectId = null
}
// 停止订阅状态检查
if (this.checkSubscribeId) {
clearTimeout(this.checkSubscribeId)
this.checkSubscribeId = null
}
// 订阅状态置false
Object.keys(this.subscribes).forEach((key) => {
this.subscribes[key]['subscribed'] = false
})
// 连接
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) => 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 baseUrl = API_URL + API_URL_PREFIX
const instance = new StompInstance({
url: `${baseUrl}/stomp/ws`,
debug: false,
})
export default instance
/**
* 封装uniapp的WebSocket对象,使其与原生类似,供stomp使用
*/
class UniWebSocket {
ws: UniApp.SocketTask
activeClose: boolean
openListener: Function[]
closeListener: Function[]
errorListener: Function[]
messageListener: Function[]
constructor(url: string, protocols: string[]) {
// 是否主动关闭连接
this.activeClose = false
// 相关事件数组
this.openListener = []
this.closeListener = []
this.errorListener = []
this.messageListener = []
// 创建连接
const ws = uni.connectSocket({
url,
protocols,
complete: () => {},
})
// 连接开启
ws.onOpen((res) => {
this.onopen(res)
for (const i in this.openListener) {
this.openListener[i](res)
}
})
// 连接关闭
ws.onClose((res) => {
// 主动关闭连接不进行回调
if (this.activeClose) {
this.activeClose = false
} else {
this.onclose(res)
}
for (const i in this.closeListener) {
this.closeListener[i](res)
}
})
// 连接异常
ws.onError((res) => {
this.onerror(res)
for (const i in this.errorListener) {
this.errorListener[i](res)
}
})
// 接收消息
ws.onMessage((res) => {
this.onmessage(res)
for (const i in this.messageListener) {
this.messageListener[i](res)
}
})
this.ws = ws
}
/**
* 添加监听
* @param {string} eventName
* @param {Function} callback
*/
addEventListener(eventName: string, callback: Function) {
if (eventName === 'open') {
this.openListener.push(callback)
} else if (eventName === 'close') {
this.closeListener.push(callback)
} else if (eventName === 'error') {
this.errorListener.push(callback)
} else if (eventName === 'message') {
this.messageListener.push(callback)
}
}
/**
* 移除最后一个监听
* @param {string} eventName
*/
removeEventListener(eventName: string) {
if (eventName === 'open') {
this.openListener.pop()
} else if (eventName === 'close') {
this.closeListener.pop()
} else if (eventName === 'error') {
this.errorListener.pop()
} else if (eventName === 'message') {
this.messageListener.pop()
}
}
/**
* 连接开启
* @param {Object} _res
*/
onopen(_res: object) {}
/**
* 连接关闭
* @param {Object} _res
*/
onclose(_res: object) {}
/**
* 连接异常
* @param {Object} _res
*/
onerror(_res: object) {}
/**
* 接收消息
* @param {Object} _res
*/
onmessage(_res: object) {}
/**
* 发送消息
* @param {string | ArrayBuffer} data
*/
send(data: string | ArrayBuffer) {
this.ws.send({
data: data,
})
}
/**
* 关闭连接
*/
close() {
this.activeClose = true
this.ws.close({
complete: () => {},
})
}
}
export default UniWebSocket
......@@ -5,6 +5,7 @@ declare global {
const API: typeof import('@/api/services/mods')['API']
const EffectScope: typeof import('vue')['EffectScope']
const Message: typeof import('@/common')['Message']
const Stomp: typeof import('@/utils/stomp')['default']
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']
const customRef: typeof import('vue')['customRef']
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论