前言
本文是一篇新手向的文章,内容尽量通俗易懂,是作者在学习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;
},
}