基于Websocket 和 Vue 实现简易聊天室

一、项目介绍

该聊天室系统是一个技术先进、功能全面的通信平台,为提供多元化的在线交流体验而设计。包括个性化的用户信息设置(头像、用户名、密码)、多样的交流方式(群聊、私聊)、方便的社交互动(添加、删除好友)、以及丰富的选项确保了用户的交流体验连贯且愉快。

在技术方面,这个系统采用了Typescript、Vue2.6.x、Socket.io、Vuex、Nestjs、Typeorm和ES6+等现代技术构建,确保了平台的高效性和稳定性。Typescript提高了代码的可读性和可维护性,Vue2.6.x作为前端框架提供了灵活的用户界面设计,而Socket.io等技术则确保了实时通信的流畅性。此外,使用SASS作为CSS预处理器,不仅增强了样式的可定制性,也提高了开发效率。这些技术的结合为用户提供了一个既安全又易用的聊天环境,满足现代通讯的各种需求。

二、实现思路

前端工作原理

使用 Vue2.6.x 创建用户界面
Vue.js 提供响应式和可组合的视图组件来构建用户界面。
通过数据绑定和虚拟DOM,Vue.js 能够高效地渲染和更新页面视图。

Vuex 管理应用状态
Vuex 作为状态管理库,用于存储和管理所有组件的共享状态,例如用户信息、聊天记录等。
它保证状态的一致性和响应式更新,让状态变化可预测和易于追踪。

SASS 处理样式
SASS 提供了更动态和组织化的方式来编写CSS,通过变量、混合、嵌套等功能提高开发效率。

后端工作原理

Nestjs 构建高效的服务端应用
Nestjs 作为服务端框架,提供了一套完整的架构模式,包括控制器、服务、模块等,以支持复杂的后端逻辑。
它集成了TypeScript,带来强类型和面向对象编程的优势。

Socket.io 实现实时通信
通过 Socket.io 在客户端和服务器之间建立WebSocket连接,实现实时、双向的通信。

Typeorm 管理数据库交互
Typeorm 作为ORM工具,简化了数据库操作,允许使用对象编程的方式来处理数据库查询和更新。

系统整合

当用户在前端界面进行操作(如发送消息)时,Vue.js 处理数据展示和用户交互,Vuex 管理状态变化,而SASS确保样式一致性。
这些操作通过 HTTP 请求或 WebSocket 连接发送到后端。Nestjs 处理这些请求,执行相应的业务逻辑(如保存消息、更新用户状态等)。
数据库操作由 Typeorm 处理,确保数据的持久化和一致性。
后端处理完毕后,通过 Socket.io 将任何必要的更新实时推送回客户端,Vue.js 然后更新视图展示新的状态。

三、具体实现

服务器

客户端与服务器建立连接与断开连接

async handleConnection(client: Socket): Promise<string> {
    const userRoom = client.handshake.query.userId;
    // 连接默认加入"聊天室"房间
    client.join(this.defaultGroup);
    // 进来统计在线人数
    this.getActiveGroupUser();
    // 用户独有消息房间 根据userId
    if(userRoom) {
    //如果存在用户ID如果存在用户ID,客户端加入独有消息房间
      client.join(userRoom);
    }
    return '连接成功';
  }

  // socket断连钩子
  async handleDisconnect():Promise<any> {
    this.getActiveGroupUser();
  }

创建群组
首先验证请求用户的存在性,然后检查群组名是否已被占用,接着执行创建群组的操作,并通知客户端操作结果。如果用户不存在或群组名已存在,会发送相应的失败消息。如果创建成功,则会发送成功消息,并更新群组用户列表。

// 使用装饰器订阅WebSocket消息事件'addGroup',用于创建群组
  @SubscribeMessage('addGroup')
  async addGroup(@ConnectedSocket() client: Socket, @MessageBody() data: Group): Promise<any> {
    // 检查请求创建群组的用户是否存在
    const isUser = await this.userRepository.findOne({userId: data.userId});

    // 如果用户存在,则进行下一步
    if(isUser) {
      // 检查群组名是否已经被使用
      const isHaveGroup = await this.groupRepository.findOne({ groupName: data.groupName });

      // 如果群组名已存在,给客户端发送群组已存在的消息
      if (isHaveGroup) {
        this.server.to(data.userId).emit('addGroup', { code: RCode.FAIL, msg: '该群名字已存在', data: isHaveGroup });
        return; 
      }

      // 如果群组名不符合验证条件(具体验证逻辑不在代码段中提供),则不执行后续操作
      if(!nameVerify(data.groupName)) {
        return; 
      }

      // 保存群组信息到数据库
      data = await this.groupRepository.save(data);

      // 将创建群组的客户端加入到新创建的群组中
      client.join(data.groupId);

      // 保存群组用户关系到数据库
      const group = await this.groupUserRepository.save(data);

      // 向客户端发送成功创建群组的消息
      this.server.to(group.groupId).emit('addGroup', { code: RCode.OK, msg: `成功创建群${data.groupName}`, data: group });

      // 更新群组中的活跃用户信息
      this.getActiveGroupUser();
    } else {
      // 如果用户不存在,给客户端发送没有创建群组资格的消息
      this.server.to(data.userId).emit('addGroup', { code: RCode.FAIL, msg: `你没资格创建群` });
    }
  }

