使用 Express、TypeScript、Socket.IO 和 Next.js 构建实时聊天室应用程序

在本教程中,我们将创建一个实时聊天室应用程序。我们将把它分为两个主要部分:

后端:使用 Express、TypeScript、Socket.IO 和 MongoDB。
前端:使用 Next.js 和 TailwindCSS。

第 1 部分:后端(Express、TypeScript、Socket.IO 和 MongoDB)

1.1 设置后端

首先,让我们为后端初始化一个新的 Node.js 项目。我们将使用 Express 构建 API,使用 TypeScript 实现类型安全,使用 Socket.IO 进行实时通信,并使用 MongoDB 进行数据存储。

步骤 1:初始化项目

运行以下命令初始化项目并安装必要的依赖项:

mkdir chat-app-backend 
cd chat-app-backend 
npm init -y 
npm install express socket.io mongoose dotenv cors 
npm install typescript ts-node-dev @types/express @types/socket.io @types/node --save-dev

然后,在 package.json 文件中,更新脚本:

"scripts": {
    "dev": "ts-node-dev --respawn --transpile-only api/index.ts",
    "build": "tsc",
    "start": "node dist/index.js",
  },

接下来,通过运行以下命令设置 TypeScript:

npx tsc --init

现在,更新您的 tsconfig.json,确保为您的项目正确配置:

{ 
  “compilerOptions” : { 
    “target” : “ES6” ,
    “module” : “commonjs” ,
    “strict” : true,
    “esModuleInterop” : true,
    “skipLibCheck” : true,
    “forceConsistentCasingInFileNames” : true,
    “outDir” : “。/dist” ,
    “rootDir” : “。/api” 
  } ,
  “include” : [ “api/**/*” , “api/index.ts” ] ,
  “exclude” : [ “node_modules” ] 
}

步骤 2 :项目结构

为后端创建以下文件夹结构:

chat-app-backend/
├── api/
│   ├── configs/
│   ├── controllers/
│   ├── exceptions/
│   ├── models/
│   ├── routes/
│   ├── services/
│   ├── app.ts
│   └── index.ts
├── .env
├── package.json
└── tsconfig.json

步骤3:MongoDB配置

configs/文件夹中,创建一个mongo.config.ts文件来处理 MongoDB 连接:

import mongoose from 'mongoose';

const connectMongo = async () => {
  try {
    await mongoose.connect(process.env.MONGO_URI as string);
    console.log('MongoDB connected');
  } catch (error) {
    console.error('MongoDB connection error:', error);
  }
};

export default connectMongo;

.env文件中,添加 MongoDB 连接字符串:

PORT=5000
MONGO_URI=mongodb://localhost:27017/chat-app

步骤4:创建用户和消息模型

接下来,我们将使用 Mongoose定义我们的用户消息模型。

user.model.ts

import mongoose, { Schema, Document } from 'mongoose';

export interface IUser extends Document {
  name: string;
  socketId: string;
  createdAt: Date;
}

const UserSchema: Schema = new Schema({
  name: { type: String, required: true },
  socketId: { type: String, required: true },
  createdAt: { type: Date, default: Date.now },
});

export const User = mongoose.model<IUser>('User', UserSchema);

message.model.ts:

import mongoose, { Schema, Document } from 'mongoose';

export interface IMessage extends Document {
  senderId: string;
  text: string;
  createdAt: Date;
}

const MessageSchema: Schema = new Schema({
  roomId: { type: String, required: true },
  senderId: { type: String, required: true },
  username: { type: String, required: true },
  text: { type: String, required: true },
  createdAt: { type: Date, default: Date.now },
});

export const Message = mongoose.model<IMessage>('Message', MessageSchema);

room.model.ts:

import mongoose, { Schema, Document } from 'mongoose';

export interface IRoom extends Document {
  name: string;
  participants: string[];
}

const RoomSchema = new Schema<IRoom>({
  name: { type: String, required: true, unique: true },
  participants: [{ type: String, required: true }],
});

export const Room = mongoose.model<IRoom>('Room', RoomSchema);

步骤 5:用户和消息的服务层

现在,让我们在与模型交互的 services/ 中实现服务逻辑。

user.service.ts

import { IUser, User } from '../models/user.model';

export const createOrUpdateUser = async (
  name: string,
  socketId: string,
): Promise<IUser> => {
  const existingUser = await User.findOne({ name });

  if (existingUser) {
    existingUser.socketId = socketId;
    return await existingUser.save();
  }

  const newUser = new User({ name, socketId });
  return await newUser.save();
};

export const deleteUserBySocketId = async (socketId: string): Promise<void> => {
  await User.deleteOne({ socketId });
};

message.service.ts:

import { Message, IMessage } from '../models/message.model';

export const createMessage = async (
  roomId: string,
  senderId: string,
  username: string,
  text: string,
): Promise<IMessage> => {
  const message = new Message({ roomId, senderId, username, text });
  return await message.save();
};

export const getMessagesByRoomId = async (
  roomId: string,
): Promise<IMessage[]> => {
  return await Message.find({ roomId });
};

