← 目录从零开始学习React

实时聊天应用

2024/9/24

构建一个完整的实时聊天应用,使用 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,确保消息实时同步,同时支持群聊、好友管理以及权限功能。