加入群组
首先进行两个主要的数据库查询:一是确认请求加入群组的用户确实存在,二是确认所请求加入的群组也存在。如果用户和群组都是有效的,它会处理用户加入群组的逻辑,包括更新数据库中的用户-群组关联信息,并使用户的WebSocket客户端加入相应的群组房间。一旦用户加入,服务器会向该群组房间中的所有客户端广播一条消息,通知他们有新用户加入。如果有任何问题,如用户或群组不存在,服务器将发送错误消息给发起请求的用户。

  // 使用装饰器订阅WebSocket消息事件'joinGroup',用于用户加入群组
  @SubscribeMessage('joinGroup')
  async joinGroup(@ConnectedSocket() client: Socket, @MessageBody() data: GroupMap): Promise<any> {
    // 首先确认请求加入群组的用户是否存在
    const isUser = await this.userRepository.findOne({ userId: data.userId });

    // 如果用户存在
    if(isUser) {
      // 然后确认请求加入的群组是否存在
      const group = await this.groupRepository.findOne({ groupId: data.groupId });

      // 查询用户和群组的关联信息
      let userGroup = await this.groupUserRepository.findOne({ groupId: group.groupId, userId: data.userId });

      // 重新获取一次用户信息(可能用于获取最新的用户状态或其他信息)
      const user = await this.userRepository.findOne({ userId: data.userId });

      // 如果群组和用户都有效
      if (group && user) {
        // 如果用户尚未加入该群组
        if (!userGroup) {
          // 设置关联的群组ID
          data.groupId = group.groupId;
          // 在数据库中保存用户和群组的关系
          userGroup = await this.groupUserRepository.save(data);
        }

        // 将用户的客户端加入到该群组的Socket.IO房间中
        client.join(group.groupId);

        // 准备返回的响应数据
        const res = { group: group, user: user };

        // 向群组中的所有成员广播某用户加入群组的消息
        this.server.to(group.groupId).emit('joinGroup', {
          code: RCode.OK,
          msg: `${user.username}加入群${group.groupName}`,
          data: res
        });

        // 更新群组中的活跃用户信息
        this.getActiveGroupUser();
      } else {
        // 如果用户或群组无效,则向请求的用户发送加入群组失败的消息
        this.server.to(data.userId).emit('joinGroup', { code: RCode.FAIL, msg: '进群失败', data: '' });
      }
    } else {
      // 如果用户不存在,则发送没有资格加入群组的消息
      this.server.to(data.userId).emit('joinGroup', { code: RCode.FAIL, msg: '你没资格进群'});
    }
  }

用户加入群组
首先确认执行添加好友操作的用户是存在的,然后检查是否尝试添加自己为好友以及两个用户之间是否已经是好友。如果两个用户还不是好友,在数据库中创建这两个用户之间的好友关系,并获取双方的历史私聊消息。如果用户试图添加不存在的用户为好友,会收到错误提示。

// 订阅WebSocket消息事件'addFriend',用于添加好友
  @SubscribeMessage('addFriend')
  async addFriend(@ConnectedSocket() client: Socket, @MessageBody() data: UserMap): Promise<any> {
    // 验证请求添加好友的用户是否存在
    const isUser = await this.userRepository.findOne({userId: data.userId});

    // 如果用户存在
    if(isUser) {
      // 检查是否提供了有效的好友ID和用户ID,以及用户不能添加自己为好友
      if (data.friendId && data.userId && data.userId !== data.friendId) {
        // 检查这两个用户之间是否已经存在好友关系
        const relation1 = await this.friendRepository.findOne({ userId: data.userId,
          friendId: data.friendId });
        const relation2 = await this.friendRepository.findOne({ userId: data.friendId,
          friendId: data.userId });
        // 创建一个由两个用户ID组成的房间ID
        const roomId = data.userId > data.friendId ?
            data.userId + data.friendId : data.friendId + data.userId;

        // 如果已经存在好友关系,则发送失败消息
        if (relation1 || relation2) {
          this.server.to(data.userId).emit('addFriend',
              { code: RCode.FAIL, msg: '已经有该好友', data: data });
          return;
        }

        // 检查待添加的好友是否存在
        const friend = await this.userRepository.findOne({userId: data.friendId});
        const user = await this.userRepository.findOne({userId: data.userId});
        
        // 如果该好友不存在,发送失败消息
        if (!friend) {
          this.server.to(data.userId).emit('addFriend',
              { code: RCode.FAIL, msg: '该好友不存在', data: '' });
          return;
        }

        // 在数据库中保存两个用户之间的好友关系
        await this.friendRepository.save(data);
        // 创建反向关系并保存
        const friendData = JSON.parse(JSON.stringify(data));
        const friendId = friendData.friendId;
        friendData.friendId = friendData.userId;
        friendData.userId = friendId;
        delete friendData._id; // 删除不需要的_id字段
        await this.friendRepository.save(friendData);

        // 让用户加入一个共有的房间,可能用于私聊
        client.join(roomId);

        // 获取最近的私聊消息记录
        let messages = await getRepository(FriendMessage)
            .createQueryBuilder("friendMessage")
            .orderBy("friendMessage.time", "DESC")
            .where("friendMessage.userId = :userId AND friendMessage.friendId = :friendId", { userId: data.userId, friendId: data.friendId })
            .orWhere("friendMessage.userId = :friendId AND friendMessage.friendId = :userId", { userId: data.userId, friendId: data.friendId })
            .take(30)
            .getMany();
        messages = messages.reverse(); // 将消息顺序反转,以得到正确的时间顺序

        // 如果有消息记录,将消息记录添加到用户信息中
        if(messages.length) {
          // @ts-ignore
          friend.messages = messages;
          // @ts-ignore
          user.messages = messages;
        }

        // 向请求添加好友的用户发送成功消息
        this.server.to(data.userId).emit('addFriend',
            { code: RCode.OK, msg: `添加好友${friend.username}成功`, data: friend });
        // 同时向被添加的好友发送通知消息
        this.server.to(data.friendId).emit('addFriend',
            { code: RCode.OK, msg: `${user.username}添加你为好友`, data: user });
      } else {
        // 如果提供的好友ID无效或试图添加自己为好友,发送失败消息
        this.server.to(data.userId).emit('addFriend',
            { code: RCode.FAIL, msg: '不能添加自己为好友', data: '' });
      }
    } else {
      // 如果用户不存在,发送没有添加好友权限的失败消息
      this.server.to(data.userId).emit('addFriend',
          {code: RCode.FAIL, msg:'你没资格加好友' });
    }
  }

