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。
不论使用哪一个,都会分别公开 NestExpressApplication
和 NestFastifyApplication
接口,使得可以让对象具有特定框架的方法。
一般无需指定,除非需要访问使用对应 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 {}