nest 简单入门

NEST

Nest 是 Node 后台框架,结合有 OOP, FP, FRP,使用 TS 开发。

# 全局安装脚手架
npm i -g @nestjs/cli
# 新建项目
# --strict 参数会使得使用 ts 严格模式
nest new [your-project-name] --strict
# ts 初学者项目
git clone https://github.com/nestjs/typescript-starter.git project
# js 初学者项目
git clone https://github.com/nestjs/javascript-starter.git project

在 src 目录下,存放的是项目的代码文件。

  • app.controller.spec.ts

    具有单一路由的基本控制器的单元测试。

  • app.controller.ts

    具有单一路由的基本控制器。

  • app.module.ts

    应用根模块。

  • app.service.ts

    具有单一方法的基本服务。

  • main.ts

    入口文件。

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

Nest 默认使用的是 express。也可以根据需求切换为 fastify。
不论使用哪一个,都会分别公开 NestExpressApplicationNestFastifyApplication 接口,使得可以让对象具有特定框架的方法。
一般无需指定,除非需要访问使用对应 API。

const app = await NestFactory.create<NestExpressApplication>(AppModule)

使用 npm run start 开启项目。
可以传递 -- -b swc 参数,这个参数指定使用 SWC 构建器,会加速开发时的构建速度。

在开发时,使用 npm run start:dev 自动热更新。

控制器

负责处理传入请求并将响应返回。

可以使用脚手架自带的 CRUD 生成器。

nest g resource [name]
import { Controller, Get } from '@nestjs/common'

// 使用 nest g controller [name] 可以快速创建控制器
@Controller('first') // 传入字符串将作为路由前缀
export class FirstController {

  @Get() // 声明为 GET 请求 将映射为 /first 路由
  // @Get('/first') // 将映射为 /first/first 路由
  findAll(): string {
    // 当访问 /first 时 将调用此方法
    // 虽然需要声明方法才能绑定路由
    // 但此方法的名称对于 Nest 没有任何意义
    return 'First Find All'
  }
}

响应

Nest 使用两种不同选项操作响应。

推荐的标准选项

使用此方法时,请求处理返回 JS 对象或数组时将自动序列化,当返回基本类型时则仅发送值而不进行序列化。

此外,除使用 201 的 POST 请求外,默认的 HTTP 状态码将始终为 200。

可以添加 @HttpCode 装饰器更改。

库特定的选项

可使用特定于库 (如 Express) 响应对象,可以使用装饰器处理。

findAll(@Res() response),通过此方法,可以使用该对象公开的原生响应处理方法构建响应。

注意!Nest 会监测处理程序何时使用 @Res() 或 @Next(), 如果同时使用这两种方法,则单一路由程序标准方法将禁用,且不会按照预期工作。如果需要同时使用,则需要使用 @Res({ passthrough: true })。

请求对象

Nest 提供对于底层平台请求对象的访问,默认为 Express,通过使用 @Req() 装饰器指示 Nest 注入访问请求对象。

import { Controller, Get, Req } from '@nestjs/common'
// 需要首先安装 @types/express
import { type Request } from 'express'

@Controller('first')
export class FirstController {
  @Get()
  findAll(@Req() request: Request): string {
    return 'first'
  }
}

Nest 提供开箱即用的装饰器用于获取请求对象信息。

装饰器 请求对象
@Request(), @Req() req
@Response(), @Res() * res
@Nest() next
@Session() req.session
@Param(key?: string) req.params / req.params[key]
@Body(key?: string) req.body / req.body[key]
@Query(key?: string) req.query / req.query[key]
@Headers(name?: string) req.headers / req.headers[name]
@Ip() req.ip
@HostParam() req.hosts

Nest 提供 @Res()@Response() 装饰器,前者为后者的别名。

都直接暴露底层 response 对象接口。使用时,仍需要导入 @types/express 类型。

当使用者两者时,需要手动处理响应,即必须调用 res.json() 等返回响应,否则请求将一直被挂起。

其他 HTTP 请求

Nest 提供所有标准的 HTTP 方法装饰器。

以及一个 @All() 装饰器处理所有 HTTP 方法。

import { Controller, Get, Post } from '@nestjs/common'

@Controller('first')
export class FirstController {
  @Post()
  create(): string {
    return 'c'
  }

  @Get()
  findAll(): string {
  return 'd'
  }
}

路由通配

仅支持 Express 支持的路由通配符。

?,+,*,() 都可以使用,对应是正则表达式的子集。

连字符 - 和 . 由基于字符串的路径逐字解释。

@Get('a*b') // 将支持 acb, a_b, 等
findAll() {
  return ''
}

状态码

import { Controller, Get, Post, HttpCode } from '@nestjs/common'

@Controller('first')
export class FirstController {
  @Post()
  @HttpCode(204) // 更改 HTTP 状态码
  create(): string {
    return 'c'
  }

  @Get()
  findAll(): string {
  return 'd'
  }
}

Header 标头

import { Controller, Get, Post, HttpCode, Header } from '@nestjs/common'