处理发送到群组的消息
首先确保发送消息的用户是注册且认证过的用户,并且用户是群组的一部分。如果用户尝试发送消息到一个他不属于的群组或没有指定群组,他会收到一个错误消息。对于图片消息,代码将图片保存在服务器的静态目录中,并将消息内容更新为图片的文件路径。然后,设置消息的时间戳为当前时间,并保存消息到群消息数据库中。最后,消息被发送到群组中的所有成员。如果用户不存在,则会收到一个错误消息,表明他没有发送消息的权限。

// 订阅WebSocket消息事件'groupMessage',用于处理发送到群组的消息
  @SubscribeMessage('groupMessage')
  async sendGroupMessage(@MessageBody() data: GroupMessageDto): Promise<any> {
    // 验证发送消息的用户是否存在
    const isUser = await this.userRepository.findOne({userId: data.userId});

    // 如果用户存在
    if(isUser) {
      // 验证用户是否属于该群组
      const userGroupMap = await this.groupUserRepository.
      findOne({userId: data.userId, groupId: data.groupId});

      // 如果用户不属于该群组或未提供群组ID,向用户发送错误消息
      if(!userGroupMap || !data.groupId) {
        this.server.to(data.userId).
        emit('groupMessage', {code: RCode.FAIL, msg: '群消息发送错误', data: ''});
        return; // 结束函数执行
      }

      // 如果消息类型为图片
      if(data.messageType === 'image') {
        // 创建一个随机文件名
        const randomName = `${Date.now()}$${data.userId}$${data.width}$${data.height}`;
        // 创建文件写入流并保存图片到指定的'static'目录
        const stream = createWriteStream(join('public/static', randomName));
        stream.write(data.content); // 写入图片数据
        data.content = randomName; // 更新消息内容为图片的文件名
      }

      // 设置消息的发送时间为当前服务器时间
      data.time = new Date().valueOf();

      // 将消息保存到群消息记录中
      await this.groupMessageRepository.save(data);

      // 向整个群组广播消息
      this.server.to(data.groupId).emit('groupMessage', 
          {code: RCode.OK, msg: '', data: data});
    } else {
      // 如果用户不存在,向用户发送没有发送消息权限的错误消息
      this.server.to(data.userId).emit('groupMessage', 
          {code: RCode.FAIL, msg: '你没资格发消息' });
    }
  }

