前言
nestjs的课程真的没有,不管是B站还是Y站,都寥寥无几,英文课程我是真的听不懂。这个教程已经是很好的教程了,最起码能够让我入门,把关键点都讲到了。
当然课程学习很是需要精力,特别是一些概念、操作步骤等等,需要我将这个笔记尽可能的完善,需要时间。要坚持下去,因为nestjs框架的用法,和很多很流行的框架是一致的,搞清楚一项就能够搞清楚别的框架了。
Nestjs 是一个用于构建高效可扩展的一个基于Node js 服务端应用程序开发框架,并且完全支持 typeScript。结合了 AOP 面向切面的编程方式。
什么是AOP?
https://zhuanlan.zhihu.com/p/161705262
https://zhuanlan.zhihu.com/p/657849682
看了曾探的设计模式一书,里面介绍了使用高阶函数来实现AOP的功能,我想可以作为理解AOP的入门。
x1// 通过扩展 Function.prototype 来实现 AOP23Function.prototype.before = function (beforeFn) {4var _self = this; // 保存原函数的引用5return function () {6// 返回包含了原函数和新函数的“代理”函数7beforeFn.apply(this, arguments); // 执行新函数,修正this8return _self.apply(this, arguments); // 执行原函数9};10};1112Function.prototype.after = function (afterFn) {13var _self = this;14return function () {15var ret = _self.apply(this, arguments);16afterFn.apply(this, arguments);17return ret;18};19};2021var func = function () {22console.log(2);23};2425func = func26.before(function () {27console.log(1);28})29.after(function () {30console.log(3);31});3233func();执行结果:
这样,就能够让123按照顺序输出。
但是上面的after函数里面,我有一个疑问,就是
var ret = _self.apply(this, arguments),返回的ret在after.apply(this, arguments)后面,那么函数会在after之后执行吗?而要求的是函数在after之前执行。可以输出ret看一下,是什么?
ret是undefined,那么说明这个赋值操作是没有效果的,将after里面改为这样,看执行效果:
xxxxxxxxxx71Function.prototype.after = function (afterFn) {2var _self = this;3return function () {4_self.apply(this, arguments);5afterFn.apply(this, arguments);6};7};
没有任何问题。
我还有一个疑问,就是before和after里面都有这段代码:
_self.apply(this, arguments);,那么原函数会执行两次吗?但是输出结果明确告诉我们,原函数不会执行两次,那么里面到底是怎么执行的呢?将after里面的这段代码
_self.apply(this, arguments);注释后执行:
只输出了3,输出此时的
_self看一下:
说明after里面的
_self其实已经有before函数在里面了,实际上执行的是上面这个函数,所以原函数不会执行两次。
联想到nestjs,比如说日志记录、性能监控、事务管理、缓存管理、权限控制、异常处理等功能,都有AOP的思想在里面,当然nestjs里面实现的逻辑很复杂,但知道最简单的实现方式,能够帮助我更好的理解。因为我总是会怀疑,这能够实现吗?
nestjs 还是一个spring MVC 风格的框架,其中有依赖注入,IOC,控制反转,都是借鉴了Angular。
nestjs 的底层代码运用了 express 和 Fastify。在他们的基础上提供了一定程度的抽象,同时也将其 API 直接暴露给开发人员。这样可以轻松使用每个平台的无数第三方模块。
nestjs内置框架express,默认框架express。能够快速构建服务端应用程序,且学习成本非常低,容易上手。
express官网: https://www.expressjs.com.cn/
nestjs另一个内置框架:Fastify。
Fastify特性:

在学习 nestjs 之前需要先了解其设计模式。
Inversion of Control字面意思是控制反转,具体定义是高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。
依赖注入(Dependency Injection)其实和IoC是同根生,这两个原本就是一个东西,只不过由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以2004年大师级人物Martin Fowler又给出了一个新的名字:“依赖注入”。 类A依赖类B的常规表现是在类A中使用类B的instance(实例)。
为什么需要IOC和DI?想一想团队合作的时候,合并代码的时候,如果不想到处都是代码冲突,那么最好是公共模块和业务模块分开,并且业务模块之间的耦合尽量小,这样合作起来才顺利。
老师在视频讲解里面的例子是这样的:
xxxxxxxxxx211class A {2name: string;3constructor() {4this.name = "小满";5}6}78// 在B和C类中,想用到A类的name属性,于是可以像下面这样写,没有问题9class B {10a: any;11constructor() {12this.a = new A().name;13}14}1516class C {17b: any;18constructor() {19this.b = new A().name;20}21}但如果A类中的name是传递过来的,不是固定值,会怎么样呢?
可以看到,B类和C类里面的代码都需要进行修改,在实例化A类的时候,要显式的传值。那么此时A和B、C类之间的关系就是强耦合的。
解决办法就是使用依赖注入:
xxxxxxxxxx491// 定义两个普通的类,A和C2class A {3name: string;4constructor(name: string) {5this.name = name;6}7}89class C {10age: number;11constructor(age: number) {12this.age = age;13}14}1516// 创建一个容器来收集引用17class Container {18module: any;19constructor() {20this.module = {};21}2223// 定义一个注入方法,这样就可以收集到类的引用了24provide(key: string, module: any) {25// 注意两个module是不同的,一个是类的属性,一个是传递过来的引用实例26this.module[key] = module;27}2829// 定义一个get方法,用于取出类的引用30get(key: string) {31return this.module[key];32}33}3435// 实例化容器36const module = new Container();37// 然后将两个类注入进去38module.provide("a", new A("小满啪啪啪"));39module.provide("c", new C(18));4041// 上面的步骤,注入完成之后,就可以用依赖注入的方式来引用这些类的实例42class B {43a: any;44c: any;45constructor(module: Container) {46this.a = module.get("a");47this.c = module.get("c");48}49}老师的原话:在引入IOC容器container之后,B和A的代码逻辑已经解耦。可以单独扩展其他功能,也可以方便地加入其他模块(比如C)。所以在面对复杂的后端业务逻辑中,引入IoC可以降低组件之间的耦合度,实现系统各层之间的解耦,减少维护和理解成本。
从上面的代码可以看到,如果B类中想用到A类或C类的实例,只需要从容器中取出来使用即可,而不必在B类中实例化一个A类或C类的实例,这样B类与A类、C类就解耦了。
那Container、实例化Container、将各种类注入Container实例,这些步骤在nestjs里面是谁在操作呢?我好像写的最多的就是各种各样的类,从来没有进行过这些操作啊?
这些是nestjs框架在操作,它帮助我们完成了这些步骤,我们只需要做的就是在B类里面引入实例化对象来使用即可。这部分在xxx.controller.ts、xxx.service.ts里面的constructor里面看到的最多。
案例未使用控制反转和依赖注入之前的代码
xxxxxxxxxx191class A {2 name: string3 constructor(name: string) {4 this.name = name5 }6}7 8class B {9 age:number10 entity:A11 constructor (age:number) {12 this.age = age;13 this.entity = new A('小满')14 }15}16 17const c = new B(18)18 19c.entity.name我们可以看到,B 中代码的实现是需要依赖 A 的,两者的代码耦合度非常高。当两者之间的业务逻辑复杂程度增加的情况下,维护成本与代码可读性都会随着增加,并且很难再多引入额外的模块进行功能拓展。
为了解决这个问题可以使用IOC容器
xxxxxxxxxx421class A {2 name: string3 constructor(name: string) {4 this.name = name5 }6}7 8 9class C {10 name: string11 constructor(name: string) {12 this.name = name13 }14}15//中间件用于解耦16class Container {17 modeuls: any18 constructor() {19 this.modeuls = {}20 }21 provide(key: string, modeuls: any) {22 this.modeuls[key] = modeuls23 }24 get(key) {25 return this.modeuls[key]26 }27}28 29const mo = new Container()30mo.provide('a', new A('小满1'))31mo.provide('c', new C('小满2'))32 33class B {34 a: any35 c: any36 constructor(container: Container) {37 this.a = container.get('a')38 this.c = container.get('c')39 }40}41 42new B(mo)其实就是写了一个中间件,来收集依赖,主要是为了解耦,减少维护成本。
更多关于IOC的资料,参考:https://zhuanlan.zhihu.com/p/658932974
后端MVC概念的理解:
1)Model(模型)
含义:模型是应用程序中用于处理数据的部分。它主要负责数据的存储、检索和业务逻辑的执行。模型通常与数据库或其他数据源进行交互,并且包含了数据的验证、关联和操作等功能。
示例:
xxxxxxxxxx61from django.db import models23class Product(models.Model):4 name = models.CharField(max_length = 200)5 price = models.DecimalField(max_digits = 10, decimal_places = 2)6 stock = models.IntegerField()Product模型类定义了商品的基本数据结构。它可以通过 Django 的 ORM(对象关系映射)与数据库进行交互,比如将商品数据保存到数据库中、从数据库中查询商品信息等。同时,模型还可以包含业务逻辑,例如,在这个Product类中可以定义一个方法来计算商品的总价值(total_value = price * stock)。2)View(视图)
含义:视图主要负责将数据以合适的格式呈现给用户或者其他系统。在后端应用中,对于 RESTful API 而言,视图通常是指将数据转换为合适的格式(如 JSON)返回给客户端。它是用户和系统之间交互的接口部分,决定了用户看到的数据外观。
示例:
xxxxxxxxxx141from django.http import JsonResponse2from.models import Product34def product_list(request):5 products = Product.objects.all()6 product_list = []7 for product in products:8 product_dict = {9 'name': product.name,10 'price': product.price,11 'stock': product.stock12 }13 product_list.append(product_dict)14 return JsonResponse(product_list, safe=False)product_list从Product模型中获取所有商品对象,然后将每个商品的相关信息提取出来,组成一个字典列表,最后将这个列表以 JSON 格式返回给客户端。3)Controller(控制器)
含义:控制器是在模型和视图之间起到协调作用的组件。它接收用户的请求,根据请求的类型和内容调用相应的模型方法来处理数据,然后将处理后的数据传递给视图进行展示。控制器决定了应用程序如何响应不同的用户请求,是业务逻辑的调度者。
示例:
@RestController注解来定义。假设我们有一个用户注册的功能。xxxxxxxxxx221import org.springframework.beans.factory.annotation.Autowired;2import org.springframework.web.bind.annotation.PostMapping;3import org.springframework.web.bind.annotation.RequestBody;4import org.springframework.web.bind.annotation.RestController;5import com.example.demo.model.User;6import com.example.demo.service.UserService;789public class UserController {10 11 private UserService userService;1213 ("/register")14 public String registerUser( User user) {15 boolean result = userService.register(user);16 if (result) {17 return "User registered successfully";18 } else {19 return "Registration failed";20 }21 }22}UserController接收一个用户注册的POST请求(通过@PostMapping注解定义),将请求体中的用户数据(@RequestBody User user)传递给UserService(这是业务逻辑处理部分,属于模型层的范畴)进行注册操作。根据UserService返回的结果,控制器决定返回给客户端相应的消息,这个消息最终会通过视图层(在这种情况下,Spring Boot 会自动将字符串转换为合适的响应格式,如 JSON 或者纯文本)返回给客户端。那么在nestjs中,应该怎么理解MVC呢?

参考:https://www.zhihu.com/question/544494969/answer/3027687155
1)Model(模型)
user.entity.ts文件就是模型的一部分。xxxxxxxxxx101import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';2@Entity()3export class User {4 @PrimaryGeneratedColumn()5 id: number;6 @Column()7 name: string;8 @Column()9 email: string;10}User类定义了数据库中user表的结构,包括id、name和email字段。它可以用于数据库的增删改查操作,比如在数据访问层(Repository)中作为操作的数据对象。2)View(视图)
.html等视图文件。在 NestJS 的控制器(Controller)方法中,会通过@Get、@Post等装饰器定义的路由处理函数返回数据给客户端,这个返回的数据可以看作是 “视图” 部分。user.controller.ts中的一个获取用户信息的方法:xxxxxxxxxx101import { Controller, Get } from '@nestjs/common';2import { UserService } from './user.service';3@Controller('users')4export class UserController {5 constructor(private readonly userService: UserService) {}6 @Get(':id')7 async findOne(id: string): Promise<User> {8 return this.userService.findOne(id);9 }10}GET请求到/users/:id这个路由时,控制器的findOne方法会调用服务层(user.service.ts)的findOne方法获取用户数据,并将其返回给客户端。返回的用户数据(这里假设是User类型的对象)的格式就是客户端看到的 “视图”。3)Controller(控制器)
.controller.ts结尾)负责处理传入的请求并返回响应。它是应用程序中处理 HTTP 请求的入口点。user.controller.ts文件,它使用@Controller装饰器定义了一个基本的路由前缀(/users),并在其中定义了具体的路由处理方法(如findOne方法),这些方法通过 HTTP 方法装饰器(如@Get、@Post等)来处理不同类型的请求。注意:
我在学习nestjs的时候,有一个错误的理解:装饰器就是为了实现依赖注入。直到我想要将nestjs教程做成xmind时,不得不提炼一下知识点时,我才发现这是一个错误理解。装饰器能够在nestjs中实现依赖注入,但是在nestjs中,装饰器的作用还有很多,以下是它的作用。
一、控制反转(IoC)和依赖注入
@Injectable()装饰器:
- 用于标记一个类可以被 NestJS 的依赖注入系统管理。
- 当一个服务类被标记为
@Injectable()后,NestJS 可以在需要这个服务的地方自动实例化并注入该服务。- 例如,在一个控制器中,如果需要使用某个服务,可以将该服务作为参数传入控制器的构造函数,NestJS 会自动查找并注入这个服务实例。
- 这使得代码的模块间耦合度降低,提高了代码的可维护性和可测试性。
二、定义路由和处理请求
@Controller()装饰器:
- 用于定义一个控制器类,该类负责处理特定的路由请求。
- 可以通过设置装饰器的参数来指定控制器的路由前缀,例如
@Controller('users')表示这个控制器处理以/users开头的路由请求。
@Get()、@Post()、@Put()、@Delete()等装饰器:
- 用于定义控制器中的具体路由处理方法。
- 比如
@Get('profile')表示处理 GET 请求到/users/profile的路由,方法内部可以编写具体的业务逻辑来响应这个请求。三、配置参数和元数据
@Param()、@Query()、@Body()等装饰器:
- 用于从请求中提取特定类型的参数。
@Param('id')可以提取路由参数中的id值,@Query('page')可以提取查询参数中的page值,@Body()可以提取请求体中的数据。- 这些装饰器使得在处理请求时能够方便地获取和使用各种类型的输入数据。
四、实现中间件和过滤器
@UseMiddleware()装饰器:
- 用于将中间件应用到特定的路由或控制器上。
- 中间件可以在请求处理管道中执行一些通用的逻辑,如日志记录、身份验证、错误处理等。
@UseFilters()装饰器:
- 用于将异常过滤器应用到特定的路由或控制器上。
- 过滤器可以捕获和处理在请求处理过程中发生的异常,返回统一的错误响应格式。
总之,装饰器在 NestJS 中起到了关键的作用,它们帮助开发者以一种简洁、声明式的方式构建强大的应用程序,提高了开发效率和代码的可维护性。
这一节主要讲在typescript中,装饰器的几种用法,就这么四种用法,真的是不多,可以从ts的定义里面看到,装饰器就是一个函数,里面有哪些参数、可以用这些参数做什么,这些都可以从类型定义看出来。


注意:上面的装饰器是typescript传统的装饰器,现在传统和新版本的装饰器可以共存,这就是为什么我ctrl+点击ClassDecorator,会在lib.decorators.legacy.d.ts这个文件里面出现了,因为我知道legacy就是遗产、遗留的意思。
新版本的装饰器,可以在阮一峰的typescript教程里面查看,使用起来区别还是很大的,但是这里用传统的装饰器来学习nestjs,没有任何问题,只需要知道它的原理就行了。
装饰器是一种特殊的类型声明,他可以附加在类,方法,属性,参数上面。
如果是自己创建的项目里面,要写装饰器,需要开启一项配置。(像nestjs这些框架,如果使用它的脚手架来创建项目,就是默认打开这项配置的。)

