实时聊天应用
构建一个完整的实时聊天应用,使用 WebSocket 或 GraphQL 订阅,具备以下功能:
- 好友列表:可以查看好友,添加/删除好友,显示好友在线状态。
- 群聊列表:支持发起群聊、显示群聊成员在线状态,群聊管理(群主/管理员解散群聊、踢人等功能)。
- 实时消息:好友和群聊中的消息可以实时同步。
1. 技术栈
- 前端:React + Apollo Client(GraphQL)/WebSocket + Ant Design(UI 框架)
- 后端:Node.js + GraphQL + WebSocket + MongoDB
- 数据库:MongoDB 存储用户、聊天、群组等信息
- 身份验证:JWT(用于用户登录和权限验证)
2. 项目结构
text
/client # 前端应用
/src
/components # React 组件
/graphql # GraphQL 订阅与查询
/pages # 页面路由
/styles # 样式
/server # 后端应用
/models # Mongoose 数据模型
/resolvers # GraphQL 解析器
/subscriptions # GraphQL 订阅
/schema # GraphQL 模式定义
/websockets # WebSocket 实现3. 数据库设计(MongoDB)
用户模型(User)
js
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
password: { type: String, required: true },
friends: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
online: { type: Boolean, default: false },
});
module.exports = mongoose.model('User', UserSchema);群聊模型(Group)
js
const mongoose = require('mongoose');
const GroupSchema = new mongoose.Schema({
name: { type: String, required: true },
members: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
admins: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
owner: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
});
module.exports = mongoose.model('Group', GroupSchema);消息模型(Message)
js
const mongoose = require('mongoose');
const MessageSchema = new mongoose.Schema({
sender: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
content: { type: String, required: true },
group: { type: mongoose.Schema.Types.ObjectId, ref: 'Group' },
private: { type: Boolean, default: false },
recipients: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
timestamp: { type: Date, default: Date.now },
});
module.exports = mongoose.model('Message', MessageSchema);4. GraphQL 模式定义
js
const { gql } = require('apollo-server');
const typeDefs = gql`
type User {
id: ID!
username: String!
online: Boolean!
friends: [User]
}
type Group {
id: ID!
name: String!
members: [User]
admins: [User]
owner: User!
}
type Message {
id: ID!
sender: User!
content: String!
group: Group
private: Boolean
recipients: [User]
timestamp: String!
}
type Query {
currentUser: User
friends: [User]
groups: [Group]
}
type Mutation {
addFriend(friendId: ID!): User
removeFriend(friendId: ID!): User
createGroup(name: String!): Group
sendMessage(content: String!, groupId: ID, recipientId: ID): Message
leaveGroup(groupId: ID!): Group
deleteGroup(groupId: ID!): Group
}
type Subscription {
messageReceived(groupId: ID): Message
userStatusChanged(userId: ID!): User
}
`;
module.exports = typeDefs;5. GraphQL 解析器
用户解析器
js
const User = require('./models/User');
const Group = require('./models/Group');
const Message = require('./models/Message');
const { PubSub } = require('apollo-server');
const pubsub = new PubSub();
const MESSAGE_RECEIVED = 'MESSAGE_RECEIVED';
const USER_STATUS_CHANGED = 'USER_STATUS_CHANGED';
const resolvers = {
Query: {
currentUser: async (_, args, { userId }) => {
return await User.findById(userId);
},
friends: async (_, args, { userId }) => {
const user = await User.findById(userId).populate('friends');
return user.friends;
},
groups: async (_, args, { userId }) => {
return await Group.find({ members: userId }).populate('members');
},
},
Mutation: {
addFriend: async (_, { friendId }, { userId }) => {
const user = await User.findById(userId);
const friend = await User.findById(friendId);
user.friends.push(friendId);
friend.friends.push(userId);
await user.save();
await friend.save();
return friend;
},
removeFriend: async (_, { friendId }, { userId }) => {
const user = await User.findById(userId);
user.friends = user.friends.filter((id) => id.toString() !== friendId);
await user.save();
return user;
},
createGroup: async (_, { name }, { userId }) => {
const group = new Group({ name, owner: userId, members: [userId], admins: [userId] });
await group.save();
return group;
},
sendMessage: async (_, { content, groupId, recipientId }, { userId }) => {
let message;
if (groupId) {
const group = await Group.findById(groupId);
message = new Message({
sender: userId,
content,
group: groupId,
});
group.members.forEach((member) => {
pubsub.publish(MESSAGE_RECEIVED, { messageReceived: message });
});
} else {
message = new Message({
sender: userId,
content,
recipients: [recipientId],
private: true,
});
pubsub.publish(MESSAGE_RECEIVED, { messageReceived: message });
}
await message.save();
return message;
},
leaveGroup: async (_, { groupId }, { userId }) => {
const group = await Group.findById(groupId);
group.members = group.members.filter((id) => id.toString() !== userId);
await group.save();
return group;
},
deleteGroup: async (_, { groupId }, { userId }) => {
const group = await Group.findById(groupId);
if (group.owner.toString() === userId) {
await group.remove();
return group;
}
throw new Error('Only the group owner can delete the group.');
},
},
Subscription: {
messageReceived: {
subscribe: (_, { groupId }) => pubsub.asyncIterator(MESSAGE_RECEIVED),
},
userStatusChanged: {
subscribe: (_, { userId }) => pubsub.asyncIterator(USER_STATUS_CHANGED),
},
},
};
module.exports = resolvers;6. 前端实现
用户界面设计(React + Ant Design)
jsx
import React, { useState, useEffect } from 'react';
import { useQuery, useMutation, useSubscription } from '@apollo/client';
import { Layout, Menu, Input, Button } from 'antd';
import { FRIENDS_QUERY, GROUPS_QUERY, SEND_MESSAGE_MUTATION, MESSAGE_RECEIVED_SUBSCRIPTION } from './graphql/queries';
const { Sider, Content } = Layout;
const ChatApp = () => {
const { data: friendsData } = useQuery(FRIENDS_QUERY);
const { data: groupsData } = useQuery(GROUPS_QUERY);
const [sendMessage] = useMutation(SEND_MESSAGE_MUTATION);
const { data: newMessage } = useSubscription(MESSAGE_RECEIVED_SUBSCRIPTION);
const [message, setMessage] = useState('');
useEffect(() => {
if (newMessage) {
// Update UI with new message
}
}, [newMessage]);
const handleSendMessage = () => {
sendMessage({ variables: { content: message } });
setMessage('');
};
return (
<Layout>
<Sider>
<Menu mode="inline">
<Menu.ItemGroup title="Friends">
{friendsData?.friends.map((friend) => (
<Menu.Item key={friend.id}>{friend.username} ({friend.online ? 'Online' : 'Offline'})</Menu.Item>
))}
</Menu.ItemGroup>
<Menu.ItemGroup title="Groups">
{groupsData?.groups.map((group) => (
<Menu.Item key={group.id}>{group.name}</Menu.Item>
))}
</Menu.ItemGroup>
</Menu>
</Sider>
<Layout>
<Content>
<div>
<Input
value={message}
onChange={(e) => setMessage(e.target.value)}
onPressEnter={handleSendMessage}
/>
<Button onClick={handleSendMessage}>Send</Button>
</div>
</Content>
</Layout>
</Layout>
);
};
export default ChatApp;7. WebSocket 实现
如果使用 WebSocket 而非 GraphQL 订阅,可以将 WebSocket 实现为独立服务,监听用户的消息事件,广播给其他用户。
总结
这套代码展示了如何使用 React、GraphQL(或 WebSocket)、MongoDB 构建一个完整的实时聊天应用。通过 GraphQL 订阅或者 WebSocket,确保消息实时同步,同时支持群聊、好友管理以及权限功能。