通过 4 个步骤为 React/Redux 编写自己的 WebSocket 中间件

2025-06-08

通过 4 个步骤为 React/Redux 编写自己的 WebSocket 中间件

如果您想将 WebSocket 与 Redux 结合使用,并且不喜欢依赖项,那么只要您了解一些基本原理以及各个部分如何组合在一起,编写自己的中间件并不难。在本文中,我将讲解如何编写自己的 WebSocket 中间件,并讲解前端的整个 WebSocket 流程。该项目的代码可以在这里找到。

步骤 1:定义用于建立 websocket 连接的操作

我已经定义了一个const返回对象或“动作”类型的对象WS_CONNECT.

export const wsConnect = host => ({ type: 'WS_CONNECT', host });

有些人会创建一个actions.js文件来保存所有 Action。我更喜欢将所有 Redux Action、Reducer 和 Function 放在同一个文件中,并按类别分组。目前我的项目有 3 个模块,分别是 websocket、game 和 account。

我的 websocket 模块如下所示,并且它有我的WS_CONNECT操作:



// modules/websocket.js 

export const wsConnect = host => ({ type: 'WS_CONNECT', host });
export const wsConnecting = host => ({ type: 'WS_CONNECTING', host });
export const wsConnected = host => ({ type: 'WS_CONNECTED', host });
export const wsDisconnect = host => ({ type: 'WS_DISCONNECT', host });
export const wsDisconnected = host => ({ type: 'WS_DISCONNECTED', host });



Enter fullscreen mode Exit fullscreen mode

*通常我会在这里使用类似 : 这样的 reducer case WS_CONNECT,但对于 websockets 我其实不需要它,因为我不需要将数据保存在 redux store 中。我会在附加部分展示一个案例,说明它在哪里很有用。

步骤 2:发送操作以打开新的 websocket 连接

我的项目类似于一个聊天应用程序,用户可以加入房间。一旦用户加入房间,我希望与房间建立 WebSocket 连接。这是一种方法,另一种方法是将整个项目封装在一个 WebSocket 连接中,我在本文的“附加信息”部分提供了一个示例。

在下面的示例中,我在componentDidMount用户进入房间时建立了一个新的 websocket 连接。我使用了 token 身份验证,这没问题,但我建议使用websocket 的会话身份验证,因为无法在 header 中传递 token。我正在调度wsConnect上面定义的函数,但它不会执行任何操作,因为我还没有编写中间件。



// pages/Game.js
import React from 'react';
import { connect } from 'react-redux';
import { wsConnect, wsDisconnect } from '../modules/websocket';
import { startRound, leaveGame, makeMove } from '../modules/game';
import WithAuth from '../hocs/AuthenticationWrapper';

class Game extends React.Component {
  componentDidMount() {
    const { id } = this.props;
    if (id) {
      this.connectAndJoin();
    }
  }

  connectAndJoin = () => {
    const { id, dispatch } = this.props;
    const host = `ws://127.0.0.1:8000/ws/game/${id}?token=${localStorage.getItem('token')}`;
    dispatch(wsConnect(host));
  };


  render() {
    // abridged for brevity
    return `${<span> LOADING </span>}`;
  }

}

const s2p = (state, ownProps) => ({
  id: ownProps.match && ownProps.match.params.id,
});
export default WithAuth(connect(s2p)(Game));



Enter fullscreen mode Exit fullscreen mode

步骤3:编写websocket中间件

好的,如果您已经完成了类似上述操作,那么您已经编写并调度了一个 action,就像使用普通的 redux 一样。唯一的区别是您不需要在 reducer 中调度该 action(至少在本例中我不需要)。但是,目前还没有任何动作。您需要先编写 websocket 中间件。重要的是要理解,您调度的每个 action 都会应用于您拥有的每个中间件

这是我的中间件文件,我将详细分解:



//middleware/middleware.js 

import * as actions from '../modules/websocket';
import { updateGame, } from '../modules/game';

const socketMiddleware = () => {
  let socket = null;

  const onOpen = store => (event) => {
    console.log('websocket open', event.target.url);
    store.dispatch(actions.wsConnected(event.target.url));
  };

  const onClose = store => () => {
    store.dispatch(actions.wsDisconnected());
  };

  const onMessage = store => (event) => {
    const payload = JSON.parse(event.data);
    console.log('receiving server message');

    switch (payload.type) {
      case 'update_game_players':
        store.dispatch(updateGame(payload.game, payload.current_player));
        break;
      default:
        break;
    }
  };

  // the middleware part of this function
  return store => next => action => {
    switch (action.type) {
      case 'WS_CONNECT':
        if (socket !== null) {
          socket.close();
        }

        // connect to the remote host
        socket = new WebSocket(action.host);

        // websocket handlers
        socket.onmessage = onMessage(store);
        socket.onclose = onClose(store);
        socket.onopen = onOpen(store);

        break;
      case 'WS_DISCONNECT':
        if (socket !== null) {
          socket.close();
        }
        socket = null;
        console.log('websocket closed');
        break;
      case 'NEW_MESSAGE':
        console.log('sending a message', action.msg);
        socket.send(JSON.stringify({ command: 'NEW_MESSAGE', message: action.msg }));
        break;
      default:
        console.log('the next action:', action);
        return next(action);
    }
  };
};

export default socketMiddleware();


Enter fullscreen mode Exit fullscreen mode

调度 WS_CONNECT 并创建一个新的 WebSocket()。查看上面的代码,当我调度操作时WS_CONNECT,你可以看到我还有一个action.type用于WS_CONNECT建立 WebSocket 连接的调用。WebSocket对象是 JavaScript 自带的。我使用在操作中传递的主机 URL 建立了一个新的连接。

