nestjs学习3:核心概念扫盲

40 阅读5分钟

声明:本文来自于光神的文章,复制到这里只是为了自己更好的复习。

Nest 是对标 Java 的 Spring 框架的后端框架,它有很多概念。对于新手来说,上来就接触这些概念可能会有点懵,今天主要是过一下概念。

我们通过不同的 url 来访问后端接口:/user/create 、/user/list、/book/create、/book/list,不同的 url 是不同的路由。

这些路由在 Controller 里声明:

image.png

在 class 上和它方法的方法上加上 @Controller、@Get、@Post 的装饰器就可以了。

controller里面的方法,比如 create、findAll等叫做 handler,是处理路由的。

post 的请求体,get 的请求参数,都可以通过装饰来取:

通过 @Param 取 url 中的参数,比如 /user/111 里的 111:

image.png

通过 @Query 来取 url 中的 query 参数,比如 /user/xx?id=222 里的 222

image.png

通过 @Body 取 /book/create 的请求体内容:

image.png

@Param、@Query、@Body装饰器在其中起了什么作用呢?

  • 当 NestJS 应用启动、加载控制器类时,装饰器会先执行(装饰器的执行时机是类 / 方法定义时,而非请求到来时),@Param('id') 的核心逻辑如下:
import 'reflect-metadata'; 
export function Param(paramKey?: string) {
    return (target: object, methodKey: string | symbol, paramIndex: number) => {
        const METADATA_KEY = 'nest:params:metadata';
        // 先获取该方法已有的元数据
        const existingMetadata = Reflect.getMetadata(METADATA_KEY, target, methodKey) || [];
        existingMetadata[paramIndex] = { 
            type: 'param', // 标识参数类型是路由参数 
            key: paramKey, // 要提取的参数名(比如 'id') 
        };
        Reflect.defineMetadata(METADATA_KEY, existingMetadata, target, methodKey);
}
  • 当客户端发起 GET /users/123 请求时,NestJS 的底层 HTTP 适配器(Express/Fastify)解析请求,得到 request 对象,其中 request.params = { id: '123' }。NestJS 会创建执行上下文(ExecutionContext),把 requestresponse、路由匹配结果等都封装进去。NestJS 的参数处理器(Parameter Handler)会执行以下操作:
function getParamValue(target: object, methodKey: string, paramIndex: number, context: ExecutionContext) {
  // 1. 获取步骤 1 中存入的元数据
  const METADATA_KEY = 'nest:params:metadata';
  const paramMetadata = Reflect.getMetadata(METADATA_KEY, target, methodKey)[paramIndex];
  
  // 2. 从上下文获取 request 对象
  const request = context.switchToHttp().getRequest();
  
  // 3. 根据元数据提取对应值
  if (paramMetadata.type === 'param') {
    // 如果指定了 key(比如 'id'),取单个值;否则取整个 params 对象
    return paramMetadata.key ? request.params[paramMetadata.key] : request.params;
  }
}
  • NestJS 把上面提取到的 '123' 赋值给 findOne 方法的第 0 个参数(也就是 id),执行 findOne 函数时id 被赋值为 '123'

总的来说就是这样:

  1. 定义元数据:应用启动时,@Param 装饰器通过 Reflect.defineMetadata 把参数要从哪里取值的规则存在方法的元数据中;

  2. 运行时注入:请求到来时,NestJS 先通过 Reflect.getMetadata 读取元数据,再从请求上下文的 request.params 中提取对应值,最后注入到函数参数中执行;

  3. 核心依赖:整个过程的基础是 Reflect Metadata API,它让 NestJS 能 “记住” 装饰器的规则,实现参数的自动注入,无需手动操作 request 对象

框架这样做的好处是什么呢?

好处是我们无需手动处理request对象,全是框架帮我们做了。你在写express时是不是自己从request对象中获取参数,特别繁琐。

回归正题,也就是说 controller 是处理路由和解析请求参数的

请求参数解析出来了,下一步就是做业务逻辑的处理了,这些东西不写在 controller 里,而是放在 service 里。

image.png

service 里做业务逻辑的具体实现,比如操作数据库等

同理,/book/list、/book/create 接口是在另一个 BookController 里,它的业务逻辑实现也是在 BookService 里。

很明显,UserController 和 UserService 是一块的,BookController 和 BookService 是一块的。

所以,Nest 有了模块的划分,每个模块里都包含 controller 和 service:

image.png

通过 @Module 声明模块,它包含 controllers 和 providers。

为啥不是 services 而是 providers 呢?

因为 Nest 实现了一套依赖注入机制,叫做 IoC(Inverse of Control 反转控制)。

简单说就是你只需要声明依赖了啥就行,不需要手动去 new 依赖,Nest 的 IoC 容器会自动给你创建并注入依赖。

比如这个 UserController 依赖了 JwtService,那只需要通过 @Inject 声明依赖,然后就可以在方法里用了。

image.png

运行的时候会自动查找这个 JwtServcie 的实例来注入。

在 @Module 里的 providers 数组里,就是声明要往 IoC 容器里提供的对象,所以这里叫做 providers。

provider 有很多种写法:

image.png

默认的 XxxService 只是一种简化的写法。

还可以直接 useValue 创建,通过 useFactory 创建等。

image.png

刚才提到了 IoC 会自动从容器中查找实例来注入,注入的方式有两种:

属性注入和构造器注入。

image.png

这种写在构造器里的依赖,就是构造器注入。

@Inject 写在属性上的依赖,就是属性注入。

效果是一样的,只是注入的时机不同。

每个模块都会包含 controller、service、module、dto、entities 这些东西:

image.png

controller 是处理路由,解析请求参数的。

service 是处理业务逻辑的,比如操作数据库。

dto 是封装请求参数的。

entities 是封装对应数据库表的实体的。

nest 应用跑起来后,会从 AppModule 开始解析,初始化 IoC 容器,加载所有的 service 到容器里,然后解析 controller 里的路由,接下来就可以接收请求了。

应用中会有很多 controller、service,那如果是跨多个 controller 的逻辑呢?

这种在 Nest 提供了 AOP (Aspect Oriented Programming 面向切面编程)的机制

image.png

具体来说,有 Middleware、Guard、Interceptor、Pipe、Exception Filter 这五种。

image.png

它们都是在目标 controller 的 handler 前后,额外加一段逻辑的。

比如你可以通过 interceptor 实现请求到响应的时间的记录:

image.png

这种逻辑适合放在 controller 里么?

不适合,这种通用逻辑应该通过 interceptor 等方式抽离出来,然后需要用的时候在 controller 上声明一下:

image.png

我们可以通过一个简单的例子来理解下 @UseInterceptors的逻辑:

function Intercept(interceptor: { before?: Function; after?: Function }) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // 保存原始方法
    const originalMethod = descriptor.value;

    // 重写方法
    descriptor.value = function (...args: any[]) {
      // 执行前置拦截逻辑
      if (interceptor.before) {
        interceptor.before(args);
      }

      // 调用原始方法
      const result = originalMethod.apply(this, args);

      // 执行后置拦截逻辑
      if (interceptor.after) {
        interceptor.after(result);
      }

      // 返回原始方法的结果
      return result;
    };

    return descriptor;
  };
}

class MyClass {
  @Intercept({
    before: (args: any[]) => {
      console.log(`[BEFORE] Method is about to be called with args: ${JSON.stringify(args)}`);
    },
    after: (result: any) => {
      console.log(`[AFTER] Method returned: ${JSON.stringify(result)}`);
    },
  })
  myMethod(arg1: string, arg2: number) {
    console.log("Executing myMethod");
    return `Received: ${arg1}, ${arg2}`;
  }
}

const instance = new MyClass();
instance.myMethod("Hello", 42);

这些就是 Nest 的核心概念了。