Nest.js实现一个简单的聊天室

350 阅读4分钟

本文将介绍如何使用 Nest.js 和 Uni-app 实现一个简单的实时聊天应用。后端使用 @nestjs/websockets 和 socket.io,前端使用 uni-app 并集成 socket.io-client。这个项目允许多个用户同时加入聊天并实时交换消息。

效果图:

GIF 2024-9-27 9-27-11.gif

一、准备工作

安装 Node.js 和 npm 全局安装 Nest.js CLI

npm install -g @nestjs/cli

二、后端:Nest.js 实现 WebSocket 服务

  1. 创建 Nest.js 项目

首先使用 CLI 创建一个新的 Nest.js 项目:

nest new nest-chat

选择使用 npm 或 yarn 进行依赖管理。

  1. 安装 WebSocket 和 Socket.io 相关依赖

在项目中,安装 WebSocket 和 socket.io 相关的依赖包:

npm install @nestjs/websockets socket.io
  1. 创建 WebSocket 网关

在 src 目录下创建一个 chat 模块,并在该模块中创建 WebSocket 网关:

nest g module chat
nest g gateway chat/chat

这将生成一个 WebSocket 网关类。修改 chat.gateway.ts 文件,添加基本的 WebSocket 聊天功能:

    import {
        SubscribeMessage,
        WebSocketGateway,
        OnGatewayInit,
        WebSocketServer,
        OnGatewayConnection,
        OnGatewayDisconnect,
      } from '@nestjs/websockets';
      import { Logger } from '@nestjs/common';
      import { Socket, Server } from 'socket.io';
      
      @WebSocketGateway({
        namespace: 'chat',
        cors: {
          origin: '*',
        },
      })
      export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
        @WebSocketServer() server: Server;
        private logger: Logger = new Logger('ChatGateway');
        users = 0;
      
        @SubscribeMessage('msgToServer')
        handleMessage(client: Socket, payload: string): void {
          // 获取当前时间并格式化为“YYYY-MM-DD HH:mm:ss”
          const currentTime = new Date().toLocaleString('zh-CN', {
            year: 'numeric',
            month: '2-digit',
            day: '2-digit',
            hour: '2-digit',
            minute: '2-digit',
            second: '2-digit',
            hour12: false, // 使用24小时制
          }).replace(/\//g, '-').replace(/,/, ' '); // 替换分隔符以符合所需格式
      
          // 创建一个新的消息对象,包含时间和消息内容
          const messageWithTime = {
            time: currentTime, // 当前时间
            data: payload,
          };
          
          this.server.emit('msgToClient', messageWithTime); // 发送包含时间的消息对象
        }
      
        afterInit(server: Server) {
          this.logger.log('Init');
        }
      
        handleDisconnect(client: Socket) {
          this.logger.log(`Client disconnected: ${client.id}`);
          this.users--;
          // 通知连接的客户端当前用户数量
          this.server.emit('users', this.users);
        }
      
        handleConnection(client: Socket, ...args: any[]) {
          this.logger.log(`Client connected: ${client.id}`);
          this.users++;
          // 通知连接的客户端当前用户数量
          this.server.emit('users', this.users);
        }
      }
  1. 将 WebSocket 网关注册到模块中

在 chat.module.ts 中,将 ChatGateway 加入到模块的 providers 中:


    import { Module } from '@nestjs/common';
    import { ChatGateway } from './chat.gateway';
    //cli: nest g module chat
    @Module({
      providers: [ChatGateway],
    })
    export class ChatModule {}
  1. 在主模块中导入 ChatModule

打开 app.module.ts,并导入 ChatModule:


    import { Module } from '@nestjs/common';
    import { AppController } from './app.controller';
    import { AppService } from './app.service';
    import { ChatModule } from './chat/chat.module';
    @Module({
      imports: [ChatModule ],
      controllers: [AppController],
      providers: [AppService],
    })
    export class AppModule {}

至此,后端部分已经完成,接下来启动 Nest.js 服务:

npm run start

三、前端:Uni-App 实现客户端

1.安装 socket.io-client

在 Uni-App 项目中,使用 npm 安装 socket.io-client:

npm install socket.io-client

2.在pages下新建两个文件页面

