Nest系列(1):项目初探,请求响应全流程

453 阅读7分钟

初识 NestJS

先来看下官网对 NestJS 的介绍。

Nest (NestJS) 是一个用于构建高效、可扩展的 Node.js 服务器端应用程序的开发框架。它利用 JavaScript 的渐进增强的能力,使用并完全支持 TypeScript (仍然允许开发者使用纯 JavaScript 进行开发),并结合了 OOP (面向对象编程)、FP (函数式编程)和 FRP (函数响应式编程)。

在底层,Nest 构建在强大的 HTTP 服务器框架上,例如 Express (默认),并且还可以通过配置从而使用 Fastify

Nest 在这些常见的 Node.js 框架 (Express/Fastify) 之上提高了一个抽象级别,但仍然向开发者直接暴露了底层框架的 API。这使得开发者可以自由地使用适用于底层平台的无数的第三方模块。

通过这段文字,可知道以下几点:

  1. NestJS 是一个基于 Node.js 的服务端应用开发框架
  2. 完全支持 TypeScript,也可以使用纯 JS 开发
  3. 支持多种编程范式,例如 OOP 和 FR
  4. 底层处理 HTTP 请求,还是使用了 Express(默认) ,也可切换为 Fastify
  5. 有无数的第三方模块可供使用

下面就开始上手 NestJS 应用的开发吧。

快速初始化

Nestjs 官方提供了一个很好用的脚手架工具,可以用来快速创建,开发和构建一个 Nestjs 应用。

首先全局安装脚手架工具:

npm install -g @nestjs/cli

查看脚手架版本号:

nest -v
9.1.8

使用脚手架提供的 new 命令创建项目:

nest new hello-nest

命令行中会提示选择一个你想要的包管理工具,本文使用 Pnpm :

image-20230113172003243

脚手架工具创建了很多文件,然后安装依赖中:

image-20230113172025874

稍等片刻,这是项目项目初始化完成后的样子:

image-20230113172043299

根据提示,进入项目目录,执行启动命令,即可运行一个 Nestjs 应用了:

$ cd hello-nest
$ pnpm run start

image-20230114143246000

在命令行窗口中,有一些打印的信息,我们可以简单看一下。

> hello-nest@0.0.1 start D:\2023code\hello-nest
这一行是项目的名字以及版本号,和项目所在路径
​
> nest start
这一行是启动项目的命令
​
下面是应用运行的日志信息
[Nest] 42416  - 2023/01/14 14:32:29     LOG [NestFactory] Starting Nest application...
NestFactory(Nest 工厂类)日志,启动了 Nest 应用
​
[Nest] 42416  - 2023/01/14 14:32:29     LOG [InstanceLoader] AppModule dependencies initialized +24ms
InstanceLoader(实例加载器类) 日志,App 模块的依赖完成初始化
​
[Nest] 42416  - 2023/01/14 14:32:29     LOG [RoutesResolver] AppController {/}: +5ms
RoutesResolver(路由解析器类) 日志,App 控制器建立了 ‘/’ 这个路由    
​
[Nest] 42416  - 2023/01/14 14:32:29     LOG [RouterExplorer] Mapped {/, GET} route +2ms
RouterExplorer(路由管理器类) 日志,完成了 'GET /' 路由的映射
​
[Nest] 42416  - 2023/01/14 14:32:29     LOG [NestApplication] Nest application successfully started +1ms
NestApplication(Nest 应用程序类) 日志,Nest 应用成功启动

所以现在,浏览器就可以访问/ 这个路由了。

Hello, world

Nest 应用默认的端口是 3000,打开浏览器,访问 localhost:3000,就可以看到如下界面:

image-20230114144932740

接下来我们去看下源码,看下 NestJS 是如何将这个 Hello, World 响应给浏览器的。

分析 Hello, World

下面是刚刚所创建的 Nest 项目的目录结构。

image-20230114150635459

我们先来关注下 src 目录,它下面有五个 .ts 文件,分别是:

  • app.controller.spec.ts:App 控制器的单元测试文件
  • app.controller.ts:App 模块的控制器
  • app.module.ts:App 模块,也是 Nest 应用的根模块
  • app.service.ts:App 模块的服务层代码
  • main.ts:Nest 应用入口文件,用来创建 Nest 应用实例和启动应用

