跳至主要内容

如何与 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>
);
}
提示

关于 useEffect 钩子的使用,可以在 下面 找到一些说明。

然后,子组件可以使用状态和 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 包含一些可以帮助您的功能。请查看

返回示例列表