❤️‍🔥 前端 进阶 全栈 | 基于WebSocket 构建的【在线聊天室】💥

前言

作为前端工程师,想要进阶成为全栈工程师是一个很好的目标。全栈工程师具备前端和后端开发的能力,能够独立完成整个软件项目的开发工作。在这篇文章中,我们将通过 在线聊天室 这个项目和大家一起探讨前端进阶为全栈的一些重要步骤和技能。✌🏿

项目演示

仓库地址

Github前端地址:github.com/Tooy8/TooyM...

Github后端地址:github.com/Tooy8/TooyM...

技术栈

🧠Nest.js(Node服务端框架)+ 数据库Sequelize(Mysql) + JWT + Websocket

🧠React/Nextjs + Redux-toolkit + styled-components

Nest.js

熟悉常见的后端框架和工具是前端进阶为全栈的重要一步。比如,学习使用Node.js可以让你编写服务器端的JavaScript代码,使用Express或Nest等框架可以快速构建后端应用程序。 在本项目中,我们使用的就是Nest框架✔️

定义

Nest.js 是一个用于构建高效、可扩展的服务器端应用程序的渐进式 Node.js 框架。它基于 TypeScript 构建,并采用了许多面向对象编程(OOP)的概念,使得开发者可以更轻松地编写可维护的代码。 Nest.js 提供了许多有用的功能和特性,使得构建复杂的应用程序变得更加容易。🤔

特点

以下是一些 Nest.js 的重要特点👇

  • 模块化:Nest.js 应用程序由模块组成,每个模块都有自己的责任和功能。
  • 控制器和路由:Nest.js 的控制器用于处理来自客户端的请求,并根据路由将请求分发到相应的处理程序。
  • 依赖注入:Nest.js 通过依赖注入容器来管理应用程序的各个组件以及它们之间的依赖关系。
  • 中间件:Nest.js 支持使用中间件来处理请求和响应,可以用于处理身份验证、日志记录、错误处理等操作。
  • 数据库集成:Nest.js 提供了对多个数据库的集成支持,包括关系型数据库(如 MySQL、PostgreSQL)和 NoSQL 数据库(如 MongoDB)。😪

具体使用方法参照 官方文档

在线聊天室

下面我们就来看看如何使用Nest来构建项目的后端部分😏

目录

js 复制代码
📦src
 ┣ 📂auth
 ┣ 📂common
 ┣ 📂message
 ┣ 📂user
 ┣ 📂ws
 ┣ 📜app.controller.spec.ts
 ┣ 📜app.controller.ts
 ┣ 📜app.module.ts
 ┣ 📜app.service.ts
 ┣ 📜main.ts
  • auth 模块负责对用户身份进行认证
  • common 文件夹里存放着自定义装饰器
  • message 模块负责管理用户的聊天消息
  • user 模块负责管理用户的数据
  • ws 模块负责即时通讯的核心逻辑

身份认证

如果用户想要进行登录,提交信息后就会调用 auth.service.ts 中的 validateUser 方法来验证该用户是否是有效用户

使用 bcrypt.compare 方法来比较传入的密码 pass 和从数据库中查询到的用户记录的密码是否匹配。

js 复制代码
async validateUser(username: string, pass: string): Promise<any> {
        //查询数据库中是否存在该用户名对应的用户记录。
        const user = await this.userService.findOne(username);
        if (user) {
            const passwordCompare = await bcrypt.compare(pass, user.password)
            if (user && passwordCompare) {
                const { password, ...result } = user;
                return result;
            }
            return null
        } else {
            return null
       }
    }

如果该用户为有效用户,则调用 login 方法

使用 this.jwtService.sign(payload) 方法 对 payload 对象进行签名,生成一个带有密钥的 JWT

js 复制代码
async login(user: any) {
        const payload = { username: user.username, sub: user.userId };
        return {
            code: '200',
            access_token: this.jwtService.sign(payload),
            msg:'登录成功'
        };
    }

那么如何生成全局守卫,将未携带token的接口进行拦截呢🤔

这一点我在之前的文章就提到过 《 JWT是什么🤓-怎么在 【Nest.js】 中使用 JWT🦄》

用户模块、消息模块

用户模块和消息模块其实很类似💤,都是先连接数据库,然后在 service 中封装业务逻辑和数据处理,最后在 控制器 处理传入请求并返回响应💯

连接数据库,在前面的文章也提到过《解放双手😭 在【Nest】中通过【TypeORM】连接数据库🤩》