@Controller('first')
export class FirstController {
  @Post()
  @HttpCode(204) // 更改 HTTP 状态码
  @Header('Cache-Control', 'none') // 更改响应标头
  create(): string {
    return 'c'
  }

  @Get()
  findAll(): string {
  return 'd'
  }
}

重定向

import { Controller, Get, Post, HttpCode, Header, Redirect, HttpRedirectResponse } from '@nestjs/common'

@Controller('first')
export class FirstController {
  @Post()
  @HttpCode(204) // 更改 HTTP 状态码
  @Header('Cache-Control', 'none') // 更改响应标头
  create(): string {
    return 'c'
  }

  @Get()
  // 接收 url, code
  // 两者皆可选 默认 302
  @Redirect('https://example.com', 301)
  findAll(): HttpRedirectResponse  {
  return {
      // 动态重定向
     url: 'https://example.com/v5'
    }
  }
}

路由参数

import { Controller, Get, Post, Param } from '@nestjs/common'

@Controller('first')
export class FirstController {
  @Post()
  create(): string {
    return 'c'
  }

  @Get(':id')
  // 参数路由应该在静态路径后声明
  // 使得可以防止参数化路由拦截静态路径流量
  findAll(@Param() params: any ): string {
  return params.id
  }


  @Get('find/:id')
  findAll(@Param('id') id: string): string {
  return id
  }
}

子域路由

@Controller 可以采用 host 选项要求传入请求的 HTTP 主机匹配特定值。

// Fastify 缺乏嵌套路由支持 因此需要使用默认的 Express 适配器
// host 可以捕获动态值
@Controller({ host: ':account.example.com' })
export class AdminController {
  @Get()
  index(@HostParam('account') account: string): string {
    return account
  }
}

作用域

Nest 中几乎所有内容都是传入请求之间共享的。

NodeJS 不遵循请求/响应多线程无状态模型,该模型每个请求都由单独线程处理。

使用单实例对于应用来说是完全安全的。

某些边缘情况可能会不一样。比如 GQL。

异步

Nest 支持 async/await

每个异步函数都会返回 Promise。使得可以返回一个 Nest 能够自行解析的延迟值。

@Get()
async find(): Promise<string[]> {
  return []
}

// or

// 使用 RxJS
@Get()
find(): Observable<string[]> {
 return of([])
}

请求负载

使用 @Body() 装饰器让 POST 路由获取客户端参数。

如果是 TS,则首先需要确定 DTO (Data Transfer Object) 格式。

可以使用 TS 接口或简单的类确定。推荐使用的是类。

因为类是 ES6 一部分,会在编译后的 JS 中保存,TS 接口则会在转换过程中被移除,使得 Nest 在运行时无法引用。

// create-first.dto.ts
export class CreateFirstDto {
  name: string
}

// first.controller.ts
@Post()
async create(@Body() CreateFirstDto: CreateFirstDto) {
  // TODO...
}

使用控制器

虽然定义了控制器,但 Nest 仍然不知道控制器的存在,也不会创建它。

控制器属于模块,因此需要在 @Module() 装饰器使用。

// app.module.ts
import { Module } from '@nestjs/common'
import { FirstController } from './first/first.controller'

@Module({
 controllers: [FirstController]
})
export class AppModule {}

提供器

是 Nest 中的一个基本概念。

许多基本的 Nest 类可以被视为提供器,比如服务,存储库,工厂,助手等。

提供器的主要思想是它可以作为依赖注入。

这意味着对象之间可以创建各种关系。且功能可以很大程度上委托给 Nest 运行时。

建议遵循 SOLID 原则!

服务

可以使用 nest g service first 创建服务。

import { Injectable } from '@nestjs/common'
import { First } from '@/types' // 使用一个接口

// 附加元数据
// 该元数据声明 此类 可由 NestIoC 管理
@Injectable()
export class FirstService {
  private readonly f: First[] = []

  create (f: First) {
    this.f.push(f)
  }

  find(): First[] {
    return this.f
  }
}

使用服务

// first.controller.ts
import { Controller, Post } from '@nestjs/common'
import { FirstService } from './first.service'

@Controller('first')
export class FirstController {
  constructor (private firstService: FirstService) {}

  @Post()
  create(): string {
    return 'c'
  }
}

依赖注入

Nest 是围绕依赖注入这个强大的设计模式构建的。

由于 TS 的功能,管理依赖非常容易,因为它们按照类型解析。

class FC {
  // Nest 将通过创建并返回 FS 的实例来解析 cS
 constructor (private fS: FS) {}
}

作用域

提供者通常具备与应用同步的生命周期,启动应用时,会解析每个依赖,使得必须实例化每个提供者。

同样的,当应用关闭时,每个提供器都会被销毁。

也有方法使得提供者生命周期限定在请求范围内。

自定义提供器

Nest 内置了一个控制反转 (IoC) 容器, 可以解决提供器之间的关系。

可选提供器

有时,有可能有不一定需要解决的依赖。在没有传递任何内容的情况下,需要使用默认值。

此情况下,依赖可选。

使用 @Optional() 装饰器在构造函数声明提供器可选。