添加好友
首先确保发送消息的用户是注册且认证过的用户,并且用户是群组的一部分。如果用户尝试发送消息到一个他不属于的群组或没有指定群组,他会收到一个错误消息。对于图片消息,代码将图片保存在服务器的静态目录中,并将消息内容更新为图片的文件路径。然后,设置消息的时间戳为当前时间,并保存消息到群消息数据库中。最后,消息被发送到群组中的所有成员。如果用户不存在,则会收到一个错误消息,表明他没有发送消息的权限。

 // 订阅WebSocket消息事件'addFriend',用于添加好友
  @SubscribeMessage('addFriend')
  async addFriend(@ConnectedSocket() client: Socket, @MessageBody() data: UserMap): Promise<any> {
    // 验证请求添加好友的用户是否存在
    const isUser = await this.userRepository.findOne({userId: data.userId});

    // 如果用户存在
    if(isUser) {
      // 检查是否提供了有效的好友ID和用户ID,以及用户不能添加自己为好友
      if (data.friendId && data.userId && data.userId !== data.friendId) {
        // 检查这两个用户之间是否已经存在好友关系
        const relation1 = await this.friendRepository.findOne({ userId: data.userId,
          friendId: data.friendId });
        const relation2 = await this.friendRepository.findOne({ userId: data.friendId,
          friendId: data.userId });
        // 创建一个由两个用户ID组成的房间ID
        const roomId = data.userId > data.friendId ?
            data.userId + data.friendId : data.friendId + data.userId;

        // 如果已经存在好友关系,则发送失败消息
        if (relation1 || relation2) {
          this.server.to(data.userId).emit('addFriend',
              { code: RCode.FAIL, msg: '已经有该好友', data: data });
          return;
        }

        // 检查待添加的好友是否存在
        const friend = await this.userRepository.findOne({userId: data.friendId});
        const user = await this.userRepository.findOne({userId: data.userId});

        // 如果该好友不存在,发送失败消息
        if (!friend) {
          this.server.to(data.userId).emit('addFriend',
              { code: RCode.FAIL, msg: '该好友不存在', data: '' });
          return;
        }

        // 在数据库中保存两个用户之间的好友关系
        await this.friendRepository.save(data);
        // 创建反向关系并保存
        const friendData = JSON.parse(JSON.stringify(data));
        const friendId = friendData.friendId;
        friendData.friendId = friendData.userId;
        friendData.userId = friendId;
        delete friendData._id; // 删除不需要的_id字段
        await this.friendRepository.save(friendData);

        // 让用户加入一个共有的房间,可能用于私聊
        client.join(roomId);

        // 获取最近的私聊消息记录
        let messages = await getRepository(FriendMessage)
            .createQueryBuilder("friendMessage")
            .orderBy("friendMessage.time", "DESC")
            .where("friendMessage.userId = :userId AND friendMessage.friendId = :friendId", { userId: data.userId, friendId: data.friendId })
            .orWhere("friendMessage.userId = :friendId AND friendMessage.friendId = :userId", { userId: data.userId, friendId: data.friendId })
            .take(30)
            .getMany();
        messages = messages.reverse(); // 将消息顺序反转,以得到正确的时间顺序

        // 如果有消息记录,将消息记录添加到用户信息中
        if(messages.length) {
          // @ts-ignore
          friend.messages = messages;
          // @ts-ignore
          user.messages = messages;
        }

        // 向请求添加好友的用户发送成功消息
        this.server.to(data.userId).emit('addFriend',
            { code: RCode.OK, msg: `添加好友${friend.username}成功`, data: friend });
        // 同时向被添加的好友发送通知消息
        this.server.to(data.friendId).emit('addFriend',
            { code: RCode.OK, msg: `${user.username}添加你为好友`, data: user });
      } else {
        // 如果提供的好友ID无效或试图添加自己为好友,发送失败消息
        this.server.to(data.userId).emit('addFriend',
            { code: RCode.FAIL, msg: '不能添加自己为好友', data: '' });
      }
    } else {
      // 如果用户不存在,发送没有添加好友权限的失败消息
      this.server.to(data.userId).emit('addFriend',
          {code: RCode.FAIL, msg:'你没资格加好友' });
    }
  }

加入私聊的socket连接
首先检查传入的用户ID和好友ID是否有效。如果它们有效,查询数据库来确认这两个用户是否真的是好友。如果是,用户的Socket客户端会加入一个特定的房间,该房间ID是由这两个用户的ID组合而成,用于他们之间的私聊。如果一切顺利,用户会收到一个确认消息,告知他们已经成功进入了私聊的Socket连接。

// 使用装饰器订阅WebSocket消息事件'joinFriendSocket',用于加入私聊的socket连接
@SubscribeMessage('joinFriendSocket')
async joinFriend(@ConnectedSocket() client: Socket, @MessageBody() data: UserMap): Promise<any> {
  // 确保传入了有效的用户ID和好友ID
  if(data.friendId && data.userId) {
    // 查询数据库确认这两个用户是否有好友关系
    const relation = await this.friendRepository.findOne({ userId: data.userId, friendId: data.friendId });
    // 生成一个房间ID,使用两个用户ID组合成的字符串,确保唯一性
    const roomId = data.userId > data.friendId ? data.userId + data.friendId : data.friendId + data.userId;
    
    // 如果用户间存在好友关系
    if(relation) {
      // 客户端加入一个以roomId命名的Socket.IO房间,这个房间用于私聊
      client.join(roomId);
      // 向加入房间的用户发送成功消息
      this.server.to(data.userId).emit('joinFriendSocket', { code: RCode.OK, msg: '进入私聊socket成功', data: relation });
    }
  }
}

发送私聊消息
确认执行发送操作的用户是否注册且认证过。如果用户试图发送消息但没有提供有效的用户ID和好友ID,他们会收到错误消息。如果发送的是图片消息,代码会在服务器上保存图片,并将消息内容更新为图片的文件名。然后,代码设置消息的时间戳为当前时间,并将消息保存在数据库中。最后,消息被发送到指定的私聊房间中的所有用户。如果用户不存在,则会收到一个错误消息,说明他们没有发送消息的权限。

// 订阅WebSocket消息事件'friendMessage',用于发送私聊消息
  @SubscribeMessage('friendMessage')
  async friendMessage(@ConnectedSocket() client: Socket, @MessageBody() data: FriendMessageDto): Promise<any> {
    // 检查发送消息的用户是否存在
    const isUser = await this.userRepository.findOne({userId: data.userId});

    // 如果用户存在
    if(isUser) {
      // 确保传入了有效的用户ID和好友ID
      if(data.userId && data.friendId) {
        // 根据用户ID生成房间ID,用于私聊
        const roomId = data.userId > data.friendId ? data.userId + data.friendId : data.friendId + data.userId;

        // 如果消息类型为图片
        if(data.messageType === 'image') {
          // 生成一个随机文件名,包括当前时间戳、房间ID和图片尺寸
          const randomName = `${Date.now()}$${roomId}$${data.width}$${data.height}`;
          // 创建文件写入流,保存图片到服务器上的'static'目录
          const stream = createWriteStream(join('public/static', randomName));
          stream.write(data.content); // 写入图片数据
          data.content = randomName; // 更新消息内容为图片文件名
        }

        // 设置消息的时间戳
        data.time = new Date().valueOf();

        // 保存消息到数据库
        await this.friendMessageRepository.save(data);

        // 向房间里的所有成员广播私聊消息
        this.server.to(roomId).emit('friendMessage', {code: RCode.OK, msg:'', data});
      }
    } else {
      // 如果用户不存在,向尝试发送消息的用户发送错误消息
      this.server.to(data.userId).emit('friendMessage', {code: RCode.FAIL, msg:'你没资格发消息', data});
    }
  }