pages/index/index

    <template>
      <view class="container" :style="gradientStyle">
        <view class="header">
          <text class="title">加入聊天室</text>
        </view>
        
        <view class="input-container">
          <input class="nickname-input" type="text" v-model="nickname" @confirm="joinChatroom" placeholder="请输入真实昵称" />
          <input class="code-input" type="text" v-model="code" @confirm="joinChatroom" placeholder="请输入验证码" />
        </view>
        
        <view class="button-container">
          <button class="join-button" @click="joinChatroom">点击加入</button>
        </view>
      </view>
    </template>
     
    <script>
    export default {
      data() {
        return {
          nickname: '', // 用户输入的昵称
          code: '', // 验证码输入
          colorIndex: 0, // 用于记录当前颜色索引
          gradientInterval: null // 定时器引用
        };
      },
      computed: {
        gradientStyle() {
          return {
            background: this.getDynamicGradient()
          };
        }
      },
      methods: {
        getDynamicGradient() {
          const colors = [
            '#ff7e5f',
            '#feb47b',
            '#ff6a6a',
            '#ffba6a',
            '#fffb6a',
            '#6aff6a',
            '#6afffb',
            '#6a6aff',
            '#ba6aff',
            '#ff6aff'
          ];
          // 计算背景颜色
          return `linear-gradient(135deg, ${colors[this.colorIndex]}, ${colors[(this.colorIndex + 1) % colors.length]})`;
        },
        joinChatroom() {
          if (this.nickname.trim() === '') {
            uni.showToast({
              title: '你必须给老子输入你的昵称',
              icon: 'error'
            });
            return;
          }
          if (this.code.trim() !== '1210') {
            uni.showToast({
              title: '验证码错误!请输入',
              icon: 'error'
            });
            return;
          }
          // 将用户的昵称保存到全局或跳转到聊天页面
          uni.navigateTo({
            url: `/pages/list/list?nickname=${this.nickname}&password=${this.code}`
          });
        }
      },
      created() {
        // 创建定时器以更新背景颜色
        this.gradientInterval = setInterval(() => {
          this.colorIndex = (this.colorIndex + 1) % 10; // 每秒改变颜色索引
          this.$forceUpdate(); // 强制更新以应用新的背景色
        }, 1000);
      },
      beforeDestroy() {
        clearInterval(this.gradientInterval); // 清除定时器
      }
    }
    </script>
     
    <style scoped>
    .container {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      height: 100vh;
      transition: background 1s ease; /* 背景渐变的过渡效果 */
    }
     
    .header {
      margin-bottom: 20px;
    }
     
    .title {
      font-size: 36rpx;
      font-weight: bold;
      color: #fff; /* 修改标题颜色为白色 */
    }
     
    .input-container {
      margin-bottom: 20px;
      width: 80%;
    }
     
    .nickname-input, .code-input {
      width: 100%;
      height: 80rpx;
      padding: 0 20rpx;
      border: 1px solid #ccc;
      border-radius: 10rpx;
      font-size: 32rpx;
      background-color: #fff;
      margin-bottom: 10px; /* 为验证码输入框增加底部间距 */
    }
     
    .button-container {
      width: 80%;
    }
     
    .join-button {
      width: 100%;
      height: 80rpx;
      background-color: #007aff;
      color: #fff;
      font-size: 32rpx;
      border: none;
      border-radius: 10rpx;
      text-align: center;
      line-height: 80rpx;
    }
     
    .join-button:active {
      background-color: #005bb5;
    }
    </style>

