From 229894d52a0f4cdca90bebfde323104d1810ef4f Mon Sep 17 00:00:00 2001 From: ZhangXianQiang <1135831638@qq.com> Date: 星期三, 03 七月 2024 17:18:16 +0800 Subject: [PATCH] feat:添加webscoket --- .env.development | 3 src/hooks/useWebScoket.js | 188 +++++++++++++++++++++++++++++++++++++++++++++++ src/views/exam/index.vue | 35 +++++++- 3 files changed, 220 insertions(+), 6 deletions(-) diff --git a/.env.development b/.env.development index f6b70c2..4a153b1 100644 --- a/.env.development +++ b/.env.development @@ -1 +1,2 @@ -VITE_BASE_API_URL = \ No newline at end of file +VITE_BASE_API_URL = +VITE_SCOKET_URL = ws://192.168.3.64:8000/websocket/ \ No newline at end of file diff --git a/src/hooks/useWebScoket.js b/src/hooks/useWebScoket.js new file mode 100644 index 0000000..5f3da78 --- /dev/null +++ b/src/hooks/useWebScoket.js @@ -0,0 +1,188 @@ +import { ref, onBeforeUnmount, onMounted } from 'vue'; + +const DEFAULT_OPTIONS = { + url: '', // websocket url + heartBeatData: '', // 浣犵殑蹇冭烦鏁版嵁 + heartBeatInterval: 60 * 1000, // 蹇冭烦闂撮殧锛屽崟浣峬s + reconnectInterval: 5000, // 鏂嚎閲嶈繛闂撮殧锛屽崟浣峬s + maxReconnectAttempts: 10 // 鏈�澶ч噸杩炴鏁� +}; + +export const SocketStatus = { + Connecting: '姝e湪杩炴帴...', //琛ㄧず姝e湪杩炴帴锛岃繖鏄垵濮嬬姸鎬併�� + Connected: '杩炴帴宸插缓绔�', //琛ㄧず杩炴帴宸茬粡寤虹珛銆� + Disconnecting: '杩炴帴姝e湪鍏抽棴', //琛ㄧず杩炴帴姝e湪鍏抽棴銆� + Disconnected: '杩炴帴宸叉柇寮�' //琛ㄧず杩炴帴宸茬粡鍏抽棴 +}; + +const SocketCloseCode = 1000; + +export default function useWebSocket(options = {}) { + const state = { + options: { ...DEFAULT_OPTIONS, ...options }, + + socket: null, + reconnectAttempts: 0, + reconnectTimeout: null, + + heartBetaSendTimer: null, // 蹇冭烦鍙戦�佸畾鏃跺櫒 + heartBetaTimeoutTimer: null // 蹇冭烦瓒呮椂瀹氭椂鍣� + }; + + // 杩炴帴鐘舵�� + const status = ref(SocketStatus.Disconnected); + const message = ref(null); + const error = ref(null); + + // 杩炴帴 + const connect = (connectCallBack) => { + disconnect(); + + status.value = SocketStatus.Connecting; + if (!window.navigator.onLine) { + setTimeout(() => { + status.value = SocketStatus.Disconnected; + }, 500); + return; + } + + state.socket = new WebSocket(state.options.url); + + state.socket.onopen = openEvent => { + console.log('socket杩炴帴:', openEvent); + state.reconnectAttempts = 0; + status.value = SocketStatus.Connected; + error.value = null; + startHeartBeat(); + if(connectCallBack) { + connectCallBack(); + } + }; + + state.socket.onmessage = msgEvent => { + console.log('socket娑堟伅:', msgEvent); + + // 鏀跺埌浠讳綍鏁版嵁锛岄噸鏂板紑濮嬪績璺� + startHeartBeat(); + + const { data } = msgEvent; + const msg = JSON.parse(data); + + //蹇冭烦鏁版嵁, 鍙嚜琛屼慨鏀� + if (+msg.msg_id === 0) { + return; + } + message.value = msg; + }; + + state.socket.onclose = closeEvent => { + console.log('socket鍏抽棴:', closeEvent); + status.value = SocketStatus.Disconnected; + // 闈炴甯稿叧闂�,灏濊瘯閲嶈繛 + if (closeEvent.code !== SocketCloseCode) { + reconnect(); + } + }; + + state.socket.onerror = errEvent => { + console.log('socket鎶ラ敊:', errEvent); + status.value = SocketStatus.Disconnected; + error.value = errEvent; + // 杩炴帴澶辫触锛屽皾璇曢噸杩� + reconnect(); + }; + }; + + const disconnect = () => { + if (state.socket && (state.socket.OPEN || state.socket.CONNECTING)) { + console.log('socket鏂紑杩炴帴'); + status.value = SocketStatus.Disconnecting; + state.socket.onmessage = null; + state.socket.onerror = null; + state.socket.onclose = null; + // 鍙戦�佸叧闂抚缁欐湇鍔$ + state.socket.close(SocketCloseCode, 'normal closure'); + status.value = SocketStatus.Disconnected; + state.socket = null; + } + stopHeartBeat(); + stopReconnect(); + }; + + // 鍙戦�佹秷鎭� + const sendMessage = (data) => { + if (state.socket && status.value === SocketStatus.Connected) { + state.socket.send(JSON.stringify(data)); + console.log('socket鍙戦��:', JSON.stringify(data)); + } + }; + + const startHeartBeat = () => { + stopHeartBeat(); + onHeartBeat(() => { + if (status.value === SocketStatus.Connected) { + state.socket.send(state.options.heartBeatData); + console.log('socket蹇冭烦鍙戦��:', state.options.heartBeatData); + } + }); + }; + + const onHeartBeat = callback => { + state.heartBetaSendTimer = setTimeout(() => { + callback && callback(); + state.heartBetaTimeoutTimer = setTimeout(() => { + // 蹇冭烦瓒呮椂,鐩存帴鍏抽棴socket,鎶涘嚭鑷畾涔塩ode=4444, onclose閲岃繘琛岄噸杩� + state.socket.close(4444, 'heart timeout'); + }, state.options.heartBeatInterval); + }, state.options.heartBeatInterval); + }; + + const stopHeartBeat = () => { + state.heartBetaSendTimer && clearTimeout(state.heartBetaSendTimer); + state.heartBetaTimeoutTimer && clearTimeout(state.heartBetaTimeoutTimer); + }; + + // 閲嶈繛 + const reconnect = () => { + if (status.value === SocketStatus.Connected || status.value === SocketStatus.Connecting) { + return; + } + stopHeartBeat(); + if (state.reconnectAttempts < state.options.maxReconnectAttempts) { + console.log('socket閲嶈繛:', state.reconnectAttempts); + + // 閲嶈繛闂撮殧锛�5绉掕捣姝ワ紝涓嬫閫掑1绉� + const interval = Math.max(state.options.reconnectInterval, state.reconnectAttempts * 1000); + console.log('闂撮殧鏃堕棿锛�', interval); + state.reconnectTimeout = setTimeout(() => { + if (status.value !== SocketStatus.Connected && status.value !== SocketStatus.Connecting) { + connect(); + } + }, interval); + state.reconnectAttempts += 1; + } else { + status.value = SocketStatus.Disconnected; + stopReconnect(); + } + }; + + // 鍋滄閲嶈繛 + const stopReconnect = () => { + state.reconnectTimeout && clearTimeout(state.reconnectTimeout); + }; + + onBeforeUnmount(() => { + if (status.value === SocketStatus.Connected || status.value === SocketStatus.Connecting) { + disconnect(); + } + }); + + return { + status, + message, + error, + connect, + disconnect, + sendMessage + }; +} \ No newline at end of file diff --git a/src/views/exam/index.vue b/src/views/exam/index.vue index 0918145..591e735 100644 --- a/src/views/exam/index.vue +++ b/src/views/exam/index.vue @@ -130,7 +130,7 @@ </template> <script setup> -import { ref, watchEffect } from 'vue'; +import { ref, watchEffect, watch, onMounted } from 'vue'; import { storeToRefs } from 'pinia'; import { Close, Timer } from '@element-plus/icons-vue'; import AnswerTag from './components/answer-tag/index.vue'; @@ -146,15 +146,19 @@ import AnswerShort from './components/answer-main/answer-short/index.vue'; import AnswerCount from './components/answer-main/answer-count/index.vue'; -import { useExamStore } from '@/store/index.js'; +import { useExamStore, useUserStore } from '@/store/index.js'; import { useRouter } from 'vue-router'; import { submitExam } from '@/api/modules/exam.js'; +import useWebScoket from '@/hooks/useWebScoket.js'; const router = useRouter(); const examStore = useExamStore(); +const userStore = useUserStore(); + +const { userInfo } = storeToRefs(userStore); const { currentType, currentIndex, examDetail, examType, examInfo } = storeToRefs(examStore); const typeComponent = { @@ -172,17 +176,25 @@ const submitDialog = ref(false); const timeDialog = ref(false); +const { status, message, error, connect, disconnect, sendMessage } = useWebScoket({ + url: 'ws://192.168.3.64:8000/websocket/' + userInfo.value.id, + heartBeatData: 'ping' +}); + +// 涓婁竴棰� const prevQuestion = () => { currentIndex.value--; checkList(); }; +// 涓嬩竴棰� const nextQuestion = () => { currentIndex.value++; checkList(); }; +// 鏌ヨ棰樼洰 const checkList = () => { let tempIndex = 0; const typeQuestion = examDetail.value.find((typeItem, index) => { @@ -218,7 +230,6 @@ }; - // 閫�鍑鸿�冭瘯 const closeClick = () => { quitDialog.value = true; @@ -244,12 +255,15 @@ // 鏃堕棿缁撴潫 const timeOut = () => { - resetAllDialog(); - timeDialog.value = true; const temp = { ...examInfo.value, titleList: examDetail.value }; + timeDialog.value = true; + resetAllDialog(); + + disconnect(); + submitExam(temp).then(res => { returnBack(); }).catch(() => { @@ -274,6 +288,17 @@ }); examStore.setProgress(progress); }); + +watch(() => message.value, (msg) => { + console.log(msg); +}); + + +// -----------------------------------鐢熷懡鍛ㄦ湡 +onMounted(() => { + // 杩炴帴webscoket + connect(); +}); </script> <style lang="scss" scoped> -- Gitblit v1.8.0