下面以 user 模块为例👇

Service 中对数据进行增删改查

js 复制代码
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { InjectModel } from '@nestjs/sequelize';
import { User } from './model/user.model';
@Injectable()
export class UserService {
  //模型注入
  constructor(@InjectModel(User) private userModel: typeof User) {}
  async create(createUserDto: CreateUserDto) {
    // 根据传入的 createUserDto 中的属性进行初始化。
    let res = await this.userModel.build({
      ...createUserDto,
    });
    await res.save();
    return res;
  }
  //查找全部
  async findAll() {
    let res = await this.userModel.findAll();
    return res;
  }
  async find(createUserDto: CreateUserDto) {
    let res = await this.userModel.findOne({
      // where 字段用于指定查询条件
      where: {
        ...createUserDto,
      },
    });
    return res;
  }
  //根据名字查找
  async findOne(username: string) {
    let res = await this.userModel.findOne({
      where: {
        username,
      },
    });
    return res !== null ? res : null;
  }
  //上传头像
  async uploadAvatar(username: string, avatar: string) {
    let res = await this.userModel.update(
      {
        avatar,
      },
      {
        where: {
          username: username,
        },
      },
    );
    return {
      code: '200',
      msg: '上传成功',
      data: res,
    };
  }
  // 判断是否有头像
  async hasAvatar(username: string) {
    let res = await this.userModel.findOne({
      where: {
        username: username,
      },
    });
    return res.avatar !== null;
  }
}

Controller 处理传入请求并返回响应

js 复制代码
export class UserController {
  constructor(private readonly userService: UserService) {}
  //查找全部
  @Public()
  @Post('all')
  async findAll() {
    let res = await this.userService.findAll();
    return res;
  }
  // 上传头像
  @Public()
  @Post('avatar')
  async uploadAvatar(@Body() body: uploadAvatarDto) {
    let { username, avatar } = body;
    if (avatar.length < 20) {
      avatar = `https://q2.qlogo.cn/headimg_dl?dst_uin=${avatar}&spec=100`;
    }
    let res = await this.userService.uploadAvatar(username, avatar);
    return res;
  }
  // 判断是否有头像
  @Public()
  @Post('hasavatar')
  async hasAvatar(@Body() body: hasAvatarDto) {
    let { username } = body;
    let res = await this.userService.hasAvatar(username);
    return res;
  }
}

即时通讯的核心逻辑

在 Nest.js 中使用 Socket.io,可以实现客户端与服务端之间的实时双向通信。

我们先来了解一下服务端和客户端如何互传数据

客户端向服务端发送消息

客户端需要使用 Socket.io 客户端库与服务端建立连接,并通过该连接发送消息。

js 复制代码
import io from 'socket.io-client';

const socket = io('http://localhost:3000'); // 替换为实际的服务器地址

socket.emit('message', { content: 'Hello, server!' });

服务端向客户端发送消息

js 复制代码
import { WebSocketGateway, WebSocketServer, SubscribeMessage } from '@nestjs/websockets';
import { Server } from 'socket.io';

@WebSocketGateway()
export class MyGateway {
  @WebSocketServer()
  server: Server;

  @SubscribeMessage('message')
  handleMessage(client: any, payload: any): void {
    this.server.emit('message', '欢迎连接到 WebSocket 服务器');
  }
}

客户端接收服务端发送的消息

js 复制代码
// 引入 Socket.IO 客户端库
import io from 'socket.io-client';

// 连接到 Socket.IO 服务端
const socket = io('http://localhost:3000');

// 监听 'message' 事件,表示服务端发来了消息
socket.on('message', data => {
  console.log('收到服务端发送的数据:', data);
});

服务端逻辑

清楚以上逻辑后,就能编写服务端逻辑了👇