pages/list/list:

    <template>
      <view class="container" :style="gradientStyle">
        <view class="header">
          <text class="user-count">当前用户数量: {{ userCount }}</text>
        </view>
     
        <view class="message-container">
          <view v-for="(message, index) in messages" :key="index"
            :class="{'my-message': message.data.name === name, 'other-message': message.data.name !== name}">
            <text>{{ message.data.name }}: {{ message.data.text }}</text>
            <text style="margin-left: 100px;font-weight: normal;font-size: 12px;">{{message.time}}</text>
          </view>
        </view>
     
        <view class="input-container">
          <input v-model="text" placeholder="输入您的消息" class="input" @confirm="sendMessage"/>
          <button @click="sendMessage" class="send-button">发送</button>
        </view>
      </view>
    </template>
     
    <script>
    import io from 'socket.io-client';
     
    export default {
      data() {
        return {
          name: '',
          text: '',
    	  code:'',
          userCount: 0,
          messages: [],
          socket: null,
          colorIndex: 0, // 用于记录当前颜色索引
          gradientInterval: null // 定时器引用
        };
      },
      onLoad(e) {
        this.name = e.nickname; // 从传递的参数中获取昵称
    	this.code=e.password
      },
      onShow() {
      	if (this.code !== '1210') {
      	      // 如果不符合条件,返回上一页
      	     uni.navigateTo({
      	     	url: '/pages/index/index'
      	     })
      	    }
      },
      computed: {
        gradientStyle() {
          return {
            background: this.getDynamicGradient()
          };
        }
      },
      methods: {
        getDynamicGradient() {
          const colors = [
            '#ff7e5f',
            '#feb47b',
            '#ff6a6a',
            '#ffba6a',
            '#fffb6a',
            '#6aff6a',
            '#6afffb',
            '#6a6aff',
            '#ba6aff',
            '#ff6aff'
          ];
          // 计算背景颜色
          return `linear-gradient(135deg, ${colors[this.colorIndex]}, ${colors[(this.colorIndex + 1) % colors.length]})`;
        },
        sendMessage() {
          if (this.validateInput()) {
            const message = {
              name: this.name,
              text: this.text,
            };
            this.socket.emit('msgToServer', message);
            this.text = '';
          }
        },
        receivedUsers(message) {
          this.userCount = message;
        },
        receivedMessage(message) {
          this.messages.push(message);
        },
        validateInput() {
          return this.name.length > 0 && this.text.length > 0;
        },
        disconnectSocket() {
          if (this.socket) {
            this.socket.disconnect(); // 断开 WebSocket 连接
            this.socket = null; // 清空 socket 对象
          }
        },
      },
      created() {
        this.socket = io('http://192.168.31.76:3000/chat');
        this.socket.on('msgToClient', (message) => {
          this.receivedMessage(message);
        });
        this.socket.on('users', (message) => {
          this.receivedUsers(message);
        });
     
        // 创建定时器
        this.gradientInterval = setInterval(() => {
          this.colorIndex = (this.colorIndex + 1) % 10; // 每秒改变颜色索引
          this.$forceUpdate(); // 强制更新以应用新的背景色
        }, 1000);
      },
      beforeDestroy() {
        this.disconnectSocket(); // 在组件销毁前断开 WebSocket 连接
        clearInterval(this.gradientInterval); // 清除定时器
      },
    };
    </script>
     
    <style scoped>
    .container {
      display: flex;
      flex-direction: column;
      height: 100vh;
    }
     
    .header {
      display: flex;
      justify-content: flex-end;
      padding: 10px;
    }
     
    .user-count {
      margin-right: 20px;
      color: #fff;
      font-weight: 700;
    }
     
    .message-container {
      flex: 1;
      overflow-y: auto;
      padding: 10px;
    }
     
    .my-message {
      text-align: right;
      background-color: #f0f0f0;
      margin: 10px 0;
      padding: 10px;
      border-radius: 5px;
      width: fit-content;
      max-width: 80%;
      align-self: flex-end;
      margin-left: auto;
      font-weight: 700;
    }
     
    .other-message {
      text-align: left;
      background-color: orange;
      margin: 5px 0;
      padding: 10px;
      border-radius: 5px;
      width: fit-content;
      max-width: 80%;
      align-self: flex-start;
      margin-right: auto;
      color: #fff;
      font-weight: 700;
    }
     
    .input-container {
      display: flex;
      position: fixed;
      bottom: 0;
      width: 100%;
      padding: 10px;
      background-color: rgba(255, 255, 255, 0.9); /* 半透明背景 */
    }
     
    .input {
      flex: .98;
      margin-right: 5px;
      background-color: #f0f0f0;
      padding: 10px;
      border-radius: 5px;
    }
     
    .send-button {
      width: 100px;
      background-color: #007aff;
      color: white;
      border: none;
      border-radius: 5px;
      height: 45px;
    }
    </style>

3 然后运行uni项目

下面给大家提供代码地址,可以与你的同事们私密聊天

github:github.com/dashen-lvwe…