当前,模块化思想已广泛应用于软件开发领域。它通过将大型软件应用划分为相互独立且功能明确的模块或组件,实现了类似搭积木的过程。同样,Nest框架也提供了一种结构化和模块化的方法来管理应用程序中的不同部分。本节将学习如何创建并使用这些模块。
模块通过@Module装饰器来声明。每个应用都会有一个根模块,Nest框架会从根模块开始收集各个模块之间的依赖关系,形成依赖关系树。在应用初始化时,根据依赖关系树实例化不同的模块对象,具体如图3-8所示。
图3-8 模块依赖关系
在模块树中,每个模块都有自己独立的作用域。它们之间的代码是相互隔离的,各自拥有自己的控制器(Controllers)、服务提供者(Providers)、中间件(Middlewares)和其他组件。当然,它们也可以通过一定的方式进行相互共享。
下面通过创建一个名为nest-module的项目来演示。运行“nest n nest-module -p pnpm”命令,具体的运行结果如图3-9所示。
图3-9 创建项目
首先来看app.module.ts文件,代码如下:
import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; @Module({ imports: [], controllers: [AppController], providers: [AppService], }) export class AppModule {}
其中,AppModule是默认的根模块。类装饰器@Module的参数中,controllers用于注入该模块的控制器集合,而providers用于注入该模块的服务提供者,这些服务提供者将在该模块中共享。
Imports用于导入应用中的其他模块,默认为空。下面以创建新的User和Order模块为例,运行如下命令:
// 生成User模块 nest g resource User --no-spec // 生成Order模块 nest g resource Order --no-spec
--no-spec表示不生成单元测试文件,运行结果如图3-10所示。
图3-10 生成User和Order模块
回到AppModule根模块,可以看到UserModule和OrderModule被自动导入根模块,成为AppModule的子模块。具体代码如下:
import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { UserModule } from './user/user.module'; import { OrderModule } from './order/order.module'; @Module({ // 模块被自动导入了 imports: [UserModule, OrderModule], controllers: [AppController], providers: [AppService], }) export class AppModule {}
既然有imports,那么必然也有exports。假设存在这样的需求,Order模块需要依赖User模块中的UserService。这时可以将UserService添加到UserModule的exports中,使之成为共享服务。这样,Order模块只需导入UserModule即可访问UserService。下面通过一个案例来演示。
在User模块中导出User服务:
@Module({ controllers: [UserController], providers: [UserService], // 导出UserService服务 exports: [UserService] }) export class UserModule {}
接着在Order模块中导入User模块,代码如下:
@Module({ // 导入UserModule imports: [UserModule], controllers: [OrderController], providers: [OrderService] }) export class OrderModule {}
此时,在Order模块的任何地方,都可以共享UserService服务了。下面来演示一下。
在order.controller.ts中定义路由方法getOrder,然后调用order.service.ts中的getOrderDesc方法:
在order.service.ts中通过属性注入UserService依赖,同时调用UserService的getUserHello方法,最终返回一个问候语字符串,代码如下:
除使用属性注入依赖外,还可以使用构造函数注入:
最后,在浏览器中访问http://localhost:3000/order,页面成功返回了内容,这表明实现了模块间的数据交互,如图3-11所示。
图3-11 代码运行结果
如果某个模块在多个地方被引用,为了简化管理,可以使用@Global装饰器将其声明为全局模块。这样,便可以直接注入通过exports导出的providers,而无须在每个模块的imports中重复声明它们。代码如下:
需要注意的是,使用全局模块之前,需要确保你的模块确实需要全局使用,以避免不必要地增加模块之间的耦合性。
前面介绍的都是静态模块的绑定和使用。Nest中还提供了动态加载模块的功能,使得应用可以在运行时创建模块,通常用于动态读取配置或根据权限判断来加载模块。
创建动态模块的第一步是在需要使用动态模块的地方,例如某个服务或控制器中,使用forFeature方法来动态加载模块。代码如下:
动态加载模块时,调用loadModule方法即可加载相应的模块。代码如下:
除loadModule方法之外,还有register和forRoot方法可用于加载动态模块。其中,register方法通常用于与外部模块或第三方库集成时,它能将外部模块动态加载到Nest.js模块中。
而forRoot方法在Nest中通常用于注册根模块,例如配置根模块的全局服务或中间件等。代码如下:
以上介绍了加载静态和动态模块的几种方式。动态模块的加载方式在后续章节中会频繁出现,特别是与第三方服务集成时。