在本教程中,我们将创建一个实时聊天室应用程序。我们将把它分为两个主要部分:
后端:使用 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.IO与HTTP集成并启动服务器:
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.ts
和socketClient.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 和 MongoDB(后端)以及 Next.js 和 TailwindCSS(前端)成功构建了一个实时聊天应用程序。我们还介绍了如何使用 Socket.IO 处理实时通信,并采用面向服务的方法确保项目结构简洁。这种设置可实现可扩展和可维护的开发。
您可以根据需要定制和扩展本项目,添加身份验证、房间创建、私人消息等功能。
作者:D2Y MVN
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/im/53038.html