diff --git a/src/components/layout/layout-traffic.tsx b/src/components/layout/layout-traffic.tsx index c4282cf..e920e7c 100644 --- a/src/components/layout/layout-traffic.tsx +++ b/src/components/layout/layout-traffic.tsx @@ -5,6 +5,7 @@ import { useClashInfo } from "@/hooks/use-clash"; import { useVerge } from "@/hooks/use-verge"; import { TrafficGraph, type TrafficRef } from "./traffic-graph"; import { useLogSetup } from "./use-log-setup"; +import { useWebsocket } from "@/hooks/use-websocket"; import parseTraffic from "@/utils/parse-traffic"; // setup the traffic @@ -18,58 +19,36 @@ const LayoutTraffic = () => { const trafficRef = useRef(null); const [traffic, setTraffic] = useState({ up: 0, down: 0 }); - const wsRef = useRef(null); - const [refresh, setRefresh] = useState({}); - // setup log ws during layout useLogSetup(); + const { connect, disconnect } = useWebsocket((event) => { + const data = JSON.parse(event.data) as ITrafficItem; + trafficRef.current?.appendData(data); + setTraffic(data); + }); + useEffect(() => { if (!clashInfo) return; const { server = "", secret = "" } = clashInfo; - const ws = new WebSocket(`ws://${server}/traffic?token=${secret}`); - - ws.addEventListener("message", (event) => { - const data = JSON.parse(event.data) as ITrafficItem; - trafficRef.current?.appendData(data); - setTraffic(data); - }); - - ws.addEventListener("error", () => { - setTimeout(() => { - if (document.visibilityState === "visible") { - setRefresh({}); - } - }, 1000); - }); - - ws.addEventListener("close", () => { - setTimeout(() => { - if (document.visibilityState === "visible") { - setRefresh({}); - } - }, 1000); - }); - - wsRef.current = ws; + connect(`ws://${server}/traffic?token=${secret}`); return () => { - ws?.close(); - wsRef.current = null; + disconnect(); }; - }, [clashInfo, refresh]); + }, [clashInfo]); useEffect(() => { + // 页面隐藏时去掉请求 const handleVisibleChange = () => { + if (!clashInfo) return; if (document.visibilityState === "visible") { // reconnect websocket - if ( - wsRef.current && - wsRef.current.readyState !== WebSocket.CONNECTING - ) { - setRefresh({}); - } + const { server = "", secret = "" } = clashInfo; + connect(`ws://${server}/traffic?token=${secret}`); + } else { + disconnect(); } }; diff --git a/src/components/layout/use-log-setup.ts b/src/components/layout/use-log-setup.ts index ebe0d38..8dde19d 100644 --- a/src/components/layout/use-log-setup.ts +++ b/src/components/layout/use-log-setup.ts @@ -1,9 +1,10 @@ import dayjs from "dayjs"; -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { useRecoilValue, useSetRecoilState } from "recoil"; import { getClashLogs } from "@/services/cmds"; import { useClashInfo } from "@/hooks/use-clash"; import { atomEnableLog, atomLogData } from "@/services/states"; +import { useWebsocket } from "@/hooks/use-websocket"; const MAX_LOG_NUM = 1000; @@ -14,7 +15,14 @@ export const useLogSetup = () => { const enableLog = useRecoilValue(atomEnableLog); const setLogData = useSetRecoilState(atomLogData); - const [refresh, setRefresh] = useState({}); + const { connect, disconnect } = useWebsocket((event) => { + const data = JSON.parse(event.data) as ILogItem; + const time = dayjs().format("MM-DD HH:mm:ss"); + setLogData((l) => { + if (l.length >= MAX_LOG_NUM) l.shift(); + return [...l, { ...data, time }]; + }); + }); useEffect(() => { if (!enableLog || !clashInfo) return; @@ -22,21 +30,10 @@ export const useLogSetup = () => { getClashLogs().then(setLogData); const { server = "", secret = "" } = clashInfo; - const ws = new WebSocket(`ws://${server}/logs?token=${secret}`); + connect(`ws://${server}/logs?token=${secret}`); - ws.addEventListener("message", (event) => { - const data = JSON.parse(event.data) as ILogItem; - const time = dayjs().format("MM-DD HH:mm:ss"); - setLogData((l) => { - if (l.length >= MAX_LOG_NUM) l.shift(); - return [...l, { ...data, time }]; - }); - }); - - ws.addEventListener("error", () => { - setTimeout(() => setRefresh({}), 1000); - }); - - return () => ws?.close(); - }, [clashInfo, enableLog, refresh]); + return () => { + disconnect(); + }; + }, [clashInfo, enableLog]); }; diff --git a/src/hooks/use-websocket.ts b/src/hooks/use-websocket.ts new file mode 100644 index 0000000..1c92e7b --- /dev/null +++ b/src/hooks/use-websocket.ts @@ -0,0 +1,49 @@ +import { useRef } from "react"; + +export type WsMsgFn = (event: MessageEvent) => void; + +interface Options { + errorCount?: number; // default is 5 + retryInterval?: number; // default is 2500 +} + +export const useWebsocket = (onMessage: WsMsgFn, options?: Options) => { + const wsRef = useRef(null); + const timerRef = useRef(null); + + const disconnect = () => { + if (wsRef.current) { + wsRef.current.close(); + wsRef.current = null; + } + if (timerRef.current) { + clearTimeout(timerRef.current); + } + }; + + const connect = (url: string) => { + let errorCount = options?.errorCount ?? 5; + + if (!url) return; + + const connectHelper = () => { + disconnect(); + + const ws = new WebSocket(url); + wsRef.current = ws; + + ws.addEventListener("message", onMessage); + ws.addEventListener("error", () => { + errorCount -= 1; + + if (errorCount >= 0) { + timerRef.current = setTimeout(connectHelper, 2500); + } + }); + }; + + connectHelper(); + }; + + return { connect, disconnect }; +}; diff --git a/src/pages/connections.tsx b/src/pages/connections.tsx index 7ea1444..85f97e4 100644 --- a/src/pages/connections.tsx +++ b/src/pages/connections.tsx @@ -17,6 +17,7 @@ import { closeAllConnections } from "@/services/api"; import { atomConnectionSetting } from "@/services/states"; import { useClashInfo } from "@/hooks/use-clash"; import { BaseEmpty, BasePage } from "@/components/base"; +import { useWebsocket } from "@/hooks/use-websocket"; import ConnectionItem from "@/components/connection/connection-item"; import ConnectionTable from "@/components/connection/connection-table"; @@ -53,15 +54,9 @@ const ConnectionsPage = () => { return connections; }, [connData, filterText, curOrderOpt]); - useEffect(() => { - if (!clashInfo) return; - - const { server = "", secret = "" } = clashInfo; - const ws = new WebSocket(`ws://${server}/connections?token=${secret}`); - - ws.addEventListener("message", (event) => { + const { connect, disconnect } = useWebsocket( + (event) => { const data = JSON.parse(event.data) as IConnections; - // 尽量与前一次connections的展示顺序保持一致 setConnData((old) => { const oldConn = old.connections; @@ -93,9 +88,19 @@ const ConnectionsPage = () => { return { ...data, connections }; }); - }); + }, + { errorCount: 3, retryInterval: 1000 } + ); - return () => ws?.close(); + useEffect(() => { + if (!clashInfo) return; + + const { server = "", secret = "" } = clashInfo; + connect(`ws://${server}/connections?token=${secret}`); + + return () => { + disconnect(); + }; }, [clashInfo]); const onCloseAll = useLockFn(closeAllConnections);