Nest入门教程(一:控制反转和依赖注入、装饰器)

240 阅读8分钟

一、IOC控制反转和依赖注入

控制反转

传统的编程模式中,对象的创建和依赖是由使用对象的代码来控制,需要知道什么时候创建/使用对象,而在IOC中,控制权交给了容器,负责创建对象并且解决对象之间的依赖关系,使得在修改某一类的时候,调用者不需要重新修改其他创建对象的地方,实现了对象之间的松耦合。

依赖注入

控制反转是一种思想,它告诉我们解决问题的思路,但没有具体告诉我们应该怎么做。而依赖注入,就是实现IOC的方式,通过依赖注入(DI),容器将依赖关系自动注入到指定对象中,而不是让对象自己去创建或者查找其他依赖的对象。实现方式可以是通过构造函数注入属性注入或者方法注入

举例:

Vue中也体现了这一点:InjectProvider,在组件通信中,祖先组件中提供依赖给后代组件使用,后代无需关心祖先组件是如何创建和销毁依赖。

容器

IOC中的容器可以理解为依赖调度中心,负责管理所有的对象生命周期管理和依赖关系,在Nest中有内置的IOC容器,它是IOC的核心。通过配置中心或注解的方式来定义对象之间的依赖关系。

示例

创建一个公共类 A, 业务类 B 和 业务类 C 分别依赖 A

// 公共类
class A {
    name: string
    constructor() {
        this.name = "jmin"
    }
}

// 业务类
class B {
    b: string
    constructor() {
        this.b = new A().name
    }
}
// 业务类
class C {
    c: string
    constructor() {
        this.c = new A().name
    }
}

当存在这种依赖关系的时候,尝试修改一下 公共类A,将name改为动态赋值。

class A {
    name: string
    constructor(name: string) {
        this.name = name
    }
}

那么,这时候依赖公共类的业务类BC都需要在对象创建的时候修改创建方式,在大型应用中,如果有100个业务类依赖公共类A呢?

IOC/DI为了解决这个问题应运而生。

我们来看下通过IOC方式维护的示例:

// 公共类
class A {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

// 业务类
class B {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

// 容器类
class Container {
  map: any;
  constructor() {
    this.map = {};
  }
  provide(key: string, value: any) {
    this.map[key] = value;
  }
  get(key: string) {
    return this.map[key];
  }
}
// 新建容器进行收集引用
const container = new Container();
// 提供依赖
container.provide('a', new A('a'));
container.provide('b', new B('b'));

// C类可以随意在容器中获取想要依赖的类
class C {
  a: A;
  b: B;
  constructor() {
    this.a = container.get('a');
    this.b = container.get('b');
  }
}

上述代码中,容器中维护着多个公共类A、B,它们在某个时机统一注册到容器中(依赖注入),而此时业务类C可以随意在容器中获取想要依赖的类,并且当A、B改变,不需要修改到调用方,实现逻辑解耦。

Nest中实现依赖注入

Nest注册依赖

Nest中,IOC容器是内置的nest运行时系统,使用module向容器注册依赖

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

module可以接受多个诸如controllersproviders的依赖。

定义提供者provider

通过@Injectable()装饰器将service类标识为提供者,此时,IOC容器将会接管这个类的控制权。

import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

将提供者注入到控制器中

Controller不做具体业务逻辑,它只负责分发请求,具体的请求逻辑由Service提供者管理,所以我们将提供者注入到控制器中,通过构造函数注入声明了对AService的依赖。

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

总体的注册过程如下: 当 Nest IoC 容器实例化一个 AppController 时,它首先查找任何依赖。 当它找到AppService 依赖时,它会根据注册步骤对 AppService 令牌执行查找,返回AppService 类。

二、装饰器

装饰器是一种特殊的类型声明,他可以附加在类,方法,属性,参数上面

注意:

装饰器是实验特性,需要在 tsconfig.json 中启用:

{
  "experimentalDecorators": true,
  "emitDecoratorMetadata": true
}

类装饰器

给类添加元数据、注册依赖,主要是通过@符号添加装饰器

他会自动把class的构造函数传入到装饰器的第一个参数 target

然后通过prototype可以自定义添加属性和方法

const decotators = (target: any) => {
  target.prototype.name = 'aiolimp';
};

@decotators
class Aiolimp {
  constructor() {}
}

const a: any = new Aiolimp();
console.log(a.name);

属性装饰器

设置默认值、自动绑定、依赖注入等。同样使用@符号给属性添加装饰器,他会返回两个参数

  • 原型对象

  • 属性的名称

const currency: PropertyDecorator = (target: any, key: string | symbol) => {
  console.log(target, key); // {} name
};

class A {
  @currency
  public name: string;
  constructor() {
    this.name = '';
  }
  getName() {
    return this.name;
  }
}

nest-装饰器.png

参数装饰器

获取请求参数、上下文信息等,同样使用@符号给属性添加装饰器,他会返回两个参数

  • 原型对象
  • 方法的名称
  • 参数的位置,从0开始
const cur: ParameterDecorator = (target: any, key: string | symbol | undefined, index: number) => {
  console.log(target, key, index); // {} getName 1
};

class Xiaoman {
  public name: string;
  constructor() {
    this.name = '';
  }
  getName(name: string, @cur age: number) {
    return this.name;
  }
}

方法装饰器

用于权限校验、日志打印、缓存处理等,同样使用@符号给属性添加装饰器,他会返回两个参数

