前言

上篇文章《NestJS 扫盲篇:TypeScript 类和装饰器》中,我们介绍了 TS 中类和装饰器的用法。Nest 应用基于面向对象和面向切面开发,大量应用了类和装饰器。所以掌握这两个语法,再去学习 Nest,是一个很有必要的基础。

MVC 分层架构

在本系列更文的第一篇,较为细致的分析了 Hello World 的示例代码。它只有一个根模块,也就是 App 模块,然后使用控制器接收请求,再调用服务完成具体业务的处理,最后将结果响应给客户端,如下图所示:

Nest 核心概念:模块_Node

这其实就是一个经典的 MVC 分层架构:

  • Model:业务模型层或者数据模型层,用来处理业务,是整个分层架构中最复杂的部分
  • View:视图层,就是用户界面
  • Controller:控制器层,解耦视图层和模型层,职责也很明确和简单,在 Web 应用中就是接收用户请求,调用 Model 层处理后将结果响应给视图层

Nest 核心概念:模块_Nest.js_02

在 Nest 示例代码中,主要涉及到的概念有:

  • app.controller.ts:App 控制器,对应的 MVC 中的 C 层。
  • app.service.ts:App 服务,对应 MVC 中的 M 层。
  • app.module.ts:App 模块,它负责将控制器和服务组织到一起。

你会发现,为啥没看到 View 呢?因为在 MVC 盛行的年代,前后端都是耦合在一起的,拿 Java 来说,View 层就是 JSP 。对于现在前后端分离的项目来说,View 层就是 React,Vue 等框架写的可以独立部署的页面应用。

写一个 Nest 应用,最基本的就是写模块,控制器和服务。下面就是对这三个概念的介绍。

模块

如何理解模块

首先要区分 Nest 模块和平时前端开发中经常提及的“前端模块化”中的模块。后者是属于 JS 语言层面的概念,是一种语法,比如标准的ES Module 规范中的模块就是使用 export 导出,使用 import 导入,还有 CommonJS 规范使用 module.exports 导出,使用 require 导入。

而前者是 Nest 应用层面上的概念,跟它对应的应该是前端组件化中的组件,比如 Vue 开发就是用组件组成一个完整的页面。Nest 使用模块来组织应用的结构,比如一个博客系统中,可以分为用户模块,文章模块等。

每个 Nest 应用都有一个根模块,按照约定大于配置的思想,都会定义在 app.module.ts 文件中,是一个名为 AppModule 的类。其他的模块都是它的子模块,都会被它引入,组成一个完整的应用。

Nest 核心概念:模块_Nest.js_03

模块的代码表示

Nest 模块在定义上是使用装饰器 @Module() 装饰的类,使用元数据描述一些模块的属性,用以组织该模块。

所谓元数据,就是描述数据的数据。简单理解的话,装饰器工厂接受的参数就是元数据,它们不直接参与到你的编码中,它们用来描述这个模块的组成。@Module() 装饰器接受一个描述模块属性的对象,有四个属性:

属性

描述

providers

声明一组提供者,由 Nest 容器负责实例化,在当前模块中共享

controllers

声明一组控制器,模块必选的属性

imports

导入模块的列表,这些模块导出了此模块中所需提供者

exports

由本模块提供并应在其他模块中可用的提供者的子集

看下示例,AppModule 类使用 @Module() 装饰,所以它就是一个 Nest 模块了。看下装饰器的类型声明,按住 Ctrl 键,点击 @Moudle 就可以跳转:

export declare function Module(metadata: ModuleMetadata): ClassDecorator;

可以看到,装饰器其实一个函数。而@Module是一个装饰器工厂,接收的参数就是模块元数据对象,装饰器工厂返回一个类装饰器。

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';

@Module({
  // 该模块所需的其他模块。根模块需要将所有模块都导入,才能让应用正常运行。
  // 有的子模块可能依赖另一个子模块,也要通过此选项导入。
  // 一个更直观的例子,后面要用到ORM框架比如 TypeOrm,Mongoose,它们就要在此导入再使用。
  imports: [UserModule],
    
  // 模块装饰器的必填项。因为要用它处理请求,没有它,应用接收不到请求也就没有存在的意义了。
  controllers: [AppController],
    
  // 模块的提供者,就是能提供能力,供模块的其他部分使用。服务就是最常用的一种提供者.它可以写核心逻辑,操作数据库等等,然后在控制器中被使用。
  providers: [AppService],
})
export class AppModule {}


模块的组织

模块在使用装饰器声明时,需要传入一些元数据信息,比如它的控制器和提供者。

在模块的目录组织上,一个良好的风格是将该模块相关的资源放到同一个文件夹中管理,比如用户模块有控制器,服务,还有数据层的 dto 文件等等,就可以放到 user 目录下:

src
├──user
│    ├──dto
│    │   └──create-user.dto.ts
│    ├──interfaces
│    │     └──user.interface.ts 
│    ├─user.service.ts
│    ├─user.controller.ts
│    └──user.module.ts
├──app.module.ts
└──main.ts

然后,新建的目录一定要在 app.module.ts 中引入:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';

@Module({
  imports: [UserModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

在《Nest 应用目录结构和脚手架命令》中介绍了常用的脚手架命令,要新建一个模块,可以直接使用 nest g module

$ nest g module user

CLI 会自动在 src 目录下新建 user 目录和对应的模块文件,之后再使用命名创建对应的控制器和服务等内容。

使用脚手架命令创建的好处是省时省力,能自动在根模块 AppModule 中注册,能自动处理控制器和服务之间的关系。

总结

本文介绍了经典的 MVC 分层架构,以及各层和 Nest 应用的对照。然后讲解了 Nest 模块的具体定义,使用 @Module 装饰器声明的类,就是模块。@Module() 接收一些信息来描述此模块的构成,主要是声明此模块所依赖的其他模块,控制器,提供者,和要导出的供其他模块所使用的部分。

下篇文章会继续讲解 Nest 控制器,它主要使用若干个装饰器用来处理请求,调用服务,最后响应数据。

感谢阅读,再会!