获取群和好友的数据


  // 获取所有群和好友数据
  @SubscribeMessage('chatData') 
  async getAllData(@ConnectedSocket() client: Socket,  @MessageBody() user: User):Promise<any> {
    const isUser = await this.userRepository.findOne({userId: user.userId, password: user.password});
    if(isUser) {
      let groupArr: GroupDto[] = [];
      let friendArr: FriendDto[] = [];
      const userGather: {[key: string]: User} = {};
      let userArr: FriendDto[] = [];
    
      const groupMap: GroupMap[] = await this.groupUserRepository.find({userId: user.userId}); 
      const friendMap: UserMap[] = await this.friendRepository.find({userId: user.userId});

      const groupPromise = groupMap.map(async (item) => {
        return await this.groupRepository.findOne({groupId: item.groupId});
      });
      const groupMessagePromise = groupMap.map(async (item) => {
        let groupMessage = await getRepository(GroupMessage)
        .createQueryBuilder("groupMessage")
        .orderBy("groupMessage.time", "DESC")
        .where("groupMessage.groupId = :id", { id: item.groupId })
        .take(30)
        .getMany();
        groupMessage = groupMessage.reverse();
        // 这里获取一下发消息的用户的用户信息
        for(const message of groupMessage) {
          if(!userGather[message.userId]) {
            userGather[message.userId] = await this.userRepository.findOne({userId: message.userId});
          }
        }
        return groupMessage;
      });

      const friendPromise = friendMap.map(async (item) => {
        return await this.userRepository.findOne({
          where:{userId: item.friendId}
        });
      });
      const friendMessagePromise = friendMap.map(async (item) => {
        const messages = await getRepository(FriendMessage)
          .createQueryBuilder("friendMessage")
          .orderBy("friendMessage.time", "DESC")
          .where("friendMessage.userId = :userId AND friendMessage.friendId = :friendId", { userId: item.userId, friendId: item.friendId })
          .orWhere("friendMessage.userId = :friendId AND friendMessage.friendId = :userId", { userId: item.userId, friendId: item.friendId })
          .take(30)
          .getMany();
        return messages.reverse();
      });

      const groups: GroupDto[]  = await Promise.all(groupPromise);
      const groupsMessage: Array<GroupMessageDto[]> = await Promise.all(groupMessagePromise);
      groups.map((group,index)=>{
        if(groupsMessage[index] && groupsMessage[index].length) {
          group.messages = groupsMessage[index];
        }
      });
      groupArr = groups;

      const friends: FriendDto[] = await Promise.all(friendPromise);
      const friendsMessage: Array<FriendMessageDto[]> = await Promise.all(friendMessagePromise);
      friends.map((friend, index) => {
        if(friendsMessage[index] && friendsMessage[index].length) {
          friend.messages = friendsMessage[index];
        }
      });
      friendArr = friends;
      userArr = [...Object.values(userGather), ...friendArr];

      this.server.to(user.userId).emit('chatData', {code:RCode.OK, msg: '获取聊天数据成功', data: {
        groupData: groupArr,
        friendData: friendArr,
        userData: userArr
      }});
    }
  }

退出群聊

  // 退群
  @SubscribeMessage('exitGroup') 
  async exitGroup(@ConnectedSocket() client: Socket,  @MessageBody() groupMap: GroupMap):Promise<any> {
    if(groupMap.groupId === this.defaultGroup) {
      return this.server.to(groupMap.userId).emit('exitGroup',{code: RCode.FAIL, msg: '默认群不可退'});
    }
    const user = await this.userRepository.findOne({userId: groupMap.userId});
    const group = await this.groupRepository.findOne({groupId: groupMap.groupId});
    const map = await this.groupUserRepository.findOne({userId: groupMap.userId, groupId: groupMap.groupId});
    if(user && group && map) {
      await this.groupUserRepository.remove(map);
      this.server.to(groupMap.userId).emit('exitGroup',{code: RCode.OK, msg: '退群成功', data: groupMap});
      return this.getActiveGroupUser();
    }
    this.server.to(groupMap.userId).emit('exitGroup',{code: RCode.FAIL, msg: '退群失败'});
  }

