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

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

效果图:

一、准备工作
  1. 安装 Node.js 和 npm
  2. 全局安装 Nest.js CLI
npm install -g @nestjs/cli
二、后端:Nest.js 实现 WebSocket 服务
1. 创建 Nest.js 项目

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

nest new nest-chat

选择使用 npmyarn 进行依赖管理。

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

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

npm install @nestjs/websockets socket.io
3. 创建 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);
    }
  }
  
4. 将 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 {}
5. 在主模块中导入 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 - dashen-lvweijie/chatRoom: nest.js的简单聊天室功能

 

首先,需要安装 `@nestjs/typeorm` 和 `typeorm` 依赖,用于连接数据库和操作数据库。 然后,在 Nest.js 中创建一个 Service: ```typescript import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from './user.entity'; @Injectable() export class UserService { constructor( @InjectRepository(User) private readonly userRepository: Repository<User>, ) {} async findAll(): Promise<User[]> { return this.userRepository.find(); } async findOneById(id: number): Promise<User> { return this.userRepository.findOne(id); } async create(user: User): Promise<User> { return this.userRepository.save(user); } async update(id: number, user: User): Promise<User> { await this.userRepository.update(id, user); return this.userRepository.findOne(id); } async delete(id: number): Promise<void> { await this.userRepository.delete(id); } } ``` 在这个 Service 中,我们使用 `@InjectRepository(User)` 注解来注入 `User` 实体类的 Repository 对象。然后,我们定义了一些方法来操作数据库,例如 `findAll()` 方法用于查询所有用户信息,`findOneById(id: number)` 方法用于根据 id 查询用户信息,`create(user: User)` 方法用于创建用户信息,`update(id: number, user: User)` 方法用于更新用户信息,`delete(id: number)` 方法用于删除用户信息。 最后,我们需要在 Nest.js 中将这个 Service 注册为一个 Provider,例如: ```typescript import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { UserService } from './user.service'; import { UserController } from './user.controller'; import { User } from './user.entity'; @Module({ imports: [TypeOrmModule.forFeature([User])], providers: [UserService], controllers: [UserController], }) export class UserModule {} ``` 在这个 Module 中,我们使用 `TypeOrmModule.forFeature([User])` 方法来注册 `User` 实体类,并将 `UserService` 注册为一个 Provider。如果需要使用这个 Service,只需要在 Controller 中注入即可。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值