在 React/Redux 应用中使用 WebSockets
介绍
WebSocket 是一种在服务器和客户端之间建立长期连接的便捷方式。在 Web 客户端需要不断检查服务器数据的情况下,基于 REST 的实现可能会导致更长的轮询时间和其他相关复杂性。WebSocket 提供双向通信流,允许将数据从服务器推送到客户端。
尽管使用 WebSockets 非常简单,但将其集成到 React+Redux 应用程序中可能会很棘手。本指南将使用一个实际示例来探索将 WebSockets 集成到 React 应用程序的不同模式,并讨论每种模式的优缺点。完整代码可在Github Repo中找到
聊天应用程序
在今天的指南中,您将创建一个基本的聊天应用程序。该应用程序将提供以下功能:
- 任何人都可以创建和加入新的聊天室
- 创建房间后,会创建一个唯一代码,以便其他人可以加入
- 任何加入房间的人都必须先输入用户名
- 加入房间后,用户可以看到该房间的所有过去消息
- 房间内的用户可以实时聊天
为了避免应用程序过于复杂,没有实现用户身份验证、房间列表和私人聊天等功能,但欢迎您实现这些功能来测试所学到的概念。
首先,本指南将向您展示如何创建一个提供上述列表中 1-4 项功能的基本 React 应用。这四个功能不需要 WebSockets 来实现,可以使用您日常使用的 React 工具来实现。本指南将使用 Redux 和Redux Toolkit进行状态管理,并使用Axios进行 API 连接。聊天服务器将支持 HTTP REST API 进行 CRUD 操作,以及 WebSockets 进行套接字相关功能。
WebSockets主要用于提供用户和服务器之间的双向通信,当用户在房间中输入消息时,这些消息会被发送到服务器并存储在聊天记录中,以便之后加入的用户可以看到,并广播给房间中的所有其他用户。
从接收用户的角度来看,这可以看作是一个长轮询需求,因为客户端需要不断向服务器查询新消息。因此,这是一个利用 WebSocket 优势的绝佳机会。
聊天服务器
首先,您需要服务器组件才能使整个聊天应用程序正常运行。为了管理本指南的范围,不会详细讨论服务器的实现,但服务器代码可在 [Github Repo] [https://github.com/ManZzup/plguides/tree/master/11-using-websockets-in-your-react-redux-app/chat-server](Github Repo) 中找到。
本指南将简要介绍服务器的结构及其提供的功能。服务器提供两个关键 API。
REST API
REST API 是一种标准 API,它通过 HTTP 从 Web 应用异步发送请求。它支持两个端点:
- /room?name="权力的游戏"创建新的聊天室并返回唯一代码
- /room/{unique-id}在给定唯一代码时验证房间并发送房间的聊天记录
WebSocket 接口
基于套接字的API有助于实现持续的双向通信。WebSocket 依赖于共享通道上的基于事件的机制,而不是单个端点。本指南将在实际实施中探讨这一点。
为了保持服务器简单,所有数据将使用基本数据结构存储在内存中,从而使您可以将注意力集中在 Web 应用程序方面。
基本 React Redux 应用
在将 WebSockets 引入应用程序之前,您将创建应用程序的其余部分并设置 Redux。基本应用程序非常简单,只有两个组件:
- HomeComponent包含创建和进入房间的选项
- ChatRoomComponent提供了一个简单的聊天界面
然后,您将使用单个 Reducer 来存储聊天记录和其他存储元素。在添加 WebSocket 之前,不会使用发送和接收消息的操作。除了这两个组件之外,代码将遵循标准的 Redux 模式。
注意我们按照 React 文档的建议在整个指南中使用 React Hooks。如果您不熟悉如何将 Hooks 与 Redux 结合使用,请查看本指南使用 React Hooks 简化 Redux 绑定。
// actions.js
import axios from 'axios';
import { API_BASE } from './config';
export const SEND_MESSAGE_REQUEST = "SEND_MESSAGE_REQUEST"
export const UPDATE_CHAT_LOG = "UPDATE_CHAT_LOG"
// These are our action types
export const CREATE_ROOM_REQUEST = "CREATE_ROOM_REQUEST"
export const CREATE_ROOM_SUCCESS = "CREATE_ROOM_SUCCESS"
export const CREATE_ROOM_ERROR = "CREATE_ROOM_ERROR"
// Now we define actions
export function createRoomRequest(){
return {
type: CREATE_ROOM_REQUEST
}
}
export function createRoomSuccess(payload){
return {
type: CREATE_ROOM_SUCCESS,
payload
}
}
export function createRoomError(error){
return {
type: CREATE_ROOM_ERROR,
error
}
}
export function createRoom(roomName) {
return async function (dispatch) {
dispatch(createRoomRequest());
try{
const response = await axios.get(`${API_BASE}/room?name=${roomName}`)
dispatch(createRoomSuccess(response.data));
}catch(error){
dispatch(createRoomError(error));
}
}
}
export const JOIN_ROOM_REQUEST = "JOIN_ROOM_REQUEST"
export const JOIN_ROOM_SUCCESS = "JOIN_ROOM_SUCCESS"
export const JOIN_ROOM_ERROR = "JOIN_ROOM_ERROR"
export function joinRoomRequest(){
return {
type: JOIN_ROOM_REQUEST
}
}
export function joinRoomSuccess(payload){
return {
type: JOIN_ROOM_SUCCESS,
payload
}
}
export function joinRoomError(error){
return {
type: JOIN_ROOM_ERROR,
error
}
}
export function joinRoom(roomId) {
return async function (dispatch) {
dispatch(joinRoomRequest());
try{
const response = await axios.get(`${API_BASE}/room/${roomId}`)
dispatch(joinRoomSuccess(response.data));
}catch(error){
dispatch(joinRoomError(error));
}
}
}
export const SET_USERNAME = "SET_USERNAME"
export function setUsername(username){
return {
type: SET_USERNAME,
username
}
}
// reducers.js
import { CREATE_ROOM_SUCCESS, JOIN_ROOM_SUCCESS, SET_USERNAME} from './actions';
const initialState = {
room: null,
chatLog: [],
username: null
}
export default function chatReducer(state, action) {
if (typeof state === 'undefined') {
return initialState
}
switch(action.type){
case CREATE_ROOM_SUCCESS:
state.room = action.payload;
break;
case JOIN_ROOM_SUCCESS:
state.room = action.payload;
break;
case SET_USERNAME:
state.username = action.username;
break;
}
return state
}
import React, { useState } from 'react';
import logo from './logo.svg';
import './App.css';
import { Provider, useSelector, useDispatch } from 'react-redux'
import store from './store';
import { createRoom, setUsername, joinRoom } from './actions';
function ChatRoom() {
const [usernameInput, setUsernameInput] = useState("");
const username = useSelector(state => state.username);
const dispatch = useDispatch();
function enterRooom(){
dispatch(setUsername(usernameInput));
}
return (
<div>
{!username &&
<div className="user">
<input type="text" placeholder="Enter username" value={usernameInput} onChange={(e) => setUsernameInput(e.target.value)} />
<button onClick={enterRooom}>Enter Room</button>
</div>
}
{username &&
<div className="room">
<div className="history"></div>
<div className="control">
<input type="text" />
<button>Send</button>
</div>
</div>
}
</div>
)
}
function HomeComponent(){
const [roomName, setRoomName] = useState("");
const [roomId, setRoomId] = useState("");
const currentRoom = useSelector(state => state.room);
const dispatch = useDispatch();
return (
<>
{!currentRoom &&
<div className="create">
<div>
<span>Create new room</span>
<input type="text" placeholder="Room name" value={roomName} onChange={(e) => setRoomName(e.target.value)} />
<button onClick={() => dispatch(createRoom(roomName))}>Create</button>
</div>
<div>
<span>Join existing room</span>
<input type="text" placeholder="Room code" value={roomId} onChange={(e) => setRoomId(e.target.value)} />
<button onClick={() => dispatch(joinRoom(roomId))}>Join</button>
</div>
</div>
}
{currentRoom &&
<ChatRoom />
}
</>
);
}
function App() {
return (
<Provider store={store}>
<div className="App">
<HomeComponent />
</div>
</Provider>
)
}
export default App;
此阶段的应用程序支持创建房间并通过唯一代码加入房间。接下来,重点是将 WebSockets 添加到组合中。
添加 WebSockets
为了方便在 React 中进行套接字通信,你将使用事实上的库socket.io-client。使用命令npm install -S socket.io-client来安装它。
有多种方法可以为 React 应用添加 WebSocket 支持。每种方法都有其优点和缺点。本指南将介绍一些常见模式,但仅详细探讨我们正在实施的模式。
组件级集成
在此方法中,您可以将 WebSockets 部分视为单独的实用程序。您将在AppComponent init 处以单例形式启动套接字连接,并使用套接字实例来侦听与特定组件相关的套接字消息。示例实现如下:
import { socket } from 'socketUtil.js';
import { useDispatch } from 'react-redux';
function ChatRoomComponent(){
const dispatch = useDispatch();
useEffect(() => {
socket.on('event://get-message', payload => {
// update messages
useDispatch({ type: UPDATE_CHAT_LOG }, payload)
});
socket.on('event://user-joined', payload => {
// handling a new user joining to room
});
});
// other implementations
}
如上所示,这完全隔离了 Redux 和 WebSocket 实现,并提供了即插即用模式。如果您正在为现有应用程序的几个组件实现 WebSocket,则此方法很有用,例如,如果您有一个博客应用程序并且想要提供实时推送通知。在这种情况下,您只需要通知组件的 WebSocket,这种模式将是一种干净的实现方式。但如果应用程序套接字繁重,这种方法最终会成为开发和维护的负担。套接字实用程序独立运行,与 React 生命周期配合不佳。此外,每个组件中的多个事件绑定最终会减慢整个应用程序的速度。
免责声明:本内容来源于第三方作者授权、网友推荐或互联网整理,旨在为广大用户提供学习与参考之用。所有文本和图片版权归原创网站或作者本人所有,其观点并不代表本站立场。如有任何版权侵犯或转载不当之情况,请与我们取得联系,我们将尽快进行相关处理与修改。感谢您的理解与支持!
请先 登录后发表评论 ~