refactor: ♻️ websocket 示例和 hooks 完善重构
This commit is contained in:
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user