  • 原型对象

  • 方法的名称

  • 属性描述符 可写对应writable,可枚举对应enumerable,可配置对应configurable

const fun: MethodDecorator = (target: any, key: string | symbol, descriptor: any) => {
  console.log(target, key, descriptor);
  // {} getName {
  // value: [Function: getName],
  // writable: true,
  // enumerable: false,
  // configurable: true
  // }
};

class B {
  public name: string;
  constructor() {
    this.name = '';
  }
  @fun
  getName(name: string, age: number) {
    return this.name;
  }
}

类型作用示例框架
类装饰器给类添加元数据、注册依赖@Injectable()(NestJS)
方法装饰器权限校验、日志打印、缓存处理等@Get()(NestJS)
属性装饰器设置默认值、自动绑定、依赖注入等@Inject()
参数装饰器获取请求参数、上下文信息等@Body()、@Param()

示例:通过装饰器实现一个Get请求

安装axios依赖

npm install axios -S

定义控制器UserController

class UserController {
  constructor() {}
}

定义装饰器

带参数的装饰器

ES6中不支持直接调用装饰器,当需要支持传参时(比如 @Get('/user') 中的 '/user'),要 return 一个函数,通过装饰器工厂,也叫柯里化

const Get = (url: string): MethodDecorator => {
  return (target: any, key: string | symbol, descriptor: any) => {
  };
};

class UserController {
  constructor() {}
  @Get('https://api.apiopen.top/api/getHaoKanVideo?page=0&size=10')
  getUser(res: any, status: any) {
    console.log(res.data.result.list, status);
  }
}
  • 外层函数 Get(path: string) 是你传参的部分;
  • 内层函数 (target, key,descriptor) 是装饰器真正作用于类方法时调用的;

不带参数的装饰器

如果你写一个 不需要参数的装饰器,可以省略外层 return:

function Log(target: any, propertyKey: string) {
  console.log(`${propertyKey} 被装饰`)
}
@Log
hello() {}

Get请求完整代码

import axios from 'axios';
// 定义装饰器
const Get = (url: string): MethodDecorator => {
  return (target: any, key: string | symbol, descriptor: any) => {
    // 定义 descriptor 的类型 通过 descriptor描述符里面的value 把axios的结果返回给当前使用装饰器的函数
    const func = descriptor.value; // 获取原始方法getUser
    axios
      .get(url)
      .then((res) => {
        func(res, {
          status: 200,
        });
      })
      .catch((err) => {
        func(err, {
          status: 200,
        });
      });
    return descriptor;
  };
};

// 定义控制器
// - 当类被加载时,装饰器会立即执行
// - Get 装饰器在返回的装饰器函数中直接调用了axios.get()
// - 请求完成后,又直接调用了原始方法 func(res, {status: 200})
class UserController {
  constructor() {}
  @Get('https://api.apiopen.top/api/getHaoKanVideo?page=0&size=10')
  getUser(res: any, status: any) {
    console.log(res.data.result.list, status);
  }
}

Nest中的装饰器

NestJS 中,装饰器是核心机制之一,广泛用于控制反转(IoC)、依赖注入(DI)、路由、参数提取、元数据定义等场景。Nest 的装饰器本质上是函数,通过它们可以声明式地定义类的行为、结构和元数据

1.类装饰器(用于定义组件/模块/控制器等)

装饰器说明
@Controller()声明该类为控制器(Controller)
@Injectable()声明该类可以被注入(服务等)
@Module()声明该类为模块
@Global()声明该模块为全局模块
@Injectable()
export class UserService {
  getUser() {
    return { name: 'Tom' }
  }
}

@Controller('user')
export class UserController {
  constructor(private userService: UserService) {}

  @Get()
  getUser() {
    return this.userService.getUser()
  }
}

2.方法装饰器(用于声明路由、生命周期钩子)

装饰器说明
@Get()声明 GET 路由
@Post()声明 POST 路由
@Put()声明 PUT 路由
@Delete()声明 DELETE 路由
@OnModuleInit()生命周期钩子方法装饰器

3.参数装饰器(用于提取请求参数)

装饰器说明
@Param()获取 URL 参数
@Query()获取查询参数(?key=value)
@Body()获取 POST/PUT 请求体
@Req()获取原始请求对象(Request)
@Res()获取原始响应对象(Response)
@Headers()获取请求头信息
@Get(':id')
getUser(@Param('id') id: string, @Query('verbose') verbose: boolean) {
  return { id, verbose }
}

4.自定义装饰器(基于 Reflect元数据)

可以使用 Reflect.defineMetadata() 与 Reflect.getMetadata() 自定义装饰器行为:

import 'reflect-metadata'

function Role(role: string) {
  return Reflect.metadata('role', role)
}

@Role('admin')
class AdminService {}

5.依赖注入相关装饰器(高级)

装饰器说明
@Inject()显式注入标识符或Token
@Optional()注入失败时不抛错
@HostParam()用于网关参数注入(WebSocket等)

NestJS 的装饰器实现了声明式编程风格,将业务逻辑与框架结构清晰分离,大幅提升可维护性和开发效率。它们是 依赖注入(DI)机制模块化架构路由控制 的基础。