从 main.ts 看起

打开 src/main.ts,这是 Nest 应用的入口文件。代码很简单:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
​
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

该文件封装了一个 bootstrap 函数,用于创建 Nest 应用。内部调用 NestFactory 工厂类的 create 方法,将 AppModule 传入,创建应用实例。最后应用调用 listen 方法,监听 3000 端口,完成 web 服务的启动。

如果你写过 Vue ,可以将此文件和 Vue 项目的入口文件 main.js做一个类比。NestFactory.create 好比是 createApp 方法,AppModule 模块好比是 App.vue根组件。如果你使用 Express 或者 Koa,listen 方法更不用多说,可以类比为组件的挂载 mount 方法,它是创建应用的最后一步。

app.module.ts

Nest 应用是以模块 Module 为单元的。模块有两个核心成员,控制器提供者。可以将模块理解为前端 Vue 项目中的组件app.module.ts 导出一个 AppModule 类,是 Nest 应用的根模块,就好比是 Vue 应用中的根组件。

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
​
@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

在该文件中,导入了 App 模块的控制器 AppController服务 AppService,然后通过装饰器(类似于 Java 中的注解) @Module,将 AppModule 类声明为一个模块。

装饰器 @Module 接收了几个属性, controllersproviders。前者接收控制器,后者接收提供者,Nest 中提供者有很多中,代码中的 AppService 是提供者的一种。通过装饰器和这两个属性,一个模块就能将控制器和服务组织到一起

app.controller.ts

这是 App 模块的控制器层代码,用来接收用户请求,调用服务层处理后,再将结果返回给客户端。

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
​
@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}
​
  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

它首先导入了 Nestjs 核心包提供的两个装饰器,然后导入了 App 模块的服务类。通过 @Controller 装饰器,将 AppController 类声明为一个控制器。代码中类的构造方法是一种简写模式,作用是将 AppService 服务类的实例挂载到了控制器的实例上,并且还是私有成员,用伪代码表示就是:

class AppController {
    constructor() {
        this.appService = new AppService()
    }
}

但是实际上 AppService 服务类并不是在这里实例化的,而是作为依赖交给依赖注入容器的去管理的。

所以在类的成员方法中就可以直接调用 this.appService了。

然后又使用了 @Get 装饰器声明了 getHello 方法,表示此方法用于处理 HHTPGET请求。@Controller@Get 都可以接收字符串类型的参数用来拼接路由,比如 @Controller('news')@Get('list'),拼接出来的路由就是 /news/list。示例代码中装饰器都省略了参数,默认为空字符串,所对应的路由就是根路由 /。就好比 Express 中:

app.get('/', getHello)
app.get('/news/list', getNewsList)

getHello 方法内部调用了 App 服务层的 getHello 方法,所以接下来就该去看看 app.service.ts 了。

app.service.ts

这是 AppModule 的服务层文件,也是真正处理业务的地方。

import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

该文件导入了一个 @Injectable 装饰器,它将 AppService 类型声明为了“提供者”。Nest 中有很多种提供者,这里的“服务”就是一种最常用的提供者。这里涉及到依赖注入的概念,提供者在依赖注入中的角色就是就是可以被注入到容器中的类。

这段代码可以先简单理解为使用了这个装饰器之后,AppService 类的实例可以直接在控制器中使用,而无需手动导入和实例化

AppService 类有一个 getHello 方法,返回 Hello, World! 字符串,这就是我们在浏览器中看到的那个。它返回的字符串,交给了 App 控制器,控制器又返回给了浏览器,至此,一个完整的流程就走完了。

nest 应用逻辑流程

总结

本文介绍了使用 @nest/cli 脚手架快速创建和启动一个 Nest 应用,随后又对“Hello, World”示例代码做了分析,简单介绍了一些 TypeSscript 语法,比如装饰器,和一些 Nest 的概念。相信看到这里,大家基本上了解了 Nest 应用接收到用户请求后,走了哪些流程,最终完成了响应。

我们下篇文章见。

本文正在参加「金石计划」