删除好友

 // 删好友
  @SubscribeMessage('exitFriend') 
  async exitFriend(@ConnectedSocket() client: Socket,  @MessageBody() userMap: UserMap):Promise<any> {
    const user = await this.userRepository.findOne({userId: userMap.userId});
    const friend = await this.userRepository.findOne({userId: userMap.friendId});
    const map1 = await this.friendRepository.findOne({userId: userMap.userId, friendId: userMap.friendId});
    const map2 = await this.friendRepository.findOne({userId: userMap.friendId, friendId: userMap.userId});
    if(user && friend && map1 && map2) {
      await this.friendRepository.remove(map1);
      await this.friendRepository.remove(map2);
      return this.server.to(userMap.userId).emit('exitFriend',{code: RCode.OK, msg: '删好友成功', data: userMap});
    }
    this.server.to(userMap.userId).emit('exitFriend',{code: RCode.FAIL, msg: '删好友失败'});
  }

获取在线用户

 // 获取在线用户
  async getActiveGroupUser() {
    // 从socket中找到连接人数
    // @ts-ignore;
    let userIdArr = Object.values(this.server.engine.clients).map(item=>{
      // @ts-ignore;
      return item.request._query.userId;
    });
    // 数组去重
    userIdArr = Array.from(new Set(userIdArr));

    const activeGroupUserGather = {};
    for(const userId of userIdArr) {
      const userGroupArr = await this.groupUserRepository.find({userId: userId});
      const user = await this.userRepository.findOne({userId: userId});
      if(user && userGroupArr.length) {
        userGroupArr.map(item => {
          if(!activeGroupUserGather[item.groupId]) {
            activeGroupUserGather[item.groupId] = {};
          }
          activeGroupUserGather[item.groupId][userId] = user;
        });
      }
    }

    this.server.to(this.defaultGroup).emit('activeGroupUser',{
      msg: 'activeGroupUser', 
      data: activeGroupUserGather
    });
  }
}

客户端

主组件
通过展示当前路由匹配的组件,还可以根据状态显示背景图片。它使用Vuex存储的状态来获取用户信息和背景图片URL,并根据当前设备类型来设置应用的移动端状态。如果没有设置背景图片,它将使用默认背景。

<template>
  <div id="app">
    <router-view />
    <img class="background" v-if="background" :src="background" alt="" />
  </div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { namespace } from 'vuex-class';
import { DEFAULT_BACKGROUND } from '@/const';
const appModule = namespace('app');

@Component
export default class GenalChat extends Vue {
  @appModule.Getter('user') user: User;
  @appModule.Getter('background') background: string;
  @appModule.Mutation('set_mobile') setMobile: Function;
  @appModule.Mutation('set_background') set_background: Function;

  mounted() {
    this.setMobile(this.isMobile());
    if (!this.background || !this.background.trim().length) {
      this.set_background(DEFAULT_BACKGROUND);
    }
  }

  isMobile() {
    let flag = navigator.userAgent.match(
      /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i
    );
    return flag && flag.length;
  }
}


</script>
<style lang="scss">
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
  height: 100%;
  width: 100%;
  overflow: hidden;
  background-size: cover;
  color: rgba(255, 255, 255, 0.85);
  background-color: #fff;
  .background {
    position: absolute;
    object-fit: cover;
    width: 100%;
    height: 100%;
  }
}
</style>

主界面
定义了多个组件分别负责应用中的各个部分,比如搜索、消息显示等功能。这个组件通过Vuex与全局状态管理系统交互,处理诸如登录、注册、发送消息等功能。此外使用了响应式设计,使得在不同大小的屏幕上都能提供良好的用户体验。

<template>
  <!-- 定义了聊天应用的主容器,并通过style绑定设置了背景图片 -->
  <div class="chat" :style="{ '--bg-image': `url('${background}')` }">
    <!-- 引入音乐组件 -->
    <genal-music></genal-music>
    <!-- 如果visibleTool为true,则显示工具栏组件 -->
    <div class="chat-part1" v-if="visibleTool">
      <genal-tool @logout="logout"></genal-tool>
    </div>
    <!-- 聊天室列表和搜索功能的容器 -->
    <div class="chat-part2">
      <genal-search @addGroup="addGroup" @joinGroup="joinGroup" @addFriend="addFriend" @setActiveRoom="setActiveRoom"></genal-search>
      <genal-room @setActiveRoom="setActiveRoom"></genal-room>
    </div>
    <!-- 聊天消息界面的容器 -->
    <div class="chat-part3">
      <!-- 展示一个消息图标,点击时触发toggleDrawer方法 -->
      <a-icon class="chat-team" type="message" @click="toggleDrawer" />
      <!-- 展示一个折叠/展开图标,点击时触发toggleTool方法 -->
      <div class="chat-tool">
        <a-icon type="menu-fold" @click="toggleTool" v-if="visibleTool" />
        <a-icon type="menu-unfold" @click="toggleTool" v-else />
      </div>
      <!-- 如果activeRoom存在,则显示消息组件 -->
      <genal-message v-if="activeRoom"></genal-message>
    </div>
    <!-- 定义一个侧边抽屉组件,用于在小屏幕上显示聊天室列表和搜索功能 -->
    <a-drawer placement="left" :closable="false" :visible="visibleDrawer" @close="toggleDrawer" style="height:100%">
      <div class="chat-drawer">
        <genal-search @addGroup="addGroup" @joinGroup="joinGroup" @addFriend="addFriend" @setActiveRoom="setActiveRoom"></genal-search>
        <genal-room @setActiveRoom="setActiveRoom"></genal-room>
      </div>
    </a-drawer>
    <!-- 登录和注册的组件 -->
    <genal-join @register="handleRegister" @login="handleLogin" :showModal="showModal"></genal-join>
  </div>
