带有 socket.io 的 React-Redux 和 Websockets

IT技术 reactjs socket.io redux middleware
2021-03-29 11:07:03

我是 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>

您如何看待这种做法,它是否是更好的实施方式?

2个回答

剧透:我目前正在开发一个开源聊天应用程序。

您可以通过将操作与中间件分开,甚至将套接字客户端与中间件分开来更好地做到这一点。因此,导致这样的事情:

  • Types -> REQUEST, SUCCESS, FAILURE 类型为每个请求(非强制性)。
  • Reducer -> 存储不同的状态
  • 操作-> 发送操作以连接/断开连接/发出/监听。
  • 中间件-> 处理您的操作,并将当前操作传递或不传递给套接字客户端
  • 客户端-> 套接字客户端 (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(动作types[0]):正在请求(action.type发送到减速器)。
  • 成功(动作types[1]):请求成功(action.type以及action.result发送到减速器的服务器响应)。
  • FAILURE (action 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 });
      })
  };
}

套接字客户端.js

唯一一个可以加载和管理 socket.io-client 的。

[可选](参见代码中的下面 1)。socket.io 的一个非常有趣的特性是你可以有消息确认,这将是执行 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();
    });
  }
}

应用程序.js

在我们的应用程序启动时,我们初始化SocketClient并将其传递给商店配置。

const socketClient = new SocketClient();
const store = configureStore(initialState, socketClient, apiClient);

配置Store.js

我们将socketMiddleware新初始化的SocketClient加入到 store 中间件中(还​​记得我们告诉你的参数,我们稍后会解释吗?)。

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 调用来遵循此策略。

设法弄清楚!谢谢!
2021-05-28 11:07:03
@ViktorSarström 那on确实是客户您可以使用(client) => client.on('event', fun)
2021-06-09 11:07:03
奇迹般有效!我一直在试图弄清楚如何真正将消息返回给客户端。我将假设您应该为此使用 on(event, fun) 函数?
2021-06-12 11:07:03
你如何在 Redux 中设置 socketio .on() 事件侦听器?
2021-06-16 11:07:03
@Pibo 这真的取决于您的服务器端实现。不要选择会话存储< - >,因为饼干的安全性(通常不值得)。通过使用 socket.io,您可以在连接时或稍后发送信息通过使用 JavaScript,您可以访问本地存储并通过 socket.io 发送此数据(服务器应稍后处理它以授予您(或不授予)访问权限)。这种变化将适合,SocketClient.js因为它是不断的行动。
2021-06-18 11:07:03

为此,我使用createAsyncThunk@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
      );
    },
  },
});