服务器的 socket.io 侦听器无缘无故触发两次
Server's socket.io listener fires twice for no apparent reason
我正在使用 express.js、react.js 和 socket.io 构建具有聊天功能的应用程序。 Here is a minimal version with the bug reproduced on Github.
问题是服务器的 socket.on()
总是触发两次,原因我看不出来。
据我所知,客户端只发送一个 .emit
,但服务器的 .on
总是触发两次(而且我很确定事件处理程序没有绑定两次),无论连接了多少个客户端,或者实际上我是如何重构代码的。
发送客户端的表单(输入+按钮)将消息附加到其本地消息列表,然后分派MESSAGE_NEW
到reducer。
首先是 Socket.js
文件。
import React from 'react'
import socketio from 'socket.io-client'
export const socket = socketio.connect('ws://localhost:3001')
export const SocketContext = React.createContext()
这是来自 Chat.js
的提交处理程序代码(这似乎工作正常,只发送一次):
const handleSubmit = e => {
e.preventDefault()
if (message === '') return
setMessages(messages => [...messages, message])
dispatch({ type: 'MESSAGE_NEW', payload: message })
setMessage('')
}
这里是 /Reducer.js
,.emit
是信号(似乎也能正常工作,只触发一次)。
import { useContext } from 'react'
import { SocketContext } from './Socket'
export default function Reducer(state, action) {
const socket = useContext(SocketContext)
switch (action.type) {
case 'MESSAGE_NEW':
// this only fires once, as it should
console.log('emitting socket')
socket.emit('message:new', action.payload)
break
default:
return state
}
}
但这里是 /server/index.js
的相关部分,这就是问题所在。无论我尝试过什么,.on
总是触发两次。
io.on('connection', socket => {
console.log('New client connected:', socket.id)
socket.on('message:new', message => {
// this always fires twice, i do not understand why
console.log('message:new: ', message, socket.id)
socket.broadcast.emit('message:new', message)
})
})
更新: 我发现将 socket.emit
从 Reducer.js
移动到 Chat.js
中的 handleSubmit
使其正常工作美好的。我不明白为什么 Reducer 不会触发两次。
在 https://github.com/Heilemann/sockettest/blob/c1df22349ac1877ed625a01b14f48730b7af122d/client/src/Chat.jsx#L14-L22 中,您添加了一个事件侦听器,稍后再次尝试将其删除。但是既然你写出了处理函数
message => {
setMessages([...messages, message])
}
每次删除都没有效果,因为要删除的处理程序与您添加的处理程序不同。您必须将处理函数分配给一个变量并在 .on
和 .off
.
中使用该变量
即使这不能解决问题,仍然值得做。
您的组件正在渲染两次,调用套接字两次,给出两条连接消息。
像这样使用您的 useEffect 确保它渲染一次
在您的 chat.js 中导入 socket.io
import socketio from 'socket.io-client'
let socket
useEffect(() => {
socket = socketio.connect('ws://localhost:3001')
return () => {
socket.off('message:new', listener)
}
},[])
在您的提交处理程序中
const handleSubmit = e => {
e.preventDefault()
if (message === '') return
setMessages(messages => [...messages, message])
socket.emit('message:new', message)
//dispatch({ type: 'MESSAGE_NEW', payload: message })
setMessage('')
}
传入一个空数组作为第二个参数意味着 useEffect 只会被调用一次。底线是确保 socket.io 客户端被调用一次。 props 和 state 的变化会导致组件重新渲染,进而导致套接字被多次连接。您可以探索更清洁的其他模式。但是,这就是您需要做的。
根据React documentation's on strict mode:
Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:
- Class component
constructor
, render
, and shouldComponentUpdate
methods
- Class component static
getDerivedStateFromProps
method
- Function component bodies
- State updater functions (the first argument to
setState
)
- Functions passed to
useState
, useMemo
, or useReducer
注意最后一行:传递给useReducer
的函数将被调用两次;这就是为什么您的消息被发送了两次。您可以像这样更新减速器来确认这一点:
case "MESSAGE_NEW":
alert("I'm running");
由于 React.StrictMode
.
,您会注意到此警报出现了两次
如果删除 src/App.js
中的 <React.StrictMode>
包装器,test
消息将不再重复;但是,它并没有解决这里的根本问题。
潜在的问题是您正在让您的减速器(应该是纯的)执行不纯的副作用(在本例中,通过 websocket 发送消息)。此外,reducer 函数还有一个 useContext
调用——从 React 的角度来看,这也是一个空操作,不是好的做法。
正如您在问题中遇到的那样,只需将 socket.emit
移到 handleSubmit
中,您就可以开始了。或者,如果你想走 reducer 路线,你可以考虑更复杂的设置;一些流行的配置包括 Redux + Redux Thunk,或 Redux + Redux Saga。您的里程可能会有所不同。
我调试了你的代码,发现减速器触发两次的问题。
解决方法是删除 <React.StrictMode>
index.js
.
中您的 App 标签周围的标签
有关 StrictMode
的更多信息:https://reactjs.org/docs/strict-mode.html
我正在使用 express.js、react.js 和 socket.io 构建具有聊天功能的应用程序。 Here is a minimal version with the bug reproduced on Github.
问题是服务器的 socket.on()
总是触发两次,原因我看不出来。
据我所知,客户端只发送一个 .emit
,但服务器的 .on
总是触发两次(而且我很确定事件处理程序没有绑定两次),无论连接了多少个客户端,或者实际上我是如何重构代码的。
发送客户端的表单(输入+按钮)将消息附加到其本地消息列表,然后分派MESSAGE_NEW
到reducer。
首先是 Socket.js
文件。
import React from 'react'
import socketio from 'socket.io-client'
export const socket = socketio.connect('ws://localhost:3001')
export const SocketContext = React.createContext()
这是来自 Chat.js
的提交处理程序代码(这似乎工作正常,只发送一次):
const handleSubmit = e => {
e.preventDefault()
if (message === '') return
setMessages(messages => [...messages, message])
dispatch({ type: 'MESSAGE_NEW', payload: message })
setMessage('')
}
这里是 /Reducer.js
,.emit
是信号(似乎也能正常工作,只触发一次)。
import { useContext } from 'react'
import { SocketContext } from './Socket'
export default function Reducer(state, action) {
const socket = useContext(SocketContext)
switch (action.type) {
case 'MESSAGE_NEW':
// this only fires once, as it should
console.log('emitting socket')
socket.emit('message:new', action.payload)
break
default:
return state
}
}
但这里是 /server/index.js
的相关部分,这就是问题所在。无论我尝试过什么,.on
总是触发两次。
io.on('connection', socket => {
console.log('New client connected:', socket.id)
socket.on('message:new', message => {
// this always fires twice, i do not understand why
console.log('message:new: ', message, socket.id)
socket.broadcast.emit('message:new', message)
})
})
更新: 我发现将 socket.emit
从 Reducer.js
移动到 Chat.js
中的 handleSubmit
使其正常工作美好的。我不明白为什么 Reducer 不会触发两次。
在 https://github.com/Heilemann/sockettest/blob/c1df22349ac1877ed625a01b14f48730b7af122d/client/src/Chat.jsx#L14-L22 中,您添加了一个事件侦听器,稍后再次尝试将其删除。但是既然你写出了处理函数
message => {
setMessages([...messages, message])
}
每次删除都没有效果,因为要删除的处理程序与您添加的处理程序不同。您必须将处理函数分配给一个变量并在 .on
和 .off
.
即使这不能解决问题,仍然值得做。
您的组件正在渲染两次,调用套接字两次,给出两条连接消息。
像这样使用您的 useEffect 确保它渲染一次
在您的 chat.js 中导入 socket.io
import socketio from 'socket.io-client'
let socket
useEffect(() => {
socket = socketio.connect('ws://localhost:3001')
return () => {
socket.off('message:new', listener)
}
},[])
在您的提交处理程序中
const handleSubmit = e => {
e.preventDefault()
if (message === '') return
setMessages(messages => [...messages, message])
socket.emit('message:new', message)
//dispatch({ type: 'MESSAGE_NEW', payload: message })
setMessage('')
}
传入一个空数组作为第二个参数意味着 useEffect 只会被调用一次。底线是确保 socket.io 客户端被调用一次。 props 和 state 的变化会导致组件重新渲染,进而导致套接字被多次连接。您可以探索更清洁的其他模式。但是,这就是您需要做的。
根据React documentation's on strict mode:
Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:
- Class component
constructor
,render
, andshouldComponentUpdate
methods- Class component static
getDerivedStateFromProps
method- Function component bodies
- State updater functions (the first argument to
setState
)- Functions passed to
useState
,useMemo
, oruseReducer
注意最后一行:传递给useReducer
的函数将被调用两次;这就是为什么您的消息被发送了两次。您可以像这样更新减速器来确认这一点:
case "MESSAGE_NEW":
alert("I'm running");
由于 React.StrictMode
.
如果删除 src/App.js
中的 <React.StrictMode>
包装器,test
消息将不再重复;但是,它并没有解决这里的根本问题。
潜在的问题是您正在让您的减速器(应该是纯的)执行不纯的副作用(在本例中,通过 websocket 发送消息)。此外,reducer 函数还有一个 useContext
调用——从 React 的角度来看,这也是一个空操作,不是好的做法。
正如您在问题中遇到的那样,只需将 socket.emit
移到 handleSubmit
中,您就可以开始了。或者,如果你想走 reducer 路线,你可以考虑更复杂的设置;一些流行的配置包括 Redux + Redux Thunk,或 Redux + Redux Saga。您的里程可能会有所不同。
我调试了你的代码,发现减速器触发两次的问题。
解决方法是删除 <React.StrictMode>
index.js
.
有关 StrictMode
的更多信息:https://reactjs.org/docs/strict-mode.html