React-Redux 和 Websockets socket.io
React-Redux and Websockets with socket.io
我是 React-Redux 技术的新手,我希望您能帮助我实现一些实现。
我想用套接字实现一个聊天应用程序 (socket.io)。首先,用户必须注册(我在服务器端使用护照),然后,如果注册成功,用户必须连接到 webSocket。
我认为最好的办法是为所有操作使用像管道这样的中间件,并根据什么样的操作获取中间件,做不同的事情。
如果操作类型是AUTH_USER
,创建客户端-服务器连接并设置所有来自服务器的事件。
如果操作类型是 MESSAGE
向服务器发送消息。
代码片段:
-----socketMiddleware.js----
import { AUTH_USER, MESSAGE } from '../actions/types';
import * as actions from 'actions/socket-actions';
import io from 'socket.io-client';
const socket = null;
export default function ({ dispatch }) {
return next => action => {
if(action.type == AUTH_USER) {
socket = io.connect(`${location.host}`);
socket.on('message', data => {
store.dispatch(actions.addResponse(action.data));
});
}
else if(action.type == MESSAGE && socket) {
socket.emit('user-message', action.data);
return next(action)
} else {
return next(action)
}
}
}
------index.js------
import {createStore, applyMiddleware} from 'redux';
import socketMiddleware from './socketMiddleware';
const createStoreWithMiddleware = applyMiddleware(
socketMiddleware
)(createStore);
const store = createStoreWithMiddleware(reducer);
<Provider store={store}>
<App />
</Provider>
您如何看待这种做法,它是更好的实施方式吗?
剧透:我目前正在开发一个开源聊天应用程序。
您可以通过将操作与中间件分开,甚至将套接字客户端与中间件分开来做得更好。因此,结果是这样的:
- 类型 -> 每个请求的请求、成功、失败类型(非强制性)。
- Reducer -> 存储不同的状态
- 操作 -> 发送操作以连接/断开连接/发出/监听。
- Middleware -> 对待你的action,把当前的action传不传给socket client
- 客户端 -> 套接字客户端(socket.io).
下面的代码取自正在开发的真实应用程序(有时略有编辑),它们足以满足大多数情况,但某些东西如 SocketClient 可能不是 100%完成.
操作数
您希望操作尽可能简单,因为它们通常是重复的工作,您最终可能会有很多操作。
export function send(chatId, content) {
const message = { chatId, content };
return {
type: 'socket',
types: [SEND, SEND_SUCCESS, SEND_FAIL],
promise: (socket) => socket.emit('SendMessage', message),
}
}
请注意,socket 是一个参数化函数,这样我们就可以在整个应用程序中共享相同的 socket 实例,而不必担心任何导入(稍后我们将展示如何执行此操作)。
中间件(socketMiddleware.js):
我们将使用与 erikras/react-redux-universal-hot-example 类似的策略,但用于套接字而不是 AJAX。
我们的套接字中间件将只负责处理套接字请求。
中间件将动作传递给套接字客户端,并调度:
- REQUEST (action
types[0]
): 正在请求(action.type
发送到reducer)。
- SUCCESS(操作
types[1]
):请求成功(action.type
并且服务器响应 action.result
被发送到 reducer)。
- FAILURE(操作
types[2]
):请求失败(action.type
和服务器响应 action.error
被发送到 reducer)。
export default function socketMiddleware(socket) {
// Socket param is the client. We'll show how to set this up later.
return ({dispatch, getState}) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
/*
* Socket middleware usage.
* promise: (socket) => socket.emit('MESSAGE', 'hello world!')
* type: always 'socket'
* types: [REQUEST, SUCCESS, FAILURE]
*/
const { promise, type, types, ...rest } = action;
if (type !== 'socket' || !promise) {
// Move on! Not a socket request or a badly formed one.
return next(action);
}
const [REQUEST, SUCCESS, FAILURE] = types;
next({...rest, type: REQUEST});
return promise(socket)
.then((result) => {
return next({...rest, result, type: SUCCESS });
})
.catch((error) => {
return next({...rest, error, type: FAILURE });
})
};
}
SocketClient.js
唯一一个永远加载和管理 socket.io-客户端的。
[可选](参见下面代码中的 1)。 关于 socket.io 的一个非常有趣的特性是您可以拥有 message acknowledgements,这将是执行 HTTP 请求时的典型回复。我们可以使用它们来验证每个请求是否正确。请注意,为了使用此功能服务器 socket.io 命令也必须具有此最新确认参数。
import io from 'socket.io-client';
// Example conf. You can move this to your config file.
const host = 'http://localhost:3000';
const socketPath = '/api/socket.io';
export default class socketAPI {
socket;
connect() {
this.socket = io.connect(host, { path: socketPath });
return new Promise((resolve, reject) => {
this.socket.on('connect', () => resolve());
this.socket.on('connect_error', (error) => reject(error));
});
}
disconnect() {
return new Promise((resolve) => {
this.socket.disconnect(() => {
this.socket = null;
resolve();
});
});
}
emit(event, data) {
return new Promise((resolve, reject) => {
if (!this.socket) return reject('No socket connection.');
return this.socket.emit(event, data, (response) => {
// Response is the optional callback that you can use with socket.io in every request. See 1 above.
if (response.error) {
console.error(response.error);
return reject(response.error);
}
return resolve();
});
});
}
on(event, fun) {
// No promise is needed here, but we're expecting one in the middleware.
return new Promise((resolve, reject) => {
if (!this.socket) return reject('No socket connection.');
this.socket.on(event, fun);
resolve();
});
}
}
app.js
在我们的应用程序启动时,我们初始化 SocketClient
并将其传递给商店配置。
const socketClient = new SocketClient();
const store = configureStore(initialState, socketClient, apiClient);
configureStore.js
我们将 socketMiddleware
和我们新初始化的 SocketClient
添加到存储中间件(还记得我们告诉过你的那个参数吗?我们稍后会解释?)。
export default function configureStore(initialState, socketClient, apiClient) {
const loggerMiddleware = createLogger();
const middleware = [
...
socketMiddleware(socketClient),
...
];
[没什么特别的] 动作类型常量
没什么特别的 = 你通常会做的事。
const SEND = 'redux/message/SEND';
const SEND_SUCCESS = 'redux/message/SEND_SUCCESS';
const SEND_FAIL = 'redux/message/SEND_FAIL';
[没什么特别的]减速机
export default function reducer(state = {}, action = {}) {
switch(action.type) {
case SEND: {
return {
...state,
isSending: true,
};
}
default: {
return state;
}
}
}
这可能看起来需要做很多工作,但一旦设置完成,一切都是值得的。您的相关代码将更易于阅读、调试,并且您将更不容易出错。
PS: 您也可以通过 AJAX API 调用来遵循此策略。
为此我使用了 createAsyncThunk
function from @reduxjs/toolkit。它会自动生成 pending
、fulfilled
和 rejected
.
等类型
我在他的回答中使用了与@zurfyx 相同的 socketService。
动作看起来像这样:
const sendMessage = createAsyncThunk(
'game/send-message',
async function (text, { getState }) {
const roomToken = selectRoomToken(getState());
return await socketService.emit('send-message', { text, roomToken });
}
);
减速器看起来像这样:
const gameSlice = createSlice({
name: 'game',
initialState: { },
reducers: {},
extraReducers: {
[sendMessage.pending]: (state, action) => {
state.messages.push({
id: action.meta.requestId,
text: action.meta.arg,
my: true,
});
},
[sendMessage.rejected]: (state, action) => {
state.messages = state.messages.filter(
ms => ms.id !== action.meta.requestId
);
},
},
});
中间件的工作解决方案:
import { Middleware } from 'redux';
import { io, Socket } from 'socket.io-client';
import { RootState } from '../index';
import { SERVER_URL } from '../../api/consts';
import { actions } from '../features/chat/slice';
export const socketMiddleware: Middleware = (store) => {
let socket: Socket;
return (next) => (action) => {
const state = store.getState() as RootState;
const needInitSocket = state.auth.isAuth && !socket;
if (needInitSocket) {
socket = io(SERVER_URL, { transports: ['websocket', 'polling'], withCredentials: true });
socket.on('connect', () => {
store.dispatch(actions.connectionEstablished());
});
socket.on('YOUR_EVENT', (data) => {
store.dispatch(actions.doSomething(data));
})
}
// to emit data to server
if (actions.sendMessage.match(action) && socket) {
socket.emit('ON_ROOM_MESSAGE', action.payload);
}
next(action);
};
};
I worked with redux-toolkit
我是 React-Redux 技术的新手,我希望您能帮助我实现一些实现。
我想用套接字实现一个聊天应用程序 (socket.io)。首先,用户必须注册(我在服务器端使用护照),然后,如果注册成功,用户必须连接到 webSocket。
我认为最好的办法是为所有操作使用像管道这样的中间件,并根据什么样的操作获取中间件,做不同的事情。
如果操作类型是AUTH_USER
,创建客户端-服务器连接并设置所有来自服务器的事件。
如果操作类型是 MESSAGE
向服务器发送消息。
代码片段:
-----socketMiddleware.js----
import { AUTH_USER, MESSAGE } from '../actions/types';
import * as actions from 'actions/socket-actions';
import io from 'socket.io-client';
const socket = null;
export default function ({ dispatch }) {
return next => action => {
if(action.type == AUTH_USER) {
socket = io.connect(`${location.host}`);
socket.on('message', data => {
store.dispatch(actions.addResponse(action.data));
});
}
else if(action.type == MESSAGE && socket) {
socket.emit('user-message', action.data);
return next(action)
} else {
return next(action)
}
}
}
------index.js------
import {createStore, applyMiddleware} from 'redux';
import socketMiddleware from './socketMiddleware';
const createStoreWithMiddleware = applyMiddleware(
socketMiddleware
)(createStore);
const store = createStoreWithMiddleware(reducer);
<Provider store={store}>
<App />
</Provider>
您如何看待这种做法,它是更好的实施方式吗?
剧透:我目前正在开发一个开源聊天应用程序。
您可以通过将操作与中间件分开,甚至将套接字客户端与中间件分开来做得更好。因此,结果是这样的:
- 类型 -> 每个请求的请求、成功、失败类型(非强制性)。
- Reducer -> 存储不同的状态
- 操作 -> 发送操作以连接/断开连接/发出/监听。
- Middleware -> 对待你的action,把当前的action传不传给socket client
- 客户端 -> 套接字客户端(socket.io).
下面的代码取自正在开发的真实应用程序(有时略有编辑),它们足以满足大多数情况,但某些东西如 SocketClient 可能不是 100%完成.
操作数
您希望操作尽可能简单,因为它们通常是重复的工作,您最终可能会有很多操作。
export function send(chatId, content) {
const message = { chatId, content };
return {
type: 'socket',
types: [SEND, SEND_SUCCESS, SEND_FAIL],
promise: (socket) => socket.emit('SendMessage', message),
}
}
请注意,socket 是一个参数化函数,这样我们就可以在整个应用程序中共享相同的 socket 实例,而不必担心任何导入(稍后我们将展示如何执行此操作)。
中间件(socketMiddleware.js):
我们将使用与 erikras/react-redux-universal-hot-example 类似的策略,但用于套接字而不是 AJAX。
我们的套接字中间件将只负责处理套接字请求。
中间件将动作传递给套接字客户端,并调度:
- REQUEST (action
types[0]
): 正在请求(action.type
发送到reducer)。 - SUCCESS(操作
types[1]
):请求成功(action.type
并且服务器响应action.result
被发送到 reducer)。 - FAILURE(操作
types[2]
):请求失败(action.type
和服务器响应action.error
被发送到 reducer)。
export default function socketMiddleware(socket) {
// Socket param is the client. We'll show how to set this up later.
return ({dispatch, getState}) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
/*
* Socket middleware usage.
* promise: (socket) => socket.emit('MESSAGE', 'hello world!')
* type: always 'socket'
* types: [REQUEST, SUCCESS, FAILURE]
*/
const { promise, type, types, ...rest } = action;
if (type !== 'socket' || !promise) {
// Move on! Not a socket request or a badly formed one.
return next(action);
}
const [REQUEST, SUCCESS, FAILURE] = types;
next({...rest, type: REQUEST});
return promise(socket)
.then((result) => {
return next({...rest, result, type: SUCCESS });
})
.catch((error) => {
return next({...rest, error, type: FAILURE });
})
};
}
SocketClient.js
唯一一个永远加载和管理 socket.io-客户端的。
[可选](参见下面代码中的 1)。 关于 socket.io 的一个非常有趣的特性是您可以拥有 message acknowledgements,这将是执行 HTTP 请求时的典型回复。我们可以使用它们来验证每个请求是否正确。请注意,为了使用此功能服务器 socket.io 命令也必须具有此最新确认参数。
import io from 'socket.io-client';
// Example conf. You can move this to your config file.
const host = 'http://localhost:3000';
const socketPath = '/api/socket.io';
export default class socketAPI {
socket;
connect() {
this.socket = io.connect(host, { path: socketPath });
return new Promise((resolve, reject) => {
this.socket.on('connect', () => resolve());
this.socket.on('connect_error', (error) => reject(error));
});
}
disconnect() {
return new Promise((resolve) => {
this.socket.disconnect(() => {
this.socket = null;
resolve();
});
});
}
emit(event, data) {
return new Promise((resolve, reject) => {
if (!this.socket) return reject('No socket connection.');
return this.socket.emit(event, data, (response) => {
// Response is the optional callback that you can use with socket.io in every request. See 1 above.
if (response.error) {
console.error(response.error);
return reject(response.error);
}
return resolve();
});
});
}
on(event, fun) {
// No promise is needed here, but we're expecting one in the middleware.
return new Promise((resolve, reject) => {
if (!this.socket) return reject('No socket connection.');
this.socket.on(event, fun);
resolve();
});
}
}
app.js
在我们的应用程序启动时,我们初始化 SocketClient
并将其传递给商店配置。
const socketClient = new SocketClient();
const store = configureStore(initialState, socketClient, apiClient);
configureStore.js
我们将 socketMiddleware
和我们新初始化的 SocketClient
添加到存储中间件(还记得我们告诉过你的那个参数吗?我们稍后会解释?)。
export default function configureStore(initialState, socketClient, apiClient) {
const loggerMiddleware = createLogger();
const middleware = [
...
socketMiddleware(socketClient),
...
];
[没什么特别的] 动作类型常量
没什么特别的 = 你通常会做的事。
const SEND = 'redux/message/SEND';
const SEND_SUCCESS = 'redux/message/SEND_SUCCESS';
const SEND_FAIL = 'redux/message/SEND_FAIL';
[没什么特别的]减速机
export default function reducer(state = {}, action = {}) {
switch(action.type) {
case SEND: {
return {
...state,
isSending: true,
};
}
default: {
return state;
}
}
}
这可能看起来需要做很多工作,但一旦设置完成,一切都是值得的。您的相关代码将更易于阅读、调试,并且您将更不容易出错。
PS: 您也可以通过 AJAX API 调用来遵循此策略。
为此我使用了 createAsyncThunk
function from @reduxjs/toolkit。它会自动生成 pending
、fulfilled
和 rejected
.
我在他的回答中使用了与@zurfyx 相同的 socketService。
动作看起来像这样:
const sendMessage = createAsyncThunk(
'game/send-message',
async function (text, { getState }) {
const roomToken = selectRoomToken(getState());
return await socketService.emit('send-message', { text, roomToken });
}
);
减速器看起来像这样:
const gameSlice = createSlice({
name: 'game',
initialState: { },
reducers: {},
extraReducers: {
[sendMessage.pending]: (state, action) => {
state.messages.push({
id: action.meta.requestId,
text: action.meta.arg,
my: true,
});
},
[sendMessage.rejected]: (state, action) => {
state.messages = state.messages.filter(
ms => ms.id !== action.meta.requestId
);
},
},
});
中间件的工作解决方案:
import { Middleware } from 'redux';
import { io, Socket } from 'socket.io-client';
import { RootState } from '../index';
import { SERVER_URL } from '../../api/consts';
import { actions } from '../features/chat/slice';
export const socketMiddleware: Middleware = (store) => {
let socket: Socket;
return (next) => (action) => {
const state = store.getState() as RootState;
const needInitSocket = state.auth.isAuth && !socket;
if (needInitSocket) {
socket = io(SERVER_URL, { transports: ['websocket', 'polling'], withCredentials: true });
socket.on('connect', () => {
store.dispatch(actions.connectionEstablished());
});
socket.on('YOUR_EVENT', (data) => {
store.dispatch(actions.doSomething(data));
})
}
// to emit data to server
if (actions.sendMessage.match(action) && socket) {
socket.emit('ON_ROOM_MESSAGE', action.payload);
}
next(action);
};
};
I worked with redux-toolkit