nestjs 学习笔记-概念篇

307 阅读6分钟

前言

本文是一篇新手向的文章,内容尽量通俗易懂,是作者在学习nestjs 过程中整理的知识点和一些感悟,分享出来,希望大家看了也有启发,可以抛砖引玉。

从09年出现node 开始,后续出现express koa ,都只提供基础的服务框架。为了满足复杂的企业开发需求,出现eggjs,nestjs,一个基于koa , 另一个底层基于express(也可手动切换成fastify)。从个人体验而言,更喜欢nestjs,有人称它为nodejs 版的Spring ,它和前端另一个框架 Angular 也有这个共通之处,它们有一个很重要的概念:依赖注入,这是本文的重点之一。本篇主要讲解nestjs 的相关理论部分,实际操作部分会放在下一篇文章中更新。

初始化项目

项目环境:

node 16.15.0
npm 8.5.5
nest 8.2.5

先安装脚手架 nest cli

npm i -g @nestjs/cli

新建项目

nest new word-processing

新建完成之后 就会出现下面这个目录结构

.
├── README.md
├── nest-cli.json
├── package.json
├── src
│   ├── app.controller.spec.ts
│   ├── app.controller.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   └── main.ts
├── test
│   ├── app.e2e-spec.ts
│   └── jest-e2e.json
├── tsconfig.build.json
├── tsconfig.json
└── yarn.lock

项目建立完成! 运行 npm run start:dev 就可以在localhost:3000 访问项目啦

nestjs 前置知识

大家看了目录发现,和express 不一样 ,没有单独的路由文件。只有 app.controller.ts

他是长这样的

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

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

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


启动时会发现

LOG [RoutesResolver] AppController {/words}: +7ms
LOG [RouterExplorer] Mapped {/words/hello, GET} route +3ms

意思是这一组接口前缀是 words。访问 /words/hello 可以返回getHello 里的内容。

其中@Controller@Get 被称为 “装饰器” ,同时引出两个概念控制反转(Inversion of Control,IoC)依赖注入(Dependency Injection) ,控制反转是一种设计思想,依赖注入是实现控制反转的一种设计模式 ,允许在类外创建依赖对象,并将这些对象提供给类。这个样的设计模式有什么好处呢,主要是符合 松耦合,高内聚,符合单一职责原则(有没有觉得到哪都能听到这句话?),下面开始详细讲解。

IOC 和 DI

我们假设遇到类之间的依赖关系,比如类People 依赖于类Car,People类的方法中会调用Car 类的实列

class Cloth {
    wear(name: String) {
        console.log(name + " wear cloth ")
    }
}

class People {
    private name: String;
    private cloth: Cloth;
    constructor(name: String) {
        this.name = name;
        this.cloth = new Cloth()
    }
    wearCloth() {
        this.cloth.wear(this.name)
    }
}

const boy = new People("Kei")

boy.wearCloth()

这样就是最原始的写法,People 依赖于Car,产生了模块间的耦合。为了解决模块间的强耦合性,IoC的概念就产生了。下面是改进后的写法,

interface Cloth {
    wear: (name: String) => void
}

class Suits implements Cloth {
    wear(name: String) {
        console.log(name + " wear suits")
    }
}

class People {
    private name: String;
    private cloth: Cloth;
    constructor(name: String,cloth: Cloth) {
        this.name = name;
        this.cloth = cloth;
    }
    wearCloth() {
        this.cloth.wear(this.name)
    }
}

const boy = new People("Kei", new Suits())

boy.wearCloth()

然而在实际项目上类之间的关系要复杂的多,这里我们使用IoC 容器来整合。

class Container {
    public static importModule(T: any): void { };
    public static getModule<T = any>(module: String): T {
        return
    };
    constructor() { }
}

interface Cloth {
    wear: (name: String) => void
}

class Suits implements Cloth {
    wear(name: String) {
        console.log(name + " wear suits")
    }
}
Container.importModule(Suits)

class People {
    private cloth: Cloth
    constructor() {
        this.cloth = Container.getModule("Suits")
    }
    wearCloth() {
        this.cloth.wear("Kei")
    }
}


总结下就是 A类的功能实现需要依赖B 类。假如在A类内部实例化B类,日后如果B类修改可能导致A类的逻辑也要随之变动,我们把它称之为耦合性较高。要解决这个问题,就是要把A类对B类的控制交给第三方(IOC 容器)去做 ,通过构造函数的方式,把B类的实例注入到A类里去,这样就达到了解耦的目的。

装饰器

接着再聊聊装饰器,上面 app.controller.ts 里面的@Controller@GET 都是nestjs 基于其本身容器提供的功能。其中@Controller 属于 类装饰器,@Get 属于 方法装饰器

类装饰器 入参只有一个就是这个类本身(不是类的原型对象),它可以覆盖类的属性和方法,甚至返回一个新类覆盖掉整个类

const Controller = (path?: string): ClassDecorator => {
    return (target) => {
        console.log("Controller", path, target)
    };
};

方法装饰器包含类的原型,方法名以及 方法属性描述符。我们可以拿到方法原本的实现,保证原型的逻辑正常执行的时候,增加新的逻辑。

const GET = (path: string): MethodDecorator => {
    return (_target, _propertyKey, descriptor:TypedPropertyDescriptor<T>) => {
        console.log("GET", path, descriptor.value)//descriptor.value 即方法原本的实现
    };
};

RxJS

rxjs 是一个针对异步事件流的工具库。当我们新建一个interceptor 的时候会有如下代码

import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle();
  }
}

intercept 函数返回值Observable 类型,那它又是什么呢?

  • 首先,Observable 是一个函数

  • 它是基于观察者模式,创建的一个事件生产的容器,对其进行订阅(subscribe) 可以源源不断的得到它发送的数据

这样可能还不形象,我们可以把它和Promise 做一下类比

Promise

  • 一次只能出触发一个事件
  • 建立Promise 会立即执行事件,并且不能注销事件

Observable

  • 可以源源不断的触发事件
  • 只有在被订阅的时候才会被触发,并且可以注销事件

看上去可以把他理解成一个升级版的Promise

nestjs 理论知识

Module

Module 是一个 被@Module() 装饰器注释的类,它接受4个参数分别是

  • imports :导入其他模块中导出的Providers,以实现共享
  • providers :模块中所有用到的功能类,模块内共享实用;
  • controllers:控制器
  • exports:导出其他模块需要共享的Providers

共享模块:一个Serivce也可以被多个Module导入使用,并且都是单例模式,如果我们希望providers 实例能够复用的话,可以用下面的方法

//a.module.ts
import { Module } from '@nestjs/common';
import { AppService } from './app.service';

@Module({
  providers: [AppService],
  exports: [AppService]
})
export class ModuleA { }

//b.module.ts
import { Module } from '@nestjs/common';
import { ModuleA } from './a.module'

@Module({
	imports:[ModuleA]
})
export class ModuleB { }

Provider

Provider 是一个被@Injectable() 装饰器注释的类。 在我们的初始代码中 app.service.ts 里面的 AppService 类就是被@Injectable()装饰的类。把他和app.controller.ts 里面的类注册进app.module.ts 里,并且在 controller 的构造函数里加入相应的字段就可以在controller 里使用service 的功能了。这一切都是基于nestjs Ioc 完成的。

上面代码的provider 是最简化的写法,等同于

@Module({
  providers: [ { 
   provide: AppService, 
   useClass: AppService,
 }]
})

provider 主要有几种类型

值提供者 : 一般是给 Nest容器 注入常量 或者 替换provide 的实现用于测试用

//provider
const connectionConfig = { host: "", port: "" }
const connectionProvider = {
    provide: 'CONNECTION_CONFIG',
    userValue: connectionConfig
}

// 使用方法
@Injectable()
export class Repository {
  constructor(@Inject('CONNECTION_CONFIG') connectionConfig: ConnectionConfig) {}
}

类提供者 :可以指定一个动态类去解析

const configServiceProvider = {
  provide: ConfigService,
  useClass:
    process.env.NODE_ENV === 'development'
      ? DevelopmentConfigService
      : ProductionConfigService,
};

工厂提供者 : 允许动态创建 提供者(通过工厂函数返回提供者),工厂函数可以完全独立,也可以依赖其他 提供者。Nest 在实例化的时候会将这些参数注入到函数里,inject 数组和 工程函数的参数按照顺序一一对应

const configServiceProvider = {
  provide: "CONNECTION",
  useFactory:(configProvider:ConfigProvider){
    const options = configProvider.getConfigs()
    return Connection(options);
  },
  inject:[ConfigProvider]// 依赖其他 提供者
};

这是可以使用异步提供者的

const configServiceProvider = {
  provide: 'ASYNC_CONNECTION',
  useFactory: async () => {
    const connection = await createConnection(options);
    return connection;
  },
}