room.service.ts:

import { Room } from '../models/room.model';

export const createRoom = async (roomName: string) => {
  const room = new Room({ name: roomName });
  await room.save();
  return room;
};

export const getAllRooms = async () => {
  return await Room.find();
};

export const addUserToRoom = async (roomId: string, userId: string) => {
  const room = await Room.findById(roomId);
  if (room) {
    room.participants.push(userId);
    await room.save();
  }
};

步骤6:Socket.IO控制器

controllers/文件夹中,创建socket.controller.ts以处理所有套接字事件。

socket.controller.ts

import { Server, Socket } from 'socket.io';
import * as userService from '../services/user.service';
import * as messageService from '../services/message.service';

export const handleSocketConnection = (io: Server) => {
  io.on('connection', (socket: Socket) => {
    console.log(`User connected: ${socket.id}`);

    socket.on('join', async ({ name, roomId }) => {
      await userService.createOrUpdateUser(name, socket.id);
      socket.join(roomId);
      console.log(`${name} joined room: ${roomId}`);
    });

    socket.on('message', async ({ roomId, senderId, username, text }) => {
      const message = await messageService.createMessage(
        roomId,
        senderId,
        username,
        text,
      );
      io.to(roomId).emit('message', message);
    });

    socket.on('disconnect', async () => {
      await userService.deleteUserBySocketId(socket.id);
      console.log(`User disconnected: ${socket.id}`);
    });
  });
};

步骤7:房间控制器

controllers/文件夹中,创建room.controller.ts以处理所有房间聊天功能。

room.controller.ts

import { Request, Response } from 'express';
import * as userService from '../services/user.service';
import * as messageService from '../services/message.service';
import * as roomService from '../services/room.service';

export const joinChat = async (req: Request, res: Response): Promise<void> => {
  try {
    const { name, socketId, roomId } = req.body;
    if (!name || !socketId || !roomId) {
      res
        .status(400)
        .json({ message: 'Name, socketId, and roomId are required' });
      return;
    }

    const user = await userService.createOrUpdateUser(name, socketId);
    await roomService.addUserToRoom(roomId, user._id as string);
    res.status(201).json({ message: 'User joined', user });
  } catch (error) {
    console.error('Error in joinChat:', error);
    res.status(500).json({ message: 'Internal server error' });
  }
};

export const sendMessage = async (
  req: Request,
  res: Response,
): Promise<void> => {
  try {
    const { senderId, roomId, username, text } = req.body;
    if (!senderId || !roomId || !username || !text) {
      res.status(400).json({ message: 'All fields are required' });
      return;
    }

    const message = await messageService.createMessage(
      roomId,
      senderId,
      username,
      text,
    );
    res.status(201).json({ message: 'Message sent', data: message });
  } catch (error) {
    console.error('Error in sendMessage:', error);
    res.status(500).json({ message: 'Internal server error' });
  }
};

export const getRoomMessages = async (
  req: Request,
  res: Response,
): Promise<void> => {
  try {
    const { roomId } = req.params;
    const messages = await messageService.getMessagesByRoomId(roomId);
    res.status(200).json(messages);
  } catch (error) {
    console.error('Error in getRoomMessages:', error);
    res.status(500).json({ message: 'Internal server error' });
  }
};

export const getRooms = async (req: Request, res: Response): Promise<void> => {
  try {
    const rooms = await roomService.getAllRooms();
    res.status(200).json(rooms);
  } catch (error) {
    console.error('Error in getRooms:', error);
    res.status(500).json({ message: 'Internal server error' });
  }
};

export const createRoom = async (
  req: Request,
  res: Response,
): Promise<void> => {
  try {
    const { roomName } = req.body;
    if (!roomName) {
      res.status(400).json({ message: 'All fields are required' });
      return;
    }

    const message = await roomService.createRoom(roomName);
    res.status(201).json({ message: 'Message sent', data: message });
  } catch (error) {
    console.error('Error in createRoom:', error);
    res.status(500).json({ message: 'Internal server error' });
  }
};

步骤 8:设置Exception

exceptions/文件夹中,创建httpException.ts用于处理来自 http 的异常。

httpException.ts

export class HttpException extends Error {
  status: number;
  message: string;

  constructor(status: number, message: string) {
    super(message);
    this.status = status;
    this.message = message;
  }
}

步骤 9:设置 Express App

app.ts中,设置 Express 服务器配置:

app.ts

import express, { Application, NextFunction, Request, Response } from 'express';
import { createServer } from 'http';
import dotenv from 'dotenv';
import cors from 'cors';
import { Server as SocketIOServer } from 'socket.io';
import connectMongo from './configs/mongo.config';
import chatRoutes from './routes/chat.routes';
import { handleSocketConnection } from './controllers/socket.controller';

connectMongo();
dotenv.config();

const app: Application = express();
const httpServer = createServer(app);
const io = new SocketIOServer(httpServer, {
  cors: {
    origin: 'http://localhost:3000',
    methods: ['GET', 'POST'],
    credentials: true,
  },
});

