如何开始使用 Typescript 和 React 中的 WebSockets 创建一个简单的聊天应用程序
示例源码:ws
下一篇:https://blog.csdn.net/hefeng_aspnet/article/details/148898147
介绍
WebSocket 是一项我目前还没有在工作中使用过的技术,但我知道它是一款值得了解的实用工具。我写这篇文章的目的是积累一些在简单的聊天应用中实现 WebSocket 的经验,以便将来能够在更复杂的产品中使用它们。
什么是 WebSocket?
WebSockets 是一种行业标准方式,允许客户端和服务器实时交换消息,而无需刷新页面或轮询更改。
它们通常用于同时向大量接收者广播相同的数据(消息),支持流式传输实时比分更新、发送交通更新、分发通知或新闻警报以及传输实时财务信息(如股票报价和市场更新)等用例。
2011 年 12 月,互联网工程任务组 (IETF) 标准化了 WebSocket 协议,目前所有现代浏览器都支持该协议。MDN对WebSocket 协议的描述如下:
WebSocket API是一项先进的技术,它能够在用户浏览器和服务器之间建立双向交互式通信会话。使用此 API,您可以向服务器发送消息并接收事件驱动的响应,而无需轮询服务器以获取回复。
项目计划
计划是使用 WebSocket 服务器开发一个简单的聊天应用,该应用可以接受来自多个客户端的连接。它会接收来自客户端的新消息,然后将这些消息广播给当前连接到它的所有客户端。
还想将消息保存到数据库中,以便用户在加载页面时可以看到历史消息,以及从 WebSocket 收到的新消息。为了实现这一点,我的服务器应用需要两个函数:
1、WebSocket 服务器用于接受新消息并将其广播给连接的客户端
2、允许客户端获取现有消息的 HTTP 服务器
当新消息到达服务器时,我计划将消息保存到数据库,然后通过 WebSocket 广播到连接的客户端。
项目设置
在工作中经常使用项目,yarn workspaces但从未从零开始为个人项目创建过。我想借此机会尝试一下,在一个“monorepo”中创建两个包:一个用于客户端,另一个用于服务器。
在后端,决定使用 .io 包ws。我知道我可以使用Socket.io来实现同样的功能,但据我了解,ws它更轻量级,因此也更简单,非常适合我的简单项目。关于 WebSocket 工具的优缺点,已经有很多文章进行了探讨,我发现这篇文章很有帮助。
如果我要实现更复杂的功能,我想我会花时间使用Socket.io,但对于这个项目,我的目标是了解 WebSocket 的基础知识。为此,我希望选择一个不太抽象或复杂的软件包,因为我觉得这意味着我会更多地学习框架而不是底层技术。
Socket.io有一个客户端版本,但在后端isomorphic-ws使用时似乎使用是最好的选择。ws
NoSQL 数据库可能是消息应用程序的更好选择,但由于我熟悉它,并且复杂程度较低,因此我决定使用 Postgres 和简单的messages表进行存储。
该项目(示例源码:ws)用 编写typescript并使用prettier,eslint以便nodemon于开发。
设置 WebSocket 服务器
首先,我需要创建简单的 HTTP 服务器,以允许用户获取历史消息,并定义获取新消息并将其插入数据库的方法。
我使用express并创建了一个端点来获取所有现有消息。然后,在我的消息存储库文件中,我创建了两个方法 - 一个用于getMessages,另一个用于insertMessage。然后,该服务器监听端口 4000。
import express, { Request, Response } from 'express';
import { getAllMessages } from './messages/messages.controller';
import { Pool } from 'pg';
import cors from 'cors';
import { Message, insertMessage } from './messages/messages.repository';
const app = express();
const db = new Pool();
app.use(express.json());
app.use(cors());
app.get('/messages', async (_: Request, res: Response) => {
const messages = await getAllMessages({ db });
res.send(messages);
});
const start = (): void => {
try {
app.listen(4000, () => console.log('Server started on port 4000'));
} catch (error) {
console.error(error);
process.exit(1);
}
};
void start();
index.ts接下来,我使用 ws文档作为指南,将 WebSocket 服务器添加到我的文件中。
import { WebSocketServer, WebSocket } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
// HTTP server setup goes here
const start = (): void => {
try {
app.listen(4000, () => console.log('Server started on port 4000'));
wss.on('connection', (ws) => {
ws.on('error', console.error);
ws.on('message', (msg, isBinary) => {
const msgAsString = msg.toString('utf-8');
const msgObject = JSON.parse(msgAsString) as Message;
insertMessage(msgObject, { db }).catch((e) => console.error(e));
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(msgAsString, { binary: isBinary });
}
});
});
});
wss.on('close', () => console.log('Connection closed'));
} catch (error) {
console.error(error);
process.exit(1);
}
};
wss.on('close', () => console.log('Connection closed'));
void start();
这里,WebSocket 服务器 ( wss) 被实例化并设置为 8080 端口。连接后,我们会监听错误和消息。在ws.on(’message’…函数中,我们获取消息(以字符串形式发送的对象)并进行解析,以便读取其中的各个组成部分。
该insertMessage函数会先将其保存到数据库,然后再通过forEach循环将其广播给每个连接的客户端。我惊讶地发现,WebSocket 服务器的广播功能其实可以归结为一个简单的 for 循环!
创建客户端
接下来,我需要一种让用户与 WebSocket 服务器交互并输入和查看消息的方式。
在客户端,我使用了包,并且 WebSocket 设置在我的文件isomorphic-ws中如下所示:index.ts
import WebSocket from 'isomorphic-ws';
export const ws = new WebSocket('ws://localhost:8080/');
ws.onopen = () => console.log('WebSocket connected');
ws.onclose = () => console.log('WebSocket disconnected');
该变量ws被导出,然后在我们需要与其交互的组件中导入。
它在Form组件中用于提交表单。createMessage此处的函数设置了消息id、userId时间戳createdAt。
import { ws } from './index';
export const Form = ({ userId }: { userId: string }) => {
const [input, setInput] = useState('');
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!input) return;
const messageToSend = createMessage(input, userId);
ws.send(JSON.stringify(messageToSend));
setInput('');
};
return ({/* the form */});
};
ws.send将消息对象(作为字符串)传输到监听端口 8080 的服务器。
接下来,我们需要显示消息。为此,我创建了一个MessageList组件,它会在加载时从我在服务器上设置的 HTTP 端点获取数据库中的所有消息。
之后,我们监听onmessage来自 WebSocket 的事件——这是client.send当 WebSocket 向连接的客户端广播时,for 循环调用的另一端。我们将新消息从字符串解析为Message对象,并将其添加到 messages 数组的末尾。
因此,历史消息在加载(或刷新)时来自数据库,任何新消息都会通过 WebSocket 连接立即显示。
其中和子组件上有一些样式Message,但您可以在 GitHub 存储库中查看这些详细信息。
import { useEffect, useState } from 'react';
import { ws } from './index';
import { Message } from './message';
export interface Message {
readonly id: string;
readonly content: string;
readonly created: string;
readonly userId: string;
}
export const MessageList = ({ userId }: { userId: string }) => {
const [messages, setMessages] = useState<Message[]>([]);
useEffect(() => {
async function getAllMessages() {
const res = await fetch(`http://localhost:4000/messages`);
if (!res.ok) throw new Error(res.statusText);
const response = (await res.json()) as Message[];
setMessages(response);
}
getAllMessages().catch((e) => console.log(e));
}, []);
ws.onmessage = (e) => {
const msgObject = JSON.parse(e.data as string) as Message;
setMessages([...messages, msgObject]);
};
return (
<List>
{messages.map((message) => (
<Message message={message} myUserId={userId} />
))}
</List>
);
};
整合起来
同时运行服务器和客户端后,我们可以访问localhost:3000并查看前端界面。打开多个窗口将创建多个与 WebSocket 的连接。
前端会检查userId消息中的 是否是分配给该客户端的 。如果是,则将消息显示为Me: …;如果不是,则显示为Them:…。我本可以在用户、身份验证和样式方面做得更多,使其更加完善,但这并不是项目的真正目的。
挑战
最初,尝试只从服务器获取新消息,并将它们分散到 React 状态中已有的消息之上。我发现这会导致“过多重新渲染”的问题,所以我决定让它在刷新/加载时获取数据库中的所有消息。虽然这种方式无法扩展,但目前为止已经达到了目的。
在开始写代码之前,我了解到 WebSocket 有时会断开连接,所以最终可能会出现服务器不知道客户端是否断开连接,而客户端也不知道服务器是否断开连接的情况。我在开发过程中确实经常遇到这种情况,感觉 WebSocket 连接相当“脆弱”。
下一步
为了解决 WebSocket 连接断开的问题,建议设置“心跳”,让服务器和客户端互相 ping 一下,检查它们是否仍然连接。我决定不在这个项目中实现这个功能,但这个功能肯定会是下一个要实现的,因为它会对用户体验的稳定性产生很大的影响。
读到过关于 WebSocket 无法保证消息传递的文章。我猜你可以让服务器每次都返回一个确认消息已收到的确认,这样一来,客户端每次收到来自服务器的消息时也必须返回一个确认——但这会使双向流量翻倍。这也是Socket.io的一个原因,它似乎既能保证消息的传递,又能保证消息的顺序(文档)。同样,Socket.io为 WebSocket 提供了一套更完善的工具,ws你可以自由地使用它来实现自己的解决方案。
还想在用户方面做更多改进——最初允许用户设置用户名,或许还可以设置头像,然后在其他人的消息中查看他们的信息,这样就能清楚地知道他们来自哪里。我还想实现不同的聊天“房间”,这样人们就可以选择他们想发送消息的群组。
总结
很高兴接触了一些 WebSocket 的基础知识,并且学到了很多关于它们工作原理的知识。凭借我现有的基础知识,我想以后我会尝试使用Socket.io,ws并利用它更强大的功能。
如果您喜欢此文章,请收藏、点赞、评论,谢谢,祝您快乐每一天。