import { Injectable, Optional, Inject } from '@nestjs/common';

@Injectable()
export class HttpService<T> {
  // 使用了自定义提供者 这就是使用 HTTP_OPTIONS 自定义 TOKEN 的原因
  constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}
}

基于属性的注入

在某些特殊情况下,基于属性的注入很有用。

比如顶层类依赖于一个或多个提供器,则通过从构造函数在子类中调用 super(), 将它们一路向上传递可能会非常乏味。

因此,可以在属性级别使用 @Inject() 装饰器。

如果提供者没有继承自另一个类,则应该使用基于构造函数的注入。

@Injectable()
export class HTTPService<T> {
  @Inject('HTTP_OPTIONS')
  private readonly HTTPClient: T
}

注册提供器

和使用控制器一样,需要注册提供器服务。使得可以注入。

通过编辑模块文件将服务添加。

// app.module.ts
import { Module } from '@nestjs/common'
import { FirstController } from './first/first.controller'
import { FirstService } from './first/first.service'

@Module({
 controllers: [FirstController],
  providers: [FirstService]
})
export class AppModule {}

模块

是使用 @Module() 装饰器的类。

提供 Nest 用于组织应用结构的元数据。


每个应用都至少需要有一个模块作为根模块。

根模块会作为 Nest 构建应用的起点,也就是 Nest 用于解析模块和提供器关系及依赖的内部的数据结构。

虽然非常小的应用可能只有根模块。但这并非典型情况。

@Module() 采用单个对象,属性:

  • providers

    由 Nest 注入器实例化且在至少在该模块共享的提供程序列表。

  • controllers

    模块中定义的必须实例化的控制器列表。

  • imports

    此模块需要的提供程序的导入模块列表。

  • exports

    此模块提供的提供器子集可以在导入这个模块的其他模块中可用。可以使用提供器本身或其 Token。


模块默认封装提供器,使得不可能注入既不属于当前模块,也不从导入模块声明的提供程序。

因此,可以将模块导出的提供程序视为模块的公共接口或 API。


功能模块

使用 nest g module [name] 可创建模块。

// first.module.ts
import { Module } from '@nestjs/common'
import { FirstController } from './first.controller'
import { FirstService } from './first.service'

@Module({
 controllers: [FirstController],
  providers: [FirstService]
})
export class FirstModule {}

// app.module.ts
import { Module } from '@nestjs/common'
import { FirstModule } from './first/first.module'

@Module({
  imports: [FirstModule]
})
export class AppModule {}

共享模块

默认情况下,模块是单例的。因此可以在多个模块之间共享任何提供者的同一实例。

// first.module.ts
import { Module } from '@nestjs/common'
import { FirstController } from './first.controller'
import { FirstService } from './first.service'

@Module({
 controllers: [FirstController],
  providers: [FirstService],
  exports: [FirstService] // 导出 FirstService 让任何导入了 FirstModule 的模块都可以使用
})
export class FirstModule {}

重新导出

模块可以导出内部提供者,也可以重新导出导入的模块。

@Module({
  imports: [CommonModule],
  exports: [CommonModule],
})
export class CoreModule {}

模块的依赖注入

模块也可以注入提供程序。比如用于配置目的。

但是由于循环依赖,模块类本身不能作为提供器注入。

@Module({
  imports: [CommonModule],
  exports: [CommonModule],
})
export class CoreModule {
  constructor (private CommonModule: CommonModule) {}
}

全局模块

Nest 将程序封装在模块作用域内,如果不首先导入模块,则无法使用模块的提供者。

可以使用 @Global() 可以将模块全局化,全局模块应该仅注册一次。通常由根模块或核心模块注册。

// first.module.ts
import { Module, Global } from '@nestjs/common'
import { FirstController } from './first.controller'
import { FirstService } from './first.service'

@Global()
@Module({
 controllers: [FirstController],
  providers: [FirstService],
  exports: [FirstService]
})
export class FirstModule {}

动态模块

使得可以轻松创建可自定义的模块。

这些模块可以动态注册和提供配置程序。

// 动态模块例子
import { Module, DynamicModule } from '@nestjs/common';
import { createDatabaseProviders } from './database.providers';
import { Connection } from './connection.provider';

@Module({
  providers: [Connection],
  exports: [Connection],
})
export class DatabaseModule {
  // forRoot() 方法可以同步或异步返回动态模块
  static forRoot(entities = [], options?): DynamicModule {
    const providers = createDatabaseProviders(options, entities);
    return {
      // 全局作用域注册动态模块 则使用 global: true 属性
      module: DatabaseModule,
      providers: providers,
      exports: providers,
    };
  }
}

使用动态模块

import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { User } from './users/entities/user.entity';

@Module({
  imports: [DatabaseModule.forRoot([User])],
  exports: [DatabaseModule], // 重新导出可以省略 forRoot 调用
})
export class AppModule {}

nest 简单入门
http://localhost:8080/archives/7e6faee2-7847-47e2-8621-85246a424e6e
作者
inksha
发布于
2024年09月22日
许可协议