带有socket.io的React-Redux和Websockets

时间:2016-06-17 08:40:23

标签: reactjs socket.io redux middleware

我是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 个答案:

答案 0 :(得分:32)

Spoiler:我目前正在开发什么样的开源聊天应用程序。

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

  • 类型 - &gt;每个请求的REQUEST,SUCCESS,FAILURE类型(非强制性)。
  • 减速机 - &gt;存储不同的状态
  • 操作 - &gt;发送动作以连接/断开/发出/听。
  • 中间件 - &gt;处理您的操作,并将当前操作传递给套接字客户端
  • 客户 - &gt;套接字客户端(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是一个参数化函数,这样我们可以在整个应用程序中共享相同的套接字实例,我们不必担心任何导入(我们将在以后说明如何执行此操作) )。

中间件(socketMiddleware.js):

我们将使用与erikras/react-redux-universal-hot-example类似的策略,但对于socket而不是AJAX。

我们的套接字中间件将负责仅处理套接字请求。

中间件将操作传递到套接字客户端,并调度:

  • 请求(操作types[0]):正在请求(action.type被发送到reducer)。
  • 成功(操作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-client的人。

[可选](请参阅下面的代码中的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调用来遵循此策略。

答案 1 :(得分:2)

为此,我使用了 createAsyncThunk 中的 @reduxjs/toolkit 函数。它会自动生成 pendingfulfilledrejected 等类型。

我在他的回答中使用了与 @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
      );
    },
  },
});