在2.4节中,我们介绍了AOP(面向切面编程)架构理念的基本概念及其在Nest中的应用方式。本节将介绍Nest中的另一种核心设计思想——IoC(控制反转)思想。IoC思想贯穿于整个Nest应用的开发过程。接下来,让我们一起学习IoC如何解决问题,并学习在Nest中如何应用IoC。
在后端体系中,通常包含以下几个组件。
● Controller(控制器):负责接收客户端的请求。
● Service(服务):处理业务逻辑。
● Dao(数据访问对象)或Repository(仓库):负责对数据执行增删改查(CRUD)操作。
● DataSource(数据源):根据配置信息连接和管理数据库。
这意味着在开发过程中,你需要按照合适的顺序创建这些组件。例如:
const dataSource = new DataSource(config) const dao = new Dao(dataSource) const Service = new Service(dao) const Controller = new Controller(Service)
在后端架构中,当多个Controller模块调用同一个Service类时,我们希望确保它们使用的是同一个实例,即维持单例模式。在大型应用中,手动管理这种依赖关系可能会变得复杂,这是传统后端开发经常面临的一个挑战。
幸运的是,IoC(Inverse Of Control,控制反转)提供了一种解决方案。IoC容器在应用初始化时,会查找每个类上声明的依赖,并按顺序创建相应的实例,然后管理这些实例。当需要使用某个依赖时,IoC容器会提供相应的对象实例。
依赖注入(DI)是实现IoC的一种常见方式。为什么称之为“控制反转”呢?让我们通过一个生活化的比喻来说明:
想象一下,通常我们做饭之前需要准备各种食材,有时还要在市场上讨价还价,回家后要清洗、切割食材,烹饪时还要考虑食材的下锅顺序等,这个过程相当烦琐。那么,有没有更简单的方法呢?
答案是肯定的。如果我们的目的是为了解决饥饿问题,为什么不选择去餐厅就餐呢?我们只需告诉服务员:“请给我来一份番茄炒蛋饭。”
此时,后厨接到通知,控制权已经转移。厨师们会根据菜单要求,有序地处理后厨的所有事务,并最终为我们提供一份美味的“番茄炒蛋饭”。
在这个比喻中,后厨相当于IoC容器,菜单相当于在类上声明的依赖,服务员则相当于依赖注入的过程。这样,IoC容器会根据类上声明的依赖来创建和管理对象。
通过IoC,我们从主动创建和维护对象转变为被动等待依赖注入,实现了从主动下厨到等待服务员上菜的转变,这就是IoC控制反转的精髓。
本小节将介绍在Nest中如何实现IoC。首先,通过运行nest n ioc-test命令新建一个项目,并在创建过程中选择pnpm作为包管理器,如图2-38所示。
图2-38 创建ioc-test项目
进入该目录,运行pnpm start:dev命令启动服务。服务启动成功后的界面如图2-39所示。
图2-39 启动服务
下面来看在Nest中如何组织代码。打开app.controller.ts文件,核心代码如下:
@Controller() export class AppController { constructor(private readonly appService: AppService) {} @Get() getHello(): string { return this.appService.getHello(); } }
可见,AppController类通过@Controller装饰器来修饰,表示它可以进行依赖注入,由Nest内置的IoC容器接管。
接着打开app.service.ts,代码如下:
import { Injectable } from '@nestjs/common'; @Injectable() export class AppService { getHello(): string { return 'Hello World!'; } }
AppService类通过@Injectable进行装饰,表示这个类(class)可以被注入,同时也可以注入其他对象中。我们再创建一个user中间件,如图2-40所示。
图2-40 创建user中间件
打开user.middleware.ts文件,代码如下:
import { Injectable, NestMiddleware } from '@nestjs/common'; @Injectable() export class UserMiddleware implements NestMiddleware { use(req: any, res: any, next: () => void) { next(); } }
我们发现UserMiddleware也是通过@Injectable装饰器来标记的。这可能会让读者产生一些疑问:
为什么控制器是单独使用@Controller来装饰的?
除了已知的服务(Service)和中间件(Middleware)使用@Injectable装饰,还有哪些组件也使用它?
对于第一个问题,控制器(Controller)只用于处理请求,不作为依赖对象被其他对象组件注入。我们可以把控制器看作是消费者,而服务(Service)和中间件(Middleware)则是提供者。
通常使用@Injectable进行装饰的还包括过滤器(Filter)、拦截器(Interceptor)、提供者(Provider)、网关(Gateway)等。在没有特殊情况下,Nest中所有需要依赖注入的模块都可以使用@Injectable进行标记,以便IoC容器进行收集和管理。
回到代码中,这些组件会在AppModule中进行引入,如图2-41所示。
图2-41 依赖注入
@Module装饰器在Nest中用于定义模块,这些模块包含需要注入的组件,例如控制器(Controllers)。控制器仅能作为消费者被注入,而提供者(Providers),如服务(Services),既可以作为依赖被注入,也可以注入其他依赖对象中。
除此之外,在Nest中实现模块化管理非常简单。imports属性用于引入其他模块,这有助于实现功能逻辑的分组和重用。例如,我们可以创建一个UserModule,如图2-42所示。
图2-42 创建user模块
可见,在AppModule中自动引入了UserModule,如图2-43所示。
图2-43 依赖自动引入
另外,如果还有aaaModule、bbbModule等模块,它们也会自动在AppModule中引入,最终交给Nest工厂方法完成应用的创建,如图2-44所示。
图2-44 应用主入口
有了这些步骤,在初始化过程中,Nest就可以轻松通过模块之间的依赖关系找到UserModule模块。根据@module中声明的依赖关系,UserController只需在构造函数中声明对UserService的依赖,而不需要显式创建实例就可以调用UserService的实例方法,如图2-45所示。
图2-45 调用服务方法
这就是Nest的依赖注入与模块机制。在后续章节中,我们将频繁使用这些机制,并享受IoC带来的便捷性。