使用 Solid JS + Node JS + Websockets 创建聊天应用

本文分享如何使用 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.",
            };
        }
    };
}

这是前端的服务部分,在此之前,我在前端使用了我上面描述的方法。我知道这不一致,但没关系。这项服务由用户界面使用,我稍后会展示用户界面代码,但这才是真正的问题。

消息传递

下图将有助于解释消息传递的工作原理:

使用 Solid JS + Node JS + Websockets 创建聊天应用
工作管道

后端如下:

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

(0)

相关推荐

发表回复

登录后才能评论