本文分享如何使用 Solid JS + Node JS + Websockets 创建一个聊天应用程序。
流程:
- 处理房间
- 处理消息传递
先处理房间,因为需要它们来进行消息传递。
房间
先给房间取个名字,在这里使用的是 Prisma,你可以使用任何你想要的东西。
这是 Prisma 架构文件:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model Room {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String @db.VarChar(255)
}
对于 HTTP 部分,这里选择使用 Express,它非常简单直观。下面是控制器的代码:
import { Router } from "express";
import { prisma } from "../..";
export const RoomRouter = Router();
export enum MessageType {
Text,
}
RoomRouter.get("/", async (req, res) => {
const _rooms = prisma.room.findMany();
const _count = prisma.room.count();
const [rows, count] = await Promise.all([_rooms, _count]);
res.json({
rows,
count,
});
});
RoomRouter.post("/", async (req, res) => {
const room = await prisma.room.create({ data: { name: req.body.name } });
res.status(201).json({ entity: room, message: `Room '${room.name} created successfully.` });
});
这里没有验证或错误处理,也没有测试,这是一个玩具应用程序,所以我不担心这个。
在真正的应用程序中,我会在这里引入一层间接层,即服务抽象层,然后控制器将与该服务交互。例如:
interface IRoomService {
getRooms(opts?: Opts): Promise<GetResult<Room, string>>,
createRoom(data: CreateRoom): Promise<CreateResult<CreateRoom, string>>
}
type Room = {
id: number;
createdAt: Date;
updatedAt: Date;
name: string;
}
// These are the filters
type Opts = {
name?: string
}
// This is somewhat inspired by Rust's result enum
type GetResult<Entity, Err> = {
error: Err | null;
data: {
rows: Entity[];
count: number;
message: string;
}
}
type CreateResult<Entity, Err> = {
error: Err | null;
data: {
message: string;
entity: Entity;
}
}
type CreateRoom = {
name: string;
}
// A class will then implement this service, like so:
// This is an in-memory storage that leverages the singleton
// pattern, but you can imagine this can be anything. It makes the
// code testable as well. Right now, this service has no external
// services as dependencies so this is very easy to test.
class RoomService implements IRoomService {
rooms: Room[] = [];
private static instance: RoomService;
static getInstance() {
if (!RoomService.instance) {
RoomService.instance = new RoomService();
}
return RoomService.instance;
}
async getRooms(opts?: Opts): Promise<GetResult<Room, string>> {
return {
error: null,
data: {
rows: RoomService.getInstance().rooms,
count: RoomService.getInstance().rooms.length,
message: "Room retrieved successfully",
},
};
}
async createRoom(data: CreateRoom): Promise<CreateResult<CreateRoom, string>> {
const roomsLength = RoomService.getInstance().rooms.length;
const newRoom = {
id: roomsLength + 1,
createdAt: new Date(),
updatedAt: new Date(),
name: data.name,
};
RoomService.getInstance().rooms.push(newRoom);
return {
error: null,
data: {
entity: newRoom,
message: `Room ${data.name} created successfully.`,
},
};
}
}
但我们不会这么做,现在这样就很好。
接下来是前端
export interface IRoomService {
getRooms: (opts: GetRoomOpts) => Promise<GetResult<Room, GetRoomError>>;
createRoom: (data: CreateRoom) => Promise<CreateResult<Room, CreateRoomErr>>;
}
export class RoomService implements IRoomService {
private static instance: RoomService | null = null;
static getInstance(): RoomService {
if (RoomService.instance) {
return RoomService.instance;
} else {
RoomService.instance = new RoomService();
return RoomService.instance;
}
}
getRooms = async (_opts: GetRoomOpts): Promise<GetResult<Room, GetRoomError>> => {
try {
const response = await axios.get(API + "/room", { withCredentials: true });
return {
rows: response.data.rows,
count: response.data.count,
error: null,
};
} catch (err: any) {
return {
rows: [],
count: 0,
error: err?.response?.data?.message || "Something went wrong",
};
}
};
createRoom = async (data: CreateRoom): Promise<CreateResult<Room, CreateRoomErr>> => {
try {
const response = await axios.post(API + "/room", data, { withCredentials: true });
return {
entity: response.data.entity,
error: null,
message: response.data.message,
};
} catch (err: any) {
return {
message: err?.response?.data?.message || "Something went wrong.",
entity: null,
error: err?.response?.data?.message || "Something went wrong.",
};
}
};
}
这是前端的服务部分,在此之前,我在前端使用了我上面描述的方法。我知道这不一致,但没关系。这项服务由用户界面使用,我稍后会展示用户界面代码,但这才是真正的问题。
消息传递
下图将有助于解释消息传递的工作原理:
后端如下:
io.on("connection", (socket) => {
// Listen to join room event only when connected
socket.on("join-room", async (data: JoinRoomMessage) => {
await socket.join(data.room.toString());
// Emit ACK, because this can fail, and if does,
// the client can know
socket.emit("join-room-ack", { success: true, user: data.user } as JoinRoomAck);
});
// when a message is received from a client, it's sent to all clients
// in the room (socket io has default rooms support).
// Because the client sending the message is in the same room, it also
// receives this message, it adds it to the UI after receiving this
// event
socket.on("message", (msg: Message) => {
io.in(msg.targetRoom.toString()).emit("room-message", msg);
});
socket.on("disconnect", (reason, description) => {
socket.disconnect(true);
});
});
前端使用订阅者机制,就像 RxJS 风格的订阅者一样。当一个房间加入时,会传递一个 onMessage 处理程序,然后,当服务收到消息时,会调用其所有订阅者。
从技术上讲,我们也可以在这里利用 Solid 的反应模型。这就是我要说的,这样做在 React 中不会有问题,但在 Solid 中完全没问题。
// MessageService.ts
export const [messages, setMessages] = createStore<MessageList>([]);
class MessageService implements IMessageService {
constructor() {
// ...
this.socket.on("room-message", (msg) => {
setMessages([...messages, msg])
})
}
}
但我们不会再这样做了,下面是实际的工作:
import { Socket, io } from "socket.io-client";
export interface IMessageService {
joinRoom: (room: number, user: User, onMessage: NotifyFn) => void;
sendMessage: (message: Message) => void;
}
type NotifyFn = (message: Message) => void;
export enum MessageType {
Text,
}
export class MessageService implements IMessageService {
socket: Socket | null = null;
subscribers: NotifyFn[] = [];
private static instance: MessageService | null = null;
static getInstance(): MessageService {
if (MessageService.instance) {
return MessageService.instance;
} else {
MessageService.instance = new MessageService();
return MessageService.instance;
}
}
// A helper function
createJoinRoomMessage(room: number, user: User) {
return {
room,
user,
};
}
// This is the main function
joinRoom(room: number, user: User, onMessage: NotifyFn) {
// Connect to backend
const socket = io("http://localhost:8000");
socket.connect();
// Join room message
socket.emit("join-room", this.createJoinRoomMessage(room, user));
// On success ACK, add the callback to the subscribers list
socket.on("join-room-ack", (ack: JoinRoomAck) => {
if (ack.success) {
this.socket = socket;
this.subscribers.push(onMessage);
}
});
// Then, when we receive a message, notify all the subscribers
socket.on("room-message", (msg: Message) => {
this.subscribers.forEach((fn) => fn(msg));
});
}
// Pretty simple
sendMessage(message: Message) {
if (this.socket) {
this.socket.emit("message", message);
}
}
}
你可能已经注意到,这项服务也使用了单例模式。
我们所做的只是构建一个聊天应用程序,它可以在任何地方使用。用户界面可以使用任何你喜欢的框架,甚至是普通的 Javascript。我们的业务逻辑与用户界面没有任何关联,这一点非常了不起。
这里有前端代码和后端代码的链接。它还使用了一点 tailwind,除此之外,考虑到应用程序的性质,它非常简单,这也是我懒得把它放在这里的原因,因为它非常琐碎。我就不浪费你们的时间了。
作者:Shahid Khan
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/im/37183.html