参考资料:
学完这篇 Nest.js 实战,还没入门的来锤我!(长文预警)
Nest.js 实战系列二-手把手带你-实现注册、扫码登录、jwt认证等
本文文章仓库链接:nest: 快速上手nestjs+prisma
创建
项目开发离不开工程化的部分,比如创建项目、编译构建、开发时 watch 文件变动自动构建等。
Nest 项目自然也是这样,所以它在 @nestjs/cli 这个包里提供了 nest 命令。
可以直接 npx 执行,npm 会把它下载下来然后执行:
npx @nestjs/cli new 项目名
也可以安装到全局,然后执行,更推荐这种
npm install -g @nestjs/cli
nest new 项目名
不过后者要时不时升级下版本,不然可能用它创建的项目版本不是最新的:
npm update -g @nestjs/cli
- nest new 快速创建项目
- nest generate 快速生成各种代码
- nest build 使用 tsc 或者 webpack 构建代码
- nest start 启动开发服务,支持 watch 和调试
- nest info 打印 node、npm、nest 包的依赖版本
项目结构
app.controller.ts | 单个路由的基本控制器(Controller) |
app.controller.spec.ts | 针对控制器的单元测试 |
app.module.ts | 应用程序的根模块(Module). |
app.service.ts | 具有单一方法的基本服务(Service) |
main.ts | 应用程序的入口文件将使用核心函数 NestFactory 来创建一个 Nest 应用程序实例. |
npm run start:dev
这个命令将监视你的文件,自动重新编译并重新加载服务器。
ClI提供了一个可靠的开发工作流程。因此,生成的 Nest 项目预先安装了代码检查工具(eslint)和代码格式化工具(prettier).
# Lint and autofix with eslint
$ npm run lint
# Format with prettier
$ npm run format
第一个接口
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
// 应用程序的入口文件将使用核心函数 NestFactory 来创建一个 Nest 应用程序实例.
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);// 启动了我们的 HTTP 监听器,使应用程序能够处理传入的 HTTP 请求。
}
bootstrap();
这里看到的Hello World就是接口地址http://localhost:9080返回的内容, 不信我们也可以使用常用 Postman看看:
说明Nest.js创建项目默认就给写了一个接口例子,那就通过这个接口例子来看,我们应该怎么实现一个接口。
前边看到mian.ts中也没有别的文件引入, 只有AppModule, 打开src/app.module.ts:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
// 应用程序的根模块.
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
.mudule文件需要使用一个@Module()
装饰器的类,装饰器可以理解成一个封装好的函数,其实是一个语法糖(对装饰器不了解的,可以看走近MidwayJS:初识TS装饰器与IoC机制)。@Module()
装饰器接收四个属性:providers、controllers、imports、exports。
- providers:Nest.js注入器实例化的提供者(服务提供者),处理具体的业务逻辑,各个模块之间可以共享(注入器的概念后面依赖注入部分会讲解);
- controllers:处理http请求,包括路由控制,向客户端返回响应,将具体业务逻辑委托给providers处理;
- imports:导入模块的列表,如果需要使用其他模块的服务,需要通过这里导入;
- exports:导出服务的列表,供其他模块导入使用。如果希望当前模块下的服务可以被其他模块共享,需要在这里配置导出;
在app.module.ts中,看到它引入了app.controller.ts和app.service.ts,分别看一下这两个文件:
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
使用@Controller装饰器来定义控制器, @Get是请求方法的装饰器,对getHello方法进行修饰, 表示这个方法会被GET请求调用。
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
从上面,我们可以看出使用@Injectable修饰后的 AppService, 在AppModule中注册之后,在app.controller.ts中使用,我们就不需要使用new AppService()去实例化,直接引入过来就可以用。
路由装饰器
Nest.js中没有单独配置路由的地方,而是使用装饰器。Nest.js中定义了若干的装饰器用于处理路由。
@Controller
如每一个要成为控制器的类,都需要借助@Controller装饰器的装饰,该装饰器可以传入一个路径参数,作为访问这个控制器的主路径:
// 主路径为 app
@Controller("app")
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
HTTP方法处理装饰器
@Get、@Post、@Put等众多用于HTTP方法处理装饰器,经过它们装饰的方法,可以对相应的HTTP请求进行响应。同时它们可以接受一个字符串或一个字符串数组作为参数,这里的字符串可以是固定的路径,也可以是通配符。
继续修改app.controller.ts,看下面的例子:
// 主路径为 app
@Controller("app")
export class AppController {
constructor(private readonly appService: AppService) {}
// 1. 固定路径:
// 可以匹配到 get请求,http://localhost:9080/app/list
@Get("list")
getHello(): string {...}
// 可以匹配到 post请求,http://localhost:9080/app/list
@Post("list")
create():string{...}
// 2.通配符路径(?+* 三种通配符 )
// 可以匹配到 get请求, http://localhost:9080/app/user_xxx
@Get("user_*")
getUser(){return "getUser"}
// 3.带参数路径
// 可以匹配到put请求,http://localhost:9080/app/list/xxxx
@Put("list/:id")
update(){ return "update"}
}
这里要提一个关于路由匹配时的注意点, 当我们有一个put请求,路径为/app/list/user,此时,我们在app.controller.ts控制器文件中增加一个方法:
@Put("list/user")
updateUser(){
return {userId:1}
}
你觉得这个路由会被匹配到吗?我们测试一下:
发现/app/list/user匹配到的并不是updateUser方法, 而是update方法。这就是我要说的注意点。
如果因为在匹配过程中, 发现@Put("list/:id")已经满足了,就不会继续往下匹配了,所以 @Put("list/user")装饰的方法应该写在它之前。
全局路由前缀
除了上面这些装饰器可以设置路由外, 我们还可以设置全局路由前缀, 比如给所以路由都加上/api前缀。此时需要修改main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api'); // 设置全局路由前缀
await app.listen(3000);
}
bootstrap();
此时之前的路由,都要变更为:http://localhost/api/xxx
到此我们认识了Controller
、Service
、Module
、路由以及一些常用的装饰器
编写代码
写代码之前首先介绍几个nest-cli提供的几个有用的命令:
//语法
nest g [文件类型] [文件名] [文件目录]
nest g resource user
自动生成这个接口所有部分
nest g resource posts // 创建所有资源
nest g mo posts --no-spec //创建模块
nest g s posts --no-spec //创建服务类
nest g co posts --no-spec //创建控制器
--no-spec 就是不要测试文件
注意创建顺序: 先创建Module, 再创建Controller和Service, 这样创建出来的文件在Module中自动注册,反之,后创建Module, Controller和Service,会被注册到外层的app.module.ts
创建模块
nest g mo posts
创建了一个 posts模块,文件目录不写,默认创建和文件名一样的posts目录,在posts目录下创建一个posts.module.ts。
执行完命令后,我们还可以发现同时在根模块app.module.ts中引入PostsModule这个模块,也在@Model装饰器的inports中引入了PostsModule
创建控制器
nest g co posts
此时创建了一个posts控制器,命名为posts.controller.ts以及一个该控制器的单元测试文件.
执行完命令, 文件posts.module.ts中会自动引入PostsController,并且在@Module装饰器的controllers中注入。
创建服务类
nest g service posts
创建app.service.ts文件,并且在app.module.ts文件下,@Module装饰器的providers中注入注入。
看一下现在的目录结构:
(如果执行命令的时候加了--no-spec那么不会生成测试文件)
连接Mysql
数据库
Naviact Premium
Prisma连接数据库
前置知识
首先,简单说一下什么是ORM?
我们如果直接使用Node.js操作mysql提供的接口, 那么编写的代码就比较底层, 例如一个插入数据代码:
// 向数据库中插入数据
connection.query(`INSERT INTO posts (title, content) VALUES ('${title}', '${content}')`,
(err, data) => {
if (err) {
console.error(err)
} else {
console.log(data)
}
})
考虑到数据库表是一个二维表,包含多行多列,例如一个posts的表:
mysql> select * from posts;
+----+--------+------------+
| id | title | content |
+----+-------------+--------------+
| 1 | Nest.js入门 | 文章内容描述 |
+----+--------+------------+
每一行可以用一个JavaScript对象来表示, 比如第一行:
{
id: 1,
title:"Nest.js入门",
content:"文章内容描述"
}
这就是传说中的ORM技术(Object-Relational Mapping)
,把关系数据库的变结构映射到对象上。
所以就出现了Sequelize
、typeORM
、Prisma
这些ORM框架来做这个转换, 我们这里选择Prisma
来操作数据库。
TypeORM:传统的 ORM 框架
TypeORM 是一种传统的 ORM(对象关系映射)框架,它将数据库表映射为实体类(entity),将表之间的关联映射为实体类属性的关联。
完成实体类和表的映射后,通过调用 userRepository 和 postRepository 的 API(如 find、delete、save 等),
TypeORM 会自动生成对应的 SQL 语句并执行。这就是对象关系映射的概念,即将对象和关系型数据库之间进行映射。
Prisma:颠覆传统的 ORM
Prisma 与 TypeORM 不同,它没有实体类的概念。
相反,Prisma 创造了一种领域特定语言(DSL),类似这样:
将数据库表映射为 DSL 中的 model,然后编译这个 DSL 将生成 Prisma Client 的代码:
之后,可以调用 Prisma Client 的 API(如 find、delete、create 等)来进行 CRUD(创建、读取、更新、删除)操作。虽然 Prisma 使用了 DSL 的语法,但整个流程与 TypeORM 类似。
Prisma使用方式
pnpm install prisma // 安装
npx prisma init // 初始化
// 例如连接mysql:修改.env文件 [DATABASE_URL="mysql://账号:密码@主机:端口/库名"]
DATABASE_URL="mysql://root:123456@localhost:3306/test
// 修改prisma/schema.prisma进行数据库连接
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
定义 Model
在 schema 文件中定义数据模型(model),例如:
// 生成器
generator client {
provider = "prisma-client-js"
}
// 数据源
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
// 定义模型
model Post {
id Int @id @default(autoincrement()) //表示id字段是整数且自增,主键
title String //表示title字段是字符串类型
publish Boolean @default(false) //表示发布字段是布尔值默认false
author User @relation(fields: [authorId], references: [id]) //表示author字段关联User用户表,关联关系:authorId 关联User表的id,外键
authorId Int //表示authorId字段是关联id,和User表的id进行关联,定义User表和Post表是一对多的关系
}
model User {
id Int @id @default(autoincrement()) //主键
name String
email String @unique // 表示email字段是字符串且是唯一的
posts Post[] //表示User表和Post表是一对多的关系
}
生成 Prisma Client 代码
pnpm add @prisma/client@5.16.1 // 安装@prisma/client包( prisma 客户端)
npx prisma migrate dev // 会根据 schema 文件生成 sql 并执行
npx prisma migrate dev --name test // 也可以通过这个指定名字。
npx prisma db push // 这个命令用于快速同步 Prisma 模型定义和数据库结构,而不需要创建明确的迁移文件。
安装 VSC 插件
在编辑模式文件前,在 VS Code 中安装 Prisma 插件,它针对 .prisma 文件提供了代码高亮、格式化、自动补全、跳转定义和检查的功能。没有这个插件的加持,模式文件就是一个纯文本。
CRUD
创建模型
// 定义数据模型(model)
model User {
id Int @id @default(autoincrement()) //主键
username String @db.VarChar(100)
nickname String @db.VarChar(100)
password String @db.VarChar(100)
avatar String
email String
role String @db.VarChar(50)
createTime DateTime @default(now())
updateTime DateTime @default(now())
posts Post[] //表示User表和Post表是一对多的关系
}
model Post {
id Int @id @default(autoincrement()) //表示id字段是整数且自增,主键
title String
content String?
published Boolean @default(false) //表示发布字段是布尔值默认false
author User? @relation(fields: [authorId], references: [id]) // 表示author字段关联User用户表
authorId Int? // 关联关系:authorId 关联User表的id,外键表示authorId字段是关联id,和User表的id进行关联,定义User表和Post表是一对多的关系
}
执行指令:npx prisma migrate dev // 会根据 schema 文件生成 sql 并执行
- 如果你在应用程序的数据模型中进行了更改(例如添加新的表、更改字段类型等),你可以运行
npx prisma migrate dev
来创建一个新的数据库迁移文件,该文件包含了这些变更。
npx prisma migrate dev --name test // 也可以通过这个指定名字。
注入 Prisma 客户端
确保在 NestJS 的模块中正确注入 Prisma 客户端。例如,在 NestJS 的根模块或者相关的特定模块中进行注入:
import { Module } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';
@Module({
imports: [],
controllers: [PostsController],
providers: [PostsService,PrismaClient],
})
export class PostsModule {}
Service
import { PrismaClient } from '@prisma/client';
import { Injectable } from '@nestjs/common';
@Injectable()
// 们创建了一个PostsService,它使用了 Prisma 客户端来执行与帖子相关的 CRUD 操作。
export class PostsService {
constructor(private prisma: PrismaClient) {}
async findAllPosts() {
return this.prisma.post.findMany();
}
async findPostsById(id: number) {
return this.prisma.post.findUnique({
where: { id },
});
}
async createPosts(data: any) {
return this.prisma.post.create({
data,
});
}
async updatePosts(id: number, data: any) {
return this.prisma.post.update({
where: { id },
data,
});
}
async deletePosts(id: number) {
return this.prisma.post.delete({
where: { id },
});
}
}
Controller
import { Controller, Get, Post, Body, Param, Put, Delete } from '@nestjs/common';
import { PostsService } from './posts.service';
import { Post as MyPost } from '@prisma/client';
@Controller('posts')
export class PostsController {
constructor(private readonly postsService: PostsService) {}
@Get()
async findAll(): Promise<MyPost[]> {
return this.postsService.findAllPosts();
}
@Get(':id')
async findOne(@Param('id') id: string): Promise<MyPost | null> {
return this.postsService.findPostsById(+id);
}
@Post()
async create(@Body() postData: { title: string; content?: string; published?: boolean; authorId: number }): Promise<MyPost> {
return this.postsService.createPosts(postData);
}
@Put(':id')
async update(@Param('id') id: string, @Body() updateData: { title?: string; content?: string; published?: boolean }): Promise<MyPost | null> {
return this.postsService.updatePosts(+id, updateData);
}
@Delete(':id')
async remove(@Param('id') id: string): Promise<MyPost | null> {
return this.postsService.deletePosts(+id);
}
}
(因为存在外键原因,这里手动增加user表的一条数据,定义id为1即可)
{
"title": "Sample Post",
"content": "This is the content of the post.",
"published": true,
"authorId": 1
}
接口统一规范:拦截请求
拦截错误请求
首先使用命令创建一个过滤器:
nest g filter core/filter/http-exception
import {ArgumentsHost,Catch, ExceptionFilter, HttpException} from '@nestjs/common';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp(); // 获取请求上下文
const response = ctx.getResponse(); // 获取请求上下文中的 response对象
const status = exception.getStatus(); // 获取异常状态码
// 设置错误信息
const message = exception.message
? exception.message
: `${status >= 500 ? 'Service Error' : 'Client Error'}`;
const errorResponse = {
data: {},
message: message,
code: -1,
};
// 设置返回的状态码, 请求头,发送错误信息
response.status(status);
response.header('Content-Type', 'application/json; charset=utf-8');
response.send(errorResponse);
}
}
拦截成功的返回数据
首先使用命令创建一个拦截器:
nest g interceptor core/interceptor/transform
import {CallHandler, ExecutionContext, Injectable,NestInterceptor} from '@nestjs/common';
import { map, Observable } from 'rxjs';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => {
return {
data,
code: 0,
msg: '请求成功',
};
}),
);
}
}
在main.ts注册
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { TransformInterceptor } from './core/interceptor/transform/transform.interceptor';
import { HttpExceptionFilter } from './core/filter/http-exception/http-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api'); // 设置全局路由前缀
// 注册全局错误的过滤器
app.useGlobalFilters(new HttpExceptionFilter());
// 全局注册拦截器
app.useGlobalInterceptors(new TransformInterceptor());
await app.listen(3000);
}
bootstrap();
Swagger
pnpm install @nestjs/swagger swagger-ui-express -S
...
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
// 应用程序的入口文件将使用核心函数 NestFactory 来创建一个 Nest 应用程序实例.
async function bootstrap() {
...
// 设置swagger文档
const config = new DocumentBuilder()
.setTitle('管理后台')
.setDescription('管理后台接口文档')
.setVersion('1.0')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document);
await app.listen(3000);
}
bootstrap();
配置完成,我们就可以访问:http://localhost:3000/docs,此时就能看到Swagger生成的文档:
接口标签
我们可以根据Controller来分类, 只要添加@ApiTags就可以
...
import { ApiTags } from '@nestjs/swagger';
@ApiTags("文章")
@Controller('post')
export class PostsController {...}
接口说明
进一步优化文档, 给每一个接口添加说明文字, 让使用的人直观的看到每个接口的含义,不要让使用的人去猜。同样在Controller中, 在每一个路由的前面使用@ApiOperation装饰器:
...
import { ApiTags,ApiOperation } from '@nestjs/swagger';
export class PostsController {
...
@ApiOperation({ summary: '创建文章' })
@Post()
async create(@Body() post) {....}
....
}
接口传参
在posts目录下创建一个dto文件夹,再创建一个create-post.dot.ts文件:
import { ApiProperty } from '@nestjs/swagger';
export class PostDto {
@ApiProperty({ description: '文章id' })
readonly id?: number;
@ApiProperty({ description: '文章标题' })
readonly title: string;
@ApiProperty({ description: '内容' })
readonly content: string;
@ApiProperty({ description: 'published' })
readonly published?: boolean;
@ApiProperty({ description: '作者' })
readonly author?: string;
@ApiProperty({ description: 'authorId' })
readonly authorId?: number;
}
然后在Controller中对创建文章是传入的参数进行类型说明:
数据验证(异常处理)
Nest.js中数据验证,发现Nest.js中的管道就是专门用来做数据转换的
定义:管道是具有 @Injectable() 装饰器的类。管道应实现 PipeTransform 接口。
管道有两个类型:
- 转换:管道将输入数据转换为所需的数据输出
- 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常;
管道在异常区域内运行。这意味着当抛出异常时,它们由核心异常处理程序和应用于当前上下文的 异常过滤器 处理。当在 Pipe 中发生异常,controller 不会继续执行任何方法。
通俗来讲就是,对请求接口的入参进行验证和转换的前置操作,验证好了我才会将内容给到路由对应的方法中去,失败了就进入异常过滤器中。
pnpm install class-validator class-transformer -S
然后在create-post.dto.ts文件中添加验证, 完善错误信息提示:
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
export class CreatePostDto {
@ApiProperty({ description: '文章标题' })
@IsNotEmpty({ message: '文章标题必填' })
readonly title: string;
...
@IsNumber() // 使用 IsNumber 确保输入的类型是数字。
@ApiProperty({ description: '文章类型' })
readonly type: number;
}
入门阶段,我们使用的数据比较简单,上面只编写了一些常用的验证,class-validator还提供了很多的验证方法, 大家感兴趣可以自己看官方文档.
中间件logger
nest g middleware logger common
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: () => void) {
const { method, path } = req;
console.log(`${method},${path}`);
next(); // 走下一步
}
}
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PostsModule } from './posts/posts.module';
import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common';
import { LoggerMiddleware } from './common/logger/logger.middleware';
// 应用程序的根模块.根模块提供了用来启动应用的引导机制,可以包含很多功能模块。
@Module({
//一个@Module() 装饰器的类,装饰器可以理解成一个封装好的函数,其实是一个语法糖
imports: [PostsModule], // 导入模块的列表,如果需要使用其他模块的服务,需要通过这里导入;
controllers: [AppController], // 处理http请求,包括路由控制,向客户端返回响应,将具体业务逻辑委托给providers处理;
providers: [AppService], // Nest.js注入器实例化的提供者(服务提供者),处理具体的业务逻辑,各个模块之间可以共享(注入器的概念后面依赖注入部分会讲解);
// exports:导出服务的列表,供其他模块导入使用。如果希望当前模块下的服务可以被其他模块共享,需要在这里配置导出;
})
export class AppModule {
// 为 posts 路由添加中间件
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.exclude({ path: 'posts', method: RequestMethod.POST }) // post方法不会走这个中间件,但是其它方法会走
.forRoutes('posts');// 监听posts路线
}
}
角色控制守卫(guard)
我在后面还定义了一个使用守卫限制token:在后面基础服务用户登录模块login.guard.ts
守卫有单一的责任。它们根据运行时存在的某些条件(如权限、角色、ACL 等)确定给定请求是否将由路由处理程序处理。这通常称为授权。授权(及其通常与之合作的身份验证)通常由传统 Express 应用中的 中间件 处理。中间件是身份验证的不错选择,因为诸如令牌验证和将属性附加到 request 对象之类的事情与特定路由上下文(及其元数据)没有紧密联系。
nest g guard guards
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core'; // 反射期
import { Observable } from 'rxjs';
@Injectable()
export class RoleGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {// canActivate用于判断是否允许访问请求。
// 装饰器roles.decorator.ts 中定义了一个名为 roles 的元数据,它返回一个包含允许访问的角色数组。
// 通过 Reflector 获取装饰器中定义的 roles 元数据。
// 通过使用 装饰器@Roles('admin')来检查用户的角色是否匹配所需的角色(例如 'admin')
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const { user } = request.query;
return !!roles.find(role=>role === user);
}
}
装饰器
nest g decorator decorators
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { RoleGuardService } from './role-guard.service';
import { Roles } from '../decorators/roles.decorator';
import { RoleGuard } from '../guards/roles.guard';
@ApiTags('role-guard')
@Controller('role-guard')
@UseGuards(RoleGuard)
export class RoleGuardController {
constructor(private readonly roleGuardService: RoleGuardService) {}
@Get()
@Roles('admin') //只有admin才可以查询
fetch(@Query() { id }): any {
return '角色正确';
// return this.roleGuardService.findPostsById(id);
}
}
邮件服务(没用成功,后面在操作)
pnpm install --save @nestjs-modules/mailer nodemailer pug
pnpm install nestjs-config --save
一些小tips
删除node_modules后重新下载,导入包失败
- 使用 pnpm 自带的清除命令:
pnpm store prune
这个命令会清除 pnpm 的缓存和未使用的依赖项。
- 重新安装依赖:首先确认你使用了正确的命令重新安装依赖。使用 pnpm 或其他包管理工具重新安装项目依赖:
pnpm install
这将重新安装 package.json 中指定的所有依赖项
在运行有prisma的项目的时候,要先运行 prisma generate:
在终端中运行以下命令,确保生成 Prisma 客户端的代码:
npx prisma generate
基础服务
// 定义数据模型(model)
model User {
id Int @id @default(autoincrement()) //主键
username String @db.VarChar(100)
nickname String @db.VarChar(100)
password String @db.VarChar(100)
avatar String
email String
role String @db.VarChar(50)
createTime DateTime @default(now())
updateTime DateTime @default(now())
posts Post[] //表示User表和Post表是一对多的关系
}
model Post {
id Int @id @default(autoincrement()) //表示id字段是整数且自增,主键
title String
content String?
published Boolean @default(false) //表示发布字段是布尔值默认false
author User? @relation(fields: [authorId], references: [id]) // 表示author字段关联User用户表
authorId Int? // 关联关系:authorId 关联User表的id,外键表示authorId字段是关联id,和User表的id进行关联,定义User表和Post表是一对多的关系
}
启动:npx prisma generate
nest g resource user
自动生成这个接口所有部分。
(这个我自己后面代码在增加了login-user.dto.ts文件,不是自动生成的)
- DTO(数据传输对象)用于定义数据传输对象,主要用于验证和描述数据的传输格式。(常是普通的 TypeScript 类或接口,用来定义请求的数据结构或者响应的数据格式。)
- Entities(实体)是领域模型中的对象或类,用于与数据库表之间的映射和操作,主要涉及数据持久化和业务逻辑的实现。(是领域模型中的对象或者类,通常代表数据库中的一张表或者集合)
用户注册
密码我们不能直接存明文在数据库中,所以采用bcryptjs
实现加密, 然后再存入数据库。
pnpm install bcryptjs
import * as bcrypt from 'bcryptjs';
import { Injectable } from '@nestjs/common';
@Injectable()
export class EncryptionService {
private fixedSalt = 'lingyang'; // 固定加盐值,示例中使用了一个字符串,实际中可以更复杂和安全
async hashPassword(password: string): Promise<string> {
const saltRounds = 10; // 加盐轮数,可以根据需要调整
const hashedPassword = await bcrypt.hash(password + this.fixedSalt, saltRounds);
return hashedPassword;
}
async comparePasswords(plainPassword: string, hashedPassword: string): Promise<boolean> {
const isMatch = await bcrypt.compare(plainPassword + this.fixedSalt, hashedPassword);
return isMatch;
}
}
...
@Module({
controllers: [UserController],
providers: [UserService, PrismaClient, EncryptionService],
})
export class UserModule {}
import { PrismaClient } from '@prisma/client';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { EncryptionService } from './encryption.service';
@Injectable()
export class UserService {
constructor(
private prisma: PrismaClient,
private readonly encryptionService: EncryptionService,
) {}
async register(createUser: CreateUserDto) {
const { username, password } = createUser;
// 检查用户名是否已经存在
const existUser = await this.prisma.user.findFirst({
where: { username },
});
if (existUser) {
throw new HttpException('用户名已存在', HttpStatus.BAD_REQUEST);
}
// 对密码进行加密
const hashedPassword = await this.encryptionService.hashPassword(password);
// 创建新用户
const newUser = await this.prisma.user.create({
data: { ...createUser, password: hashedPassword },
});
// 不返回密码字段给用户
const { password: _, ...newUserWithoutPassword } = newUser;
return newUserWithoutPassword;
}
}
{ ...createUser, password: hashedPassword }
和{ password: hashedPassword, ...createUser }
区别
{ ...createUser, password: hashedPassword }
:
-
- 意味着如果
createUser
对象中已经有password
属性,那么这里的password: hashedPassword
将会覆盖掉原有的密码属性值。
- 意味着如果
{ password: hashedPassword, ...createUser }
:
-
- 同样地,如果
createUser
对象中已经有password
属性,那么这里的password: hashedPassword
会覆盖掉原有的密码属性值。
- 同样地,如果
tip: 后面的会覆盖前面的
用户登录
...
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
...
@ApiOperation({ summary: '登录' })
@Post('login')
login(@Body() loginDto: LoginUserDto) {
return this.userService.login(loginDto);
}
}
...
@Injectable()
export class UserService {
constructor(
private prisma: PrismaClient,
private readonly encryptionService: EncryptionService,
) {}
async register(createUser: CreateUserDto) {...}
async login(loginUser: LoginUserDto) {
const user = await this.prisma.user.findFirst({
where: {
username: loginUser.username,
},
});
if (user) {
const isPasswordCorrect = await this.encryptionService.comparePasswords(
loginUser.password,
user.password,
);
if (isPasswordCorrect) {
// 不返回密码字段给用户
const { password: _, ...userWithoutPassword } = user;
return userWithoutPassword;
} else {
throw new HttpException('密码错误', 200);
}
} else {
throw new HttpException('用户不存在', 200);
}
}
}
JWT配置
- 登录或者注册成功,我们都应该返回token
- 前端存一份,以后请求别的接口的时候,带上,
-
- 验证通过就可以请求接口数据
- 验证失败就抛出错误,不返回数据
pnpm install @nestjs/jwt
引入jwt
AppModule 里引入 JwtModule
...
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [
...
JwtModule.register({ // 注册jwt模块
global: true, // 全局使用
secret: 'dehua', // 秘钥
signOptions: { // 签名配置
expiresIn: '7d', // 过期时间
},
})],
controllers: [AppController],
providers: [AppService],
})
export class AppModule { }
global:true 声明为全局模块,这样就不用每个模块都引入它了,指定加密密钥,token 过期时间
...
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class UserService {
constructor(
private prisma: PrismaClient,
private readonly encryptionService: EncryptionService,
private readonly jwtService: JwtService,
) {}
async register(createUser: CreateUserDto) {
...
const token = await this.jwtService.signAsync({
id: newUser.id,
username: newUser.username
})
return token;
}
async login(loginUser: LoginUserDto) {
...
if (isPasswordCorrect) {
// 返回token给前端,让前端存起来
const token = await this.jwtService.signAsync({
id: user.id,
username: user.username
})
return token;
} else {
throw new HttpException('密码错误', 200);
}
...
}
}
接下来前端存储一份,请求头上携带就可以了
接口要token验证通过之后,才可以返回数据
使用守卫限制token
现在都可以查,不管有没有token,不管验证通过没有
我们现在创建一个Guard(守卫)来限制,满足才可以访问接口
nest g guard login --no-spec --flat
- --no-spec 参数告诉 NestJS 不生成单元测试规范文件。
- --flat 参数告诉 NestJS 将生成的文件放在当前文件夹中,而不是创建一个新的文件夹。
然后把他放到这里面
import { JwtService } from '@nestjs/jwt';
import {
CanActivate,
ExecutionContext,
Inject,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Request } from 'express';
import { Observable } from 'rxjs';
interface JwtUserData {
id: any;
username: string;
}
declare module 'express' {
interface Request {
user: JwtUserData;
}
}
@Injectable()
export class LoginGuard implements CanActivate {
@Inject(JwtService)
private jwtService: JwtService;
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request: Request = context.switchToHttp().getRequest();
const token = request.header('token') || '';
try {
const info = this.jwtService.verify(token);
(request as any).user = info;
return true;
} catch (e) {
throw new UnauthorizedException('登录 token 失效,请重新登录');
}
}
}
@Post()
@UseGuards(LoginGuard) // 使用守卫
@ApiOperation({ summary: '创建文章' })
async create(@Body() postData: PostDto): Promise<PostDto> {
return this.postsService.createPosts(postData);
}
获取当前token用户信息
同时更改之前帖子创建接口创建人信息由token用户信息得到而不是手动传
import { Request } from 'express';
...
@Post()
@UseGuards(LoginGuard) // 使用守卫
@ApiOperation({ summary: '创建文章' })
async create(
@Body() postData: PostDto,
@Req() req: Request,
): Promise<PostDto> {
console.log(req.user);
const _postData={...postData,authorId:req.user.id}
return this.postsService.createPosts(_postData);
}
(现在这样就和当前用户绑定啦)