refactor: ♻️ websocket 示例和 hooks 完善重构

This commit is contained in:
ray
2025-02-13 21:18:56 +08:00
parent fe44986a8f
commit 1f409a1420
2 changed files with 125 additions and 133 deletions

View File

@@ -4,7 +4,7 @@ import { getAccessToken } from "@/utils/auth";
export interface UseStompOptions { export interface UseStompOptions {
/** WebSocket 地址,不传时使用 VITE_APP_WS_ENDPOINT 环境变量 */ /** WebSocket 地址,不传时使用 VITE_APP_WS_ENDPOINT 环境变量 */
brokerURL?: string; brokerURL?: string;
/** 用于鉴权的 token不传时使用 getToken() 的返回值 */ /** 用于鉴权的 token不传时使用 getAccessToken() 的返回值 */
token?: string; token?: string;
/** 重连延迟,单位毫秒,默认为 5000 */ /** 重连延迟,单位毫秒,默认为 5000 */
reconnectDelay?: number; reconnectDelay?: number;
@@ -13,18 +13,25 @@ export interface UseStompOptions {
} }
export function useStomp(options: UseStompOptions = {}) { export function useStomp(options: UseStompOptions = {}) {
// 默认值brokerURL 从环境变量中获取token 从 getAccessToken() 获取
const defaultBrokerURL = import.meta.env.VITE_APP_WS_ENDPOINT || ""; const defaultBrokerURL = import.meta.env.VITE_APP_WS_ENDPOINT || "";
const defaultToken = getAccessToken(); const defaultToken = getAccessToken();
const brokerURL = ref(options.brokerURL ?? defaultBrokerURL); const brokerURL = ref(options.brokerURL ?? defaultBrokerURL);
const token = options.token ?? defaultToken; const token = options.token ?? defaultToken;
// 连接状态标记
const isConnected = ref(false); const isConnected = ref(false);
// 存储所有订阅
const subscriptions = new Map<string, StompSubscription>(); const subscriptions = new Map<string, StompSubscription>();
const client = ref<Client | null>(null); // 用于保存 STOMP 客户端的实例
let client = ref<Client | null>(null);
// 初始化 STOMP 客户端 /**
* 初始化 STOMP 客户端
* 只有在 brokerURL 非空时才会初始化客户端
*/
const initializeClient = () => { const initializeClient = () => {
if (!brokerURL.value) { if (!brokerURL.value) {
console.warn( console.warn(
@@ -66,6 +73,7 @@ export function useStomp(options: UseStompOptions = {}) {
watch(brokerURL, (newURL, oldURL) => { watch(brokerURL, (newURL, oldURL) => {
if (newURL !== oldURL) { if (newURL !== oldURL) {
console.log(`brokerURL changed from ${oldURL} to ${newURL}`); console.log(`brokerURL changed from ${oldURL} to ${newURL}`);
// 断开当前连接,重新激活客户端
if (client.value && client.value.connected) { if (client.value && client.value.connected) {
client.value.deactivate(); client.value.deactivate();
} }
@@ -76,42 +84,40 @@ export function useStomp(options: UseStompOptions = {}) {
// 在组件挂载时检查并初始化客户端 // 在组件挂载时检查并初始化客户端
onMounted(() => { onMounted(() => {
console.log("useStomp onMounted initializeClient");
initializeClient(); initializeClient();
}); });
// 激活连接(如果已经连接或正在激活则直接返回) /**
* 激活连接(如果已经连接或正在激活则直接返回)
*/
const connect = () => { const connect = () => {
if (client.value && (client.value.connected || client.value.active)) { if (client.value && (client.value.connected || client.value.active)) {
console.log("Already connected or connecting, skipping connect() call."); console.log("Already connected or connecting, skipping connect() call.");
return; return;
} }
if (client.value) { client.value?.activate();
client.value.activate();
} else {
console.warn("Client is not initialized.");
}
}; };
// 订阅指定主题,连接成功后自动订阅 /**
* 订阅指定主题
* @param destination 目标主题地址
* @param callback 接收到消息时的回调函数
* @returns 返回订阅 id用于后续取消订阅
*/
const subscribe = (destination: string, callback: (message: IMessage) => void): string => { const subscribe = (destination: string, callback: (message: IMessage) => void): string => {
if (!client.value) { if (client.value) {
console.error("STOMP client is not initialized."); const subscription = client.value.subscribe(destination, callback);
return ""; subscriptions.set(subscription.id, subscription);
return subscription.id;
} }
return "";
// 如果还没有连接,就先激活连接
if (!isConnected.value) {
console.log("Not connected yet. Connecting...");
connect();
}
// 连接成功后订阅主题
const subscription = client.value.subscribe(destination, callback);
subscriptions.set(subscription.id, subscription);
return subscription.id;
}; };
// 取消指定订阅 /**
* 取消指定订阅
* @param subscriptionId 要取消的订阅 id
*/
const unsubscribe = (subscriptionId: string) => { const unsubscribe = (subscriptionId: string) => {
const subscription = subscriptions.get(subscriptionId); const subscription = subscriptions.get(subscriptionId);
if (subscription) { if (subscription) {
@@ -120,17 +126,15 @@ export function useStomp(options: UseStompOptions = {}) {
} }
}; };
// 主动断开连接(如果未连接则不执行) /**
* 主动断开连接(如果未连接则不执行)
*/
const disconnect = () => { const disconnect = () => {
if (client.value && !(client.value.connected || client.value.active)) { if (client.value && !(client.value.connected || client.value.active)) {
console.log("Already disconnected, skipping disconnect() call."); console.log("Already disconnected, skipping disconnect() call.");
return; return;
} }
if (client.value) { client.value?.deactivate();
client.value.deactivate();
} else {
console.warn("Client is not initialized.");
}
isConnected.value = false; isConnected.value = false;
}; };
@@ -138,9 +142,9 @@ export function useStomp(options: UseStompOptions = {}) {
client, client,
isConnected, isConnected,
connect, connect,
subscribe, // 订阅函数放到这里 subscribe,
unsubscribe, unsubscribe,
disconnect, disconnect,
brokerURL, // 暴露 brokerURL 以便组件中动态修改 brokerURL,
}; };
} }

View File

@@ -12,9 +12,8 @@
<el-col :span="12"> <el-col :span="12">
<el-card> <el-card>
<el-row> <el-row>
<el-col :span="16"> <el-col :span="18">
<!-- 输入框允许修改 websocket 地址注意修改后不会自动更新已创建的 hook 实例 --> <el-input v-model="socketEndpoint" style="width: 200px" />
<el-input v-model="socketEndpoint" class="w-220px" />
<el-button <el-button
type="primary" type="primary"
class="ml-5" class="ml-5"
@@ -27,14 +26,13 @@
断开 断开
</el-button> </el-button>
</el-col> </el-col>
<el-col :span="8" class="text-right"> <el-col :span="6" class="text-right">
连接状态 连接状态
<el-tag v-if="isConnected" class="ml-2" type="success">已连接</el-tag> <el-tag v-if="isConnected" type="success">已连接</el-tag>
<el-tag v-else class="ml-2" type="info">已断开</el-tag> <el-tag v-else type="info">已断开</el-tag>
</el-col> </el-col>
</el-row> </el-row>
</el-card> </el-card>
<!-- 广播消息发送部分 --> <!-- 广播消息发送部分 -->
<el-card class="mt-5"> <el-card class="mt-5">
<el-form label-width="90px"> <el-form label-width="90px">
@@ -46,7 +44,6 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card> </el-card>
<!-- 点对点消息发送部分 --> <!-- 点对点消息发送部分 -->
<el-card class="mt-5"> <el-card class="mt-5">
<el-form label-width="90px"> <el-form label-width="90px">
@@ -62,32 +59,34 @@
</el-form> </el-form>
</el-card> </el-card>
</el-col> </el-col>
<!-- 消息接收显示部分 --> <!-- 消息接收显示部分 -->
<el-col :span="12"> <el-col :span="12">
<el-card> <el-card>
<div class="message-container"> <div class="chat-messages-wrapper">
<div <div
v-for="(message, index) in messages" v-for="(message, index) in messages"
:key="index" :key="index"
:class="{ :class="[
'tip-message': message.type === 'tip', message.type === 'tip' ? 'system-notice' : 'chat-message',
message: message.type !== 'tip', {
'message--sent': message.sender === userStore.userInfo.username, 'chat-message--sent': message.sender === userStore.userInfo.username,
'message--received': message.sender !== userStore.userInfo.username, 'chat-message--received': message.sender !== userStore.userInfo.username,
}" },
]"
> >
<div v-if="message.type != 'tip'" class="message-content"> <template v-if="message.type != 'tip'">
<div <div class="chat-message__content">
:class="{ <div
'message-sender': message.sender === userStore.userInfo.username, :class="{
'message-receiver': message.sender !== userStore.userInfo.username, 'chat-message__sender': message.sender === userStore.userInfo.username,
}" 'chat-message__receiver': message.sender !== userStore.userInfo.username,
> }"
{{ message.sender }} >
{{ message.sender }}
</div>
<div class="text-gray-600">{{ message.content }}</div>
</div> </div>
<div class="color-#333">{{ message.content }}</div> </template>
</div>
<div v-else>{{ message.content }}</div> <div v-else>{{ message.content }}</div>
</div> </div>
</div> </div>
@@ -99,16 +98,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { useStomp } from "@/hooks/useStomp"; import { useStomp } from "@/hooks/useStomp";
import { getAccessToken } from "@/utils/auth"; // 用于获取token import { useUserStoreHook } from "@/store/modules/user";
import { useUserStoreHook } from "@/store/modules/user"; // 获取用户信息
const userStore = useUserStoreHook(); const userStore = useUserStoreHook();
// 用于手动调整 WebSocket 地址 // 用于手动调整 WebSocket 地址
const socketEndpoint = ref(import.meta.env.VITE_APP_WS_ENDPOINT); const socketEndpoint = ref(import.meta.env.VITE_APP_WS_ENDPOINT);
// 同步连接状态 // 同步连接状态
const isConnected = ref(false);
// 消息接收列表
interface MessageType { interface MessageType {
type?: string; type?: string;
sender?: string; sender?: string;
@@ -122,50 +117,42 @@ const queneMessage = ref("Hi, " + userStore.userInfo.username + " 这里是点
const receiver = ref("root"); const receiver = ref("root");
// 调用 useStomp hook默认使用 socketEndpoint 和 token此处用 getAccessToken() // 调用 useStomp hook默认使用 socketEndpoint 和 token此处用 getAccessToken()
const { const { isConnected, connect, subscribe, disconnect, client } = useStomp({
isConnected: stompConnected,
connect,
subscribe,
disconnect,
client,
} = useStomp({
brokerURL: socketEndpoint.value,
token: getAccessToken(),
reconnectDelay: 5000,
debug: true, debug: true,
}); });
// 同步 hook 的连接状态到组件 watch(
watch(stompConnected, (newVal) => { () => isConnected.value,
isConnected.value = newVal; (connected) => {
if (newVal) { if (connected) {
// 连接成功后,订阅广播和点对点消息主题 // 连接成功后,订阅广播和点对点消息主题
subscribe("/topic/notice", (res) => { subscribe("/topic/notice", (res) => {
messages.value.push({
sender: "Server",
content: res.body,
});
});
subscribe("/user/queue/greeting", (res) => {
const messageData = JSON.parse(res.body) as MessageType;
messages.value.push({
sender: messageData.sender,
content: messageData.content,
});
});
messages.value.push({ messages.value.push({
sender: "Server", sender: "Server",
content: res.body, content: "Websocket 已连接",
type: "tip",
}); });
}); } else {
subscribe("/user/queue/greeting", (res) => {
const messageData = JSON.parse(res.body) as MessageType;
messages.value.push({ messages.value.push({
sender: messageData.sender, sender: "Server",
content: messageData.content, content: "Websocket 已断开",
type: "tip",
}); });
}); }
messages.value.push({
sender: "Server",
content: "Websocket 已连接",
type: "tip",
});
} else {
messages.value.push({
sender: "Server",
content: "Websocket 已断开",
type: "tip",
});
} }
}); );
// 连接 WebSocket // 连接 WebSocket
function connectWebSocket() { function connectWebSocket() {
@@ -179,7 +166,7 @@ function disconnectWebSocket() {
// 发送广播消息 // 发送广播消息
function sendToAll() { function sendToAll() {
if (client.value && isConnected.value) { if (client.value && client.value.connected) {
client.value.publish({ client.value.publish({
destination: "/topic/notice", destination: "/topic/notice",
body: topicMessage.value, body: topicMessage.value,
@@ -193,7 +180,7 @@ function sendToAll() {
// 发送点对点消息 // 发送点对点消息
function sendToUser() { function sendToUser() {
if (client.value && isConnected.value) { if (client.value && client.value.connected) {
client.value.publish({ client.value.publish({
destination: "/app/sendToUser/" + receiver.value, destination: "/app/sendToUser/" + receiver.value,
body: queneMessage.value, body: queneMessage.value,
@@ -214,45 +201,46 @@ onBeforeUnmount(() => {
}); });
</script> </script>
<style scoped> <style scoped lang="scss">
.message-container { .chat-messages-wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px;
} }
.message { .chat-message {
max-width: 80%;
padding: 10px; padding: 10px;
margin: 10px;
border-radius: 5px; border-radius: 5px;
&--sent {
align-self: flex-end;
background-color: #dcf8c6;
}
&--received {
align-self: flex-start;
background-color: #e8e8e8;
}
&__content {
display: flex;
flex-direction: column;
color: var(--el-text-color-primary); // 使用主题文本颜色
}
&__sender {
margin-bottom: 5px;
font-weight: bold;
text-align: right;
}
&__receiver {
margin-bottom: 5px;
font-weight: bold;
text-align: left;
}
} }
.message--sent { .system-notice {
align-self: flex-end;
background-color: #dcf8c6;
}
.message--received {
align-self: flex-start;
background-color: #e8e8e8;
}
.message-content {
display: flex;
flex-direction: column;
}
.message-sender {
margin-bottom: 5px;
font-weight: bold;
text-align: right;
}
.message-receiver {
margin-bottom: 5px;
font-weight: bold;
text-align: left;
}
.tip-message {
align-self: center; align-self: center;
padding: 5px 10px; padding: 5px 10px;
margin-bottom: 5px; font-size: 0.9em;
font-style: italic; color: var(--el-text-color-secondary);
text-align: center; background-color: var(--el-fill-color-lighter);
background-color: #f0f0f0; border-radius: 15px;
border-radius: 5px;
} }
</style> </style>