如何与 React 一起使用
¥How to use with React
本指南展示了如何在 React 应用中使用 Socket.IO。
¥This guide shows how to use Socket.IO within a React application.
示例
¥Example
结构:
¥Structure:
src
├── App.js
├── components
│ ├── ConnectionManager.js
│ ├── ConnectionState.js
│ ├── Events.js
│ └── MyForm.js
└── socket.js
Socket.IO 客户端在 src/socket.js
文件中初始化:
¥The Socket.IO client is initialized in the src/socket.js
file:
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
选项来防止此行为:
¥By default, the Socket.IO client opens a connection to the server right away. You can prevent this behavior with the autoConnect
option:
export const socket = io(URL, {
autoConnect: false
});
在这种情况下,你将需要调用 socket.connect()
来使 Socket.IO 客户端连接。例如,当用户在连接之前必须提供某种凭据时,这可能很有用。
¥In that case, you will need to call socket.connect()
to make the Socket.IO client connect. This can be useful for example when the user must provide some kind of credentials before connecting.
开发过程中,你需要在服务器上启用 CORS:
¥During development, you need to enable CORS on your server:
const io = new Server({
cors: {
origin: "http://localhost:3000"
}
});
io.listen(4000);
参考:处理 CORS
¥Reference: Handling CORS
然后,事件监听器在 App
组件中注册,该组件存储状态并通过 props 将其传递给其子组件。
¥The events listeners are then registered in the App
component, which stores the state and pass it down to its child components via props.
也可以看看:https://react.nodejs.cn/learn/sharing-state-between-components
¥See also: https://react.nodejs.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
对象,如下所示:
¥The child components can then use the state and the socket
object like this:
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
钩子的备注
¥Remarks about the useEffect
hook
清理
¥Cleanup
必须在清理回调中删除在设置函数中注册的任何事件监听器,以防止重复的事件注册。
¥Any event listeners registered in the setup function must be removed in the cleanup callback in order to prevent duplicate event registrations.
useEffect(() => {
function onFooEvent(value) {
// ...
}
socket.on('foo', onFooEvent);
return () => {
// BAD: missing event registration cleanup
};
}, []);
此外,事件监听器是命名函数,因此调用 socket.off()
仅删除此特定监听器:
¥Also, the event listeners are named functions, so calling socket.off()
only removes this specific listener:
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');
};
}, []);
依赖
¥Dependencies
onFooEvent
函数也可以这样写:
¥The onFooEvent
function could also have been written like this:
useEffect(() => {
function onFooEvent(value) {
setFooEvents(fooEvents.concat(value));
}
socket.on('foo', onFooEvent);
return () => {
socket.off('foo', onFooEvent);
};
}, [fooEvents]);
这也有效,但请注意,在这种情况下,onFooEvent
监听器将被取消注册,然后在每次渲染时再次注册。
¥This works too, but please note that in that case, the onFooEvent
listener will be unregistered then registered again on each render.
断开
¥Disconnection
如果你需要在卸载组件时关闭 Socket.IO 客户端(例如,如果仅在应用的特定部分需要连接),你应该:
¥If you need to close the Socket.IO client when your component is unmounted (for example, if the connection is only needed in a specific part of your application), you should:
确保在设置阶段调用
socket.connect()
:¥ensure
socket.connect()
is called in the setup phase:
useEffect(() => {
// no-op if the socket is already connected
socket.connect();
return () => {
socket.disconnect();
};
}, []);
在 严格模式 中,每个 Effect 都会运行两次,以便在开发过程中捕获错误,因此你将看到:
¥In Strict Mode, every Effect is run twice in order to catch bugs during development, so you will see:
设置:
socket.connect()
¥setup:
socket.connect()
清理:
socket.disconnect()
¥cleanup:
socket.disconnect()
设置:
socket.connect()
¥setup:
socket.connect()
对此 Effect 没有依赖,以防止每次渲染时重新连接:
¥have no dependency for this Effect in order to prevent a reconnection on each render:
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]);
你可以有两个效果:
¥You could have two Effects instead:
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]);
// ...
}
重要注意
¥Important notes
这些言论对于任何前端框架都有效。
¥These remarks are valid for any front-end framework.
热模块重载
¥Hot module reloading
包含 Socket.IO 客户端初始化的文件(即上例中的 src/socket.js
文件)的热重载可能会使先前的 Socket.IO 连接保持活动状态,这意味着:
¥The hot reloading of a file that contains the initialization of a Socket.IO client (i.e. the src/socket.js
file in the example above) might leave the previous Socket.IO connection alive, which means that:
你的 Socket.IO 服务器上可能有多个连接
¥you might have multiple connections on your Socket.IO server
你可能会收到来自先前连接的事件
¥you might receive events from the previous connection
唯一已知的解决方法是在更新此特定文件时进行全页重新加载(或完全禁用热重新加载,但这可能有点极端)。
¥The only known workaround is to do a full-page reload when this specific file is updated (or disable hot reloading altogether, but that might be a bit extreme).
参考:https://webpack.js.org/concepts/hot-module-replacement/
¥Reference: https://webpack.js.org/concepts/hot-module-replacement/
子组件中的监听器
¥Listeners in a child component
我们强烈建议不要在子组件中注册事件监听器,因为它将 UI 的状态与事件接收时间联系起来:如果未安装该组件,则可能会丢失一些消息。
¥We strongly advise against registering event listeners in your child components, because it ties the state of the UI with the time of reception of the events: if the component is not mounted, then some messages might be missed.
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);
};
}, []);
// ...
}
暂时断线
¥Temporary disconnections
WebSocket 连接虽然非常强大,但并不总是启动并运行:
¥While very powerful, WebSocket connections are not always up and running:
用户和 Socket.IO 服务器之间的任何事情都可能会遇到临时故障或重新启动
¥anything between the user and the Socket.IO server may encounter a temporary failure or be restarted
作为自动缩放策略的一部分,服务器本身可能会被终止
¥the server itself may be killed as part of an autoscaling policy
如果使用移动浏览器,用户可能会失去连接或从 Wi-Fi 切换到 4G
¥the user may lose connection or switch from Wi-Fi to 4G, in case of a mobile browser
这意味着你需要正确处理临时断开连接,以便为用户提供良好的体验。
¥Which means you will need to properly handle the temporary disconnections, in order to provide a great experience to your users.
好消息是 Socket.IO 包含一些可以帮助你的功能。请检查:
¥The good news is that Socket.IO includes some features that can help you. Please check: