Sus*_*van 2 websocket reactjs react-hooks
我想与服务器建立网络套接字连接。如果连接关闭,则在 5 秒后重新连接。我正在使用 React Hooks,到目前为止实现了这一目标
import React, { useRef, useState, useEffect } from 'react';`
import './App.css';
function App() {
const wsClient = useRef(null);
const [wsState, setWsState] = useState(true)
useEffect(() => {
wsClient.current = new WebSocket(url);
console.log("Trying to open ws");
setWsState(true)
wsClient.current.onopen = () => {
console.log('ws opened');
wsClient.current.send('{"type" : "hello"}')
};
wsClient.current.onclose = (event) => {
// Parse event code and log
setTimeout(() => {setWsState(false)}, 5000)
console.log('ws closed');
}
wsClient.current.onmessage = ((event) => {
// DO YOUR JOB
})
return () => {
console.log('ws closed');
wsClient.current.close();
}
}, [wsState]);
return (
<div className="App">
<Header />
<MainBody />
</div>
);
}
Run Code Online (Sandbox Code Playgroud)
当它无法与服务器连接时,这会导致重试次数呈指数增长,如果我删除setTimeout并使用 simplesetState则它可以正常工作。我无法理解这个问题,并建议实现我的目标的最佳做法是什么。
Jos*_*hua 10
我不相信效果是最好的地方。如果它是应用程序级别的,在它自己的模块中实现它可能更简单,并在需要的地方引入它。
尽管如此,要使其工作,您应该考虑管理两个独立的生命周期:组件生命周期和 websocket 生命周期。为了让它如你所愿地工作,你必须确保一个的每个状态变化与另一个的状态变化保持一致。
首先,请记住,每次数组中的依赖项更改时,您的效果都会运行。因此,在您的示例中,每次设置wsState.
要记住的另一件事是每次wsState更改时都会调用您的清理函数,您在效果中执行了两次(将其设置为true打开和false关闭)。这意味着当你创建一个新的套接字并且连接失败时,关闭事件会触发,并且它会排队状态更改。
每次尝试连接时,它都会设置wsState为true(将重新运行效果排入队列),尝试连接但连接失败,最后设置另一个超时,将状态更新为false。但是,不是在效果再次运行之前,尝试将状态设置为 true,等等。
要解决此问题,请从效果生命周期开始。你的效果应该什么时候运行?什么时候应该清理?一些想法:
这对组件意味着什么?您不想将 WS 状态包含为依赖项。但是,您确实需要状态来触发它在超时后重新运行。
这是它的样子:
import React, { useRef, useState, useEffect } from 'react';
const URL = 'ws://localhost:8888';
export default function App() {
const clientRef = useRef(null);
const [waitingToReconnect, setWaitingToReconnect] = useState(null);
const [messages, setMessages] = useState([]);
const [isOpen, setIsOpen] = useState(false);
function addMessage(message) {
setMessages([...messages, message]);
}
useEffect(() => {
if (waitingToReconnect) {
return;
}
// Only set up the websocket once
if (!clientRef.current) {
const client = new WebSocket(URL);
clientRef.current = client;
window.client = client;
client.onerror = (e) => console.error(e);
client.onopen = () => {
setIsOpen(true);
console.log('ws opened');
client.send('ping');
};
client.onclose = () => {
if (clientRef.current) {
// Connection failed
console.log('ws closed by server');
} else {
// Cleanup initiated from app side, can return here, to not attempt a reconnect
console.log('ws closed by app component unmount');
return;
}
if (waitingToReconnect) {
return;
};
// Parse event code and log
setIsOpen(false);
console.log('ws closed');
// Setting this will trigger a re-run of the effect,
// cleaning up the current websocket, but not setting
// up a new one right away
setWaitingToReconnect(true);
// This will trigger another re-run, and because it is false,
// the socket will be set up again
setTimeout(() => setWaitingToReconnect(null), 5000);
};
client.onmessage = message => {
console.log('received message', message);
addMessage(`received '${message.data}'`);
};
return () => {
console.log('Cleanup');
// Dereference, so it will set up next time
clientRef.current = null;
client.close();
}
}
}, [waitingToReconnect]);
return (
<div>
<h1>Websocket {isOpen ? 'Connected' : 'Disconnected'}</h1>
{waitingToReconnect && <p>Reconnecting momentarily...</p>}
{messages.map(m => <p>{JSON.stringify(m, null, 2)}</p>)}
</div>
);
}
Run Code Online (Sandbox Code Playgroud)
在此示例中,跟踪连接状态,但不在 useEffect 依赖项中。waitingForReconnect是,不过。它在连接关闭时设置,一段时间后取消设置,以触发重新连接尝试。
清理也会触发关闭,因此我们需要onClose通过查看客户端是否已取消引用来区分。
正如您所看到的,这种方法相当复杂,它将 WS 生命周期与组件生命周期联系起来(如果您在应用程序级别这样做,这在技术上是可以的)。
然而,一个主要的警告是很容易遇到陈旧的闭包问题。例如,addMessage可以访问局部变量消息,但由于addMessage没有作为依赖项传入,因此每次运行效果不能调用它两次,否则它会覆盖最后一条消息。(它本身不是覆盖;它实际上只是用旧的“陈旧”值覆盖状态messages,并与新值连接。调用它十次,你只会看到最后一个值。)
因此,您可以添加addMessage依赖项,但随后您将在每次渲染时断开并重新连接 websocket。您可以摆脱addMessages, 并将该逻辑移动到效果中,但是每次更新messages数组时它都会重新运行(比每次渲染的频率低,但仍然太频繁)。
因此,回到完整的循环,我建议在应用程序生命周期之外设置您的客户端。您可以使用自定义钩子来处理传入的消息,或者直接在效果中处理它们。
这是一个例子:
import React, { useRef, useState, useEffect } from 'react';
const URL = 'ws://localhost:8888';
export default function App() {
const clientRef = useRef(null);
const [waitingToReconnect, setWaitingToReconnect] = useState(null);
const [messages, setMessages] = useState([]);
const [isOpen, setIsOpen] = useState(false);
function addMessage(message) {
setMessages([...messages, message]);
}
useEffect(() => {
if (waitingToReconnect) {
return;
}
// Only set up the websocket once
if (!clientRef.current) {
const client = new WebSocket(URL);
clientRef.current = client;
window.client = client;
client.onerror = (e) => console.error(e);
client.onopen = () => {
setIsOpen(true);
console.log('ws opened');
client.send('ping');
};
client.onclose = () => {
if (clientRef.current) {
// Connection failed
console.log('ws closed by server');
} else {
// Cleanup initiated from app side, can return here, to not attempt a reconnect
console.log('ws closed by app component unmount');
return;
}
if (waitingToReconnect) {
return;
};
// Parse event code and log
setIsOpen(false);
console.log('ws closed');
// Setting this will trigger a re-run of the effect,
// cleaning up the current websocket, but not setting
// up a new one right away
setWaitingToReconnect(true);
// This will trigger another re-run, and because it is false,
// the socket will be set up again
setTimeout(() => setWaitingToReconnect(null), 5000);
};
client.onmessage = message => {
console.log('received message', message);
addMessage(`received '${message.data}'`);
};
return () => {
console.log('Cleanup');
// Dereference, so it will set up next time
clientRef.current = null;
client.close();
}
}
}, [waitingToReconnect]);
return (
<div>
<h1>Websocket {isOpen ? 'Connected' : 'Disconnected'}</h1>
{waitingToReconnect && <p>Reconnecting momentarily...</p>}
{messages.map(m => <p>{JSON.stringify(m, null, 2)}</p>)}
</div>
);
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
2076 次 |
| 最近记录: |