作用:它会自动把class的构造函数传入到装饰器的第一个参数 target,然后通过prototype可以自定义添加属性和方法。
上面的“class的构造函数”,要搞清楚是什么意思。就是constructor。
xxxxxxxxxx11declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
xxxxxxxxxx151function decotators (target:any) {2 console.log(target)3 target.prototype.name = '小满'4}5 6@decotators7class Xiaoman {8 constructor () {9 10 }11}12 13const xiaoman:any = new Xiaoman()14 15console.log(xiaoman.name);
xxxxxxxxxx11declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;使用@符号给属性添加装饰器,它会返回两个参数:
1.原型对象
2.属性的名称
xxxxxxxxxx111const currency: PropertyDecorator = (target: any, key: string | symbol) => {2 console.log(target, key)3}4 5class Xiaoman {6 @currency7 public name: string8 constructor() {9 this.name = ''10 }11}
这里我有一个疑问:这个类并没有实例化,也没有输出类的哪些信息,那执行2.ts文件的输出是哪来的呢?
其实之所以有这个疑问,就是因为没有搞清楚装饰器的作用,上面已经说了,属性装饰器会返回两个参数,而我已经在装饰器函数里面输出了这两个参数的值。那么执行这个文件的时候,会编译这个类,同样也会执行里面的装饰器函数,所以会有输出结果显示出来。
xxxxxxxxxx11declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;在按照下面的代码写之后,会报错:
虽然看不懂,但我感觉重点是这一句,于是我找到
ParameterDecorator的类型定义,发现它是这样的:
declare type ParameterDecorato = (target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) => void;propertyKey需要加一个undefined的类型,加了之后就不报错了。
但是我看到别的版本的typescript里面的定义不是这样的,等做项目的时候注意就行了。
使用@符号给参数添加装饰器,它会返回三个参数:
1.原型对象
2.方法的名称(注意:虽然修饰的是参数,但是返回的这个propertyKey,是方法的名称。)
3.参数的位置索引,从0开始(注意:这里指的是被装饰器修饰的参数,在方法里面所在的位置索引)
xxxxxxxxxx131const currency: ParameterDecorator = (target: any, key: string | symbol ,index:number) => {2 console.log(target, key,index)3}4 5class Xiaoman {6 public name: string7 constructor() {8 this.name = ''9 }10 getName(name:string,@currency age:number) {11 return this.name12 }13}
xxxxxxxxxx11declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;使用@符号给方法添加装饰器,它会返回三个参数:
1.原型对象
2.方法的名称
3.属性描述符。可写对应writable,可枚举对应enumerable,可配置对应configurable,value对应被修饰的函数本身。
xxxxxxxxxx141const currency: MethodDecorator = (target: any, key: string | symbol,descriptor:any) => {2 console.log(target, key,descriptor)3}4 5class Xiaoman {6 public name: string7 constructor() {8 this.name = ''9 }10 @currency11 getName(name:string,age:number) {12 return this.name13 }14}
注意:装饰器可以写成很多种样子,但是@后面的表达式,必须是一个函数(或者执行后可以得到一个函数)。
所以,只要下面这些方式,
@后面的表达式,最终执行后得到的是一个函数,那么写起来都是OK的。xxxxxxxxxx71@myFunc2@myFuncFactory(arg1, arg2)34@libraryModule.prop5@someObj.method(123)67@(wrap(dict['prop']))
https://wangdoc.com/typescript/decorator
那么我的这个疑问:装饰器既然是一个函数,那么在使用装饰器的时候,要不要带括号呢?
如果在编写装饰器的时候,不用传参,那么使用装饰器的时候就不要写括号,写了括号反而会报错:Unable to resolve signature of property decorator when called as an expression.。被当作了一个表达式了。
如果在编写装饰器的时候,传递了参数,一般都会用到函数的柯里化,这时候使用装饰器就要带括号了,即使不传实参。
这一节课对理解nestjs框架原理很有帮助,装饰器到底是怎么被应用的,不管是自定义的装饰器还是nestjs提供的装饰器,目前就是死记硬背在用。如果自己能够独立做出这个需求,搞清楚这一点,那么nestjs的原理就理解的很深刻了,写起代码来就很得心应手了。
1、首先使用npm init初始化项目,然后安装依赖npm install axios -S。
2、定义控制器 Controller。这里实际上就是模拟nestjs里面的xxx.controller.ts中的controller类。里面的getList就是一个方法,类似于xxx.controller.ts里面经常见到的findAll(){}。
xxxxxxxxxx91class Controller {2 constructor() {3 4 }5 getList () {6 7 }8 9}3、定义装饰器
这时候需要使用装饰器工厂(也就是函数柯里化),因为装饰器默认会塞入一些参数,那么在xxx.controller.ts里面的@Get、@Post这些装饰器中,我们是可以传递参数的,传递的参数怎么在装饰器定义的函数里面接收呢?这时候就要用到函数的柯里化了。
定义 descriptor 的类型,通过 descriptor描述符里面的value 把axios的结果返回给当前使用装饰器的函数
xxxxxxxxxx161import axios from "axios"23const Get = (url: string): MethodDecorator => {4 return (target, key, descriptor: PropertyDescriptor) => {5 const fnc = descriptor.value;6 axios.get(url).then(res => {7 fnc(res, {8 status: 200,9 })10 }).catch(e => {11 fnc(e, {12 status: 500,13 })14 })15 }16}完整代码 接口可以直接用允许跨域的
xxxxxxxxxx281import axios from 'axios'2 3const Get = (url: string): MethodDecorator => {4 return (target, key, descriptor: PropertyDescriptor) => {5 const fnc = descriptor.value;6 axios.get(url).then(res => {7 fnc(res, {8 status: 200,9 })10 }).catch(e => {11 fnc(e, {12 status: 500,13 })14 })15 }16}17 18//定义控制器19class Controller {20 constructor() {21 22 }23 @Get('https://api.apiopen.top/api/getHaoKanVideo?page=0&size=10')24 getList (res: any, status: any) {25 console.log(res.data.result.list, status)26 }27 28}
看到MethodDecorator的typescript定义,里面的descriptor的类型是
TypedPropertyDescriptor<T>,不是老师所讲的PropertyDescriptor,于是我就按照typescript定义的类型来做,但是怎么都调不通,里面的泛型T不知道该传什么进去,我详细编写了这个T的类型,但还是报错。虽然说按照老师所用的PropertyDescriptor可以调通,但是为什么不能使用typescript定义的类型呢?这一点我想搞清楚,趁现在有时间。于是我看了一下阮一峰的typescript教程,发现MethodDecorator其实不是最新的typescript装饰器标准,而是以前的:
https://wangdoc.com/typescript/decorator#%E6%96%B9%E6%B3%95%E8%A3%85%E9%A5%B0%E5%99%A8,这是最新的方法装饰器,名称是
ClassMethodDecorator,里面的参数也不一样。但由于传统的装饰器还是可以使用的,那么还是要看一下传统的方法装饰器,https://wangdoc.com/typescript/decorator-legacy#%E6%96%B9%E6%B3%95%E8%A3%85%E9%A5%B0%E5%99%A8。我发现这个文档里面的例子,也是使用的PropertyDescriptor类型:
这下就麻烦了,我就是想知道这个PropertyDescriptor类型是怎么来的,无论是百度还是bing,都无法搜索到我想要的答案,于是我把代码发给chatgpt,让它来教我。
问:
下面的代码,使用了typescript传统的方法装饰器:
xxxxxxxxxx221function enumerable(value: boolean) {2return function (3target: any,4propertyKey: string,5descriptor: PropertyDescriptor6) {7descriptor.enumerable = value;8};9}1011class Greeter {12greeting: string;1314constructor(message:string) {15this.greeting = message;16}1718@enumerable(false)19greet() {20return 'Hello, ' + this.greeting;21}22}里面的descriptor参数,类型为什么使用了PropertyDescriptor这个类型。我看见MethodDecorator里面的定义,descriptor参数的类型是TypedPropertyDescriptor
,为什么要这样写呢? 答:
在 TypeScript 中,装饰器的实现需要考虑到装饰的目标是类、属性还是方法。对于方法装饰器,
descriptor参数的类型通常是PropertyDescriptor,因为它提供了对被装饰方法的描述和控制。而对于带有泛型参数的方法装饰器,比如TypedPropertyDescriptor<T>,它提供了更多类型安全性,允许在编译时检查装饰器是否正确应用于方法。问:
如果上面的代码中,使用TypedPropertyDescriptor
定义descriptor 参数的类型,应该怎么写呢? 答:
在 TypeScript 中,如果你想使用
TypedPropertyDescriptor<T>来定义方法装饰器的descriptor参数类型,你需要做以下修改:xxxxxxxxxx221function enumerable(value: boolean) {2return function (3target: any,4propertyKey: string,5descriptor: TypedPropertyDescriptor<() => string> // Adjust the type here6) {7descriptor.enumerable = value;8};9}1011class Greeter {12greeting: string;1314constructor(message:string) {15this.greeting = message;16}1718@enumerable(false)19greet() {20return 'Hello, ' + this.greeting;21}22}在上面的代码中,我将
descriptor参数的类型从PropertyDescriptor改为了TypedPropertyDescriptor<() => string>。这里假设greet()方法返回类型为string,因此使用了() => string类型来描述。
TypedPropertyDescriptor<T>是一个泛型接口,允许你指定方法的签名,以便在装饰器中对其进行更加精确的类型控制和检查。- 在装饰器中使用
TypedPropertyDescriptor<T>可以增强 TypeScript 的类型安全性,确保装饰器在编译时正确地应用于目标方法。这种方式适用于需要对方法装饰器进行类型检查和限制的场景,特别是当装饰的方法具有特定的参数和返回类型时。
问:
上面你说“对于方法装饰器,descriptor 参数的类型通常是 PropertyDescriptor,因为它提供了对被装饰方法的描述和控制。”,我从哪里可以看到通常使用PropertyDescriptor,明明定义里面就是 TypedPropertyDescriptor
啊? 答:
理解 TypeScript 中装饰器的
descriptor参数类型选择涉及到你希望在装饰器内部对方法进行何种粒度的控制和类型检查。Method Decorator 中的 descriptor 参数类型选择:
PropertyDescriptor vs TypedPropertyDescriptor
:
- PropertyDescriptor: 这是一个通用的描述符类型,用于属性和方法的装饰器。它提供了基本的描述符信息,如是否可枚举等,但不包含具体的方法签名信息。
- TypedPropertyDescriptor
: 这是一个泛型接口,允许你指定方法的具体签名,包括参数和返回类型。在方法装饰器中,如果你需要访问和操作方法的具体类型信息,如确保方法的返回类型是特定的类型或对方法的参数类型进行检查,那么使用TypedPropertyDescriptor<T>是更为适合的选择。选择合适的类型:
- 使用 PropertyDescriptor: 如果装饰器只需对方法进行基本的控制,如修改方法的可枚举性而不需要关注具体的方法签名和类型,那么可以使用
PropertyDescriptor。- 使用 TypedPropertyDescriptor
: 如果装饰器需要更精细的类型控制,如确保方法的返回类型符合预期或对方法参数进行类型检查,那么应该使用TypedPropertyDescriptor<T>。在这种情况下,你需要提供正确的方法签名作为泛型参数,例如TypedPropertyDescriptor<() => string>表示方法返回类型为string。在实际代码中的应用:
- 根据你的示例,如果
greet()方法确实返回string类型,并且你希望装饰器确保这一点,那么你应该使用TypedPropertyDescriptor<() => string>来描述descriptor的类型。- TypeScript 的编辑器通常会根据上下文和泛型参数提供代码补全功能,帮助你选择合适的类型。此外,你也可以查阅 TypeScript 的官方文档或相关资源,以深入了解每种类型的适用场景和差异。
了解类型定义来源:
- 在 TypeScript 中,
MethodDecorator的定义确实表明descriptor参数的类型可以是TypedPropertyDescriptor<T>,这使得你可以在装饰器中对方法的具体签名进行类型安全的控制。- 如果你想查看官方文档或其他 TypeScript 相关资源,你可以进一步理解每种类型的用途和限制,以便根据实际需求正确选择
descriptor的类型。综上所述,选择
PropertyDescriptor或TypedPropertyDescriptor<T>取决于你对方法装饰器所需的精度和类型安全性的具体要求。通过合适的类型选择,可以确保在装饰器中有效地操作和控制目标方法的行为和类型。
从上面的答案,我还是没有得到我想要的回答:我从哪里可以知道,descriptor可以使用
PropertyDescriptor类型?或许就是需要经验积累吧,多看别人写的typescript代码才能知道。那么我也可以知道,本来就可以使用
TypedPropertyDescriptor<T>来定义descriptor的类型,只是我的水平不够。把代码发给chatgpt,让它来帮我解决一下。问:
我写了下面这段代码:
xxxxxxxxxx251import axios from "axios";23const Get = (url: string): MethodDecorator => {4return (target: any, key: string | symbol, descriptor: TypedPropertyDescriptor<() => void>) => {5const fnc = descriptor.value;6axios7.get(url)8.then((res) => {9fnc(res, {10status: 200,11message: "success",12});13})14.catch((err) => {});15};16};1718class Controller {19constructor() {}2021@Get("")22getList(res: any, obj: { status: number; message: string }) {23console.log(res, obj);24}25}typescript报错:
Type '(target: any, key: string | symbol, descriptor: TypedPropertyDescriptor<() => void>) => void' is not assignable to type 'MethodDecorator'.请问怎么改?
答:
在你的代码中,报错是因为
Get方法返回的装饰器类型不匹配MethodDecorator。实际上,装饰器函数需要返回一个装饰器,而不是直接定义装饰器函数本身。下面是如何修正你的代码:
xxxxxxxxxx301import axios from "axios";23// 定义 Get 装饰器工厂函数,返回一个装饰器函数4const Get = (url: string): MethodDecorator => {5return (target: any, key: string | symbol, descriptor: TypedPropertyDescriptor<() => void>) => {6const originalMethod = descriptor.value; // 保存原始方法78// 修改方法的实现9descriptor.value = function(this: any, args: any[]) {10axios11.get(url)12.then((res) => {13// 调用原始方法,并传入参数14originalMethod.apply(this, [res, { status: 200, message: "success" }]);15})16.catch((err) => {});17};1819return descriptor; // 返回修改后的 descriptor20};21};2223class Controller {24constructor() {}2526@Get("") // 使用 Get 装饰器27getList(res: any, obj: { status: number; message: string }) {28console.log(res, obj);29}30}修正的关键点在于:
- Get 装饰器工厂函数: 返回一个方法装饰器,而不是直接实现装饰器函数。这样可以确保装饰器的类型与
MethodDecorator兼容。- 修改 descriptor: 在装饰器内部,修改方法的实现,保留原始方法并用新的实现替换。这样可以在 axios 请求完成后调用原始方法,并传入响应和其他参数。
- 返回 descriptor: 最后,确保从装饰器函数返回修改后的
descriptor,以使 TypeScript 理解装饰器的应用方式。通过这些调整,你应该能够解决 TypeScript 报错并成功应用装饰器。
问:
按照你上面的代码,还是报错,报错信息如下:
Type '(target: any, key: string | symbol, descriptor: TypedPropertyDescriptor<() => void>) => TypedPropertyDescriptor<() => void>' is not assignable to type 'MethodDecorator'. Types of parameters 'descriptor' and 'descriptor' are incompatible. Type 'TypedPropertyDescriptor
' is not assignable to type 'TypedPropertyDescriptor<() => void>'. Types of property 'value' are incompatible. Type 'T | undefined' is not assignable to type '(() => void) | undefined'. Type 'T' is not assignable to type '(() => void) | undefined'.ts(2322) 答:
The error you're encountering arises because the type returned by your
Getdecorator function does not match TypeScript's expectedMethodDecoratortype. Let's address this issue step by step:1、Understanding the Error
The error message
Type '(target: any, key: string | symbol, descriptor: TypedPropertyDescriptor<() => void>) => TypedPropertyDescriptor<() => void>' is not assignable to type 'MethodDecorator'.indicates that TypeScript expects a method decorator (MethodDecoratortype) but is receiving a different type (TypedPropertyDescriptor<() => void>).2、Correcting the Decorator
To fix this, ensure your decorator function returns a proper method decorator that TypeScript can recognize. Here's how you can correct your decorator:
- Define a Factory Function: Create a factory function that returns the actual decorator function.
- Modify the Decorator Function: Inside the decorator function, modify the method's behavior using the provided
descriptor.- Return the Descriptor: Return the modified
descriptorafter making necessary changes.Here's the corrected implementation:
xxxxxxxxxx311import axios from "axios";23// Define Get decorator factory function4const Get = (url: string): MethodDecorator => {5return (target: any, key: string | symbol, descriptor: PropertyDescriptor) => {6const originalMethod = descriptor.value as Function; // Cast to Function type78// Modify method implementation9descriptor.value = function(this: any, args: any[]) {10axios11.get(url)12.then((res) => {13// Call original method with response and additional object14originalMethod.apply(this, [res, { status: 200, message: "success" }]);15})16.catch((err) => {});17};1819return descriptor; // Return modified descriptor20};21};2223// Example usage of Get decorator24class Controller {25constructor() {}2627@Get("") // Applying Get decorator to getList method28getList(res: any, obj: { status: number; message: string }) {29console.log(res, obj);30}31}3、Explanation
- Decorator Factory Function:
Getfunction now returns a method decorator (MethodDecorator) rather than trying to directly implement the decorator function itself.- Decorator Function: Inside the decorator function returned by
Get,descriptoris of typePropertyDescriptor, which matches TypeScript's expected type for method decorators.- Modification of Descriptor: The
descriptor.valueis modified to intercept the original method execution and perform additional actions (in this case, making an Axios GET request).- Returning Descriptor: Always return the modified
descriptorat the end of the decorator function to ensure TypeScript recognizes it correctly.4、Conclusion
By following these steps, you ensure that TypeScript understands the decorator's purpose and correctly infers the types involved (
PropertyDescriptorin this case). This approach aligns with TypeScript's type expectations for method decorators and allows you to apply additional behaviors to methods effectively.
还是没有解决我的问题,但是仔细一想,typescript本身也并不是完美的,说不定就是因为使用
TypedPropertyDescriptor<T>不好定义,所以后来才添加了PropertyDescriptor来解决问题。这个问题就到这里吧。
但是我还是不甘心,到底哪里不同啊?
区别就在于
PropertyDescriptor的value就定义成了any类型,我定义成这样descriptor: TypedPropertyDescriptor<any>,果然就不报错了。xxxxxxxxxx301import axios from "axios";23const Get = (url: string): MethodDecorator => {4return (target: any, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<any>) => {5const cb = descriptor.value;6axios7.get(url)8.then((res) => {9cb(res, {10status: 200,11message: "success",12});13})14.catch((err) => {15cb(err, {16status: 500,17message: "fail",18});19});20};21};2223class Controller {24constructor() {}2526@Get("https://api.apiopen.top/api/getHaoKanVideo?page=0&size=5")27getList(res: any, obj: { status: number; message: string }) {28console.log(res.data.result.list, obj);29}30}
老师的这个例子,用了前端的axios请求,并没有把nestjs里面装饰器的处理很好的展现出来。比如说nestjs里面的Get装饰器,会被用到非常多的接口上面,而Get装饰器接收一个参数,就是接口的路径,里面还可以用
:id这种方式表示动态参数。那Get装饰器接收到参数之后,怎么将参数和对应的方法结合起来的呢?应该就是用到了descriptor.value,这样就可以进行不同的处理,返回不同的结果。
那么方法里面可能还有参数装饰器,比如说Controller类还有类装饰器,类里面还有属性装饰器,这些装饰器的执行顺序是什么样的?
xxxxxxxxxx51# 安装nestjs脚手架2npm i -g @nestjs/cli3 4# 创建项目5nest new [项目名称]如果在编写过程中,需要热更新,使用npm run start:dev启动项目就可以了。
xxxxxxxxxx61{2 "start": "nest start",3 "start:dev": "nest start --watch",4 "start:debug": "nest start --debug --watch",5 "start:prod": "node dist/main"6}使用nestjs/cli创建的项目,用vscode打开的时候,会自动将换行符转换为crlf,原本的文件里面应该是lf的。这样文件打开就会一堆红色波浪线,原来以为是代码哪里写错了,不是的,刚创建的项目怎么可能有错。错误在Windows系统,将换行符改为了crlf。
最有效的方法就是将开发平台换为liunx或者mac,但这是后话了,下面讲一下Windows平台怎么处理。
1、Git设置
git在提交、拉取代码的时候,会自动转换换行符,我设置了关闭转换换行符。
xxxxxxxxxx11git config --global core.autocrlf falsegit提供了一个名为
core.eol的配置选项,用于控制换行符在提交时的换行行为,我设置为lf。xxxxxxxxxx11git config --global core.eol lf2、vscode设置
打开settings,搜索eol(end of line),然后将eol改为
\n
3、在vscode中安装editorconfig for vscode,并在项目中编写配置文件
参考:visual-studio-code - vscode如何替换所有文件的回车格式为LF呢? - SegmentFault 思否
在项目的根目录中创建并配置文件
.editorconfig,配置如下:xxxxxxxxxx91root = true23[*]4charset = utf-85indent_style = space6indent_size = 27end_of_line = lf8insert_final_newline = true9trim_trailing_whitespace = true然后只需要将爆红色波浪线的文件,使用
ctrl+s重新保存一下就可以了。上面这些设置,到底最终起作用的是哪一个配置,这我不知道,我还是都配置了吧,免得出问题。
plus:
不要想买macbook来解决这个问题,直接使用Ubuntu开发环境就行了,除非公司送给我一台macbook,否则我是不会买的,太贵了。
1、main.ts:入口文件主文件,类似于vue 的main.ts。通过 NestFactory.create(AppModule) 创建一个app,这里的AppModule就类似于vue里面绑定一个根组件App.vue(SPA模式)。app就相当于一个应用。app.listen(3000); 监听3000端口。
xxxxxxxxxx81import { NestFactory } from '@nestjs/core';2import { AppModule } from './app.module';3 4async function bootstrap() {5 const app = await NestFactory.create(AppModule);6 await app.listen(3000);7}8bootstrap();2、app.module.ts
xxxxxxxxxx121import { Module } from '@nestjs/common';2import { AppController } from './app.controller';3import { AppService } from './app.service';45// 使用一个类装饰器来修饰AppModule,现在应该看得懂了。app.module.ts根模块用于处理其它类的引用与共享。可以把它理解为IOC里面的container(容器)。6@Module({7 imports: [],8 controllers: [AppController],9 providers: [AppService],10})11export class AppModule {}123、Controller.ts 控制器
可以理解成vue的路由,private readonly appService: AppService 这一行代码就是依赖注入,不需要实例化 appService ,nestjs框架内部会自己实例化,我们只需要放上去就可以了。(如果不理解,就看一下前面的IOC内容)
xxxxxxxxxx281import { Controller, Get } from '@nestjs/common';2import { AppService } from './app.service';3 4@Controller()5export class AppController {6 constructor(private readonly appService: AppService) {}7 8 @Get()9 getHello(): string {10 return this.appService.getHello();11 }12}13 14//-----------------------------------------------------15//下面的代码是测试一下修改路由地址之后,访问是否正常16 17import { Controller, Get } from '@nestjs/common';18import { AppService } from './app.service';19 20@Controller('/get')21export class AppController {22 constructor(private readonly appService: AppService) {}23 24 @Get('/hello')25 getHello(): string {26 return this.appService.getHello();27 }28}
4、app.service.ts
这个文件主要实现业务逻辑的,当然Controller可以实现逻辑,但是就是单一的无法复用,放到app.service,如果别的模块也需要,就可以实现复用。
xxxxxxxxxx81import { Injectable } from '@nestjs/common';2 3@Injectable()4export class AppService {5 getHello(): string {6 return 'Hello World!';7 }8}之前一直对nestjs里面的IOC和装饰器不理解,为什么不直接使用express呢?原因还是在于无法做大型项目,如果项目只有一个人做,那么无所谓,随便你用什么框架,但是多个人合作之后,就要求大家的代码能够不冲突、风格统一、重要代码能够复用、快速完成任务等等,express能够做到这些吗?我想很难,因为如果它里面的一个复用组件变化了之后,用到这个组件的地方都需要修改,也许这就是难点。
不想使用nestjs主要原因还是在于我不会用装饰器,不理解IOC的原理。(2024-06-16)现在有了一点理解之后,我发现用起来真的非常简单,重点就可以放在业务逻辑上了,写项目就没有问题了。

案例生成一个用户模块
xxxxxxxxxx11nest g co user
xxxxxxxxxx11nest g mo userxxxxxxxxxx11nest g s user
xxxxxxxxxx11 nest g resource xiaoman
第一次使用这个命令的时候,除了生成文件之外还会自动使用 npm 帮我们更新资源,安装一些额外的插件,后续再次使用就不会更新了。

生成了一套标准的CURD 模板
RESTful 是一种风格,在RESTful中,一切都被认为是资源,每个资源有对应的URL标识。不是标准也不是协议,只是一种风格。当然你也可以不按照他的风格去写,不过一般都是使用这些现成的风格。
1.接口url 传统接口使用语义化的路径,定义起来很复杂。
http://localhost:8080/api/get_list?id=1
http://localhost:8080/api/delete_list?id=1
http://localhost:8080/api/update_list?id=1
RESTful接口
http://localhost:8080/api/get_list/1
RESTful 风格:一个接口就会完成“增删改查”,它是通过不同的请求方式来区分的。
查询GET
提交POST
更新 PUT PATCH
删除 DELETE

一共有三种控制方法,我们一般用第一种,因为第一种更加语义化。

在接口前面通过版本号来访问不同的接口版本。
| 控制方法 | 含义 |
|---|---|
URI Versioning | 版本将在请求的 URI 中传递(默认) |
Header Versioning | 自定义请求标头将指定版本 |
Media Type Versioning | 请求的Accept标头将指定版本 |
①在app.module.ts里面开启版本控制。
xxxxxxxxxx131import { NestFactory } from '@nestjs/core';2import { VersioningType } from '@nestjs/common';3import { AppModule } from './app.module';4 5async function bootstrap() {6 const app = await NestFactory.create(AppModule);7 // 开启版本控制,使用第一种方法,在URI里面进行控制。可以ctrl+点击VersioningType看一下,共有几种值。8 app.enableVersioning({9 type: VersioningType.URI,10 })11 await app.listen(3000);12}13bootstrap();②在需要控制版本的路径里面配置版本
这里以user.controller为例来配置版本。Controller装饰器里面的参数变为一个对象,通过version 配置版本,path就是这个Controller的访问路径。
当然如果想要控制某一个接口的版本,那么可以在这个接口上方使用@Version(1)装饰器来配置。
xxxxxxxxxx321import { Controller, Get, Post, Body, Patch, Param, Delete, Version } from '@nestjs/common';2import { UserService } from './user.service';3import { CreateUserDto } from './dto/create-user.dto';4import { UpdateUserDto } from './dto/update-user.dto';5 6@Controller({7 path:"user",8 version:'1'9})10export class UserController {11 constructor(private readonly userService: UserService) {}12 13 @Post()14 create(@Body() createUserDto: CreateUserDto) {15 return this.userService.create(createUserDto);16 }17 18 @Get()19 // @Version('1')20 findAll() {21 return this.userService.findAll();22 }23 24 @Get(':id')25 findOne(@Param('id') id: string) {26 return this.userService.findOne(+id);27 }28 29 @Patch(':id')30 update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {31 return this.userService.update(+id, updateUserDto);32 }访问配置了版本的接口时,需要在路径前面加上v版本号,比如说是版本1,那么就是v1。

这个版本控制,可能之前在大方租车见过,但是印象不深,因为毕竟不是后端。这个版本控制怎么使用呢?有没有可能v1和v2会共存呢?
老师说了一下使用场景,应该是这样的:公司决定要做一个app,然后会分阶段不断优化,在每个阶段都有其各自的git分支,比如说刚开始的版本,会在部分接口前面加上版本1,第二个分支就会加上版本2,以此类推。但如果第二个分支是从第一个分支创建的,还是有可能共存,这方面的信息还是看到了再说吧。
200 OK
304 Not Modified 协商缓存了
400 Bad Request 参数错误
401 Unauthorized token错误
403 Forbidden referer origin 验证失败
404 Not Found 接口不存在
500 Internal Server Error 服务端错误
502 Bad Gateway 上游接口有问题或者服务器问题
什么是Controller?
Controllers are responsible for handling incoming requests and returning responses to the client.
A controller's purpose is to receive specific requests for the application. The routing mechanism controls which controller receives which requests. Frequently, each controller has more than one route, and different routes can perform different actions.
In order to create a basic controller, we use classes and decorators. Decorators associate classes with required metadata and enable Nest to create a routing map (tie requests to the corresponding controllers).
nestjs提供了方法参数装饰器,用来帮助我们快速获取参数,如下:
| 装饰器名称 | 装饰器装饰的形参 | 备注 |
|---|---|---|
| @Request() / 简写 @Req | req | |
| @Response() / 简写 @Res | res | |
| @Next() | next | |
| @Session() | req.session | |
| @Param(key?: string) | req.params/req.params[key] | 相当于使用@Request req 里面的req.params/req.params[key] |
| @Body(key?: string) | req.body/req.body[key] | 相当于使用@Request req 里面的req.body/req.body[key] |
| @Query(key?: string) | query | 相当于使用@Request req 里面的req.query/req.query[key] |
| @Headers(name?: string) | req.headers/req.headers[name] | 相当于使用@Request req 里面的req.headers/req.headers[name] |
| @HttpCode |
调试工具可以使用postMan、ApiFox 等。
可以使用Request装饰器或者 Query装饰器,跟express完全一样。

也可以使用Query 直接获取 不需要在通过req.query 了

xxxxxxxxxx151import { Controller, Get, Post, Body, Patch, Param, Delete, Version, Request, Query } from '@nestjs/common';2import { UserService } from './user.service';3import { CreateUserDto } from './dto/create-user.dto';4import { UpdateUserDto } from './dto/update-user.dto';5 6@Controller('user')7export class UserController {8 constructor(private readonly userService: UserService) { }9 10 @Get()11 find(@Query() query) {12 console.log(query)13 return { code: 200 }14 }15}也可以直接读取query里面的某个参数,比如说@Query('name') query,那么此时输出query,就是传递过来的name的值。
可以使用Request装饰器 或者 Body 装饰器 跟express 完全一样

或者直接使用Body 装饰器

也可以直接读取key,写成这种形式@Body('name') body,此时输出body,就是传递过来的name的值。

xxxxxxxxxx251import { Controller, Get, Post, Body, Patch, Param, Delete, Version, Request, Query } from '@nestjs/common';2import { UserService } from './user.service';3import { CreateUserDto } from './dto/create-user.dto';4import { UpdateUserDto } from './dto/update-user.dto';5 6@Controller('user')7export class UserController {8 constructor(private readonly userService: UserService) { }9 10 @Get()11 find(@Query() query) {12 console.log(query)13 return { code: 200 }14 }15 16 @Post()17 create (@Body() body) {18 19 console.log(body)20 21 return {22 code:20023 }24 }25}在请求方法装饰器的参数里面,使用:参数名来接收动态参数,比如说@Get(":id"),接收参数名为id的动态参数。可以使用Request装饰器req.params.id或者Param装饰器param.id来接收参数。
需要注意的是,@Param装饰器没有s,但是req.params.xxxx这里面的params有s。


xxxxxxxxxx351import { Controller, Get, Post, Body, Patch, Param, Delete, Version, Request, Query } from '@nestjs/common';2import { UserService } from './user.service';3import { CreateUserDto } from './dto/create-user.dto';4import { UpdateUserDto } from './dto/update-user.dto';5 6@Controller('user')7export class UserController {8 constructor(private readonly userService: UserService) { }9 10 @Get()11 find(@Query() query) {12 console.log(query)13 return { code: 200 }14 }15 16 @Post()17 create (@Body('name') body) {18 19 console.log(body)20 21 return {22 code:20023 }24 }25 26 @Get(':id')27 findId (@Param() param) {28 29 console.log(param)30 31 return {32 code:20033 }34 }35}也可以直接读取key,写成@Param('id') param的形式,此时输出param,就是id的值。
我在调试工具随便加了一个cookie

xxxxxxxxxx351import { Controller, Get, Post, Body, Patch, Param, Delete, Version, Request, Query, Ip, Header, Headers } from '@nestjs/common';2import { UserService } from './user.service';3import { CreateUserDto } from './dto/create-user.dto';4import { UpdateUserDto } from './dto/update-user.dto';5 6@Controller('user')7export class UserController {8 constructor(private readonly userService: UserService) { }9 10 @Get()11 find(@Query() query) {12 console.log(query)13 return { code: 200 }14 }15 16 @Post()17 create (@Body('name') body) {18 19 console.log(body)20 21 return {22 code:20023 }24 }25 26 @Get(':id')27 findId (@Headers() header) {28 29 console.log(header)30 31 return {32 code:20033 }34 }35}使用 HttpCode 装饰器,控制接口返回的状态码。这个基本上不用在具体的接口路径上,而是统一封装一个ResponseResult的拦截器,直接全局使用这个拦截器就行了。

xxxxxxxxxx331import { Controller, Get, Post, Body, Patch, Param, Delete, Version, Request, Query, Ip, Header, Headers, HttpCode } from '@nestjs/common';2import { UserService } from './user.service';3import { CreateUserDto } from './dto/create-user.dto';4import { UpdateUserDto } from './dto/update-user.dto';5 6@Controller('user')7export class UserController {8 constructor(private readonly userService: UserService) { }9 10 @Get()11 find(@Query() query) {12 console.log(query)13 return { code: 200 }14 }15 16 @Post()17 create (@Body('name') body) {18 19 console.log(body)20 21 return {22 code:20023 }24 }25 26 @Get(':id')27 @HttpCode(500)28 findId (@Headers() header) {29 return {30 code:50031 }32 }33}我感到兴奋异常,因为接收参数的处理问题解决了,很大部分的问题都被解决了,之前就是在接收参数这里卡了很久,不知道该怎么处理,装饰器又看不懂,现在看得懂装饰器了,同时也可以处理单个的参数了,嘿嘿嘿,美好的日子就在不远方。
这里记录一下接收分页查询的参数该怎么处理,一般分页查询使用query方法,那么里面的pageNum和pageSize就是string类型的,但是使用typeorm语句来查询,参数必须是number类型的,所以需要在controller里面接收的时候,就进行转换。
xxxxxxxxxx251// Controller文件中23@Get()4async findAllArticle(5// 需要将几个参数转换为要求的类型,就写几个参数,要放在最前面,因为最后要写获取全部参数的形参。这个应该是目前最简单的写法了,以后能不能做成装饰器来更优雅的处理参数,留给以后来解决吧。6@Query('pagenum', ParseIntPipe) pagenum: number,7@Query('pagesize', ParseIntPipe) pagesize: number,8// 这里的queryParams,是不是剩余的参数呢?可以输出看一下,不是剩余的参数,而是所有的参数。我把它写成...queryParams,也不是剩余参数,可能的原因是使用了@Query装饰器修饰,那么queryParams接收到的就是全部参数。9@Query() queryParams10) {11const [rows, total] = await this.articleService.findAllArticle(12pagenum,13pagesize,14queryParams,15);1617return {18rows,19page: {20total,21pagenum,22pagesize,23},24};25}xxxxxxxxxx151// Service文件中23async findAllArticle(pagenum: number, pagesize: number, queryParams) {4const { cate_id, state } = queryParams;5const sql = this.articleRepository6.createQueryBuilder('article')7.leftJoinAndSelect('article.category', 'category');8cate_id && sql.where('category.id = :cate_id', { cate_id: +cate_id });9state && sql.andWhere('article.state = :state', { state });1011sql.skip((pagenum - 1) * pagesize).take(pagesize);1213const result = await sql.getManyAndCount();14return result;15}
疑问:nestjs项目中,用了jwt,就可以不用session了吧?
回答:
在 NestJS 项目中使用了 JWT(JSON Web Token)并不意味着完全不能使用 session。
JWT 有其优势,它是无状态的,服务器不需要在内存中存储会话信息,通过在令牌中包含必要的用户信息,可在不同的服务间方便地传递认证信息,并且减少了对数据库的频繁查询来获取用户会话状态。
然而,session 也有其适用场景。例如,对于一些对安全性要求极高且需要在服务器端灵活管理会话状态的应用,session 可以提供更精细的控制。而且在某些复杂的企业级应用中,可能已经有基于 session 的遗留系统需要集成。
所以,虽然使用了 JWT,但并不一定就完全不能使用 session,具体取决于项目的需求和特点。
所以我想说的是,session还是要会的,使用场景还是存在的。
疑问:那使用了jwt之后,如果还要使用session,有哪些使用场景呢?能举例说明吗?
回答:
一、复杂的用户状态管理
例如在一个在线协作编辑的应用中:
- 当用户进行文档编辑时,可能需要实时保存用户的编辑状态,如当前光标位置、选中的文本等。这些临时的、频繁变化的状态数据如果存储在 JWT 中会导致 JWT 体积过大且频繁更新。而使用 session 可以在服务器端方便地存储和管理这些复杂的状态信息,当用户再次请求时,服务器可以快速获取并恢复用户的编辑状态。
二、防止并发登录限制
在某些系统中,可能需要限制同一用户只能在一个设备上登录或者同时登录的设备数量有限制。
- 使用 session 可以在服务器端方便地跟踪用户的登录状态和设备信息。当用户在新设备上登录时,服务器可以检测到同一用户的其他活跃 session,并根据策略决定是否允许新的登录或者强制注销其他设备的登录。例如,一个企业内部管理系统可能不希望员工同时在多台设备上登录以确保数据安全和操作的可追溯性。
三、高安全性要求的场景结合
对于一些对安全性要求极高的金融或医疗应用:
- 可以同时使用 JWT 和 session 进行双重认证和授权。JWT 用于初步的身份验证,而 session 可以在服务器端进行更严格的会话管理和监控。例如,服务器可以定期检查 session 的活动状态,对于长时间未活动的 session 进行强制注销以防止潜在的安全风险。同时,结合 session 可以更方便地实施多因素认证,在用户进行关键操作时,要求额外的认证步骤,如输入动态密码或通过生物识别验证,这些额外的认证信息可以与 session 关联起来进行更精细的安全控制。
四、与遗留系统集成
如果应用需要与一些遗留的后端系统进行集成,而这些遗留系统是基于 session 进行认证和授权的。
- 为了避免对遗留系统进行大规模的改造,可以在新的应用部分使用 JWT 进行身份验证,然后在与遗留系统交互时,将 JWT 中的用户信息转换为 session 信息,以便遗留系统能够继续按照其原有的方式进行处理。例如,一个企业在进行系统升级时,新开发的前端应用使用 JWT,但后端的一些旧的业务逻辑处理模块仍然依赖于 session,通过这种方式可以实现平滑过渡和集成。
想要清楚理解session,就要弄懂一个重要的前提:session到底是如何在前后端工作的?
在学习这一小节的时候,我基本上只是看到在后端进行配置,前端只是发起了请求。这和我潜意识中认为的加密不太一样,我的潜意识里面好像就是jwt这样的形式,需要前端从后端获取token,然后放到接口的authentication里面,发送给后端进行校验。
这其实是因为我没有掌握前后端加密通信的其余方式,jwt是最近兴起的方式,主要用于移动端,现在PC端也主要在用,之前可能使用的是cookie或者session,是不是jwt是最新的,就是包治百病呢?那也不是,最起码在一些情形下使用session是更好的选择,比如说在线协同编辑,会频繁的在数据库中确认token等信息,这些信息会对服务器造成很大的压力,但是session就可以解决这个问题。
回到最初的问题,session是如何在前后端工作的,简单的说就是:
1、在nestjs项目中,配置express-session中间件,来方便的实现session功能。
2、express-session中间件会帮助在浏览器端创建一个名为session的cookie,那么处于同一域名下的前端和后端,浏览器在发送请求时会自动携带与该域名对应的 cookie。之后前端发起请求的时候,后端就能够从req.session里面获取到session相关信息。
session是服务器为每个用户的浏览器创建的一个会话对象,这个session会记录到浏览器的cookie,用来区分用户。
我们使用的是nestjs默认框架express,它也支持express的插件,所以我们就可以安装express的session。
npm i express-session --save
安装声明依赖
npm i @types/express-session -D
然后在main.ts 引入 通过app.use 注册session
xxxxxxxxxx31import * as session from 'express-session'2 3app.use(session())参数配置详解
| 配置参数 | 含义 |
|---|---|
| secret | 生成服务端session 签名,可以理解为加盐 |
| name | 生成客户端cookie 的名字,默认 connect.sid |
| cookie | 设置返回到前端 key 的属性,默认值为{ path: ‘/’, httpOnly: true, secure: false, maxAge: null }。 |
| rolling | 在每次请求时强行设置 cookie,这将重置 cookie 过期时间(默认:false) |
nestjs 配置
xxxxxxxxxx131import { NestFactory } from '@nestjs/core';2import { VersioningType } from '@nestjs/common';3import { AppModule } from './app.module';4import * as session from 'express-session'5async function bootstrap() {6 const app = await NestFactory.create(AppModule);7 app.enableVersioning({8 type: VersioningType.URI9 })10 app.use(session({ secret: "XiaoMan", name: "xm.session", rolling: true, cookie: { maxAge: null } }))11 await app.listen(3000);12}13bootstrap();前端 vue3 ts element-plus fetch
安装element
xxxxxxxxxx11npm install element-plus -S然后简单的绘制页面
xxxxxxxxxx671<template>2 <div class="wraps">3 <el-form :label-position="labelPosition" label-width="100px" :model="formLabelAlign" style="max-width: 460px">4 <el-form-item label="账号">5 <el-input v-model="formLabelAlign.name" />6 </el-form-item>7 <el-form-item label="密码">8 <el-input type="password" v-model="formLabelAlign.password" />9 </el-form-item>10 <el-form-item label="验证码">11 <div style="display:flex">12 <el-input v-model="formLabelAlign.code" />13 <img @click="resetCode" :src="codeUrl" alt="">14 </div>15 </el-form-item>16 <el-form-item>17 <el-button @click="submit">登录</el-button>18 </el-form-item>19 </el-form>20 </div>21</template>22 23<script setup lang='ts'>24import { onMounted, reactive, ref } from 'vue';25 26const codeUrl = ref<string>('/api/user/code')27 28const resetCode = () => codeUrl.value = codeUrl.value + '?' + Math.random()29 30const labelPosition = ref<string>('right')31 32const formLabelAlign = reactive({33 name: "",34 password: "",35 code: ""36})37 38const submit = async () => {39 await fetch('/api/user/create', {40 method: "POST",41 body: JSON.stringify(formLabelAlign),42 headers: {43 'content-type': 'application/json'44 }45 }).then(res => res.json())46}47</script>48 49<style>50* {51 padding: 0;52 margin: 0;53}54 55.wraps {56 display: flex;57 justify-content: center;58 align-items: center;59 height: inherit;60}61 62html,63body,64#app {65 height: 100%;66}67</style>
我们可以看到 session 已经存到了浏览器

跨域我用了本地dev 解决的
xxxxxxxxxx71proxy:{2 '/api':{3 target:'http://localhost:3000/',4 changeOrigin:true,5 rewrite: path => path.replace(/^\/api/, ''),6 }7}后端nestjs 验证码插件 svgCaptcha
xxxxxxxxxx11npm install svg-captcha -Sxxxxxxxxxx381import { Controller, Get, Post, Body, Param, Request, Query, Headers, HttpCode, Res, Req } from '@nestjs/common';2import { UserService } from './user.service';3import { CreateUserDto } from './dto/create-user.dto';4import { UpdateUserDto } from './dto/update-user.dto';5import * as svgCaptcha from 'svg-captcha';67@Controller('user')8export class UserController {9 constructor(private readonly userService: UserService) { }10 @Get('code')11 createCaptcha(@Req() req, @Res() res) {12 const captcha = svgCaptcha.create({13 size: 4,//生成几个验证码14 fontSize: 50, //文字大小15 width: 100, //宽度16 height: 34, //高度17 background: '#cc9966', //背景颜色18 })19 req.session.code = captcha.text //存储验证码记录到session20 res.type('image/svg+xml')21 res.send(captcha.data)22 }23 24 @Post('create')25 createUser(@Req() req, @Body() body) {26 console.log(req.session.code, body)27 if (req.session.code.toLocaleLowerCase() === body?.code?.toLocaleLowerCase()) {28 return {29 message: "验证码正确"30 }31 } else {32 return {33 message: "验证码错误"34 }35 }36 37 }38}
上面这段验证码的代码如何看?
1、在请求获取验证码图片的时候,将验证码的字符串存到了req.session上面,那么此时前端的req.session就会被更新。
2、当用户请求登录的时候,携带的验证码参数,会与req.session里面的验证码进行比对。
这里我有一个疑问:能不能在前端请求获取验证码图片的时候,将验证码的字符串存到这个类的属性上?然后调用createUser方法的时候,从类的属性上获取值进行比对?
可以是可以,但是会造成问题。想到没有,当一个用户使用createCaptcha时,我将验证码存到了类的属性上,下一毫秒,另一个用户请求了createUser,此时比对的是上一个用户的验证码和这一个用户填写的验证码,这二者肯定是不一样的,所以这就是问题。
这就是IOC的好处。
Providers 是 Nest 的一个基本概念。许多基本的 Nest 类可能被视为 provider ,比如说service, repository, factory, helper 等等。 他们都可以通过 constructor 注入依赖关系。 这意味着对象可以彼此创建各种关系,并且“连接”对象实例的功能在很大程度上可以委托给 Nest运行时系统。 Provider 只是一个用 @Injectable() 装饰器注释的类。
module 中引入 service,在 providers 中注入。

在Controller 就可以使用注入好的service 了 。

第一种用法就是一个语法糖,其实它的完整写法是这样的:
xxxxxxxxxx131import { Module } from '@nestjs/common';2import { UserService } from './user.service';3import { UserController } from './user.controller';4 5@Module({6 controllers: [UserController],7 providers: [{8 provide: "Xiaoman",9 // 这里的useClass要使用后端思维来理解,就是使用哪个类。不要联想到react里面的hooks。10 useClass: UserService11 }]12})13export class UserModule { }
自定义名称之后,需要用对应的Inject 取值,不然会找不到的。

通过 useValue 可以自定义注入值。(不同点在于,前面两种方法都是注入类,这里是注入值)
xxxxxxxxxx151import { Module } from '@nestjs/common';2import { UserService } from './user.service';3import { UserController } from './user.controller';4 5@Module({6 controllers: [UserController],7 providers: [{8 provide: "Xiaoman",9 useClass: UserService10 }, {11 provide: "JD",12 useValue: ['TB', 'PDD', 'JD']13 }]14})15export class UserModule { }

如果服务之间有相互的依赖或者逻辑处理,可以使用 useFactory,就是一个服务里面使用另一个服务,然后通过provide提供的name暴露出去。
xxxxxxxxxx261import { Module } from '@nestjs/common';2import { UserService } from './user.service';3import { UserService2 } from './user.service2';4import { UserService3 } from './user.service3';5import { UserController } from './user.controller';6 7@Module({8 controllers: [UserController],9 providers: [{10 provide: "Xiaoman",11 useClass: UserService12 }, {13 provide: "JD",14 useValue: ['TB', 'PDD', 'JD']15 },16 UserService2,17 {18 provide: "Test",19 inject: [UserService2],20 useFactory(UserService2: UserService2) {21 return new UserService3(UserService2)22 }23 }24 ]25})26export class UserModule { }

useFactory 返回一个promise 或者其他异步操作
xxxxxxxxxx371import { Module } from '@nestjs/common';2import { UserService } from './user.service';3import { UserService2 } from './user.service2';4import { UserService3 } from './user.service3';5import { UserController } from './user.controller';6 7@Module({8 controllers: [UserController],9 providers: [{10 provide: "Xiaoman",11 useClass: UserService12 }, {13 provide: "JD",14 useValue: ['TB', 'PDD', 'JD']15 },16 UserService2,17 {18 provide: "Test",19 inject: [UserService2],20 useFactory(UserService2: UserService2) {21 return new UserService3(UserService2)22 }23 },24 {25 // 注意:这里的sync只是提供者的名称,并不是指定为sync。真正的异步需要在useFactory里面实现。26 provide: "sync",27 async useFactory() {28 return await new Promise((r) => {29 setTimeout(() => {30 r('sync')31 }, 3000)32 })33 }34 }35 ]36})37export class UserModule { }
每个 Nest 应用程序至少有一个模块,即根模块。根模块是 Nest 开始安排应用程序树的地方。事实上,根模块可能是应用程序中唯一的模块,特别是当应用程序很小时。但是对于大型程序来说这是不可能的,在大多数情况下,您将拥有多个模块,每个模块都有一组紧密相关的功能,共同组成一个大型程序。
1.基本用法
当我们使用nest g res user 创建一个CURD 模板的时候,nestjs会自动帮我们,在app.module.ts里面引入UserModule模块。

例如 user 的 Service 想暴露给其他模块使用,就可以使用exports导出该服务。

在要使用UserService的xxx模块中,先在xxx.module.ts里面imports引入UserModule模块,然后在xxx.controller.ts或者xxx.service.ts文件中,引入UserService类,然后在contructor里面注册后再使用。(注意,单独引入UserService类是不能使用的,必须先引入UserModule)
下面以APP模块为例说明,由于App.modules 已经引入过UserModule模块,就可以直接引入并使用user模块的 Service 。


@Global()
我们给 user 模块添加 @Global(),它便注册为全局模块。此时如果别的模块要使用这个模块里面的Service,那么不需要先imports引入这个模块,而是可以直接引入service注册并使用。

在list模块使用,无须先在module import导入,而是直接引入service注册并使用。

动态模块主要就是为了给模块传递参数,可以给该模块添加一个静态方法,用来接受参数。

xxxxxxxxxx291import { Module, DynamicModule, Global } from '@nestjs/common'23interface Options {4 path: string5}6 7@Global()8@Module({9 10})11export class ConfigModule {12 static forRoot(options: Options): DynamicModule {13 return {14 module: ConfigModule,15 providers: [16 {17 provide: "Config",18 useValue: { baseApi: "/api" + options.path }19 }20 ],21 exports: [22 {23 provide: "Config",24 useValue: { baseApi: "/api" + options.path }25 }26 ]27 }28 }29} 

中间件是在路由处理程序之前调用的函数。中间件函数可以访问请求和响应对象。
中间件函数可以执行以下任务:
1.创建一个依赖注入中间件
这里的中间件使用了类的写法,怎么写呢?nestjs中的中间件要求implements实现NestMiddleware,所以可以查看NestMiddleware的类型定义,类型定义里面要求我们实现use函数,返回req、res、next参数。如果不调用next,程序将被挂起。
xxxxxxxxxx121import {Injectable,NestMiddleware } from '@nestjs/common'23// 为了得到req、res、next的类型提示,导入express里面的类型定义,此时的nestjs是express模式,因此可以这样使用。4import {Request,Response,NextFunction} from 'express'5 6@Injectable()7export class Logger implements NestMiddleware{8 use (req:Request,res:Response,next:NextFunction) {9 console.log(req)10 next()11 }12}2.使用方法
在需要用到中间件的模块里面,implements实现NestModule,此时查看NestModule的类型定义可知,需要定义一个configure方法,参数是consumer(中间件的消费者),consumer 通过 apply 注册中间件,并通过forRoutes指定中间件应用的Controller路由。
xxxxxxxxxx161import { Module,NestModule,MiddlewareConsumer } from '@nestjs/common';2import { UserService } from './user.service';3import { UserController } from './user.controller';4import { Logger } from 'src/middleware';56@Module({7 controllers: [UserController],8 providers: [UserService],9 exports:[UserService]10})11export class UserModule implements NestModule{12 configure (consumer:MiddlewareConsumer) {13 // 下面的 'user' 就表示中间件应用到UserController里面的所有路由上面。因为UserController里面的@Controller("user")。14 consumer.apply(Logger).forRoutes('user')15 }16}forRoutes里面的参数,也可以指定拦截的请求方法,比如拦截GET、POST等,forRoutes使用对象配置。
xxxxxxxxxx151import { Module,NestModule,MiddlewareConsumer,RequestMethod } from '@nestjs/common';2import { UserService } from './user.service';3import { UserController } from './user.controller';4import { Logger } from 'src/middleware';56@Module({7 controllers: [UserController],8 providers: [UserService],9 exports:[UserService]10})11export class UserModule implements NestModule{12 configure (consumer:MiddlewareConsumer) {13 consumer.apply(Logger).forRoutes({path:'user',method:RequestMethod.GET})14 }15}甚至可以直接把 UserController 塞进去,表示中间件应用到 UserController 里面的所有路由。
xxxxxxxxxx141import { Module,NestModule,MiddlewareConsumer,RequestMethod } from '@nestjs/common';2import { UserService } from './user.service';3import { UserController } from './user.controller';4import { Logger } from 'src/middleware';5@Module({6 controllers: [UserController],7 providers: [UserService],8 exports:[UserService]9})10export class UserModule implements NestModule{11 configure (consumer:MiddlewareConsumer) {12 consumer.apply(Logger).forRoutes(UserController)13 }14}注意全局中间件只能使用函数模式,案例可以做白名单拦截之类的。
xxxxxxxxxx221import { NestFactory } from '@nestjs/core';2import { AppModule } from './app.module';3 4const whiteList = ['/list']5 6function middleWareAll (req,res,next) {7 console.log(req.originalUrl,'我收全局的')8 9 if(whiteList.includes(req.originalUrl)){10 next()11 }else{12 res.send('小黑子露出鸡脚了吧')13 }14}15 16async function bootstrap() {17 const app = await NestFactory.create(AppModule);18 // 注意:middleWareAll本身就是一个函数,所以这里不需要加()19 app.use(middleWareAll)20 await app.listen(3000);21}22bootstrap();npm install cors
npm install @types/cors -D
xxxxxxxxxx271import { NestFactory } from '@nestjs/core';2import { AppModule } from './app.module';3import * as cors from 'cors'4 5const whiteList = ['/list']6 7function middleWareAll (req,res,next) {8 9 console.log(req.originalUrl,'我收全局的')10 11 if(whiteList.includes(req.originalUrl)){12 next()13 }else{14 res.send({code:200})15 }16 17 18}19 20async function bootstrap() {21 const app = await NestFactory.create(AppModule);22 // 注意:app.use()里面需要传递一个函数进去,所以cors()使用了()23 app.use(cors())24 app.use(middleWareAll)25 await app.listen(3000);26}27bootstrap();
这里说的是图片,但其实只要是前端能够上传的文件,这里都可以这样处理,也许具体的处理方式有些许差异。
multer是什么意思?我总是要弄懂英文所代表的汉语意思才能更好的理解,但是没有找到multer所表示的意思,英文词源本来就非常丰富,造词也是经常做的事情,所以只需要把它当做处理附件的一个库就行了。
可以看一下这个:https://www.cnblogs.com/l8l8/p/9098108.html。Multer是Express官方推出的,用于Node.js multipart/form-data请求数据处理的中间件。
multer和@nestjs/platform-express,其中@nestjs/platform-express是nestjs自带的,不需要安装。
需要安装multer和类型声明文件@types/multer。
在UploadModule里面imports引入MulterModule,使用它的register方法注册存放图片的目录。
需要用到multer库的diskStorage方法设置存放目录。使用path库的extname来读取文件后缀名,filename属性用于给文件重新命名。

xxxxxxxxxx211import { Module } from '@nestjs/common';2import { UploadService } from './upload.service';3import { UploadController } from './upload.controller';4import { MulterModule } from '@nestjs/platform-express'5import {diskStorage} from 'multer'6import { extname,join } from 'path';78@Module({9 imports: [MulterModule.register({10 storage:diskStorage({11 destination:join(__dirname,"../images"),12 filename:(_,file,callback) => {13 const fileName = `${new Date().getTime() + extname(file.originalname)}`14 return callback(null,fileName)15 }16 })17 })],18 controllers: [UploadController],19 providers: [UploadService]20})21export class UploadModule { }使用UseInterceptors装饰器,里面的参数有两种:FileInterceptor是单个读取,FilesInterceptor是多个读取。

xxxxxxxxxx141import { Controller, Get, Post, Body, Patch, Param, Delete,UseInterceptors,UploadedFile } from '@nestjs/common';2import { UploadService } from './upload.service';3import {FileInterceptor} from '@nestjs/platform-express'45@Controller('upload')6export class UploadController {7 constructor(private readonly uploadService: UploadService) {}8 @Post('album')9 @UseInterceptors(FileInterceptor('file'))10 upload (@UploadedFile() file) {11 console.log(file)12 return true13 }14}输出的file信息:

+++++++这里将读取多个文件的代码也粘贴出来。
使用app的useStaticAssets方法,里面的prefix用来设置虚拟前缀。
xxxxxxxxxx131import { NestFactory } from '@nestjs/core';2import { AppModule } from './app.module';3import { NestExpressApplication } from '@nestjs/platform-express'4import { join } from 'path'5async function bootstrap() {6 // 在使用 useStaticAssets 方法之前,要为NestFactory.create方法传递泛型参数 NestExpressApplication ,这样app上才有 useStaticAssets 方法。是一定要传这个泛型参数,为什么?因为不同的泛型会产生不同的返回值。7 const app = await NestFactory.create<NestExpressApplication>(AppModule);8 app.useStaticAssets(join(__dirname,'images'),{9 prefix:"/xiaoman"10 })11 await app.listen(3000);12}13bootstrap();
4、使用数据库,将附件信息存储到数据库中
之前的步骤,只是将上传的附件存储到了硬盘中,但是前端是需要查看这些附件信息的,那么必须将附件的信息存储到数据库中,并且和别的表关联起来,这样才能充分使用。
①定义upload实体类
xxxxxxxxxx211// /upload/entities/upload.entity.ts23import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';45@Entity()6export class Upload {7 @PrimaryGeneratedColumn()8 id: number;910 @Column()11 filename: string;1213 @Column()14 originalFileName: string;1516 @Column()17 mimetype: string;1819 @Column()20 size: number;21}②更改module里面的代码
xxxxxxxxxx291import { Module } from '@nestjs/common';2import { UploadController } from './upload.controller';3import { MulterModule } from '@nestjs/platform-express';4import { diskStorage } from 'multer';5import { extname, join } from 'path';6import { UploadService } from './upload.service';7import { Upload } from './entities/upload.entity';8import { TypeOrmModule } from '@nestjs/typeorm';910@Module({11 imports: [12 // 因为要在 service 里面操作附件了,所以这里不需要直接存储了。参数就是一个空对象。13 MulterModule.register({14 // storage: diskStorage({15 // destination: join(__dirname, '../images'),16 // filename: (_, file, cb) => {17 // const fileName = `${new Date().getTime() + extname(file.originalname)}`;18 // return cb(null, fileName);19 // },20 // }),21 }),22 TypeOrmModule.forFeature([Upload]),23 ],24 controllers: [UploadController],25 providers: [UploadService],26 exports:[UploadService]27})28export class UploadModule {}29
③更改controller里面的代码,使用service来做具体处理
xxxxxxxxxx251import {2 Controller,3 Post,4 Body,5 UseInterceptors,6 UploadedFile,7} from '@nestjs/common';8import { FileInterceptor } from '@nestjs/platform-express';9import { UploadService } from './upload.service'1011@Controller('upload')12export class UploadController {13 constructor(private readonly uploadService:UploadService) {}1415 // 这里使用 service 来处理附件,但是和之前的请求参数、装饰器用法,都是一样的,没有修改。那么此时的file会是什么样的呢?16 @Post()17 @UseInterceptors(FileInterceptor('file'))18 async create(@UploadedFile() file:Express.Multer.File) {19 console.log(file);2021 // 这里返回的是 fileId ,主要用于别的表中存储关联的信息22 const fileId = await this.uploadService.uploadFile({file})23 return {fileId}24 }25}
可以看到,file的内容不一样了,所以要以这个为主。
④编写service,将附件存储到硬盘,并在数据库中存储信息
xxxxxxxxxx411import { BadRequestException, Injectable } from '@nestjs/common';2import { Repository } from 'typeorm'3import { Upload } from './entities/upload.entity'4import { InjectRepository } from '@nestjs/typeorm';5import { join,extname } from 'path'6import * as fs from 'fs'78@Injectable()9export class UploadService {10 constructor(11 @InjectRepository(Upload)12 private readonly fileRepository:Repository<Upload>13 ){}14 async uploadFile(fileDto){15 console.log(fileDto,'fileDto');16 try{17 const file = fileDto.file18 // 附件存储到硬盘的根目录19 const uploadPath = join(__dirname,"../images")20 // 附件存储的名称,这里必须要设置唯一的名称,因为同个文件可能多次上传21 const filename = `${new Date().getTime() + extname(file.originalname)}`22 // 设置附件存储的详细地址,因为使用了fs.writeFileSync方法,所以要指定具体的地址+名称。23 const filePath = join(uploadPath,filename)2425 // 将附件存储到硬盘26 fs.writeFileSync(filePath,file.buffer)2728 // 将附件保存到数据库29 const savedFile = await this.fileRepository.save({30 filename,31 originalFileName:file.originalname,32 mimetype:file.mimetype,33 size:file.size34 })3536 return savedFile.id;37 }catch{38 throw new BadRequestException('Failed to upload file')39 }40 }41}输出一下fileDto的内容:

测试一下:
目前遇到最多的情况不是单独上传附件,而是将附件与表单的其它参数一起上传,那么就要求我在具体的表单接口里面,调用上传附件的接口,能这样做吗?
问:nestjs项目中,我定义了上传附件的模块upload,但是在一个接口中,我接受了附件和其它参数,我此时可以使用upload里面的上传附件的方法吗?
答:
Yes, in NestJS you can call another API from within an API endpoint. This is typically done by using HTTP clients like Axios or using NestJS's built-in
HttpServicemodule. Here’s a basic example of how you might do this:xxxxxxxxxx201import { Controller, Get, HttpService } from '@nestjs/common';23@Controller('example')4export class ExampleController {5constructor(private readonly httpService: HttpService) {}67@Get()8async getExampleData() {9try {10// Make a GET request to another API endpoint11const response = await this.httpService.get('https://api.example.com/data').toPromise();1213// Process the response as needed14return response.data;15} catch (error) {16// Handle errors17throw error;18}19}20}In this example:
HttpServiceis injected into the controller using dependency injection.- Inside the
getExampleData()method,this.httpService.get()makes a GET request tohttps://api.example.com/data..toPromise()converts the observable returned byhttpService.get()into a promise, allowing you to use async/await for cleaner asynchronous code.- You can handle the API response (
response.data) as needed and handle errors appropriately.Remember to handle errors gracefully and consider using try-catch blocks to manage exceptions that may arise from API calls.
上面chatgpt给的答案是错误的,
@nestjs/common里面没有HttpService这个方法:
所以必须另外想办法,我想既然不能直接使用这个接口,那么使用里面的uploadService是可以的吧?将具体接口里面的附件信息提取出来,交给这个uploadService来处理,一样能达到目的。
步骤如下,下面用article来指代具体的模块:
①在upload和article的实体定义中,编写关联关系。
②将uploadService在upload模块中暴露出去。
③在article模块中引入upload模块。这里一定要注意:不能直接在article的provider里面引入uploadService,引入后使用是没有效果的。而是应该在article的imports里面引入uploadModule,同时不需要在provider里面引入uploadService。
④在article.controller.ts里面的接口上,接收附件信息。
⑤在article.service.ts里面引入uploadService,注册并使用。
下面是具体步骤的操作:
①
这里有一个疑问,就是upload里面如果对应多张表,那么应该怎么写?
比如说,upload实体和多张表都有一对一的关系,该怎么写呢?是在upload里面写一个字段,然后加上多个
@OneToOne()装饰器来表示吗?仔细一想就不对,为什么?因为这是一对一的关系,即使是一对多的关系也不能这么写。应该怎么写呢?应该有几张表和upload一对一,那么就写几个字段,都用@OneToOne()来装饰,这样意思就非常明了。xxxxxxxxxx321// /upload/entities/upload.entity.ts23import { Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn } from 'typeorm';4import { User as UserEntity } from '../../user/user.entity'5import { Article as ArticleEntity } from '../../article/article.entity'67@Entity()8export class Upload {9@PrimaryGeneratedColumn()10id: number;1112@Column()13filename: string;1415@Column()16originalFileName: string;1718@Column()19mimetype: string;2021@Column()22size: number;2324// OneToOne,第一个参数指定字段对应的是哪个实体;第二个参数指定对应的是相应实体的哪个字段,这个箭头函数的形参可以随便起,但最好还是有明确意义,一般都是使用下面的字段名。25@OneToOne(() => UserEntity, userAttachment => userAttachment.file)26@JoinColumn()27userAttachment: UserEntity;2829@OneToOne(() => ArticleEntity, articleAttachment => articleAttachment.file)30@JoinColumn()31articleAttachment: ArticleEntity;32}xxxxxxxxxx291// src/article/article.entity.ts23import { Category } from 'src/category/category.entity';4import { Upload } from 'src/upload/entities/upload.entity';5import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, OneToOne } from 'typeorm';67@Entity()8export class Article {9@PrimaryGeneratedColumn()10id: number;1112@Column()13title: string;1415@Column()16pub_date: string;1718@Column()19state: '草稿' | '已发布';2021@Column({ nullable: true })22cover_img: string;2324@ManyToOne(() => Category, (category) => category.articles)25category: Category;2627@OneToOne(() => Upload, file => file.articleAttachment)28file: Upload;29}②
xxxxxxxxxx271import { Module, OnModuleInit } from '@nestjs/common';2import { UploadController } from './upload.controller';3import { MulterModule } from '@nestjs/platform-express';4import { join } from 'path';5import { UploadService } from './upload.service';6import { Upload } from './entities/upload.entity';7import { TypeOrmModule } from '@nestjs/typeorm';8import * as fs from "fs"910@Module({11imports: [12MulterModule.register({}),13TypeOrmModule.forFeature([Upload]),14],15controllers: [UploadController],16providers: [UploadService],17exports: [UploadService]18})19export class UploadModule implements OnModuleInit {20// 因为upload里面存储附件的地址是dist/images文件夹,使用了fs.writeFileSync,如果文件夹地址不存在,会报错。所以将判断是否存在文件夹的代码放到这里来,那么只需要module启动的时候判断一次就行了。21onModuleInit() {22const uploadPath = join(__dirname, "../images")23if (!fs.existsSync(uploadPath)) {24fs.mkdirSync(uploadPath, { recursive: true })25}26}27}
③
④
xxxxxxxxxx281// article.controller.ts23import {4Controller,5Body,6Post,7UseGuards,8ParseIntPipe,9UseInterceptors,10UploadedFile,11} from '@nestjs/common';12import { FileInterceptor } from '@nestjs/platform-express'13import { AuthGuard } from '@nestjs/passport';14import { CreateArticleDto } from './dto/create-article.dto';15import { ArticleService } from './article.service';1617@UseGuards(AuthGuard('jwt'))18@Controller('article')19export class ArticleController {20constructor(private readonly articleService: ArticleService) { }2122@Post()23// 获取附件参数和upload里面的接口一样,使用这么多装饰器来装饰,@Body() 可以获取body里面的其它表单参数。24@UseInterceptors(FileInterceptor('file'))25addArticle(@UploadedFile() file: Express.Multer.File, @Body() createArticleDto: CreateArticleDto) {26return this.articleService.addArticle(createArticleDto, {file});27}28}⑤
xxxxxxxxxx561// article.service.ts23import { Injectable, HttpException, HttpStatus } from '@nestjs/common';4import { InjectRepository } from '@nestjs/typeorm';5import { Article as ArticleEntity } from './article.entity';6import { Repository } from 'typeorm';7import { CreateArticleDto } from './dto/create-article.dto';8import { Category as CategoryEntity } from '../category/category.entity';9import * as dayjs from 'dayjs';10// 引入 UploadService11import { UploadService } from 'src/upload/upload.service';1213@Injectable()14export class ArticleService {15constructor(16@InjectRepository(ArticleEntity)17private readonly articleRepository: Repository<ArticleEntity>,18@InjectRepository(CategoryEntity)19private readonly categoryRepository: Repository<CategoryEntity>,20// 注册 UploadService21private readonly uploadService: UploadService,22) { }2324async findById(id: number): Promise<CategoryEntity> {25const category = await this.categoryRepository.findOne({26where: { id },27});2829// 如果没有查找到,findOne方法返回的是undefined30if (!category) {31return null;32}33return category;34}3536async addArticle(dto: CreateArticleDto, fileDto) {37const id = Number(dto.cate_id);38const category = await this.findById(id);39if (!category) {40return new HttpException('categoryId错误', HttpStatus.BAD_REQUEST);41}4243// 直接调用 uploadService的方法来处理附件,附件返回一个实例,需要这个实例保存到article中。44const file = await this.uploadService.uploadFile(fileDto)4546const data = {47dto,48pub_date: dayjs().format('YYYY-MM-DD hh:mm:ss'),49category,50file,51};52const newArticle = this.articleRepository.create(data);53await this.articleRepository.save(newArticle);54return newArticle;55}56}测试一下:
下载文件的方式有好多种,下面介绍常见的2种方式。
实际项目中,这个文件信息应该存到数据库,从数据库中取。但是这里演示就写固定值。
xxxxxxxxxx261import { Controller, Post, UseInterceptors, UploadedFile, Get, Res } from '@nestjs/common';2import { UploadService } from './upload.service';3import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express'4import type { Response } from 'express'5import { join } from 'path'67@Controller('upload')8export class UploadController {9 constructor(private readonly uploadService: UploadService) { }10 11 @Post('album')12 @UseInterceptors(FileInterceptor('file'))13 upload(@UploadedFile() file) {14 console.log(file, 'file')15 return '峰峰35岁憋不住了'16 }17 18 // 下载图片19 @Get('export')20 downLoad(@Res() res: Response) {21 const url = join(__dirname,'../images/1662894316133.png')22 // console.log(url)23 res.download(url)24 // return true25 }26}
可以使用compressing库,把文件压缩成一个zip包,前端下载的就是一个.zip文件,大附件也许可以这么做。
xxxxxxxxxx191import {zip} from 'compressing' 234@Get('stream')5 async down (@Res() res:Response) {6 const url = join(__dirname,'../images/1662894316133.png')7 const tarStream = new zip.Stream()8 await tarStream.addEntry(url)9 10 res.setHeader('Content-Type', 'application/octet-stream');11 12 res.setHeader(13 'Content-Disposition', 14 `attachment; filename=xiaoman`,15 );16 17 tarStream.pipe(res)18 19 }前端接受流,如果使用的是axios来请求接口,那么里面的responseType的值要配置一下。
xxxxxxxxxx141const useFetch = async (url: string) => {2 const res = await fetch(url).then(res => res.arrayBuffer())3 console.log(res)4 const a = document.createElement('a')5 a.href = URL.createObjectURL(new Blob([res],{6 // type:"image/png"7 }))8 a.download = 'xiaman.zip'9 a.click()10}11 12const download = () => {13 useFetch('http://localhost:3000/upload/stream')14}
为什么要介绍RxJs?因为在 Nestjs 已经内置了 RxJs ,无需安装即可使用,并且Nestjs 也会有一些基于Rxjs提供的API。

https://rxjs.dev/guide/overview

RxJs 使用的是观察者模式,用来编写异步队列和事件处理。(在nestjs中主要就是使用rxjs来编写异步队列。在前端项目中,比如说angularjs里面,可以使用rxjs来做事件处理。)下面介绍其中的三个主要概念:
Observable 可观察的物件
Subscription 监听Observable
Operators 纯函数,可以处理管道的数据,如:map、filter、concat、reduce等。
案例1
类似于迭代器中的 next 发出通知 ,complete通知完成。rxjs中使用 subscribe 订阅消息,Observable用于发出通知,observable也就是一个观察者。
xxxxxxxxxx201import { Observable } from "rxjs";2 3//类似于迭代器 next 发出通知 complete通知完成4const observable = new Observable(subscriber=>{5 subscriber.next(1)6 subscriber.next(2)7 subscriber.next(3)8 9 setTimeout(()=>{10 subscriber.next(4)11 subscriber.complete()12 },1000)13})14 15// 使用 subscribe 订阅消息16observable.subscribe({17 next:(value)=>{18 console.log(value)19 }20})
案例2
interval(500)表示五百毫秒执行一次,pipe 就是管道的意思,管道里面可以处理异步数据,这里的 map 和 filter 跟数组的方法是一样的,最后通过观察者 subscribe 接受回调。
pipe方法的作用:
在 RxJS 中,pipe 语法是一种将多个操作符串联起来形成一个操作链的语法。当一个 observable 发出值时,这个值会经过整个操作链,最后以某种形式返回。
使用 pipe() 方法来创建一个操作链。该方法接收任意数量的函数或操作符作为参数。每个函数或操作符的输入都是前一个函数或操作符返回的结果。
最简单的例子:
xxxxxxxxxx51import { interval,take } from 'rxjs'23interval(500).pipe(take(5)).subscribe((e) => {4 console.log(e);5})
将返回值改为对象:
xxxxxxxxxx51import { interval,take,map } from 'rxjs'23interval(500).pipe(take(5),map(v => ({num:v}))).subscribe((e) => {4 console.log(e);5})
过滤数据,将偶数取出来:
xxxxxxxxxx111import { interval, take, map, filter } from "rxjs";23interval(500)4 .pipe(5 take(10),6 map((v) => ({ num: v })),7 filter((v) => v.num % 2 === 0)8 )9 .subscribe((e) => {10 console.log(e);11 });
subscribe方法返回一个订阅对象,可以使用unsubscribe方法来取消订阅。下面没有使用take方法,所以会一直执行下去,直到取消订阅。
xxxxxxxxxx91import { Observable, interval, take } from "rxjs";2import { map, filter, reduce, find, findIndex } from 'rxjs/operators'34const subs = interval(500).pipe(map(v => ({ num: v })), filter(v => (v.num % 2 == 0))).subscribe((e) => {5 console.log(e)6 if (e.num == 10) {7 subs.unsubscribe()8 }9})

案例3
Rxjs 也可以处理事件,不过我们在nestjs里面就不用操作DOM了,你如果使用Angular 或者 Vue 框架,可以使用 fromEvent。
xxxxxxxxxx71import { Observable, interval, take,of,retry,fromEvent } from "rxjs";2import { map, filter,reduce,find,findIndex } from 'rxjs/operators'3 4const dom = fromEvent(document,'click').pipe(map(e=>e.target))5dom.subscribe((e)=>{6 7})拦截器具有一系列有用的功能,这些功能受面向切面编程(AOP)技术的启发。它们可以:
现在的项目中,返回给前端的格式比较乱:

如果想给前端返回一个标准的json格式,就要给返回数据做一个全局format拦截器。
xxxxxxxxxx61{2 data, //数据3 status:0,4 message:"成功",5 success:true6}1、新建common文件夹,创建response.ts文件。

2、nestjs 配合 Rxjs 格式化数据。implements实现了NestInterceptor,那么就可以注入到具体的位置,实现格式化数据。
xxxxxxxxxx231import { Injectable, NestInterceptor, CallHandler } from '@nestjs/common'2import { map } from 'rxjs/operators'3import type { Observable } from 'rxjs'45interface data<T>{6 data:T7}8 9// 使用@Injectable装饰器,让Response类可以被注入。10@Injectable()11// Response实现NestInterceptor类,里面具体该怎么写?使用ctrl+点击NestInterceptor来查看类型定义,里面有一个intercept方法,要实现这个方法。12export class Response<T = any> implements NestInterceptor {13 intercept(context, next: CallHandler): Observable<data<T>> {14 return next.handle().pipe(map(data => {15 return {16 data,17 status:0,18 success:true,19 message:"牛逼"20 }21 }))22 }23}查看类型定义可以看到:
先实现NestInterceptor的intercept方法,里面有两个参数,主要实现它的next方法,查看next的类型:
使用next的handle()方法,它从路由处理器返回一个stream,这时候就可以使用pipe来处理数据,后面为什么要用到map函数,这就暂时不知道了,先学会使用吧。
3、在main.ts 注册
xxxxxxxxxx21// 注意:这里useGlobalInterceptors里面的参数是一个实例。2app.useGlobalInterceptors(new Response())
上一章我们讲了全局响应拦截,这一章我们来讲一下全局异常拦截器。
1、common文件夹下面,新建filter.ts文件。

创建一个异常过滤器,它负责捕获作为HttpException类(比上一小节的NestInterceptor更加具体,所以写法有一些区别)实例的异常,并为它们设置自定义响应逻辑。为此,我们需要访问底层平台 Request和 Response。我们将访问Request对象,以便提取原始 url并将其包含在日志信息中。我们将使用 Response.json()方法,使用 Response对象直接控制发送的响应。
xxxxxxxxxx251 2import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common'3 4import {Request,Response} from 'express'56// 这里使用@Catch装饰器来捕获异常,参数是HttpException,说明捕获的是Http相关的异常。7@Catch(HttpException)8// HttpFilter实现 ExceptionFilter ,里面该怎么写?先查看ExceptionFilter的类型定义,里面要实现一个catch方法,并返回报错信息。9export class HttpFilter implements ExceptionFilter {10 catch(exception:HttpException, host: ArgumentsHost) {11 const ctx = host.switchToHttp()12 const request = ctx.getRequest<Request>()13 const response = ctx.getResponse<Response>()14 15 const status = exception.getStatus()16 17 response.status(status).json({18 data:exception.message,// 返回异常信息19 time:new Date().getTime(),20 success:false,21 path:request.url,// 返回异常url的路径22 status// 返回异常状态编码23 })24 }25}我怎么知道host参数上可以使用哪些方法?查看类型:
Switch context to HTTP可以翻译为:将上下文切换为http,这样就可以使用http上面的方法了。
3、注册全局异常过滤器
xxxxxxxxxx11app.useGlobalFilters(new HttpFilter())

这里有一点需要搞清楚,前一节讲了响应拦截器,这一节讲了异常拦截器,都规定了返回值的格式,具体是执行哪个拦截器呢?
这要看拦截器注册的顺序,一般都是按照拦截器注册的顺序来执行的,异常拦截器一般放在响应拦截器前面。

管道可以做两件事:
1、转换,可以将前端传入的数据转成成我们需要的数据
2、验证,类似于前端的rules,配置验证规则
这一小节讲管道转换,下一小节讲管道验证,Nestjs 提供了八个内置转换API
我们接受的动态参数希望是一个number类型,但现在是string。query或者param获取的参数都是string类型,可以通过 ParseIntPipe 来转换成 number 类型。

在apifox里面,请求 http://localhost:3000/p/123来查看输出的id,使用typeof查看id的数据类型。

可以看到,id的类型是string,如果直接传递给typeorm的数据库方法来查询,是会报错的,因为类型不正确。这时候就可以通过内置的管道去做转换。直接将转换方法传递给@Param或者@Query的第二个参数,就可以转换。

验证UUID
老师是在nestjs项目中,使用uuid库来生成一个uuid,其实直接在网页上搜索生成uuid的网站,直接获取即可。
下面是他做的事情,不用管。
安装uuid
npm install uuid -S
npm install @types/uuid -D
生成一个uuid

可以看到,如果传递的参数是uuid,那么就通过。

++++++输出看一下如果是uuid,那么此时的id是什么类型?
语法:nest g pi 文件名字

创建好之后的文件:

xxxxxxxxxx11npm i --save class-validator class-transformerxxxxxxxxxx91import {IsNotEmpty,IsString} from 'class-validator'2export class CreatePDto {3 @IsNotEmpty()//验证是否为空4 @IsString() //是否为字符串5 name:string;6 7 @IsNotEmpty()8 age:number9}

value 就是前端传过来的数据,metaData就是元数据,通过 metatype 可以实例化CreateDto这个类。

使用plainToInstance方法来实例化DTO,第一个参数是一个类,第二个参数是实例化类需要的参数。这种实例化方法从来没有见过,因为之前实例化类都是通过new来实现的。

通过 validate方法验证 DTO,返回一个promise的错误信息,如果有错误抛出。

上面的步骤是通过创建 p.pipe.ts 文件来验证管道,但实际上不用这么复杂,因为nestjs每个模块里面的pipe.ts可能都差不多,重点在于每个DTO文件中定义的验证项目和规则,所以nestjs为我们提供了全局DTO管道验证的注册方法,只需要注册了全局的管道验证,那么只需要定义好DTO的验证规则就行了。
跟自己写的效果基本类似


class-validator可以直接查看库里面的readme.md,但是全英文可能看不懂,这里有一个中文的文档,可以查看:https://wdk-docs.github.io/nestjs-docs/docs/validator/class-validator/。
里面的验证修饰符表格,将所有可用的修饰符都列出来了,先选择常用的,用起来。
这一章其实看一看就行,我如果要爬取,就会使用Python。但是nestjs项目中,如果有需求,还是需要nestjs爬虫来解决问题。
其实爬虫是一个对计算机综合能力要求比较高的技术活。
首先是要对网络协议尤其是 http 协议有基本的了解, 能够分析网站的数据请求响应。学会使用一些工具,简单的情况使用 chrome devtools 的 network 面板就够了
发现图片是在 article-content 下面的p标签

请求获取该图片

拼接url 拿到了第一页的数据

守卫有一个单独的责任。它们根据运行时出现的某些条件(例如权限,角色,访问控制列表等)来确定给定的请求是否由路由处理程序处理。这通常称为授权。在传统的 Express 应用程序中,通常由中间件处理授权(以及认证)。中间件是身份验证的良好选择,因为诸如 token 验证或添加属性到 request 对象上与特定路由(及其元数据)没有强关联。
但是中间件很蠢,中间件不知道调用 next() 函数后会执行哪个处理程序。另一方面,守卫可以访问 ExecutionContext 实例,因此确切地知道接下来要执行什么。它们的设计与异常过滤器、管道和拦截器非常相似,目的是让您在请求/响应周期的正确位置插入处理逻辑,并以声明的方式进行插入。这有助于保持代码的简洁和声明性。
tips:守卫在每个中间件之后执行,但在任何拦截器或管道之前执行。
1、创建一个守卫
xxxxxxxxxx11nest g gu [name]nest g gu role

2、守卫要求实现函数CanActivate,给定参数context执行上下文,要求返回布尔值。

3、Controller里面使用守卫
使用UseGuards控制守卫。

xxxxxxxxxx51import { Controller, Get, Post, Body, Patch, Param, Delete ,UseGuards} from '@nestjs/common'; 2 3@Controller('guard')4@UseGuards(RoleGuard)5xxxxx4、全局守卫
如果想让这个这个守卫全局都使用,那么就进行全局注册。
xxxxxxxxxx11app.useGlobalGuards(new RoleGuard())
5、针对角色控制守卫
①使用SetMetadata装饰器来针对具体的guard设置角色,第一个参数为key,表示具体是哪一个guard;第二个参数自定义,在下面的例子中,是一个数组,里面存放的role的权限。
这里有一个疑问,为什么role.guard.ts里面的类是 RoleGuard ,这里SetMetadata的key却使用role呢?是会自动将Guard去掉吗?
是不是会自动将Guard去掉我不知道,但是我知道我在第一步创建guard的时候,用的命令就是
nest g gu role,这里的role就是key。那么我看别人的项目文件的时候,因为无法知道他们的创建命令是什么,但是还是可以联想一下的。

②guard定义文件里面,使用 Reflector 反射读取setMetaData的值,去判断角色,然后做相应的处理。下面的例子是从url判断有没有admin权限。
xxxxxxxxxx221import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';2import { Observable } from 'rxjs';3import { Reflector } from '@nestjs/core'4import type { Request } from 'express'56@Injectable()7export class RoleGuard implements CanActivate {8 constructor(private Reflector: Reflector) { }9 canActivate(10 context: ExecutionContext,11 ): boolean | Promise<boolean> | Observable<boolean> {12 // 这里的admin是后端接口定义时,通过SetMetadata定义的权限数据13 const admin = this.Reflector.get<string[]>('role', context.getHandler())14 // request是前端发起的请求,这里是将权限带在query上面了,可以考虑这么做。但是一般情况下,都是将权限信息放在token里面,token里面可以放很多信息,权限信息可以从里面读取出来。15 const request = context.switchToHttp().getRequest<Request>()16 if (admin.includes(request.query.role as string)) {17 return true;18 }else{19 return false20 }21 }22}上面这个角色验证的用意要这样理解:
1、首先后端需求,定义了findAll这个接口,必须要角色admin才能正常访问,所以在后端编写接口的时候,使用SetMetadata来定义角色。
2、在守卫里面其实没有特地校验admin这个角色,而只是判断前端请求接口的时候所携带的角色,和接口定义时需要的角色,是否一致,一致就通过校验。
3、前端在请求 findAll() 这个接口的时候,就需要携带此接口需要的角色。目前是携带在query请求参数里面。
成功的

失败的

在Nestjs 中我们使用了大量装饰器 decorator ,所以Nestjs 也允许我们去自定义装饰器。

1、生成装饰器
xxxxxxxxxx11nest g d [name]xxxxxxxxxx81// 这个装饰器将 @SetMetadata('role',['admin']) 这样的代码简化了一点,并且在语义上更加友好23import { SetMetadata } from '@nestjs/common';4 5export const Role = (role: string[]) => {6 console.log(role,1)7 return SetMetadata('role', role);8}
这个例子用到了具体的nestjs的api,应该看官方文档就可以知道该怎么做,所以这里不做深究。
xxxxxxxxxx71import { SetMetadata, createParamDecorator, ExecutionContext, applyDecorators } from '@nestjs/common';2import type {Request} from 'express'3 4export const ReqUrl = createParamDecorator((data:string,ctx:ExecutionContext)=>{5 const req = ctx.switchToHttp().getRequest<Request>()6 return req.url7})
swagger用于提供给前端接口文档
1、安装依赖
xxxxxxxxxx11npm install @nestjs/swagger swagger-ui-express2、在main.ts注册swagger
xxxxxxxxxx131async function bootstrap() {2 const app = await NestFactory.create<NestExpressApplication>(AppModule);3 4 // 这一步是配置swagger选项。setTitle 设置文档的标题;setDescription 设置文档的描述信息,setVersion 设置文档的版本号。5 const options = new DocumentBuilder().setTitle('小满接口文档').setDescription('描述,。。。').setVersion('1').build()6 // 创建swagger文档7 const document = SwaggerModule.createDocument(app, options)8 // 设置 /api-docs 路由为swagger文档及其UI的主页9 SwaggerModule.setup('/api-docs', app, document)10 11 await app.listen(3000);12}13bootstrap();
打开对应的路径http://localhost:3000/api-docs/index.html即可。

现在发现并没有分组很乱。
3、可以使用@ApiTags(),为接口添加分组,一般以一个controller来分组。


4、@ApiOperation(),添加接口描述
xxxxxxxxxx71 @Get()2 @Role('admin')3 @ApiOperation({summary:"测试admin", description:"请求该接口需要amdin权限"})4 findAll(@ReqUrl('123') url:string) {5 console.log(url,'url')6 return this.guardService.findAll();7 }
5、@ApiParam(),添加param请求的动态参数描述
xxxxxxxxxx11@ApiParam({name:"id",description:"用户id",required:true})
6、@ApiQuery(),添加query请求的动态参数描述
xxxxxxxxxx11 @ApiQuery({name:"xxxx",description:"bbb"})
7、@ApiProperty(),在dto文件中定义数据请求的描述和案例
xxxxxxxxxx81import { ApiProperty } from "@nestjs/swagger"2 3export class CreateGuardDto {4 @ApiProperty({ description: "姓名", example: "小满" })5 name: string6 @ApiProperty({ description:"年龄"})7 age: number8}
8、@ApiResponse(),在具体的接口上面,自定义返回信息
xxxxxxxxxx11@ApiResponse({status:403,description:"自定义返回信息"})
9、@ApiBearerAuth(),为接口添加 jwt token
① 在main.ts的swagger配置上增加 addBearerAuth()

② 在接口或者controller上面添加@ApiBearerAuth(),这样,整个分组里面的接口或者单个接口,在文档里面测试的时候,就需要带上token了。

在文档里面添加token,这样使用@ApiBearerAuth()装饰的接口,就能自动带上token了。


请求头携带了
10、其他装饰器
上面接口的界面还不是很好看,一般都是左边是接口,右边是具体的接口文档,类似下面这样:

应该只需要额外配置一些属性就行了,到时候再学习吧。
1、安装mysql
2、ORM框架(typeOrm)
typeOrm 是 TypeScript 中最成熟的对象关系映射器( ORM )。因为它是用 TypeScript 编写的,所以可以很好地与 Nest 框架集成。
安装依赖
npm install --save @nestjs/typeorm typeorm mysql2
如果使用的是vsCode 安装数据库可视化工具。不过我使用Navicat就够了,vscode插件安装多了之后,会很慢。

新建一个库


在app.module.ts 注册

xxxxxxxxxx131TypeOrmModule.forRoot({2 type: "mysql", // 数据库类型3 username: "root", // 数据库账号4 password: "123456", // 数据库密码5 host: "localhost", // 数据库host6 port: 3306, // 数据库端口号7 database: "portal", // 库名8 entities: [__dirname + '/**/*.entity{.ts,.js}'], // 实体文件9 synchronize:true, // synchronize字段代表是否自动将实体类同步到数据库,在开发阶段可以为true,但是生产阶段要改为false,否则会出问题。可以使用process.env来判断是哪种环境。10 retryDelay:500, // 重试连接数据库间隔11 retryAttempts:10,// 重试连接数据库的次数12 autoLoadEntities:true, // 如果为true,将自动加载实体 forFeature() 方法注册的每个实体都将自动添加到配置对象的实体数组中13}),定义实体
xxxxxxxxxx111import {Entity,Column,PrimaryGeneratedColumn} from 'typeorm'2 3@Entity()4export class Guard {5 //自增列6 @PrimaryGeneratedColumn()7 id:number8 //普通列9 @Column()10 name:string11}
在xxx.module.ts文件中关联实体。
xxxxxxxxxx11 TypeOrmModule.forFeature([Guard])
TypeOrmModule就三种方法,有两种方法还是类似的,很好记。
什么是实体?
实体是一个映射到数据库表的类。你可以通过定义一个新类来创建一个实体,并用@Entity()来标记:
xxxxxxxxxx161import {Entity,Column,PrimaryGeneratedColumn} from 'typeorm'2 3@Entity()4export class Test {5 @PrimaryGeneratedColumn()6 id:number7 8 @Column()9 name:string10 11 @Column()12 password:string13 14 @Column()15 age:number16}1、主列
自动递增的主键
xxxxxxxxxx21@PrimaryGeneratedColumn()2id:number自动递增的主键,值是uuid
xxxxxxxxxx21@PrimaryGeneratedColumn("uuid")2id:number2、列类型,可以为列添加很多属性,让实体类更加接近MySQL原生语言定义。
xxxxxxxxxx81@Column({type:"varchar",length:200})2password: string3 4@Column({ type: "int"})5age: number6 7@CreateDateColumn({type:"timestamp"})8create_time:Datemysql 所有类型
int, tinyint, smallint, mediumint, bigint, float, double, dec, decimal, numeric, date, datetime, timestamp, time, year, char, varchar, nvarchar, text, tinytext, mediumtext, blob, longtext, tinyblob, mediumblob, longblob, enum, json, binary, geometry, point, linestring, polygon, multipoint, multilinestring, multipolygon, geometrycollection
3、自动生成列
就是当这张表里面创建一个数据的时候,就会在这个数据里面自动添加这一列的数据。
xxxxxxxxxx21@Generated('uuid')2uuid:string4、枚举列
xxxxxxxxxx61@Column({2 type:"enum",3 enum:['1','2','3','4'],4 default:'1'5})6xx:string
5、列选项
xxxxxxxxxx121@Column({2 type:"varchar",3 name:"ipaaa", //数据库表中的列名4 nullable:true, //在数据库中使列NULL或NOT NULL。 默认情况下,列是nullable:false5 comment:"注释",6 select:true, //定义在进行查询时是否默认隐藏此列。 设置为false时,列数据不会显示标准查询。 默认情况下,列是select:true7 default:"xxxx", //加数据库级列的DEFAULT值8 primary:false, //将列标记为主要列。 使用方式和@ PrimaryColumn相同。9 update:true, //指示"save"操作是否更新列值。如果为false,则只能在第一次插入对象时编写该值。 默认值为"true"10 collation:"", //定义列排序规则。11})12ip:stringColumnOptions中可用选项,下面列出来了,先看一下,把常用的记住:
下面给出两种特殊的列类型的用法:
simple-array 列类型
有一种称为simple-array的特殊列类型,它可以将原始数组值存储在单个字符串列中。 所有值都以逗号分隔。
xxxxxxxxxx81@Entity()2export class User {3 @PrimaryGeneratedColumn()4 id: number;5 6 @Column("simple-array")7 names: string[];8}我在想,names是以逗号分隔的字符串形式存储的,那么前端该怎么传递这个参数值呢?是传递一个数组呢?还是传递以逗号分隔的字符串?到时候试一下。
simple-json 列类型
还有一个名为simple-json的特殊列类型,它可以存储任何可以通过 JSON.stringify 存储在数据库中的值。 当你的数据库中没有 json 类型而你又想存储和加载对象,该类型就很有用了。 例如:
xxxxxxxxxx81@Entity()2export class User {3 @PrimaryGeneratedColumn()4 id: number;5 6 @Column("simple-json")7 profile: { name: string; nickname: string };8}这里老师明确说了,profile传进来就是一个对象,所以它的类型可以定义成这样:
profile: { name:string; nickname: string },但是存储的时候,因为设置了类型为simple-json,所以会将数据使用JSON.stringify()处理之后再存储。
前端使用Vue3 ts Element-ui axios,使用vite脚手架创建一个项目,然后安装依赖,将这些代码粘贴进去。
xxxxxxxxxx1241<template>2 <div class="wraps">3 <div>4 <el-input v-model="search.keyWord" style="width:300px;"></el-input>5 <el-button @click="init" style="margin-left:10px;">搜索</el-button>6 <el-button @click="openDialog" type="primary" style="margin-left:10px;">添加</el-button>7 </div>8 <el-table border :data="tableData" style="width: 100%;margin-top: 30px;">9 <el-table-column prop="name" label="名字" />10 <el-table-column prop="desc" label="描述" />11 <el-table-column prop="id" label="id" />12 <el-table-column>13 <template #default="scope">14 <el-button @click="edit(scope.row)">编辑</el-button>15 <el-button @click="deleteRow(scope.row)">删除</el-button>16 </template>17 </el-table-column>18 </el-table>19 <el-pagination @current-change="changeSize" style="float:right;margin-top:10px;" background layout="prev, pager, next" :total="total" />20 </div>21 22 <el-dialog v-model="dialogVisible" title="弹框" width="50%">23 <el-form :model="form">24 <el-form-item prop="name" label="名称">25 <el-input v-model="form.name" placeholder="名称" />26 </el-form-item>27 <el-form-item prop="desc" label="描述">28 <el-input v-model="form.desc" placeholder="描述">29 </el-input>30 </el-form-item>31 </el-form>32 <template #footer>33 <span class="dialog-footer">34 <el-button @click="close">关闭</el-button>35 <el-button type="primary" @click="save">36 保存37 </el-button>38 </span>39 </template>40 </el-dialog>41</template>42 43<script setup lang='ts'>44import { ref, reactive } from 'vue'45import type { FormInstance } from 'element-plus'46import { addUser, updateUser, delUser, getList } from '@/server'47const total = ref<number>(0)48//搜索框49const search = reactive({50 keyWord: "",51 page:1,52 pageSize:1053})54//表单55const form = reactive({56 name: "",57 desc: "",58 id: 059})60//清空数据61const resetForm = reactive({ form })62//表格数据63const tableData = ref([])64//弹框开关65const dialogVisible = ref<boolean>(false)66const openDialog = () => {67 dialogVisible.value = true;68 Object.assign(form, resetForm)69}70//初始化表格数据71const init = async () => {72 const list = await getList(search)73 tableData.value = list?.data ?? []74 total.value = list?.total ?? 075}76init()77const changeSize = (page) => {78 search.page = page79 init()80}81//保存 和修改 表格数据82const save = async () => {83 if (form.id) {84 await updateUser(form)85 } else {86 await addUser(form)87 }88 89 close()90 init()91}92//删除表格数据93const deleteRow = async (row) => {94 await delUser({ id: row.id })95 init()96}97//获取 详情98const edit = (row: any) => {99 dialogVisible.value = true;100 Object.assign(form, row)101}102//关闭弹框103const close = () => {104 dialogVisible.value = false;105}106</script>107 108<style lang='less'>109* {110 padding: 0;111 margin: 0;112}113 114html,115body {116 background: #ccc;117}118 119.wraps {120 121 height: 100vh;122 padding: 30px;123}124</style>xxxxxxxxxx111import axios from 'axios'2 3axios.defaults.baseURL = 'http://localhost:3000'4 5export const addUser = (data) => axios.post('/user',data).then(res => res.data)6 7export const getList = (data) => axios.get('/user',{params:data}).then(res => res.data)8 9export const delUser = (data) => axios.delete(`/user/${data.id}`).then(res => res.data)10 11export const updateUser = (data) => axios.patch(`/user/${data.id}`,data).then(res => res.data)创建项目之后,创建一个user模块即可,nest g res user。
xxxxxxxxxx301import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common';2import { UserService } from './user.service';3import { CreateUserDto } from './dto/create-user.dto';4import { UpdateUserDto } from './dto/update-user.dto';5 6@Controller('user')7export class UserController {8 constructor(private readonly userService: UserService) {}9 10 @Post()11 create(@Body() createUserDto: CreateUserDto) {12 return this.userService.create(createUserDto);13 }14 15 @Get()16 findAll(@Query() query:{keyWord:string,page:number,pageSize:number}) {17 return this.userService.findAll(query);18 }19 20 @Patch(':id')21 update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {22 // 这里使用了 +id ,将id值转为number类型。23 return this.userService.update(+id, updateUserDto);24 }25 26 @Delete(':id')27 remove(@Param('id') id: string) {28 return this.userService.remove(+id);29 }30}1.引入 InjectRepository typeOrm 依赖注入,接受一个实体
2.引入类型 Repository 接受实体泛型
3.Like 用于模糊查询
4.save 保存数据,find 查询,update 更新数据,delete 删除
xxxxxxxxxx471import { Injectable } from '@nestjs/common';2import { CreateUserDto } from './dto/create-user.dto';3import { UpdateUserDto } from './dto/update-user.dto';4import { Repository, Like } from 'typeorm';5import { InjectRepository } from '@nestjs/typeorm';6import { User } from './entities/user.entity';7@Injectable()8export class UserService {9 constructor(@InjectRepository(User) private readonly user: Repository<User>) { }10 create(createUserDto: CreateUserDto) {11 const data = new User()12 data.name = createUserDto.name13 data.desc = createUserDto.desc14 return this.user.save(data)15 }16 17 async findAll(query: { keyWord: string, page: number, pageSize: number }) {18 const data = await this.user.find({19 where: {20 name: Like(`%${query.keyWord}%`)21 },22 order: {23 id: "DESC"24 },25 skip: (query.page - 1)* query.pageSize,26 take:query.pageSize,27 })28 const total = await this.user.count({29 where: {30 name: Like(`%${query.keyWord}%`)31 },32 })33 // 这里使用 findManyAndCount 就可以得到list和total的值。34 return {35 data,36 total37 }38 }39 40 update(id: number, updateUserDto: UpdateUserDto) {41 return this.user.update(id, updateUserDto)42 }43 44 remove(id: number) {45 return this.user.delete(id)46 }47}xxxxxxxxxx111import { Module } from '@nestjs/common';2import { UserService } from './user.service';3import { UserController } from './user.controller';4import {TypeOrmModule} from '@nestjs/typeorm'5import { User } from './entities/user.entity';6@Module({7 imports:[TypeOrmModule.forFeature([User])],8 controllers: [UserController],9 providers: [UserService]10})11export class UserModule {}xxxxxxxxxx41export class CreateUserDto {2 name:string3 desc:string4}在我们开始的过程中,肯定不会把数据存在一个表里面,我们会进行分表,把数据分开存,然后通过关联关系,联合查询。
typeOrm 文档 一对一 | TypeORM 中文文档
前端代码还是复用上一章的,增加了一个添加Tag
xxxxxxxxxx1541<template>2 <div class="wraps">3 <div>4 <el-input v-model="search.keyWord" style="width:300px;"></el-input>5 <el-button @click="init" style="margin-left:10px;">搜索</el-button>6 <el-button @click="openDialog" type="primary" style="margin-left:10px;">添加</el-button>7 </div>8 <el-table border :data="tableData" style="width: 100%;margin-top: 30px;">9 <el-table-column prop="name" label="名字" />10 <el-table-column prop="desc" label="描述" />11 12 <el-table-column prop="id" label="id" />13 <el-table-column>14 <template #default="scope">15 <el-button @click="edit(scope.row)">编辑</el-button>16 <el-button @click="deleteRow(scope.row)">删除</el-button>17 <el-button @click="(isShowTag = true,row = scope.row)">添加tag</el-button>18 </template>19 </el-table-column>20 </el-table>21 <el-pagination @current-change="change" style="float:right;margin-top:10px;" background22 layout="prev, pager, next" :total="total" />23 </div>24 25 <el-dialog v-model="dialogVisible" title="弹框" width="50%">26 <el-form :model="form">27 <el-form-item prop="name" label="名称">28 <el-input v-model="form.name" placeholder="名称" />29 </el-form-item>30 <el-form-item prop="desc" label="描述">31 <el-input v-model="form.desc" placeholder="描述">32 </el-input>33 </el-form-item>34 </el-form>35 <template #footer>36 <span class="dialog-footer">37 <el-button @click="close">关闭</el-button>38 <el-button type="primary" @click="save">39 保存40 </el-button>41 </span>42 </template>43 </el-dialog>44 <el-dialog v-model="isShowTag" title="添加tag">45 <el-select style="width:100%" v-model="tags" multiple>46 <el-option value="tag1">tag1</el-option>47 <el-option value="tag2">tag2</el-option>48 <el-option value="tag3">tag3</el-option>49 </el-select>50 <template #footer>51 <el-button @click="addTa" type="primary">确定</el-button>52 </template>53 54 </el-dialog>55</template>56 57<script setup lang='ts'>58import { ref, reactive } from 'vue'59import type { FormInstance } from 'element-plus'60import { addUser, updateUser, delUser, getList, addTags } from '@/server'61const isShowTag = ref<boolean>(false)62const tags = ref<string[]>([])63const total = ref<number>(0)64const row = ref<{65 id?: number,66 name?: string,67 desc?: string,68 createTime?: Date69}>({})70const addTa = async () => {71 const res = await addTags({72 tags: tags.value,73 userId: row.value.id74 })75 isShowTag.value = false;76 tags.value = [];77}78//搜索框79const search = reactive({80 keyWord: "",81 page: 1,82 pageSize: 1083})84//表单85const form = reactive({86 name: "",87 desc: "",88 id: 089})90//清空数据91const resetForm = reactive({ form })92//表格数据93const tableData = ref([])94//弹框开关95const dialogVisible = ref<boolean>(false)96const openDialog = () => {97 dialogVisible.value = true;98 Object.assign(form, resetForm)99}100//初始化表格数据101const init = async () => {102 const list = await getList(search)103 tableData.value = list?.data ?? [];104 total.value = list?.total ?? 0105}106init()107const change = (page) => {108 search.page = page;109 init()110}111//保存 和修改 表格数据112const save = async () => {113 if (form.id) {114 await updateUser(form)115 } else {116 await addUser(form)117 }118 119 close()120 init()121}122//删除表格数据123const deleteRow = async (row) => {124 await delUser({ id: row.id })125 init()126}127//获取 详情128const edit = (row: any) => {129 dialogVisible.value = true;130 Object.assign(form, row)131}132//关闭弹框133const close = () => {134 dialogVisible.value = false;135}136</script>137 138<style lang='less'>139* {140 padding: 0;141 margin: 0;142}143 144html,145body {146 background: #ccc;147}148 149.wraps {150 151 height: 100vh;152 padding: 30px;153}154</style>
新增了一个接口
xxxxxxxxxx131import axios from 'axios'2 3axios.defaults.baseURL = 'http://localhost:3000'4 5export const addUser = (data) => axios.post('/user',data).then(res => res.data)6 7export const getList = (data) => axios.get('/user',{params:data}).then(res => res.data)8 9export const delUser = (data) => axios.delete(`/user/${data.id}`).then(res => res.data)10 11export const updateUser = (data) => axios.patch(`/user/${data.id}`,data).then(res => res.data)12//添加tag13export const addTags = (data) => axios.post(`/user/add/tags`,data).then(res => res.data)后端Nestjs
1.新建一个 tags.entity.ts
定义Tags的数据表
xxxxxxxxxx151import { Column, Entity, PrimaryGeneratedColumn, BeforeInsert, CreateDateColumn, Generated, OneToOne, JoinColumn, ManyToOne } from 'typeorm'2import { User } from './user.entity'3@Entity()4export class Tags {5 @PrimaryGeneratedColumn()6 id: number7 8 @Column()9 tags:string10 11 @ManyToOne(()=>User,(user)=>user.tags)12 @JoinColumn()13 user:User14 15} Module 里面需要关联tags表
xxxxxxxxxx131import { Module } from '@nestjs/common';2import { UserService } from './user.service';3import { UserController } from './user.controller';4import {TypeOrmModule} from '@nestjs/typeorm'5import { User } from './entities/user.entity';6import { Tags } from './entities/tags.entity';7// import { example } from './entities/tags.entity';8@Module({9 imports:[TypeOrmModule.forFeature([User,Tags])],10 controllers: [UserController],11 providers: [UserService]12})13export class UserModule {}然后user表跟tags表进行关联
xxxxxxxxxx231import { Column, Entity, PrimaryGeneratedColumn, BeforeInsert, CreateDateColumn, Generated, OneToOne, JoinColumn, OneToMany } from 'typeorm'2import { Tags } from './tags.entity'3@Entity()4export class User {5 @PrimaryGeneratedColumn()6 id: number7 8 @Column({ type: "varchar", length: 255 })9 name: string10 11 @Column({ type: "text" })12 desc: string13 14 @Generated('uuid')15 uuid: string16 17 @CreateDateColumn({ type: "timestamp" })18 createTime: Date19 20 @OneToMany(() => Tags, (tags) => tags.user)21 tags:Tags[]22 // example: example23}这儿我们解释一下 OneToMany 和 ManyToOne的用法
对于用户来说一个用户可以拥有多个tag 他们的关系是一对多 OneToMany
对于tag来说他们是多个tag指定单个用户,所以是 ManyToOne

OneToMany 接受两个参数
第一个参数是个函数返回关联的类 所以在user表关联tag
第二个参数 创建双向关系
ManyToOne 用法一样
xxxxxxxxxx11@OneToMany(() => Tags, (tags) => tags.user)保存该关系
沿用上一章的代码增加Controller 增加 addTags
xxxxxxxxxx351import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common';2import { UserService } from './user.service';3import { CreateUserDto } from './dto/create-user.dto';4import { UpdateUserDto } from './dto/update-user.dto';5 6@Controller('user')7export class UserController {8 constructor(private readonly userService: UserService) {}9 10 @Post('/add/tags')11 addTags (@Body() params:{tags:string[],userId:number}) {12 return this.userService.addTags(params)13 }14 15 @Post()16 create(@Body() createUserDto: CreateUserDto) {17 return this.userService.create(createUserDto);18 }19 20 @Get()21 findAll(@Query() query:{keyWord:string,page:number,pageSize:number}) {22 return this.userService.findAll(query);23 }24 25 26 @Patch(':id')27 update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {28 return this.userService.update(+id, updateUserDto);29 }30 31 @Delete(':id')32 remove(@Param('id') id: string) {33 return this.userService.remove(+id);34 }35}service 增加 addTags 方法
xxxxxxxxxx741import { Injectable } from '@nestjs/common';2import { CreateUserDto } from './dto/create-user.dto';3import { UpdateUserDto } from './dto/update-user.dto';4import { Repository, Like } from 'typeorm';5import { InjectRepository } from '@nestjs/typeorm';6import { User } from './entities/user.entity';7import { Tags } from './entities/tags.entity';8// import { example } from './entities/tags.entity';9@Injectable()10export class UserService {11 constructor(12 @InjectRepository(User) private readonly user: Repository<User>,13 @InjectRepository(Tags) private readonly tag: Repository<Tags>14 ) { }15 16//通过前端传入的userId 查到当前id 的用户信息,然后拿到前端传入的tags [tag1,tag2,tag3]17// 进行遍历 给tag实例进行赋值 然后调用保存方法添加tag 添加完之后 通过 tagList 保存该tag类18// 最后把tagList 赋给 user类的tags属性 然后重新调用save 进行更新19 20 async addTags (params:{tags:string[],userId:number}) {21 const userInfo = await this.user.findOne({where:{id:params.userId}})22 const tagList:Tags[] = []23 for (let i = 0;i<params.tags.length;i++) {24 let T = new Tags()25 T.tags = params.tags[i];26 await this.tag.save(T)27 tagList.push(T)28 }29 userInfo.tags = tagList;30 console.log(userInfo,1)31 return this.user.save(userInfo)32 }33 34 async create(createUserDto: CreateUserDto) {35 const data = new User()36 // const ex = new example()37 data.name = createUserDto.name38 data.desc = createUserDto.desc39 // await this.example.save(ex)40 return this.user.save(data)41 }42 43 async findAll(query: { keyWord: string, page: number, pageSize: number }) {44 const data = await this.user.find({45 //查询的时候如果需要联合查询需要增加 relations46 relations:['tags'],47 where: {48 name: Like(`%${query.keyWord}%`)49 },50 order:{51 id:"DESC",52 },53 skip: (query.page - 1) * query.pageSize,54 take: query.pageSize55 })56 const total = await this.user.count({57 where: {58 name: Like(`%${query.keyWord}%`)59 },60 })61 return {62 data,63 total64 }65 }66 67 update(id: number, updateUserDto: UpdateUserDto) {68 return this.user.update(id, updateUserDto)69 }70 71 remove(id: number) {72 return this.user.delete(id)73 }74}

如果需要联合查询需要增加 relations 注意看上面的代码
这个关联表看上去很奇怪,在创建user的同时创建tags。可能之前的项目经验会告诉我,tags这些标签应该是先创建,然后赋值到user上面,因为在我看来tags应该是有限的,是作为user特征来标识的。
如果这些tags没有什么具体的作用的话,是可以当做user的一个字段存储下来的,不一定要分表来存储。但是分表存储也不错,主要还是要靠后端经验的积累吧,看哪种方式更好。
这里的tags也可以当做是附件信息,附件是必须要创建一张单独的表的。
事务的四大特性
事务具有4个基本特征,分别是:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Duration),简称ACID
① 原子性 事务的原子性是指事务必须是一个原子的操作序列单元。事务中包含的各项操作在一次执行过程中,只允许出现两种状态之一,要么都成功,要么都失败
任何一项操作都会导致整个事务的失败,同时其它已经被执行的操作都将被撤销并回滚,只有所有的操作全部成功,整个事务才算是成功完成
② 一致性(Consistency) 事务的一致性是指事务的执行不能破坏数据库数据的完整性和一致性,一个事务在执行之前和执行之后,数据库都必须处以一致性状态。
③ 隔离性 事务的隔离性是指在并发环境中,并发的事务是互相隔离的,一个事务的执行不能被其它事务干扰。也就是说,不同的事务并发操作相同的数据时,每个事务都有各自完整的数据空间。
一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务是不能互相干扰的
④ 持久性(Duration) 事务的持久性是指事务一旦提交后,数据库中的数据必须被永久的保存下来。即使服务器系统崩溃或服务器宕机等故障。只要数据库重新启动,那么一定能够将其恢复到事务成功结束后的状态 原文 事务的四大特性事务的四个特性今天你学习了么的博客-CSDN博客
举例说明 例如小满要给 陈冠希 转账 三百块 ,转账需要两步首先小满-300,第二步陈冠希+300,只要任何一个步骤失败了都算失败,如果第一步成功了,第二步失败了,那小满就亏损三百块。
代码
创建 manager 模块,nest g res manager。

DTO
xxxxxxxxxx101export class CreateManagerDto {2 name:string;3 money:number;4}5 6export class transferMoneyDto {7 fromId:number;//发起人8 toId:number; //接收人9 money:number; //转账的钱10}entities
xxxxxxxxxx141import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'23@Entity()4export class Manager {5 @PrimaryGeneratedColumn()6 id: number;7 8 @Column()9 name: string;10 11 @Column()12 money: number;13 14}manager.controller
xxxxxxxxxx411import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';2import { MangerService } from './manger.service';3import { CreateMangerDto,transferMoneyDto } from './dto/create-manger.dto';4import { UpdateMangerDto } from './dto/update-manger.dto';5 6@Controller('manger')7export class MangerController {8 constructor(private readonly mangerService: MangerService) {}9 //创建人API10 @Post()11 create(@Body() createMangerDto: CreateMangerDto) {12 console.log(createMangerDto)13 return this.mangerService.create(createMangerDto);14 }15 16 //转账API17 @Post('/transferMoney')18 transferMoney(@Body() transferMoneyDto: transferMoneyDto) {19 return this.mangerService.transferMoney(transferMoneyDto);20 }21 22 @Get()23 findAll() {24 return this.mangerService.findAll();25 }26 27 @Get(':id')28 findOne(@Param('id') id: string) {29 return this.mangerService.findOne(+id);30 }31 32 @Patch(':id')33 update(@Param('id') id: string, @Body() updateMangerDto: UpdateMangerDto) {34 return this.mangerService.update(+id, updateMangerDto);35 }36 37 @Delete(':id')38 remove(@Param('id') id: string) {39 return this.mangerService.remove(+id);40 }41}service
主要看 transferMoney 里面的写法:
xxxxxxxxxx581import { Injectable } from '@nestjs/common';2import { CreateMangerDto, transferMoneyDto } from './dto/create-manger.dto';3import { UpdateMangerDto } from './dto/update-manger.dto';4import { Manager } from './entities/manager.entity'5import { InjectRepository } from '@nestjs/typeorm';6import { Repository, Like } from 'typeorm';7@Injectable()8export class MangerService {9 constructor(@InjectRepository(Manager) private readonly money: Repository<Manager>) {}10 11 create(createMangerDto: CreateMangerDto) {12 return this.money.save(createMangerDto);13 }14 15 async transferMoney(transferMoneyDto: transferMoneyDto) {16 //return this.manger.save(transferMoneyDto);17 try {18 //typeOrm 事务19 return await this.money.manager.transaction(async manager => {20 const from = await this.money.findOne({ where: { id: transferMoneyDto.fromId } })21 const to = await this.money.findOne({ where: { id: transferMoneyDto.toId } })22 console.log(from.money >= transferMoneyDto.money)23 if (from.money >= transferMoneyDto.money) {24 manager.save(Manger, { id: transferMoneyDto.fromId, money: from.money - transferMoneyDto.money })25 manager.save(Manger, { id: transferMoneyDto.toId, money: to.money + transferMoneyDto.money } )26 return {27 message: "转账成功"28 }29 } else {30 return {31 message: "转账失败 余额不足"32 }33 }34 })35 }36 catch (e) {37 return {38 message: e39 }40 }41 }42 43 findAll() {44 return `This action returns all manger`;45 }46 47 findOne(id: number) {48 return `This action returns a #${id} manger`;49 }50 51 update(id: number, updateMangerDto: UpdateMangerDto) {52 return `This action updates a #${id} manger`;53 }54 55 remove(id: number) {56 return `This action removes a #${id} manger`;57 }58}this.money.manager这里的manager不要弄混了,它是仓库实例自带的属性:
manager上有transaction方法:
rxjs的概念,在前端看来很难理解,这是因为前端的操作可视化比较强,很容易在头脑中形成先后顺序的概念,其次一个用户一个界面,不用在一个界面中同时处理非常多的事件,至少目前为止我没有遇到过。

但是后端不同,你想象一下,几万个在线用户连接一个服务器,同时发起请求,数据处理的先后顺序是不是要理清楚。那么rxjs就相当于将数据变成了数据流,数据流可以看成是水,水经过多重处理之后,根据需要可以变成饮用水、矿泉水、果汁、奶茶、咖啡等等。数据流也是一样的,经过过滤器、拦截器等变成人需要的数据流。
nestjs框架的编写就用到了rxjs,这是必须的。那么我使用nestjs框架,也要回使用rxjs,因为某些业务场景一定会用到rxjs。
rxjs其实在前端也是有应用场景的,比如说web编辑器,举个例子:实现在vscode中使用alt+鼠标选择多个代码的效果,最后统一进行复制。也就是说alt+鼠标选择,此时不会触发多次的复制事件,怎么做?此时就可以使用rxjs,将事件进行合并,具体代码还不会写。
其实代码就是要多写,到现在如果要我完整的写一个vue项目出来,恐怕也很困难,还是要参考别人的项目,就连JNPF项目不也是照抄了很多若依的代码吗?或者相反。
完全不要有太大的压力,后端其实也很简单,多写才能有更多的机会来熟悉后端的写法,才能碰到更多的问题,这样才能更容易理解别人为什么要这样设计、这样写。
最近(2025-04-28)照着nest-admin-api写自己的nestjs项目框架模板,发现有些东西理解起来并不难了,我发现其实我把自己的项目模板做好之后,写项目应该是很简单了。这样不是让自己写代码越来越简单了吗?这不是更好了吗?
如果以后,隔了很长一段时间没有写nestjs代码,那么除了看学习文档之外,就需要照着别人的项目,做一遍自己的项目框架,这样回忆起来就非常快了。这是个好方法。