JavaScript WebSocket API。JavaScript WebSocket API 包含三个实用属性:onmessageonclose和。onopen.上文中,我创建了分别名为onMessageonClose和 的处理程序来处理这三个属性onOpen。其中最重要的一个是 ,onmessage它是一个事件处理程序,用于处理从服务器收到消息的情况。WebSocket API 还包含closesend函数,我将其用于我的中间件中。

使用服务器。我不会在这篇文章中深入讨论服务器端,但服务器会向前端发送包含数据的纯文本对象,就像前端如何向服务器发送包含数据的纯文本对象一样。在onMessage接收服务器操作的函数中,我在服务器端定义了一个名为 的操作,update_game_players它会从服务器获取信息,然后我调度一个名为 的函数,该函数updateGame带有一个类型的操作,SET_GAME用于将这些信息保存到我的 Redux 存储中。



// modules/game.js 
export const updateGame = (json, player) => ({ type: 'SET_GAME', data: json, player });


const gameInitialState = { time: null };

export const gameReducer = (state = { ...gameInitialState }, action) => {
  switch (action.type) {
    case 'SET_GAME':
      return { ...state, game: action.data, current_player: action.player };
    default:
      return state;
  }


Enter fullscreen mode Exit fullscreen mode

你可能想知道default: return next(action)它的作用是什么。如前所述,所有操作都会被分发到所有中间件。这意味着,如果我有一个与套接字中间件无关但与普通 Redux 中间件相关的操作类型,我仍然需要在套接字中间件中处理它。函数的默认部分只是将操作传递下去。以下示例可以帮助说明:

当我在聊天中输入内容时,前端会NEW_MESSAGE向服务器发送一个名为 的操作,其中包含数据。WebSocket 服务器接收该数据,然后将一个有效负载发送回前端,其类型为update_game_players,其中包含与当前游戏相关的所有内容,包括任何新消息。当前端从服务器接收到该操作时,它会调度一个名为 的操作,updateGame其类型为SET_GAME该操作已调度,但套接字中间件没有任何 的处理程序,SET_GAME因此它会转到默认情况,同时转到SET_GAME我的默认 Redux 中间件中的情况。

步骤 4:使用新的中间件创建商店

这部分相对简单。如下例所示,你可以创建一个包含所有中间件的数组(我使用刚刚创建的中间件和 Redux 默认设置),然后使用Redux 提供的compose和函数创建存储。createStore



import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import { createStore, applyMiddleware, compose } from 'redux';
import { Provider } from 'react-redux';
import reduxThunk from 'redux-thunk';
import rootReducer from './modules/reducers';
import wsMiddleware from './middleware/middleware';
import App from './App';

const middleware = [reduxThunk, wsMiddleware];
const store = createStore(
  rootReducer,
  compose(
    applyMiddleware(...middleware),
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
  ),
);
const Root = ({ store }) => (
  <Router>
    <Provider store={store}>
      <Route path="/" component={App} />
    </Provider>
  </Router>
);

ReactDOM.render(<Root store={store} />, document.getElementById('root'));


Enter fullscreen mode Exit fullscreen mode

奖励:将整个项目包装在 websocket 连接中

以下是一个如何将整个项目封装在 WebSocket 连接中的示例。这也是另一种可用的模式。



// index.js abridged example showing just the root

const store = // where you create your store 
const Root = ({ store }) => (
  <Router>
    <Provider store={store}>
      <WebSocketConnection
        host={`ws://127.0.0.1:8000/ws/game?token=${localStorage.getItem('token')}`}
      >
        <Route path="/" component={App} />
      </WebSocketConnection>
    </Provider>
  </Router>
);

ReactDOM.render(<Root store={store} />, document.getElementById('root'));


Enter fullscreen mode Exit fullscreen mode

这是我的WebSocketConnection包装器,非常简单。它建立了与 websocket 的连接



// hocs/WebsocketConnection.js

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { wsConnect } from '../modules/websocket';

class WebSocketConnection extends Component {
  componentDidMount() {
    const { dispatch, host } = this.props;
      dispatch(wsConnect(host))
    }
  }

  render() {
    return <div>{this.props.children}</div>;
  }
}


export default connect()(WebSocketConnection);


Enter fullscreen mode Exit fullscreen mode

在这种情况下,我的 Reducer 略有不同。在上面的步骤 2 中,我在建立 WebSocket 连接的同时执行了所有与加入游戏相关的服务器操作。在本例中,我先打开一个通用的 WebSocket 连接,然后将加入游戏作为一个单独的操作。这意味着我需要确保我的 WebSocket 连接已经建立,然后再尝试执行其他操作,这就是为什么我现在想查看我是否已连接。



// modules/websocket.js 

const websocketInitialState = { connected: false };

export const websocketReducer = (state = { ...websocketInitialState }, action) => {
  switch (action.type) {
    case 'WS_CONNECTED':
      return { ...state, connected: true };
    default:
      return state;
  }
};


Enter fullscreen mode Exit fullscreen mode

现在,我可以使用connectedprop 来决定是否要调度操作。在 Game.js 文件中,我这样做



// pages/Game.js 

  componentDidMount() {
    const { id, connected } = this.props;
    if (connected) {
      this.connectAndJoin();
    }
  }

  connectAndJoin = async () => {
    const { id, dispatch } = this.props;
    await dispatch(joinGame(id));
  };


Enter fullscreen mode Exit fullscreen mode
链接链接:https://dev.to/aduranil/how-to-use-websockets-with-redux-a-step-by-step-guide-to-writing-understanding-connecting-socket-middleware-to-your-project-km3
PREV
Styled-Components 简介💅...
NEXT
JavaScript 中的分号:该用还是不该用?自动分号插入 (ASI) 3 条自动分号插入规则:什么时候不应该使用分号?结语