app.use(express.json());
app.use(
  cors({
    origin: 'http://localhost:3000',
    methods: ['GET', 'POST'],
    credentials: true,
  }),
);

handleSocketConnection(io);

app.use('/api/chat', chatRoutes);

// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});

export { httpServer };

步骤 10:创建主服务器(index.ts)

index.ts中,将Socket.IOHTTP集成并启动服务器:

index.ts

import 'dotenv/config';
import { httpServer } from './app';
const PORT = process.env.PORT || 5000;

httpServer.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

1.2 运行后端

现在一切都已设置好,运行后端:

npm run dev

您的后端现已运行,并通过Socket.IO启用实时通信。

第 2 部分:前端(Next.js、TailwindCSS、Socket.IO 客户端)

2.1 设置前端

步骤 1:初始化 Next.js 项目

接下来,使用Next.js 和 TypeScript 创建前端:

npx create-next-app@latest chat-app-frontend --typescript
cd chat-app-frontend
npm install socket.io-client axios moment sonner react-icons

步骤 2:创建 API 和套接字客户端

src/global/文件夹中,创建两个文件:apiClient.tssocketClient.ts

apiClient.ts

import axios from "axios";

const apiClient = axios.create({
  baseURL: "http://localhost:5000/api/chat",
  withCredentials: true,
  timeout: 10000,
  headers: {
    "Content-Type": "application/json",
  },
});

export default apiClient;

socketClient.ts:

import { io, Socket } from "socket.io-client";

const socketClient: Socket = io("http://localhost:5000", {
  withCredentials: true,
});

export default socketClient;

2.2 创建聊天页面

现在,在应用程序中创建聊天界面/page.tsx

app/page.tsx

"use client";
import apiClient from "@/global/apiClient";
import socketClient from "@/global/socketClient";
import { useRouter } from "next/navigation";
import React, { useEffect, useState } from "react";

interface Room {
  id: string;
  name: string;
}

const LoginPage: React.FC = () => {
  const [name, setName] = useState("");
  const [rooms, setRooms] = useState<Room[]>([]);
  const [selectedRoomId, setSelectedRoomId] = useState("");
  const history = useRouter();

  const fetchRooms = async () => {
    try {
      const response = await apiClient.get("/rooms");
      if (!response.data) {
        throw new Error("Failed to fetch rooms");
      }
      setRooms(response.data);
    } catch (error) {
      console.error("Error fetching rooms:", error);
    }
  };

  useEffect(() => {
    fetchRooms();
  }, []);

  const handleJoinRoom = () => {
    if (name && selectedRoomId) {
      socketClient.emit("join", { name, roomId: selectedRoomId });
      localStorage.setItem("username", name);
      history.push(`/chats/${selectedRoomId}`);
    }
  };

  return (
    <div className="flex flex-col items-center justify-center h-screen bg-gray-800 text-white">
      <h1 className="text-4xl mb-4">Join Chat Room</h1>
      <input
        type="text"
        placeholder="Enter your name"
        className="mb-4 px-6 py-4 rounded bg-gray-700 text-white"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <select
        key={selectedRoomId}
        className="mb-4 px-16 py-4 rounded bg-gray-700 text-white"
        value={selectedRoomId}
        onChange={(e) => setSelectedRoomId(e.target.value)}
      >
        <option value="" disabled>
          Select Room
        </option>
        {rooms.map((room) => (
          <option key={room.id} value={room.id}>
            {room.name}
          </option>
        ))}
      </select>
      <button
        onClick={handleJoinRoom}
        className="bg-blue-600 hover:bg-blue-500 text-white font-bold py-2 px-4 rounded"
      >
        Join Room
      </button>
    </div>
  );
};

export default LoginPage;

然后在layout.tsx中添加<Toaster />。

app/layout.tsx

import type { Metadata } from "next";
import "./globals.css";
import { Toaster } from "sonner";

export const metadata: Metadata = {
  title: "Chat App",
  description: "Chat App By Adi Munawar",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        {children}
        <Toaster />
      </body>
    </html>
  );
}

2.3 运行前端

运行 Next.js 应用程序:

npm run dev

如果你已经运行该项目,它将看起来像这样:

使用 Express、TypeScript、Socket.IO 和 Next.js 构建实时聊天室应用程序
ui 聊天应用程序登录
使用 Express、TypeScript、Socket.IO 和 Next.js 构建实时聊天室应用程序

我们使用 Express、TypeScript、Socket.IO 和 MongoDB(后端)以及 Next.js 和 TailwindCSS(前端)成功构建了一个实时聊天应用程序。我们还介绍了如何使用 Socket.IO 处理实时通信,并采用面向服务的方法确保项目结构简洁。这种设置可实现可扩展和可维护的开发。

您可以根据需要定制和扩展本项目,添加身份验证、房间创建、私人消息等功能。

作者:D2Y MVN

本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/im/53038.html

(0)

相关推荐

发表回复

登录后才能评论