</template>

<script lang="ts">
// 引入Vue相关的装饰器函数和组件
import { Component, Vue } from 'vue-property-decorator';
import GenalTool from '@/components/GenalTool.vue';
// ... 其他组件的导入略 ...

// 引入vuex-class的namespace函数,用于创建模块化的Vuex绑定
const appModule = namespace('app');
const chatModule = namespace('chat');

// 使用装饰器定义Vue组件并引入子组件
@Component({
  components: {
    GenalTool,
    // ... 其他子组件的引入略 ...
  },
})
export default class GenalChat extends Vue {
  // ... Vuex Getters, Mutations, Actions的装饰器绑定 ...

  // 定义数据属性,如模态框的显示状态,侧边抽屉的可见性等
  showModal: boolean = false;
  visibleDrawer: boolean = false;
  visibleTool: boolean = true;

  // ... Vue生命周期钩子函数和方法的定义,用于处理登录、注册、加入/退出聊天室等操作 ...
}
</script>

<script lang="ts">
// 引入Vue相关的装饰器函数和组件
import { Component, Vue } from 'vue-property-decorator';
import GenalTool from '@/components/GenalTool.vue';
// ... 其他组件的导入略 ...

// 引入vuex-class的namespace函数,用于创建模块化的Vuex绑定
const appModule = namespace('app');
const chatModule = namespace('chat');

// 使用装饰器定义Vue组件并引入子组件
@Component({
  components: {
    GenalTool,
    // ... 其他子组件的引入略 ...
  },
})
export default class GenalChat extends Vue {
  // ... Vuex Getters, Mutations, Actions的装饰器绑定 ...

  // 定义数据属性,如模态框的显示状态,侧边抽屉的可见性等
  showModal: boolean = false;
  visibleDrawer: boolean = false;
  visibleTool: boolean = true;

  // ... Vue生命周期钩子函数和方法的定义,用于处理登录、注册、加入/退出聊天室等操作 ...
}
</script>

路由管理

// 从vue包中导入Vue对象
import Vue from 'vue';
// 从vue-router包中导入VueRouter类和RouteConfig类型定义
import VueRouter, { RouteConfig } from 'vue-router';

// 显式地告诉Vue使用VueRouter插件
Vue.use(VueRouter);

// 定义应用的路由数组,每个路由对象都符合RouteConfig类型
const routes: Array<RouteConfig> = [
  {
    path: '/', // URL路径
    name: 'Chat', // 路由名称
    component: () => import('@/views/GenalChat.vue'), // 路由对应的组件,使用懒加载的方式导入
  },
];

// 创建VueRouter实例,并传入路由配置对象
const router = new VueRouter({
  mode: 'history', // 使用HTML5 History模式
  base: process.env.BASE_URL, // 应用的基本路径,通常在生产和开发环境中配置
  routes, // 应用的路由配置
});

// 导出VueRouter实例,使它可以在main.ts中被导入并用于Vue应用
export default router;

客户端与Websocket服务器进行通信
actions对象中定义的每个动作都会接收到一个与Vuex store关联的上下文对象,其中包含commit用于提交mutation,dispatch用于触发其他action,state访问当前模块的状态,rootState访问根级别的状态。
connectSocket 动作用于初始化WebSocket连接和事件监听,当发生特定的事件时,使用commit调用相应的mutation修改Vuex的状态。
handleChatData 动作用于处理通过WebSocket接收到的聊天数据,更新Vuex状态,并根据需要触发其他动作。

// 导入所需的Vuex和socket.io客户端库
import { ActionTree } from 'vuex';
import { ChatState } from './state';
import { RootState } from '../../index';
import io from 'socket.io-client';
import Vue from 'vue';
// 导入定义的mutation类型
import { ... } from './mutation-types';
import { DEFAULT_GROUP } from '@/const/index';

// 定义actions,它是ActionTree的实例,用于处理ChatState和RootState
const actions: ActionTree<ChatState, RootState> = {
  // 定义一个动作用于初始化socket连接和设置事件监听
  async connectSocket({ commit, state, dispatch, rootState }, callback) {
    // 获取用户信息并连接到WebSocket服务器,传递用户ID作为查询参数
    let user = rootState.app.user;
    let socket: SocketIOClient.Socket = io.connect(`/?userId=${user.userId}`, { reconnection: true });

    // 注册监听事件以处理服务器发来的各种消息
    socket.on('connect', async () => {
      // ...连接成功后的处理,包括保存socket对象和获取聊天数据...
    });
    socket.on('activeGroupUser', (data: any) => {
      // ...处理激活群用户的信息...
    });
    // ...处理其他socket事件如添加群组、加入群组、群消息、添加好友等...

    // socket事件监听的错误处理也在这里定义
    socket.on('exitGroup', (res: ServerRes) => {
      // ...退出群组的处理...
    });
    socket.on('exitFriend', (res: ServerRes) => {
      // ...退出好友的处理...
    });
  },

  // 处理聊天数据,该动作可能被socket事件触发
  async handleChatData({ commit, dispatch, state, rootState }, payload) {
    // ...处理接收到的聊天数据,可能包括群组信息、好友信息和用户信息...
  },
};

// 导出actions对象供Vuex store使用
export default actions;

