Nest 是啥?
Nest 是一个 nodejs 服务端应用程序开发框架,它有几个特点:
- 内置支持 Typescript
- 底层默认使用 Express 框架,也可以通过配置使用 Fastify
- 核心思想是控制反转和依赖注入
下面我们先创建一个简单的 Nest 项目,通过项目了解 Nest 中的一些基本概念,最后再聊一聊核心思想控制反转(IoC)和依赖注入(DI)是咋回事。
创建项目与基本概念
创建与启动
- 全局安装 @nestjs/cli
pnpm i -g @nestjs/cli
- 通过 cli 创建项目
nest new <project-name>
- 启动项目
cd <project-name>
pnpm run start
可以看到项目已经成功运行起来了。
项目结构与基本概念
项目的核心结构如下:
src
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
├── main.ts
main
main 文件是应用程序的入口文件,其中通过 NestFactory.create 创建应用实例,然后监听端口。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
module
- module 是使用 @Module 装饰器声明的类,其中可以注册其他 module、controller、provider ,也可以导出。
- Nest 中用模块来组织应用程序,每个应用至少需要一个根模块。
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
exports: [],
})
export class AppModule {}
controller
- controller 是被 @Controller 装饰器声明的类。
- 负责处理请求和响应,可以注入 provider,比如下面例子中创建了一个 Get 接口,其中依赖了 AppService。
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get("/hello")
getHello(): string {
return this.appService.getHello();
}
}
provider
- provider 是被 @Injectable 装饰器声明的类。
- 通常用于提供一些具体的功能实现,可以被注入到 controller 或者其他 provider 使用。
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
至此,Nest 应用程序对外提供了一个 /hello 接口,返回 "hello world!",尝试 curl 调用:
控制反转和依赖注入
控制反转和依赖注入是 Nest 的核心思想,怎么理解呢?我们来举个🌰。
传统写法的问题
一个应用程序的组织,本质上就是很多的类,互相调用来实现整体的功能。比如有两个低层类 Dog 和 Cat,用于创建猫和狗;有一个高层 Animal 类,依赖低层类去创建小动物。下面的写法中,直接在 Animal 中实例化 Dog 类并创建了一只小狗。
class Dog {
create() {
console.log("create a dog, wangwang~")
}
}
class Cat {
create() {
console.log("create a cat, miaomiao~")
}
}
class Animal {
private dog = new Dog()
create() {
this.dog.create()
}
}
这个写法会有什么问题呢?思考一下:
- 假如需求改变,不想让 Animal 创建一只小狗而是想创建一只小猫,那么需要改变 Animal 类的实现,如果想创建其他小动物就需要一次又一次的改变类的实现,
Animal 和它依赖的类产生了强耦合
。
class Animal {
private cat = new Cat()
create() {
this.cat.create()
}
}
- 假如还有很多个 Animal2、Animal3 ......,它们都想要创建一只小狗,那么代码就变成下面这样,
Dog 需要被实例化很多次
。
class Animal {
private dog = new Dog()
create() {
this.dog.create()
}
}
class Animal2 {
private dog = new Dog()
create() {
this.dog.create()
}
}
class Animal3 {
private dog = new Dog()
create() {
this.dog.create()
}
}
当应用复杂度增加,类和类之间依赖关系复杂度也随之增加,这两个问题会变得越来越严重。
改进
那更好的写法是什么呢?当 Animal 类依赖 Dog 类时,可以不在 Animal 中直接创建 Dog 的实例,而是定义一套抽象接口,Animal 类和 Dog 类都依赖于这套抽象去实现,然后在外部工厂类中创建 Dog 实例传入 Animal。即: Animal 不用关心实例如何创建,只需要按照抽象接口的规范去使用它即可
。
class Animal {
private animal
constructor(animal) {
this.animal = animal
}
create() {
this.animal.create()
}
}
class Animal2 {
private animal
constructor(animal) {
this.animal = animal
}
create() {
this.animal.create()
}
}
class Factory {
create() {
// 先创建一只狗
const dog = new Dog()
const dogAnimal = new Animal(dog)
dogAnimal.create()
// 再创建一只猫
const cat = new Cat()
const catAnimal = new Animal(cat)
catAnimal.create()
// Animal2 复用同一个 Dog 实例创建一只狗
const animal2 = new Animal2(dog)
animal2.create()
}
}
这样就解决了上面两个问题:
- 当我们想从创建一只狗变为创建一只猫时,Animal 类的实现完全不需要改动。
- 当多个 Animal 都想创建一只狗时,可以复用同一个实例。
这个例子中,将类实例化的过程交给 IoC 容器(即 Factory 类)处理,就是控制反转(IoC);在类之外创建依赖对象并提供给类,就是依赖注入(DI)。
Nest 中的 IoC
Nest 框架本身就是一个 IoC 容器,负责统一管理类的实例化以及注入,开发者不需要手动实例化,只需要向 IoC 容器拿实例化后的对象使用即可。这种将控制权交给框架的做法,有利于实现类和类之间的松耦合。
具体实现
-
module 中通过 @Module 装饰器注册依赖。
-
通过 @Injectable 装饰器定义 provider,声明这个类被 IoC 容器接管,即实例化过程委托给 IoC 容器。
-
controller 和 provider 中都可以通过 constructor 注入依赖关系。
// test.module.ts
@Module({
controllers: [TestController],
providers: [TestService],
})
export class TestModule {}
// test.service.ts
@Injectable()
export class TestService {
constructor(private test2Service: Test2Service) {}
findAll() {}
}
// test.controller.ts
@Controller('test')
export class TestController {
// 注入对 TestService 的依赖
constructor(private testService: TestService) {}
@Get()
async findAll() {
return this.testService.findAll();
}
}
最后
至此,我们简单了解了 Nest 的几个基本概念 module、controller、provider,以及它的核心思想控制反转与依赖注入。此系列后续文章会更详细的介绍它的使用并对核心源码进行解析。