通过 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 });
*通常我会在这里使用类似 : 这样的 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));
步骤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();
调度 WS_CONNECT 并创建一个新的 WebSocket()。查看上面的代码,当我调度操作时WS_CONNECT
,你可以看到我还有一个action.type
用于WS_CONNECT
建立 WebSocket 连接的调用。WebSocket对象是 JavaScript 自带的。我使用在操作中传递的主机 URL 建立了一个新的连接。
JavaScript WebSocket API。JavaScript WebSocket API 包含三个实用属性:onmessage
、onclose
和。onopen.
上文中,我创建了分别名为onMessage
、onClose
和 的处理程序来处理这三个属性onOpen
。其中最重要的一个是 ,onmessage
它是一个事件处理程序,用于处理从服务器收到消息的情况。WebSocket API 还包含close
和send
函数,我将其用于我的中间件中。
使用服务器。我不会在这篇文章中深入讨论服务器端,但服务器会向前端发送包含数据的纯文本对象,就像前端如何向服务器发送包含数据的纯文本对象一样。在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;
}
你可能想知道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'));
奖励:将整个项目包装在 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'));
这是我的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);
在这种情况下,我的 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;
}
};
现在,我可以使用connected
prop 来决定是否要调度操作。在 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));
};