用户注册和登录
代码中定义的register和login动作都是异步的,它们通过等待fetch.post方法的结果来进行HTTP请求。
每个动作都向服务器发送POST请求(分别是/auth/register和/auth/login),请求体中包含用户提供的数据(用户名、密码)。从服务器返回的响应通过processReturn函数进行处理。这个函数可能负责检查响应状态、解析数据和处理错误等。
在用户成功注册或登录后,使用commit方法触发mutations(SET_USER和SET_TOKEN),这会更新Vuex store中的用户状态和令牌信息。每个动作在处理完网络请求和状态更新后,返回处理过的数据,这些数据包括用户信息,可以用于前端的进一步处理(例如导航到主页、显示用户信息等)。

import { SET_USER, SET_TOKEN } from './mutation-types';
import { ActionTree } from 'vuex';
import { AppState } from './state';
import { RootState } from '../../index';
import fetch from '@/api/fetch';
import { processReturn } from '@/utils/common.ts';

const actions: ActionTree<AppState, RootState> = {
  async register({ commit }, payload) {
    let res = await fetch.post('/auth/register', {
      ...payload,
    });
    let data = processReturn(res);
    if (data) {
      commit(SET_USER, data.user);
      commit(SET_TOKEN, data.token);
      return data;
    }
  },
  async login({ commit }, payload) {
    let res = await fetch.post('/auth/login', {
      ...payload,
    });
    let data = processReturn(res);
    if (data) {
      commit(SET_USER, data.user);
      commit(SET_TOKEN, data.token);
      return data;
    }
  },
};

export default actions;

四、运行截图

聊天室效果:
群聊功能私聊功能

五、仓库地址

代码仓库地址

六、总结

本聊天室系统采用了Vue.js和Node.js作为主要技术栈,结合了WebSocket实现实时通信功能。
前端方面,使用了Vue.js构建用户界面,组件化的方法使得代码更易于维护和扩展。并且使用了Vue Router用于管理前端路由,实现单页应用的无刷新导航。
状态管理方面,Vuex用于管理应用级别的状态,如用户信息、聊天记录等。并且通过WebSocket与服务器进行实时通信,如即时消息传递、群组和好友的状态更新。
采用响应式设计确保应用在不同屏幕尺寸上均有良好表现。

后端方面,采用NestJS框架,提供了一个结构清晰、易于扩展的后端应用架构。提供RESTful API接口,供前端调用以执行操作,如用户注册、登录、消息发送等。使用了WebSocket通信,通过使用Socket.io实现WebSocket连接,支持实时的消息推送。使用TypeORM与数据库交互,存储用户数据、聊天记录等。在用户认证和授权方面,实现JWT基于Token的认证机制。

在最后,我想要特别表达我对孟老师深深的敬意和感激。孟老师不仅在专业知识方面表现出卓越的才能,更以其独特的教学方法和对学生深切的关怀,使得这门课程成为了我们学习生涯中的一段难忘经历。孟老师对网络编程领域的深入理解和丰富经验,为我们打开了一个全新的视野。他的课堂不仅是知识的传递,更像是一场思维的启迪和能力的锻炼。孟老师能够将复杂的概念简化,用生动实际的案例使理论内容生动活泼,让我们能够更容易地理解并应用于实践。

特别值得一提的是,孟老师总是鼓励我们主动学习和探索。在这门课程的课程实验中,孟老师不仅给予了我们必要的指导,更重要的是,他给予了我们自主探索和解决问题的空间。这种教学方式极大地激发了我们的学习兴趣和创新思维,也培养了我们自我学习的能力。孟老师的教导不仅仅是技术层面的指导,更是一种精神和态度的传承。在孟老师的引导下,我不仅学到了网络程序设计的知识,更学会了如何成为一个不断学习、不断进步的人。在此,我对孟老师表示最深的敬意和最诚挚的感谢。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基于VueWebSocket的多人在线聊天室是一种使用Vue框架搭建前端,并利用WebSocket技术实现实时通信的应用程序。在这个聊天室中,多个用户可以实时地发送和接收消息。 首先,使用Vue框架搭建前端界面。Vue框架提供了组件化的开发方式,可以方便地构建用户界面。通过Vue的指令和绑定,构建出聊天界面,包括用户列表、消息展示区和输入框。 然后,利用WebSocket技术实现实时通信。WebSocket是一种双向通信协议,可以在客户端和服务器之间建立持久的连接。在Vue中,可以使用WebSocket API来连接到服务器,并监听服务器发送的消息。当用户发送消息时,Vue会将消息发送给服务器,服务器再将消息广播给其他在线用户,实现多人聊天。 在聊天室中,用户可以实时看到其他用户发送的消息,并且可以即时回复。聊天室还可以提供其他功能,如图片和文件的发送与接收,表情的使用等。通过Vue的双向数据绑定,用户可以实时看到聊天室的最新状态。 为了保证安全性,可以使用一些认证和授权的机制。例如,用户在进入聊天室之前需要登录或注册,并提供有效的凭证。在服务器端,可以对每个连接进行身份认证,并进行权限控制,确保只有合法的用户可以参与聊天。 基于VueWebSocket的多人在线聊天室可以提供实时的通信功能,使用户可以方便地进行多人聊天和交流。这个应用程序可以在各种场景下使用,如团队协作、在线教育等,增加信息共享和沟通效率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值