js 复制代码
  @SubscribeMessage('changeRoom')
  changeRoom(
    @MessageBody() payload: any,
    @ConnectedSocket() client: Socket,
  ): void {
    // 处理聊天消息
    const { type, roomId, name } = payload || {};
    payload['users'] = '';
    payload['code'] = 200;
    payload = {
      ...payload,
      text: 123,
    };
    const room = this.myMap.get(roomId);

    if (type == 'create') {
      if (!room) {
        const roomInfo = {
          roomId,
          createUser: name,
          userList: [{ name, jionTime: +new Date() }],
        };
        this.myMap.set(roomId, roomInfo);
        payload = {
          ...payload,
          text: '您已加入房间!!!',
          code: 200,
        };
      } else {
        // 房间号已存在
        payload = {
          ...payload,
          text: '房间号已存在',
          code: 501,
        };
      }
    } else if (type == 'join') {
      // 加入房间
      if (!room) {
        payload = {
          ...payload,
          text: '房间号不存在',
          code: 502,
        };
      } else {
        const existingUser = room.userList.find((user) => user.name === name);
        if (!existingUser) {
          room.userList.push({ name, jionTime: +new Date() });
        }
        payload = {
          ...payload,
          text: name + '已进入房间',
          code: 200,
        };
      }
    } else if (type == 'leave') {
      if (Array.isArray(room.userList) && room.userList.length) {
        const index = room.userList.findIndex((item) => item.name === name);
        index != -1 && room.userList.splice(index, 1);

        payload = {
          ...payload,
          text: name + '离开了房间',
        };
        if (this.myMap.get(roomId)?.userList?.length == 0) {
          this.myMap.delete(roomId);
        }
      }
    }
    // 房间人数
    payload['users'] = this.myMap.get(roomId)?.userList?.length || 0;
    this.server.emit('message', payload);
  }

开发中遇到的问题

new Map()

在上述服务端逻辑代码里,不能直接在装饰器里创建 new Map() ,否则每次调用都会重新创建,数据不能保存

解决方法:通过使用自定义的 Provide 实现一个全局的 Map 实例。

以下是一个例子:

创建一个自定义的 Provider,例如 map.provider.ts

js 复制代码
import { Provider } from '@nestjs/common';

const MY_MAP = new Map<string, number>();

export const MapProvider: Provider = {
  provide: 'MyMap',
  useValue: MY_MAP,
};

在上述代码中,我们创建了一个新的 Map 实例 MY_MAP,并将其作为值传递给一个自定义的 Provider。Provider 使用 provide 属性指定了唯一的标识符 'MyMap',以便在其他地方注入和访问这个 Map 实例。

将这个 Provider 注册到 app.module.ts 中的 providers 数组中:

js 复制代码
import { Module } from '@nestjs/common';
import { MapProvider } from './map.provider';

@Module({
  providers: [MapProvider],
})
export class AppModule {}

在 app.module.ts 中将 MapProvider 添加到 providers 数组中,这样这个 Map 实例就会成为全局可访问的。

在其他需要使用这个全局 Map 的地方,进行依赖注入并使用它:

js 复制代码
import { Injectable, Inject } from '@nestjs/common';

@Injectable()
export class ExampleService {
  constructor(@Inject('MyMap') private readonly myMap: Map<string, number>) {}

  exampleMethod() {
    this.myMap.set('Key1', 123);
    this.myMap.set('Key2', 456);

    console.log(this.myMap.get('Key1')); // Output: 123
    console.log(this.myMap.get('Key2')); // Output: 456
  }
}

删除事件监听器

使用完socket.on后要删除,否则后续每次执行就会多执行一次

js 复制代码
socket.off("message"); // 从socket中删除先前的事件监听器 
socket.on("message", async (data) => {}

结尾

在本文中,我们简单介绍了前端进阶全栈所需的技能和知识。我们讨论了一些重要的主题,如 React、Nest.js、数据库和实时通信等,并提供了一些进一步学习和探究这些主题的资源。💋

最后,希望这篇文章对你有所帮助,让你对前端进阶全栈的方向和路径有了更清晰的认识。😘

相关推荐
cyforkk1 分钟前
Spring 异常处理器:从混乱到有序,优雅处理所有异常
java·后端·spring·mvc
tuokuac24 分钟前
nginx配置前端请求转发到指定的后端ip
前端·tcp/ip·nginx
程序员爱钓鱼27 分钟前
Go语言实战案例-开发一个Markdown转HTML工具
前端·后端·go
万少1 小时前
鸿蒙创新赛 HarmonyOS 6.0.0(20) 关键特性汇总
前端
桦说编程1 小时前
爆赞!完全认同!《软件设计的哲学》这本书深得我心
后端
thinktik1 小时前
还在手把手教AI写代码么? 让你的AWS Kiro AI IDE直接读飞书需求文档给你打工吧!
后端·serverless·aws
还有多远.1 小时前
jsBridge接入流程
前端·javascript·vue.js·react.js
蝶恋舞者1 小时前
web 网页数据传输处理过程
前端
非凡ghost1 小时前
FxSound:提升音频体验,让音乐更动听
前端·学习·音视频·生活·软件需求