如何与 React 一起使用
本指南介绍如何在 React 应用程序中使用 Socket.IO。
示例
结构
src
├── App.js
├── components
│ ├── ConnectionManager.js
│ ├── ConnectionState.js
│ ├── Events.js
│ └── MyForm.js
└── socket.js
Socket.IO 客户端在 src/socket.js
文件中初始化
src/socket.js
import { io } from 'socket.io-client';
// "undefined" means the URL will be computed from the `window.location` object
const URL = process.env.NODE_ENV === 'production' ? undefined : 'http://localhost:4000';
export const socket = io(URL);
默认情况下,Socket.IO 客户端会立即打开与服务器的连接。您可以使用 autoConnect
选项来阻止此行为
export const socket = io(URL, {
autoConnect: false
});
在这种情况下,您需要调用 socket.connect()
使 Socket.IO 客户端连接。例如,当用户必须在连接之前提供某种凭据时,这很有用。
在开发过程中,您需要在服务器上启用 CORS
const io = new Server({
cors: {
origin: "http://localhost:3000"
}
});
io.listen(4000);
参考:处理 CORS
然后,事件监听器在 App
组件中注册,该组件存储状态并通过 props 将其传递给子组件。
另请参阅:https://reactjs.ac.cn/learn/sharing-state-between-components
src/App.js
import React, { useState, useEffect } from 'react';
import { socket } from './socket';
import { ConnectionState } from './components/ConnectionState';
import { ConnectionManager } from './components/ConnectionManager';
import { Events } from "./components/Events";
import { MyForm } from './components/MyForm';
export default function App() {
const [isConnected, setIsConnected] = useState(socket.connected);
const [fooEvents, setFooEvents] = useState([]);
useEffect(() => {
function onConnect() {
setIsConnected(true);
}
function onDisconnect() {
setIsConnected(false);
}
function onFooEvent(value) {
setFooEvents(previous => [...previous, value]);
}
socket.on('connect', onConnect);
socket.on('disconnect', onDisconnect);
socket.on('foo', onFooEvent);
return () => {
socket.off('connect', onConnect);
socket.off('disconnect', onDisconnect);
socket.off('foo', onFooEvent);
};
}, []);
return (
<div className="App">
<ConnectionState isConnected={ isConnected } />
<Events events={ fooEvents } />
<ConnectionManager />
<MyForm />
</div>
);
}
然后,子组件可以使用状态和 socket
对象,如下所示
src/components/ConnectionState.js
import React from 'react';
export function ConnectionState({ isConnected }) {
return <p>State: { '' + isConnected }</p>;
}
src/components/Events.js
import React from 'react';
export function Events({ events }) {
return (
<ul>
{
events.map((event, index) =>
<li key={ index }>{ event }</li>
)
}
</ul>
);
}
src/components/ConnectionManager.js
import React from 'react';
import { socket } from '../socket';
export function ConnectionManager() {
function connect() {
socket.connect();
}
function disconnect() {
socket.disconnect();
}
return (
<>
<button onClick={ connect }>Connect</button>
<button onClick={ disconnect }>Disconnect</button>
</>
);
}
src/components/MyForm.js
import React, { useState } from 'react';
import { socket } from '../socket';
export function MyForm() {
const [value, setValue] = useState('');
const [isLoading, setIsLoading] = useState(false);
function onSubmit(event) {
event.preventDefault();
setIsLoading(true);
socket.timeout(5000).emit('create-something', value, () => {
setIsLoading(false);
});
}
return (
<form onSubmit={ onSubmit }>
<input onChange={ e => setValue(e.target.value) } />
<button type="submit" disabled={ isLoading }>Submit</button>
</form>
);
}
关于 useEffect
钩子的说明
清理
为了防止重复注册事件,必须在清理回调中删除在设置函数中注册的任何事件监听器。
useEffect(() => {
function onFooEvent(value) {
// ...
}
socket.on('foo', onFooEvent);
return () => {
// BAD: missing event registration cleanup
};
}, []);
此外,事件监听器是命名函数,因此调用 socket.off()
仅删除此特定监听器
useEffect(() => {
socket.on('foo', (value) => {
// ...
});
return () => {
// BAD: this will remove all listeners for the 'foo' event, which may
// include the ones registered in another component
socket.off('foo');
};
}, []);
依赖项
onFooEvent
函数也可以这样写
useEffect(() => {
function onFooEvent(value) {
setFooEvents(fooEvents.concat(value));
}
socket.on('foo', onFooEvent);
return () => {
socket.off('foo', onFooEvent);
};
}, [fooEvents]);
这也行得通,但请注意,在这种情况下,onFooEvent
监听器将在每次渲染时注销,然后重新注册。
断开连接
如果需要在组件卸载时关闭 Socket.IO 客户端(例如,如果连接仅在应用程序的特定部分需要),则应
- 确保在设置阶段调用
socket.connect()
useEffect(() => {
// no-op if the socket is already connected
socket.connect();
return () => {
socket.disconnect();
};
}, []);
在 严格模式 中,每个 Effect 都会运行两次,以便在开发过程中捕获错误,因此您将看到
- 设置:
socket.connect()
- 清理:
socket.disconnect()
- 设置:
socket.connect()
- 此 Effect 没有依赖项,以防止在每次渲染时重新连接
useEffect(() => {
socket.connect();
function onFooEvent(value) {
setFooEvents(fooEvents.concat(value));
}
socket.on('foo', onFooEvent);
return () => {
socket.off('foo', onFooEvent);
// BAD: the Socket.IO client will reconnect every time the fooEvents array
// is updated
socket.disconnect();
};
}, [fooEvents]);
您可以使用两个 Effect 代替
import React, { useState, useEffect } from 'react';
import { socket } from './socket';
function App() {
const [fooEvents, setFooEvents] = useState([]);
useEffect(() => {
// no-op if the socket is already connected
socket.connect();
return () => {
socket.disconnect();
};
}, []);
useEffect(() => {
function onFooEvent(value) {
setFooEvents(fooEvents.concat(value));
}
socket.on('foo', onFooEvent);
return () => {
socket.off('foo', onFooEvent);
};
}, [fooEvents]);
// ...
}
重要说明
这些说明适用于任何前端框架。
热模块替换
包含 Socket.IO 客户端初始化的文件(即上面示例中的 src/socket.js
文件)的热重载可能会使之前的 Socket.IO 连接保持活动状态,这意味着
- 您的 Socket.IO 服务器上可能有多个连接
- 您可能会收到来自先前连接的事件
唯一已知的解决方法是在更新此特定文件时执行 **完整页面重新加载**(或完全禁用热重载,但这可能有点极端)。
参考:https://webpack.js.cn/concepts/hot-module-replacement/
子组件中的监听器
我们强烈建议不要在子组件中注册事件监听器,因为它将 UI 的状态与事件接收时间绑定在一起:如果组件未挂载,则可能会错过一些消息。
src/components/MyComponent.js
import React from 'react';
export default function MyComponent() {
const [fooEvents, setFooEvents] = useState([]);
useEffect(() => {
function onFooEvent(value) {
setFooEvents(previous => [...previous, value]);
}
// BAD: this ties the state of the UI with the time of reception of the
// 'foo' events
socket.on('foo', onFooEvent);
return () => {
socket.off('foo', onFooEvent);
};
}, []);
// ...
}
临时断开连接
虽然 WebSocket 连接非常强大,但它们并不总是处于运行状态
- 用户和 Socket.IO 服务器之间的任何内容都可能遇到临时故障或重新启动
- 服务器本身可能会作为自动扩展策略的一部分被终止
- 用户可能会断开连接或在移动浏览器的情况下从 Wi-Fi 切换到 4G
这意味着您需要正确处理临时断开连接,以便为您的用户提供良好的体验。
好消息是 Socket.IO 包含一些可以帮助您的功能。请查看