如果您希望构建既快速又可靠的实时聊天应用程序,请考虑使用 Rust 和 React。Rust 以其速度和可靠性着称,而React 是最流行的用于构建用户界面的前端框架之一。
在本文中,我们将演示如何使用 Rust 和 React 构建一个实时聊天应用程序,该应用程序提供聊天、检查用户状态和指示用户何时输入的功能。我们将使用 WebSockets 启用双向客户端-服务器通信。
跳跃前进:
实时聊天应用简介
WebSocket 简介
入门
设计实时聊天应用架构
在 Rust 中构建 WebSocket 服务器创建路线处理用户会话
使用 SQLite 准备数据库生成模式创建结构设置查询通过电话号码查找用户添加新用户查找聊天室和参与者
使用 React 构建客户端 UI头像组件登录组件房间组件会话组件使用Websocket Hook使用LocalStorage Hook使用会话挂钩
构建聊天应用程序
实时聊天应用简介
实时聊天应用程序允许用户通过文本、语音或视频彼此实时交流。这种类型的应用程序允许比其他类型的通信(例如电子邮件或 IM)更即时的消息传递。
聊天应用程序必须实时工作有几个原因:
改进的性能:更直接的通信允许更自然的对话
更强的响应能力:实时功能可改善用户体验
卓越的可靠性:通过实时功能,消息丢失或延迟的机会更少
WebSocket 简介
WebSockets 在实时聊天应用程序中启用客户端和服务器之间的双向通信。使用 Rust 构建 WebSocket 服务器将使服务器能够处理大量连接而不会降低速度。这是由于 Rust 的速度和可靠性。
现在我们对 WebSockets 有了更好的了解,让我们开始构建我们的实时聊天应用程序吧!
入门
首先,让我们回顾一些先决条件:
Rust:确保您的计算机上安装了 Rust。如果没有,请使用以下命令安装它:卷曲-原型'=https' - tlsv1 。2 - sSf https : //sh.rustup.rs | sh // 如果你在 Windows 中,请在此处查看更多安装方法 https : //forge.rust-lang.org/infra/other-installation-methods.html
React:确保您的环境已准备好进行 React 开发;如果您还没有安装 React,请使用以下命令之一来安装它:// 在苹果机上 酿造安装节点 // 在 linux nvm install v14上。10 .0 // 在 Windows 上,您可以在此处下载 nodejs 安装程序 https : //nodejs.org/en/download/
接下来,运行以下命令来验证所有内容是否已安装并正常工作:
rustc——版本_
货物——版本
节点——版本
npm——版本_
设计实时聊天应用架构
让我们为我们的实时聊天应用程序创建一些设计架构。我们将构建一个简单的服务器;我们的应用程序架构将涵盖以下功能:
聊天:两个用户之间通过直接消息传递
打字指示器:当用户开始向他们输入聊天内容时通知收件人
用户状态:表示用户在线还是离线
实时聊天应用系统架构。
此架构非常简单且易于遵循。它仅由几个组件组成:
WebSocket 服务器:这是我们应用程序中最重要的组件;它处理客户和房间之间的所有通信
房间管理器:该组件负责管理我们应用程序中的所有房间。它将创建、更新和删除房间。该组件将位于 HTTP 服务器上
用户管理器:该组件负责管理我们应用程序中的所有用户。它将创建、更新和删除用户。该组件也将位于 HTTP 服务器上
消息管理器:该组件负责管理我们应用程序中的所有消息。它将创建、更新和删除消息。这个组件一将在 WebSocket 服务器和 HTTP 服务器上。它将用于存储来自 WebSockets 的传入消息,并在用户通过 Rest API 打开聊天室时检索数据库中已有的所有消息
在 Rust 中构建 WebSocket 服务器
我们可以使用许多包在 Rust 中编写 WebSocket 服务器。对于本教程,我们将使用Actix Web;它是一个成熟的软件包并且易于使用。
首先,使用以下命令创建一个 Rust 项目:
cargo new rust -反应-聊天
接下来,将这个包添加到文件中:Cargo.toml
[ package ]
name = "rust-react-chat"
version = "0.1.0"
edition = "2021"
[依赖项]
actix = “0.13.0”
actix - files = “0.6.2”
actix - web = “4.2.1”
actix - web - actors = “4.1.0”
rand = “0.8.5”
serde = “1.0 .147"
serde_json = "1.0.88"
现在,安装diesel_cli;我们将使用它作为我们的 ORM:
cargo install diesel_cli -- no - default - features -- features sqlite
项目的结构应该如下所示:
. ├──货物。lock
├── Cargo . toml
├──自述文件。md
├──聊天。分贝
├── . 环境
└──源
├──数据库. rs
├──主要。rs
├──模型. rs
├──路线. rs
├──架构. rs
├──服务器. rs
└──会话. rs
└──
静态└──用户界面
现在,这里有一些关于文件夹的信息:
src:此文件夹包含我们所有的 Rust 代码
static:此文件夹包含我们所有的静态资产、HTML 文件、JavaScript 文件和图像
ui:这个文件夹包含我们的 React 代码;我们稍后将其编译为static文件并将其导出到static文件夹
接下来,让我们编写 WebSocket 服务器的入口点:
// src/main.rs #[ macro_use ] extern crate diesel ;
使用 actix ::*;
使用 actix_cors :: Cors ;
使用 actix_files ::文件;
使用 actix_web ::{ web , http , App , HttpServer };
使用 diesel ::{ prelude ::*, r2d2 ::{ self , ConnectionManager }, };
模组数据库;
模组模型;
模组路线;
模式架构;
模组服务器;
模组会话;#[ actix_web :: main ] async fn main () -> std :: io :: Result <()> { let server = server :: ChatServer :: new (). 开始();让conn_spec = "chat.db" ; 让manager = ConnectionManager :: < SqliteConnection > :: new ( conn_spec );
let pool = r2d2::Pool::builder().build(manager).expect("Failed to create pool.");
let server_addr = "127.0.0.1";
let server_port = 8080;
let app = HttpServer::new(move || {
let cors = Cors::default()
.allowed_origin("http://localhost:3000")
.allowed_origin("http://localhost:8080")
.allowed_methods(vec!["GET", "POST"])
.allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT])
.allowed_header(http::header::CONTENT_TYPE)
.max_age(3600);
App::new()
.app_data(web::Data::new(server.clone()))
.app_data(web::Data::new(pool.clone()))
.wrap(cors)
.service(web::resource("/").to(routes::index))
.route("/ws", web::get().to(routes::chat_server))
.service(routes::create_user)
.service(routes::get_user_by_id)
.service(routes::get_user_by_phone)
.service(routes::get_conversation_by_id)
.service(routes::get_rooms)
.service(Files::new( "/" , "./static" )) }) 。工人( 2 ) 。绑定((服务器地址,服务器端口))?. 运行();
println !( "服务器运行在 http://{server_addr}:{server_port}/" );
应用程序。等待}
以下是有关我们正在使用的软件包的一些信息:
actix_cors: 将用于调试 UI;我们将接受来自或的POST 和 GET 请求localhost:3000localhost:8080
actix_web:对于 Actix Web 包中所有与 HTTP 相关的功能
actix_files:用于将静态文件嵌入到我们的路由之一
diesel:将用于从我们的 SQLite 数据库中查询数据。如果您愿意,可以将其更改为 Postgres 或 MySQL
serde_json:将用于解析我们将发送到 React 应用程序的 JSON 数据
创建路线
现在让我们为我们的服务器创建路由。由于我们将使用 REST HTTP 和 WebSocket 服务器,我们可以轻松地将所有内容放在一个文件中。
首先,添加我们需要的所有包:
// src/routes.rs
使用 std :: time :: Instant ;
使用 actix ::*;
使用 actix_files :: NamedFile ;
使用 actix_web ::{ get , post , web , Error , HttpRequest , HttpResponse , Responder };
使用 actix_web_actors :: ws ;
使用 diesel ::{ prelude ::*, r2d2 ::{ self , ConnectionManager }, };
使用 serde_json ::
JSON ;
使用 uuid :: Uuid ;
使用 crate :: db ;
使用板条箱::模型;
使用箱子::服务器;
使用 crate :: session ;
输入DbPool = r2d2 :: Pool < ConnectionManager < SqliteConnection >>;
然后,添加一个用于将主页嵌入到根 URL 的路由:
// src/routes.rs
pub async fn index () -> impl Responder { NamedFile :: open_async ( "./static/index.html" ). 等待。展开()}
这是我们的 WebSocket 服务器的入口点。现在它在路线上,但您可以将其更改为您喜欢的任何路线名称。由于我们已经在文件中注册了我们需要的所有依赖项,我们可以将依赖项传递给函数参数,如下所示:/wsmain.rs
// src/routes.rs
pub async fn chat_server (
req : HttpRequest , stream : web :: Payload , pool : web :: Data < DbPool >, srv : web :: Data < Addr < server :: ChatServer >>, ) ->结果< HttpResponse ,错误> { ws ::开始(
会话::
WsChatSession { id : 0 , hb : Instant :: now (), room : "main" . to_string (),名称:无,地址:srv 。get_ref ()。克隆(), db_pool :池, }, & req ,
溪流
) }
接下来,我们需要向我们的路由添加一个 REST API,以便获取必要的数据来使我们的聊天正常进行:
// src/routes.rs #[ post ( "/users/create" )]
pub async fn create_user (
pool : web :: Data < DbPool >, form : web :: Json < models :: NewUser >, ) ->结果< HttpResponse ,错误> { let user = web :: block ( move || { let mut conn =
游泳池。得到()?;
db :: insert_new_user (& mut conn , & form . username , & form . phone ) }) 。等待?. map_err ( actix_web :: error :: ErrorUnprocessableEntity )?; 好的( HttpResponse :: Ok () .json ( user )) } #[ get ( "/users/{user_id}" )]
pub
async fn get_user_by_id (
pool : web :: Data < DbPool >, id : web :: Path < Uuid >, ) ->结果< HttpResponse , Error > {让user_id = id 。to_owned (); let user = web :: block (移动|| { let mut conn = pool . get
()?;
db::find_user_by_uid(&mut conn, user_id)
})
.await?
.map_err(actix_web::error::ErrorInternalServerError)?;
if let Some(user) = user {
Ok(HttpResponse::Ok().json(user))
} else {
let res = HttpResponse::NotFound().body(
json!({
"error": 404,
"message": format!("No user found with phone: {id}")
})
.to_string(),
);
Ok(res)
}
}
#[get("/conversations/{uid}")]
pub async fn get_conversation_by_id(
pool: web::Data<DbPool>,
uid: web::Path<Uuid>,
) -> Result<HttpResponse, Error> {
let room_id = uid.to_owned();
let conversations = web::block(move || {
let mut conn = pool.get()?;
db::get_conversation_by_room_uid(&mut conn, room_id)
})
.await?
.map_err(actix_web::error::ErrorInternalServerError)?;
if let Some(data) = conversations {
Ok(HttpResponse::Ok().json(data))
} else {
let res = HttpResponse::NotFound().body(
json!({
"error": 404,
"message": format!("No conversation with room_id: {room_id}")
})
.to_string(),
);
Ok(res)
}
}
#[get("/users/phone/{user_phone}")]
pub async fn get_user_by_phone(
pool: web::Data<DbPool>,
phone: web::Path<String>,
) -> Result<HttpResponse, Error> {
let user_phone = phone.to_string();
let user = web::block(move || {
let mut conn = pool.get()?;
db::find_user_by_phone(&mut conn, user_phone)
})
.await?
.map_err(actix_web::error::ErrorInternalServerError)?;
if let Some(user) = user {
Ok(HttpResponse::Ok().json(user))
} else {
let res = HttpResponse::NotFound().body(
json!({
"error": 404,
"message": format!("No user found with phone: {}", phone.to_string())
})
.to_string(),
);
Ok(res)
}
}
#[get("/rooms")]
pub async fn get_rooms(
pool: web::Data<DbPool>,
) -> Result<HttpResponse, Error> {
let rooms = web::block(move || {
let mut conn = pool.get()?;
db::get_all_rooms(&mut conn)
})
.await?
.map_err(actix_web::error::ErrorInternalServerError)?;
if !rooms.is_empty() {
Ok(HttpResponse::Ok().json(rooms))
} else {
let res = HttpResponse::NotFound().body(
json!({
"error": 404,
"message" : "目前没有空房。" , }) 。to_string (), ); 好的(恢复)} }
现在,让我们处理 WebSocket 连接。首先,让我们再次导入我们需要的所有包:
// src/server.rs
使用 std :: collections ::{ HashMap , HashSet };
使用 serde_json :: json ;
使用 actix ::序曲::*;
使用 rand ::{ self , rngs :: ThreadRng , Rng };
使用 crate :: session ;#[ derive ( Message )] #[ rtype ( result = "()" )]
pub struct Message ( pub
串);#[ derive ( Message )] #[ rtype ( usize )]
pub struct Connect {
pub addr : Recipient < Message >, } #[ derive ( Message )] #[ rtype ( result = "()" )]
pub struct Disconnect {
pub id : usize , } #[ derive ( Message )] #[
rtype(result = "()")]
pub struct ClientMessage {
pub id: usize,
pub msg: String,
pub room: String,
}
pub struct ListRooms;
impl actix::Message for ListRooms {
type Result = Vec<String>;
}
#[derive(Message)]
#[rtype(结果= "()" )]
pub struct Join {
pub id : usize ,
pub name : String , }
接下来,让我们实现 trait 来管理 WebSocket 连接。此代码将处理来自用户的所有消息并将它们发送回聊天室的参与者:
// src/server.rs #[ derive ( Debug )]
pub struct ChatServer { sessions : HashMap < usize , Recipient < Message >>, rooms : HashMap < String , HashSet < usize >>, rng : ThreadRng , }
impl ChatServer {
pub fn new () -> ChatServer { let mut rooms = HashMap ::
新的();
房间。insert ( "main" .to_string ( ), HashSet :: new ()); self { sessions : HashMap :: new (),
rooms , rng : rand :: thread_rng () } }
fn send_message (& self , room : & str , message : & str , skip_id : usize )
{
if let Some(sessions) = self.rooms.get(room) {
for id in sessions {
if *id != skip_id {
if let Some(addr) = self.sessions.get(id) {
addr.do_send(Message(message.to_owned()));
}
}
}
}
}
}
impl Actor for ChatServer {
type Context = Context<Self>;
}
impl Handler<Connect> for ChatServer {
type Result = usize;
fn handle(&mut self, msg: Connect, _: &mut Context<Self>) -> Self::Result {
let id = self.rng.gen::<usize>();
self.sessions.insert(id, msg.addr);
self.rooms
.entry("main".to_string())
.or_insert_with(HashSet::new)
.insert(id);
self.send_message("main", &json!({
"value": vec![format!("{}", id)],
"chat_type": session::ChatType::CONNECT
}).to_string(), 0);
id
}
}
impl Handler<Disconnect> for ChatServer {
type Result = ();
fn handle(&mut self, msg: Disconnect, _: &mut Self::Context) -> Self::Result {
let mut rooms: Vec<String> = vec![];
if self.sessions.remove(&msg.id).is_some() {
for (name, sessions) in &mut self.rooms {
if sessions.remove(&msg.id) {
rooms.push(name.to_owned());
}
}
}
for room in rooms {
self.send_message("main", &json!({
"room": room,
"value": vec![format!("Someone disconnect!")],
"chat_type": session::ChatType::DISCONNECT
}).to_string(), 0);
}
}
}
impl Handler<ClientMessage> for ChatServer {
type Result = ();
fn handle(&mut self, msg: ClientMessage, _: &mut Self::Context) -> Self::Result {
self.send_message(&msg.room, &msg.msg, msg.id);
}
}
impl Handler<ListRooms> for ChatServer {
type Result = MessageResult<ListRooms>;
fn handle(&mut self, _: ListRooms, _: &mut Self::Context) -> Self::Result {
let mut rooms = vec![];
for key in self.rooms.keys() {
rooms.push(key.to_owned());
}
MessageResult(rooms)
}
}
impl Handler<Join> for ChatServer {
type Result = ();
fn handle(&mut self, msg: Join, _: &mut Self::Context) -> Self::Result {
let Join {id, name} = msg;
let mut rooms = vec![];
for (n, sessions) in &mut self.rooms {
if sessions.remove(&id) {
rooms.push(n.to_owned());
}
}
for room in rooms {
self.send_message(&room, &json!({
"room": room,
"value": vec![format!("Someone disconnect!")],
"chat_type":会话:: ChatType ::断开连接
})。to_string (), 0 ); }
自我。房间
。条目(名称。克隆())。or_insert_with ( HashSet :: new ) 。插入(编号);} }
处理用户会话
现在,让我们来处理用户会话。在这里,我们将收到一条消息,将其保存到数据库中,然后将其发送回聊天室中的参与者。
首先,导入所有包:
// src/session.rs
使用 std :: time ::{ Duration , Instant };
使用 actix ::序曲::*;
使用 actix_web :: web ;
使用 actix_web_actors :: ws ;
使用 serde ::{反序列化,序列化};
使用 diesel ::{ prelude ::*, r2d2 ::{ self , ConnectionManager }, };
使用 crate :: db ;
使用板条箱::模型::
新对话;
使用箱子::服务器;
您可以在此处更改与 WebSocket 的连接持续时间。所以这HEARTBEAT是与客户端保持连接的持续时间。并且CLIENT_TIMEOUT是检查客户端是否仍然连接的持续时间:
// src/session.rs const HEARBEET : Duration = Duration :: from_secs ( 5 ); const CLIENT_TIMEOUT : Duration = Duration :: from_secs ( 10 );
输入DbPool = r2d2 :: Pool < ConnectionManager < SqliteConnection >>;
现在让我们创建一些结构来存储我们需要的所有数据:
// src/session.rs #[ derive ( Debug )]
pub struct WsChatSession {
pub id : usize ,
pub hb : Instant ,
pub room : String ,
pub name : Option < String >,
pub addr : Addr < server :: ChatServer >,
pub db_pool : web :: Data < DbPool >, } #[派生(
PartialEq , Serialize , Deserialize )]
pub enum ChatType {
TYPING ,
TEXT ,
CONNECT ,
DISCONNECT , } #[ derive ( Serialize , Deserialize )] struct ChatMessage {
pub chat_type : ChatType ,
pub value : Vec < String >,
pub room_id : String ,
pub user_id :字符串
pub id : usize , }
该结构将用于以下用途:
WsChatSession: 自定义实现 Actix Web actor
ChatMessage:定义将发送给用户和从用户接收的对象
现在,让我们实现我们的会话Actor和流Handler:
// src/session.rs
impl Actor for WsChatSession {
type Context = ws :: WebsocketContext < Self >;
fn started (& mut self , ctx : & mut Self :: Context ) {
self . hb ( ctx ); 让addr = ctx 。地址();
自我。地址
_ 发送(服务器
::连接{地址:地址. 收件人(), }) 。into_actor (自我)。然后(| res , act , ctx | {
match res { Ok ( res ) = > act .id = res ,
_ => ctx . stop (), }
fut :: ready (()) }) 。
wait(ctx);
}
fn stopping(&mut self, _: &mut Self::Context) -> Running {
self.addr.do_send(server::Disconnect { id: self.id });
Running::Stop
}
}
impl Handler<server::Message> for WsChatSession {
type Result = ();
fn handle(&mut self, msg: server::Message, ctx: &mut Self::Context) -> Self::Result {
ctx.text(msg.0);
}
}
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WsChatSession {
fn handle(&mut self, item: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
let msg = match item {
Err(_) => {
ctx.stop();
return;
}
Ok(msg) => msg,
};
match msg {
ws::Message::Ping(msg) => {
self.hb = Instant::now();
ctx.pong(&msg);
}
ws::Message::Pong(_) => {
self.hb = Instant::now();
}
ws::Message::Text(text) => {
let data_json = serde_json::from_str::<ChatMessage>(&text.to_string());
if let Err(err) = data_json {
println!("{err}");
println!("Failed to parse message: {text}");
return;
}
let input = data_json.as_ref().unwrap();
match &input.chat_type {
ChatType::TYPING => {
let chat_msg = ChatMessage {
chat_type: ChatType::TYPING,
value: input.value.to_vec(),
id: self.id,
room_id: input.room_id.to_string(),
user_id: input.user_id.to_string(),
};
let msg = serde_json::to_string(&chat_msg).unwrap();
self.addr.do_send(server::ClientMessage {
id: self.id,
msg,
room: self.room.clone(),
})
}
ChatType::TEXT => {
let input = data_json.as_ref().unwrap();
let chat_msg = ChatMessage {
chat_type: ChatType::TEXT,
value: input.value.to_vec(),
id: self.id,
room_id: input.room_id.to_string(),
user_id: input.user_id.to_string(),
};
let mut conn = self.db_pool.get().unwrap();
let new_conversation = NewConversation {
user_id: input.user_id.to_string(),
room_id: input.room_id.to_string(),
message: input.value.join(""),
};
let _ = db::insert_new_conversation(&mut conn, new_conversation);
let msg = serde_json::to_string(&chat_msg).unwrap();
self.addr.do_send(server::ClientMessage {
id: self.id,
msg,
room: self.room.clone(),
})
}
_ => {}
}
}
ws::Message::Binary(_) => println!("Unsupported binary"),
ws::Message::Close(reason) => {
ctx.close(reason);
ctx.stop();
}
ws::Message::Continuation(_) => {
ctx.stop();
}
ws::Message::Nop => (),
}
}
}
impl WsChatSession {
fn hb(&self, ctx: &mut ws::WebsocketContext<Self>) {
ctx.run_interval(HEARBEET, |act, ctx| {
if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT {
act.addr.do_send(server::断开连接{ id : act . 编号});
CTX 。停止();返回;}
CTX 。平(二“” ); }); } }
准备数据库
接下来,让我们准备数据库。我们将使用 SQLite 使事情变得简单。下面是架构的外观:
该表将用于以下用途:
users:存储用户数据。由于我们目前没有实施完整的身份验证系统,因此我们暂时只保存用户名和电话号码
rooms: 存储所有聊天室的列表
conversations:列出所有消息在我们数据库中的存储位置
接下来,让我们为我们的模式生成数据库迁移:
// 壳
柴油机迁移生成 create_users
柴油迁移生成 create_rooms
柴油迁移生成 create_conversations
下面是迁移 SQL 的外观:
-迁移/ 2022-11-21-101206 _create_users /向上。_ _ _ _ _ _ 数据库
CREATE TABLE 用户(
id TEXT PRIMARY KEY NOT NULL ,
username VARCHAR NOT NULL ,
phone VARCHAR NOT NULL ,
created_at TEXT NOT NULL ,
unique (phone ))
-迁移/ 2022-11-21-101215 _create_rooms / up 。_ _ _ _ _ _ 数据库
创建表房间(
id TEXT PRIMARY KEY NOT NULL ,
name VARCHAR NOT NULL ,
last_message TEXT NOT NULL ,
participant_ids TEXT NOT NULL ,
created_at TEXT NOT NULL
)
-迁移/ 2022-11-21-101223 _create_conversations /向上。_ _ _ _ _ _ 数据库
创建表对话(
id TEXT PRIMARY KEY NOT NULL ,
room_id TEXT NOT NULL ,
user_id TEXT NOT NULL ,
content VARCHAR NOT NULL ,
created_at TEXT NOT NULL
)
我们还需要添加一些虚拟数据,以便稍后为客户端初始渲染提供一些示例:
柴油迁移生成虚拟数据
以下是数据的外观:
-迁移/ 2022-11-24-034153 _generate_dummy_data / up 。_ _ _ _ _ _ 数据库
插入用户(id 、用户名、电话、created_at )
价值观
( "4fbd288c-d3b2-4f78-adcf-def976902d50" , "Ahmad Rosid" , "123" , "2022-11-23T07:56:30.214162+00:00" ), ( "1e9a12c1-e98c-4a83-a55a-32ccd48a169a169 " , "Ashley Young" , "345" , "2022-11-23T07:56:30.214162+00:00" ), ( "1bc833808-05ed-455a-9d26-64fe1d96d62d" , "查尔斯爱德华" , "678" , “2022-12-23T07:56:30.214162+00:00”);
INSERT INTO rooms ( id , name ,
)
价值观
( "f061383b-0393-4ce8-9a85-f31d03762263" , "Charles Edward" , "嗨,你好吗?" , "1e9a12c1-e98c-4a83-a55a-32cc548a169d,1bc833808-05ed-455a-9d26-64fe1dd96 , d" 2022-12-23T07:56:30.214162+00:00" ), ( "008e9dc4-f01d-4429-ba31-986d7e63cce8" , "Ahmad Rosid" , "嗨...今天有空吗?" , "1e9a12c1-e98c- 4a83-a55a-32cc548a169d,1bc833808-05ed-455a-9d26-64fe1d96d62d" , "2022-12-23T07:56:30.214162+00:00" );
插入对话(id ,user_id ,
创建时间)
价值观
( "9aeab1a7-e063-40d1-a120-1f7585fa47d6" , "1bc833808-05ed-455a-9d26-64fe1d96d62d" , "f061383b-0393-4ce8-9a85-f31d03762263" , "你好"- , 1:27-20232 30.214162+00:00" ), ( "f4e54e70-736b-4a79-a622-3659b0b555e8" , " 1e9a12c1-e98c-4a83-a55a-32cc548a169d" , "f061383b-0393-4ce8-9a85-622d"怎么样你?” , "2022-12-23T07:56:30.214162+00:00" ), ( "d3ea6e39-ed58-4613-8922-b78f14a2676a" , "1bc833808-05ed-455a-9d26-64fe1d96d62d" ,“008e9dc4-f01d-4429-ba31-986d7e63cce8”,
“嗨……今天有空吗?” , "2022-12-23T07:56:30.214162+00:00" );
生成模式
现在让我们生成架构并运行迁移:
柴油数据库设置
柴油迁移运行
CLI 自动生成的架构如下所示:
// src/schema.rs // @Diesel CLI 自动生成。
柴油::表!{
conversations ( id ) {
id -> Text ,
room_id -> Text ,
user_id -> Text ,
content -> Text ,
created_at -> Text , } }
diesel :: table ! {
房间( id ) {
id
->文本,
名称->文本,
last_message ->文本,
participant_ids ->文本,
created_at ->文本,} }
diesel :: table !{
users ( id ) {
id -> Text ,
username -> Text ,
phone -> Text ,
created_at -> Text ,
} }
diesel :: allow_tables_to_appear_in_same_query !(
对话,
房间,
用户,);
上面的代码是自动生成的,所以不要对这个文件做任何更改。
创建结构
让我们创建一些结构来存储所有表。要记住的一件事是结构中属性的顺序应该与模式文件中的顺序相同。如果订单不匹配,您将得到错误的数据。
// src/model.rs
使用 serde ::{ Deserialize , Serialize };
使用 crate :: schema ::*; #[ derive ( Debug , Clone , Serialize , Deserialize , Queryable , Insertable )]
pub struct User {
pub id : String ,
pub username : String ,
pub phone : String ,
pub created_at : String } #[
派生(调试,克隆,PartialEq ,序列化,反序列化,可查询,可插入)]
pub struct Conversation {
pub id :String,
pub room_id :String,
pub user_id :String,
pub content :String,
pub created_at :String } #[ derive (调试,克隆,
Serialize, Deserialize, Queryable, Insertable)]
pub struct Room {
pub id: String,
pub name: String,
pub last_message: String,
pub participant_ids: String,
pub created_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NewUser {
pub username: String,
pub phone: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NewConversation {
pub user_id: String,
pub room_id: String,
pub message: String,
}
#[derive(Debug, Clone, Serialize, 反序列化)]
pub struct RoomResponse {
pub room : Room ,
pub users : Vec < User >, }
设置查询
现在,让我们从数据库中获取数据。
首先导入依赖:
// src/db.rs
使用 chrono ::{ DateTime , Utc };
使用柴油::序曲::*;
使用 std ::{ collections ::{ HashMap , HashSet }, time :: SystemTime , };
使用 uuid :: Uuid ;
使用 crate :: models ::{ Conversation , NewConversation , Room , RoomResponse , User };
输入DbError = Box <
dyn std :: error ::错误+发送+同步>;
由于 SQLite 没有日期功能构建,我们将创建一个:
// src/db.rs
fn iso_date () -> String { let now = SystemTime :: now (); 让现在:DateTime < Utc > =现在。进入();现在返回。to_rfc3339 (); }
通过电话号码查找用户
在这里,我们将设置一个查询,该查询将实现一个简单的登录功能,并使我们能够通过电话号码查找用户。我们仅将此登录方法用作示例。在生产中,您需要使用一种可以轻松验证和调试的方法:
// src/db.rs
pub fn find_user_by_phone (
conn : & mut SqliteConnection , user_phone : String , ) -> Result < Option < User >, DbError > {
使用 crate :: schema :: users :: dsl :: *; 让用户=用户
。过滤器(电话。eq (user_phone ))。首先:: <
用户> (conn)。可选()?; 好的(用户)}
添加新用户
这是一个查询,用于存储注册我们应用程序的新用户。这也是我们认证系统的一部分。同样,请不要将这种方法用于您的生产应用程序:
// src/db.rs
pub fn insert_new_user ( conn : & mut SqliteConnection , nm : & str , pn : & str ) ->结果< User , DbError > {
使用 crate :: schema :: users :: dsl :: * ; let new_user = User { id : Uuid :: new_v4 (). to_string (),用户名:
纳米。to_owned (),电话:pn 。to_owned (), created_at : iso_date (), };
diesel :: insert_into (用户)。值(&new_user )。执行( conn )?; 好的(新用户)}
添加新用户后,我们现在插入新对话:
// src/db.rs
pub fn insert_new_conversation (
conn : & mut SqliteConnection , new : NewConversation , ) ->结果< Conversation , DbError > {
使用 crate :: schema :: conversations :: dsl :: *; let new_conversation = Conversation { id : Uuid :: new_v4 (). to_string (), user_id : new
. user_id ,room_id:新的。room_id ,内容:新。message , created_at : iso_date (), };
柴油:: insert_into (对话)。价值观(& new_conversation )。执行( conn )?; 好的(新对话)}
查找聊天室和参与者
接下来,让我们设置一个查询以从数据库中获取所有聊天室和参与者:
// src/db.rs
pub fn get_all_rooms ( conn : & mut SqliteConnection ) ->结果< Vec < RoomResponse >, DbError > {
使用 crate :: schema :: rooms ;
使用 crate :: schema :: users ;让rooms_data : Vec < Room > = rooms :: table 。get_results ( conn )?; 让
mut ids = HashSet :: new (); 让mut rooms_map = HashMap :: new (); 让数据=房间数据。to_vec (); 对于&数据中的空间{ let user_ids = room
. 参与者编号
。分裂(“,”)。into_iter () 。收集::< Vec < _ >>(); 为了
用户 ID 中的 ID 。to_vec () {
ids . 插入( id . to_string ()); }
房间地图。insert ( room . id . to_string (), user_ids . to_vec ()); }让ids = ids 。into_iter ()。收集::< Vec < _ >>(); 让users_data : Vec < User > =
users::table
.filter(users::id.eq_any(ids))
.get_results(conn)?;
let users_map: HashMap<String, User> = HashMap::from_iter(
users_data
.into_iter()
.map(|item| (item.id.to_string(), item)),
);
let response_rooms = rooms_data.into_iter().map(|room| {
let users = rooms_map
.get(&room.id.to_string())
.unwrap()
.into_iter()
.map(| id | users_map .get ( id . to_owned ( )). unwrap (). clone ()) . 收集::< Vec < _ >>(); 返回RoomResponse { room , users }; }). 收集::< Vec < _ >>(); 好的(response_rooms )}
使用 React 构建客户端 UI
让我们为我们的客户端应用程序设计一个用户界面;最终结果将如下所示:
首先,使用 Next.js 创建一个 UI 项目:
yarn create next - app - js ui
将 Tailwind CSS 添加到项目中:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init - p
现在,更改 Tailwindconfig文件:
// ui/tailwind.config.js
内容:[ "./pages/**/*.{js,ts,jsx,tsx}" , "./components/**/*.{js,ts,jsx, TSX}" , ]
我们将添加此配置以将我们的 Next.js 应用程序导出为静态 HTML 页面,以便我们可以使用 Actix Web 通过文件服务器访问它们:package.json
// ui/package.json { "name" : "ui" , "version" : "0.1.0" , "private" : true , "scripts" : { "dev" : "next dev" , "build" : “下一次构建 && 下一次导出 -o ../static” , ...
接下来,将 Tailwind CSS 实用程序导入文件:globals.css
// ui/styles/global.css @tailwind base ; @尾风组件;@tailwind公用事业;
现在,让我们为我们的客户端应用程序创建一些组件。
avatar成分
在这里,我们将为每个用户创建头像:
// ui/components/avatar.js function getShortName ( full_name = '' ) { if ( full_name . includes ( " " )) { const names = full_name . 分裂(“”); 返回` $ {名称[ 0 ]。charAt ( 0 )} $ {名称[ 1 ]. charAt ( 0 )} `。
toUpperCase () } return ` $ { full_name . 切片( 0 , 2 )} `。toUpperCase () }导出默认函数Avatar ( { children , color = '' } ) { return ( < div className = 'bg-blue-500 w-[45px] h-[45px] flex items-center justify-center rounded-完整'样式= { { backgroundColor :颜色
}}> < span className = 'font-bold text-sm text-white' > { getShortName ( children )} </ span > </ div >
)}
login成分
在这里,我们将创建用户登录组件:
// ui/components/login.js import { useState } from "react" ; async function createAccount ( { username , phone } ) { try { const url = "http://localhost:8080/users/create" ; 让result = await fetch ( url , { method : "POST" , headers : { "Content-Type" : "application/json" }, body
:JSON。stringify ({用户名,电话}) }); 返回结果。json (); } catch ( e ) {返回承诺。拒绝( e ); } } async function signIn ( { phone } ) { try { const url = "http://localhost:8080/users/phone/" + phone ; 让结果
= await fetch(url);
return result.json();
} catch (e) {
return Promise.reject(e);
}
}
export default function Login({ show, setAuth }) {
const [isShowSigIn, setShowSignIn] = useState(false);
const showSignIn = () => {
setShowSignIn(prev => !prev)
}
const FormCreateUsername = ({ setAuth }) => {
const onCreateUsername = async (e) => {
e.preventDefault();
let username = e.target.username.value;
let phone = e.target.phone.value;
if (username === "" || phone === "") {
return;
}
let res = await createAccount({ username, phone });
if (res === null) {
alert("Failed to create account");
return;
}
setAuth(res)
}
return (
<form action="" className="mt-4 space-y-2" onSubmit={onCreateUsername}>
<div>
<label className="text-sm font-light">Username</label>
<input required type="text" name="username" placeholder="John Doe"
className="w-full px-4 py-2 mt-2 border rounded-md focus:outline-none focus:ring-1 focus:ring-blue-600" />
</div>
<div>
<label className="text-sm font-light">Phone</label>
<input required type="text" name="phone" placeholder="+1111..."
className="w-full px-4 py-2 mt-2 border rounded-md focus:outline-none focus:ring-1 focus:ring-blue-600" />
</div>
<div className="flex items-baseline justify-between">
<button type="submit"
className="px-6 py-2 mt-4 text-white bg-violet-600 rounded-lg hover:bg-violet-700 w-full">Submit</button>
</div>
<div className="pt-2 space-y-2 text-center">
<p className="text-base text-gray-700">Already have a username? <button onClick={showSignIn} className="text-violet-700 font-light">Sign In</button></p>
</div>
</form>
)
}
const FormSignIn = ({ setAuth }) => {
const onSignIn = async (e) => {
e.preventDefault();
let phone = e.target.phone.value;
if (phone === "") {
return;
}
let res = await signIn({ phone });
if (res === null) {
alert("Failed to create account");
return;
}
if (!res.id) {
alert(`Phone number not found ${phone}`);
return;
}
setAuth(res)
}
return (
<form action="" className="mt-4 space-y-2" onSubmit={onSignIn}>
<div>
<label className="text-sm font-light">Phone</label>
<input required type="text" name="phone" placeholder="+1111..."
className="w-full px-4 py-2 mt-2 border rounded-md focus:outline-none focus:ring-1 focus:ring-blue-600" />
</div>
<div className="flex items-baseline justify-between">
<button type="submit"
className="px-6 py-2 mt-4 text-white bg-violet-600 rounded-lg hover:bg-violet-700 w-full">Submit</button>
</div>
<div className="pt-2 space-y-2 text-center">
<p className="text-base text-gray-700">Don't have username? <button onClick={showSignIn} className="text-violet-700 font-light">Create</button></p>
</div>
</form>
)
}
return (
<div className={`${show ? '' : 'hidden'} bg-gradient-to-b from-orange-400 to-rose-400`}>
<div className="flex items-center justify-center min-h-screen">
<div className="px-8 py-6 mt-4 text-left bg-white max-w-[400px] w-full rounded-xl shadow-lg">
<h3 className="text-xl text-slate-800 font-semibold">{isShowSigIn ? 'Log in with your phone.' : 'Create your account.'}</h3>
{isShowSigIn ? <FormSignIn setAuth={setAuth} /> : <FormCreateUsername setAuth={setAuth} />}
</分区> </分区> </分区> ) }
room成分
在这里,我们将创建聊天室组件:
// ui/components/room.js import React , { useState , useEffect } from "react" ; 从“./avatar”导入头像;异步函数getRooms (
) {尝试{ const url = "http://localhost:8080/rooms" ; let result = await fetch ( url ); 返回结果。json (); } catch ( e ) {控制台。日志( e ); 返回承诺。解决(空);} }函数ChatListItem ( { onSelect , room ,
userId , index , selectedItem } ) { const {users,created_at,last_message}=room; const active=index==selectedItem; const date= new Date (created_at); const ampm=日期。getHours()>= 12 ? '下午' : '上午' ; 固定时间
= ` $ {日期。getHours ()}:$ {日期。getMinutes ()} $ { ampm } ` const name = users ?. 过滤器(用户=>用户。id != userId )。地图(用户=>用户。用户名)[ 0 ];返回( < div onClick = {() =>
onSelect ( index , {})}
className ={` $ {活动? 'bg-[#FDF9F0] border border-[#DEAB6C]' : 'bg-[#FAF9FE] border border-[#FAF9FE]' } p - 2 rounded -[ 10px ] shadow - sm cursor - pointer ` } > < div className = 'flex justify-between items-center gap-3' > < div className = 'flex gap-3 items-center w-full' > < Avatar
> { name } </ Avatar > < div className = "w-full max-w-[150px]" > < h3 className = 'font-semibold text-sm text-gray-700' > { name } </ h3 > < p className = 'font-light text-xs text-gray-600 truncate' > { last_message } </ p > </ div > </ div > < div className =
'text-gray-400 min-w-[55px]' > < span className = 'text-xs' > {time} </ span > </ div > </ div > </ div >
)}导出默认函数ChatList ( { onChatChange , userId } ){const[data,setData]=useState([])const[isLoading,
setLoading ] = useState ( false ) const [ selectedItem , setSelectedItem ] = useState ( - 1 );
useEffect ( () => {
setLoading ( true )
getRooms () . then ( ( data ) => {
setData ( data )
setLoading ( false ) }) },
[])
const onSelectedChat = (idx, item) => {
setSelectedItem(idx)
let mapUsers = new Map();
item.users.forEach(el => {
mapUsers.set(el.id, el);
});
const users = {
get: (id) => {
return mapUsers.get(id).username;
},
get_target_user: (id) => {
return item.users.filter(el => el.id != id).map(el => el.username).join("")
}
}
onChatChange({ ...item.room, users })
}
return (
<div className="overflow-hidden space-y-3">
{isLoading && <p>Loading chat lists.</p>}
{
data.map((item, index) => {
return <ChatListItem
onSelect={(idx) => onSelectedChat(idx, item)}
房间={ { ...项目. 房间,用户:项目。users }}
index ={ index }
key = { item . 房间。id }
userId ={ userId }
selectedItem ={ selectedItem } /> }) } </ div > ) }
conversation成分
在这里,我们将创建用户对话组件:
// ui/components/conversation.js import React , { useEffect , useRef } from "react" ; import Avatar from "./avatar" function ConversationItem ( { right , content , username } ) { if ( right ) { return ( < div className = 'w-full flex justify-end' > < div className = 'flex gap-3证明结束'
> < div className = 'max-w-[65%] bg-violet-500 p-3 text-sm rounded-xl rounded-br-none' > < p className = ' text - white ' > { content } < / p > </ div > < div className = 'mt-auto' > <头像> {用户名} </头像> </ div > </ div > </ div >
)}返回(
<div className='flex gap-3 w-full'>
<div className='mt-auto'>
<Avatar color='rgb(245 158 11)'>{username}</Avatar>
</div>
<div className='max-w-[65%] bg-gray-200 p-3 text-sm rounded-xl rounded-bl-none'>
<p>{content}</p>
</div>
</div>
)
}
export default function Conversation({ data, auth, users }) {
const ref = useRef(null);
useEffect(() => {
ref.current?.scrollTo(0, ref.current.scrollHeight)
}, [data]);
return (
<div className='p-4 space-y-4 overflow-auto' ref={ref}>
{
data.map(item => {
return <ConversationItem
right={item.user_id === auth.id}
content={item.content}
username={users.get(item.user_id)}
key={item. id } /> })} </ div > )}
现在让我们准备与我们的 WebSocket 服务器和 REST API 服务器交互所需的 Hooks。
useWebsocket钩
这个 Hook 用于连接到 WebSocket 服务器,使我们能够发送和接收消息:
// ui/libs/websocket.js import { useEffect , useRef } from "react" ; 导出默认函数useWebsocket ( onMessage ) { const ws = useRef ( null );
useEffect ( () => { if ( ws . current !== null ) return ; const wsUri = 'ws://localhost:8080/ws' ;
ws .
当前=新的WebSocket ( wsUri );
ws 。当前。onopen = () =>控制台。日志(“ws打开”);
ws 。当前。onclose = () =>控制台。日志(“WS关闭”); 常量wsCurrent = ws 。当前; 返回() => {
wsCurrent 。关闭
(); }; }, []);
useEffect ( () => { if (! ws . current ) return ;
ws . current . onmessage = e => {
onMessage ( e . data ) }; }, []); const sendMessage = ( msg ) = > { if ( ! ws.current ) return ;
ws 。当前。发送(消息);}返回发送消息;}
useLocalStorage钩
这个 Hook 使我们能够从 localStorage 获取用户数据:
// ui/libs/useLocalStorage import { useEffect , useState } from "react" ; 导出默认函数useLocalStorage ( key , defaultValue ) { const [ storedValue , setStoredValue ] = useState ( defaultValue ); const setValue = ( value ) => {尝试{ const valueToStore = value instanceof
功能?值(存储值):值;
setStoredValue ( valueToStore ); 如果(窗口类型!== “未定义”){窗口。本地存储。setItem ( key , JSON . stringify ( valueToStore )); } }赶上(错误){ } };
useEffect ( () => {尝试
{ const项目=窗口。本地存储。获取项目(关键);让数据=项目?JSON。解析(项目):默认值;
setStoredValue (数据) } catch (错误) {} }, []) return [ storedValue , setValue ]; }
useConversation钩
我们将使用这个 Hook 来获取基于给定房间的对话id:
从“反应”导入{ useEffect ,useState } ;const fetchRoomData = async ( room_id ) => { if (! room_id ) return ; const url = ` http : //localhost:8080/conversations/ ${room_id} ` ; try { let resp = await fetch ( url ). 然后(资源=>资源。
json ()); 返回响应;} catch ( e ) {控制台。日志( e ); } }导出默认函数useConversations ( room_id ) { const [ isLoading , setIsLoading ] = useState ( true ); const [消息, setMessages ] = useState ([]); const更新消息=
( resp = [] ) => {
setIsLoading ( false );
setMessages ( resp ) } const fetchConversations = ( id ) => {
setIsLoading ( true )
fetchRoomData ( id )。然后( updateMessages ) }
useEffect ( () => fetchConversations ( room_id ), []); 返回[
isLoading ,消息, setMessages , fetchConversations ]; }
构建聊天应用程序
现在,让我们连接所有组件和 Hooks,以使用 Next.js 在 React 中构建我们的聊天应用程序。
首先,让我们导入我们需要的所有依赖项:
// ui/pages/index.js import Head from 'next/head' import React , { useEffect , useState } from 'react' import Avatar from '../components/avatar' import ChatList from '../components/rooms '从' ../components/conversation'导入对话从' ../components/login'导入登录从'../libs/useConversation'导入useLocalStorage从
'../libs/useLocalStorage'从'../libs/useWebsocket'导入useWebsocket
现在,让我们为聊天页面设置状态:
// ui/pages/index.js ...导出默认函数Home (
) { const [房间, setSelectedRoom ] = useState ( null ); const [ isTyping , setIsTyping ] = useState ( false ); const [ showLogIn , setShowLogIn ] = useState ( false ); const [ auth , setAuthUser ] = useLocalStorage ( "user" , false ); 常量[
isLoading ,消息, setMessages , fetchConversations ] = useConversations ( "" ); ... }
以下函数将处理所有传入或传出 WebSocket 服务器的消息:
handleTyping:更新状态以显示打字指示器
handleMessage: 处理传入和传出消息到状态
onMessage:处理从 WebSocket 服务器检索到的消息
updateFocus: 告诉 WebSocket 服务器当前用户是否仍在输入消息
onFocusChange: 让 WebSocket 服务器知道当前用户何时完成输入
submitMessage:更新消息状态,然后在用户点击发送按钮时将消息发送到服务器
以下是我们将如何在代码中使用这些函数:
// ui/pages/index.js const handleTyping = ( mode ) => { if ( mode === "IN" ) {
setIsTyping ( true ) } else {
setIsTyping ( false ) } } const handleMessage = ( msg , userId ) => {
setMessages ( prev => { const item = {内容:
味精, user_id : userId }; 返回[...上一个,项目]; }) } const onMessage = ( data ) => { try {让messageData = JSON . 解析(数据);switch ( messageData.chat_type ) { case " TYPING " : {
handleTyping ( messageData.value [ _ _
0]);
return;
}
case "TEXT": {
handleMessage(messageData.value[0], messageData.user_id);
return;
}
}
} catch (e) {
console.log(e);
}
}
const sendMessage = useWebsocket(onMessage)
const updateFocus = () => {
const data = {
id: 0,
chat_type: "TYPING",
value: ["IN"],
room_id: room.id,
user_id: auth.id
}
sendMessage(JSON.stringify(data))
}
const onFocusChange = () => {
const data = {
id: 0,
chat_type: "TYPING",
value: ["OUT"],
room_id: room.id,
user_id: auth.id
}
sendMessage(JSON.stringify(data))
}
const submitMessage = (e) => {
e.preventDefault();
let message = e.target.message.value;
if (message === "") {
return;
}
if (!room.id) {
alert("Please select chat room!")
return
}
const data = {
id: 0,
chat_type: "TEXT",
value: [message],
room_id: room.id,
user_id: auth. id
}
sendMessage ( JSON . stringify ( data ))
e . 目标。留言。值= "" ;
handleMessage (消息, auth . id );
onFocusChange (); }
我们将使用以下函数来处理更新消息以及用户登录和注销的状态:
updateMessagesid:当用户切换聊天室时获取给定房间的对话
signOut:将状态更新为注销并从本地存储中删除用户数据
我们将在代码中使用这些函数,如下所示:
// ui/pages/index.js const updateMessages = ( data ) => { if (! data . id ) return ;
fetchConversations ( data.id )
setSelectedRoom ( data ) } const signOut = ( ) = > { window . 本地存储。removeItem (“用户”);
设置授权用户(假);}
使用效果
( () => setShowLogIn (! auth ), [ auth ])
现在,让我们向客户端显示所有数据:
return (
<div>
<Head>
<title>Rust with react chat app</title>
<meta name="description" content="Rust with react chat app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<Login show={showLogIn} setAuth={setAuthUser} />
<div className={`${!auth && 'hidden'} bg-gradient-to-b from-orange-400 to-rose-400 h-screen p-12`}>
<main className='flex w-full max-w-[1020px] h-[700px] mx-auto bg-[#FAF9FE] rounded-[25px] backdrop-opacity-30 opacity-95'>
<aside className='bg-[#F0EEF5] w-[325px] h-[700px] rounded-l-[25px] p-4 overflow-auto relative'>
<ChatList onChatChange={updateMessages} userId={auth.id} />
<button onClick={signOut} className='text-xs w-full max-w-[295px] p-3 rounded-[10px] bg-violet-200 font-semibold text-violet-600 text-center absolute bottom-4'>LOG OUT</button>
</aside>
{room?.id && (<section className='rounded-r-[25px] w-full max-w-[690px] grid grid-rows-[80px_minmax(450px,_1fr)_65px]'>
<div className='rounded-tr-[25px] w-ful'>
<div className='flex gap-3 p-3 items-center'>
<Avatar color='rgb(245 158 11)'>{room.users.get_target_user(auth.id)}</Avatar>
<div>
<p className='font-semibold text-gray-600 text-base'>{room.users.get_target_user(auth.id)}</p> < div className = 'text-xs text-gray-400' >{ isTyping ? “正在打字……” :“上午 10 点 15 分” </div></div></div> < hr className = ' bg - [ #F0EEF5] ' / > </div> { ( isLoading && room . id ) && < p className = "px-4 text-slate-500" >加载对话...<
data ={ messages } auth ={ auth } users = { room . users } /> < div className = 'w-full' > < form onSubmit ={ submitMessage } className = 'flex gap-2 items-center rounded-full border border-violet-500 bg-violet-200 p-1 m- 2' > <输入
onBlur ={ onFocusChange }
onFocus ={ updateFocus }
name = "message"
className = 'p-2 placeholder-gray-600 text-sm w-full rounded-full bg-violet-200 focus:outline-none' placeholder
= ' Type你的消息在这里...' /> < button type = 'submit' className = 'bg-violet-500 rounded-full py-2 px-6 font-semibold text-white text-sm' > Sent < / button > < /表格></
div > </ section >)} </ main > </ div > </ div > )
结论
在本文中,我们讨论了 WebSockets 的特性、它在 Rust 中的应用,以及如何将它与包一起使用。我们演示了如何创建高效的实时聊天应用程序,使用 React 和 Next.js 建立到 Actix Web 服务器的 WebSocket 连接。本文中的代码可在GitHub上获得。