ZhangXianQiang
2024-07-03 229894d52a0f4cdca90bebfde323104d1810ef4f
feat:添加webscoket
2个文件已修改
1个文件已添加
226 ■■■■■ 已修改文件
.env.development 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/useWebScoket.js 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/exam/index.vue 35 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.development
@@ -1 +1,2 @@
VITE_BASE_API_URL =
VITE_BASE_API_URL =
VITE_SCOKET_URL = ws://192.168.3.64:8000/websocket/
src/hooks/useWebScoket.js
New file
@@ -0,0 +1,188 @@
import { ref, onBeforeUnmount, onMounted } from 'vue';
const DEFAULT_OPTIONS = {
  url: '', // websocket url
  heartBeatData: '', // 你的心跳数据
  heartBeatInterval: 60 * 1000, // 心跳间隔,单位ms
  reconnectInterval: 5000, // 断线重连间隔,单位ms
  maxReconnectAttempts: 10 // 最大重连次数
};
export const SocketStatus = {
  Connecting: '正在连接...', //表示正在连接,这是初始状态。
  Connected: '连接已建立', //表示连接已经建立。
  Disconnecting: '连接正在关闭', //表示连接正在关闭。
  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,抛出自定义code=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
  };
}
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>