引子
Express很好用,但它太自由了,每个人的代码组织结构都大不相同,应用一旦变大或需要不断扩展,依赖关系就很容易变得混乱。
基于Node的后端框架很多,但大多都缺少了架构。良好的架构设计可以更好的组装代码,模块封装和依赖管理清晰,实现低耦合高内聚,易于修改和维护。
IOC控制反转和DI依赖注入是NestJS的核心,并且实现了一个更抽象的模块化的AOP架构。
依赖倒置原则
如果类A依赖类B,传统面向对象中,在类A内直接创建类B的实例,类A和类B直接耦合在一起,类A作为高层类直接依赖了低层类B,违反了依赖倒置原则。
依赖倒置原则:
- 高层模块不应该依赖底层模块,二者都应该依赖抽象(例如接口)。
- 抽象不应该依赖细节,细节(具体实现)应该依赖抽象。
**例子:**User写代码完成今天的工作需要一台win电脑,然后开机(open),进行工作(work)
class WinComputer { open() { console.log('win open') }}class User { computer: WinComputer = new WinComputer() work() { this.computer.open() }}user.work() // win open如果User现在需要一台Linux电脑,就需要修改User类的实现,但这样违反了开闭原则,即对扩展开放,对修改关闭。
解决这个问题也很简单,抽象一个Computer类,实现了Computer的类都可以作为User的电脑,在实例化时传入,就不用修改User类的实现了。
abstract class Computer { open() { }}class WinComputer implements Computer { open() { console.log('win open') }}class LinuxComputer implements Computer { open() { console.log('linux open') }}class User { computer: Computer constructor(computer: Computer) { this.computer = computer } work() { this.computer.open() }}const user = new User(new LinuxComputer())user.work() // linux open这样貌似没问题了,User只管要电脑,电脑在实例化时传入,换电脑也只需要修改实例化时的参数,而不需要修改User类的实现。
但,很多时候User所依赖的东西不止电脑,还有键盘、鼠标、显示器等等,如果都要在实例化时传入,并且使用一个成员去接收,可想而知代码会很臃肿、复杂。应该有一个专门的管理员来提供这些设备,User仅需要根据需求向管理员索取设备。
控制反转就是解决这个问题的。
IOC控制反转
控制反转(Inversion of Control,IOC)是面向对象的一种设计模式,完全遵守依赖倒置原则,目的是解耦。
- 控制:对程序流程(如生命周期)的控制,以及对一切资源(包括但不限于对象)的控制。
- 反转:将控制权从原来的对象本身反转到了外部容器(IOC容器),即由外部容器来控制程序流程和资源。
IOC所带来的是系统控制权的转移,将原来分散在各个对象内部的控制权,转移到了外部容器统一管理和控制。
IOC只解释了应该怎么去做才是最佳实践,但并没有规定应该怎么做,实现的方式有很多,如依赖注入、依赖查找、服务定位器等。
Nest、Spring等主流框架使用的是依赖注入。
在可维护性较好的架构中,实现一个新的需求时,往往只需要新增代码,而不需要修改任何现有的代码。
DI依赖注入
**依赖注入(Dependency Injection,DI)**是实现控制反转的一种方式。
其核心是实现IOC容器,由容器动态管理对象的创建和依赖关系。 将一个对象所依赖的其他对象通过构造函数、方法参数或者属性注入到该对象中,被动地接受依赖对象,而不是在该对象内部直接创建这些依赖对象。
一个最简单的IOC容器实现如下:
- 一个Map对象,用于存储依赖名和类的映射关系
- 一个注册方法,用于注册可依赖项
- 一个解析方法,用于解析依赖项,实例化类并返回
// IOC容器class Container { private dependencies: Map<string, any> = new Map()
// 注册依赖 register(key: string, value: any, ...args: any[]) { this.dependencies.set(key, { value, args }) }
// 解析依赖 resolve<T = any>(key: string): T { if (!this.dependencies.has(key)) { throw new Error('未注册该依赖') } const { value, args } = this.dependencies.get(key) // 实例化并返回 return Reflect.construct(value, args) }}
abstract class Computer { open() { }}class WinComputer implements Computer { open() { console.log('win open') }}class LinuxComputer implements Computer { open() { console.log('linux open') }}
const container = new Container()// 注册依赖container.register('computer', WinComputer)// container.register('computer', LinuxComputer)
class User { computer: Computer constructor() { // 依赖注入 this.computer = container.resolve<Computer>('computer') } work() { this.computer.open() }}const user = new User()user.work() // win openUser只需要一个能open的电脑来完成work,不需要关心电脑是怎么来的,只需要向容器索取即可,容器在代码运行时动态地将依赖注入到User中。当需要提供Linux电脑时,只需要改为注册LinuxComputer即可。
参考: 万字长文,讲透彻 NestJS 的设计模式 依赖注入和控制反转的理解,写的太好了。 Nest.js入门 —— 控制反转与依赖注入(一) 学习 Nestjs 前,你需要了解什么是依赖注入(原理详解)
装饰器
装饰器就是一个函数,可以注入到类、方法、属性、参数,对象上,扩展其功能,增加了代码的可读性,清晰地表达了意图。
@nestjs/cli
Nestjs 是一个用于构建高效、可扩展的 Node.js 服务器端应用程序开发框架,完全支持TypeScript
底层使用了express(默认)和Fastify,在这些框架之上提供了一定程度的抽象,同时也将其 API 直接暴露给开发人员,可以轻松使用每个平台的无数第三方模块。
使用Nest CLI快速创建项目
npm i -g @nestjs/clinest new project-name [--strict]如果打开文件报错Delete ␍ eslint,需要将endOfLine设为auto,
详见:error Delete ·· prettier/prettier #219
rules: { "prettier/prettier": ["warn", {"endOfLine": "auto"}],}// 或修改.prettierrc,需要重启vscode才生效"endOfLine": "auto"项目结构
一个基本的项目结构:
src app.controller.spec.ts // 测试文件,用于编写和执行测试,以确保代码的可靠性和正确性 app.controller.ts // 控制层,处理http请求、控制路由和调用业务层的方法 app.module.ts // 根模块,用于处理模块之间的引用和共享,它将AppController和AppService通过@Module进行注入 app.service.ts // 业务层,封装与业务相关的逻辑,比如对数据库的CRUD main.ts // 入口文件,使用NestFactory创建Nest应用程序实例nest-cli.json // Nest CLI配置文件常见后缀解释:
*.middleware.ts 中间件*.controller.ts 控制器*.decorator.ts 自定义装饰器*.entity.ts 数据对象实例(typeorm)*.interface.ts 接口*.module.ts Nest模块*.service.ts Nest服务对象*.pipe.ts Nest管道对象*.dto.ts 数据传输对象*.spec.ts 单元测试文件nest-cli.json Nest CLI配置文件
{ // 指定 JSON 文件的模式(schema),用于验证文件的结构 "$schema": "https://json.schemastore.org/nest-cli", // 指定 Nest CLI 在生成项目文件和模块时要使用的 schematics 集合。 // 这告诉 Nest CLI 使用 @nestjs/schematics 集合中的原理图来生成代码。 "collection": "@nestjs/schematics", // 项目源代码的根目录 "sourceRoot": "src", // 针对 TypeScript 编译器的选项配置 "compilerOptions": { "deleteOutDir": true }}main.ts 入口文件,使用NestFactory创建Nest应用程序实例,并监听端口,启动服务
import { NestFactory } from '@nestjs/core';import { AppModule } from './app.module';async function bootstrap() { // NestFactory创建Nest应用程序实例,将AppModule作为根模块传入,相当于app.vue const app = await NestFactory.create(AppModule); // 监听端口,启动服务 await app.listen(3000);}bootstrap();app.module.ts 根模块,用于处理模块之间的引用和共享,它将AppController和AppService通过@Module进行注入
每个模块都是一个由@Module()装饰器注释的类,模块间的关系由@Module()装饰器中携带的所有元数据描述。每个 Nest 应用程序至少有一个模块,即根模块(app.module.ts)。
import { Module } from '@nestjs/common';import { AppController } from './app.controller';import { AppService } from './app.service';// @Module类装饰器,用于定义模块@Module({ imports: [], // 导入模块的列表,这些模块导出了此模块中所需提供者 controllers: [AppController], // 控制器 providers: [AppService], // 由 Nest 注入器实例化的提供者,并且可以至少在整个模块中共享})export class AppModule {}app.controller.ts 控制层,处理http请求、控制路由和调用业务层的方法,也可以写一些单一不可复用的业务逻辑
import { Controller, Get } from '@nestjs/common';import { AppService } from './app.service';// @Controller类装饰器,用于定义控制器@Controller() // 可传入路由前缀export class AppController { // 构造函数注入AppService constructor(private readonly appService: AppService) {} // @Get方法装饰器,用于创建路由 @Get() // 可传入路由路径 getHello(): string { // 调用AppService业务层的方法 return this.appService.getHello(); }}app.service.ts 业务层,封装与业务相关的可复用的逻辑,比如对数据库的CRUD
import { Injectable } from '@nestjs/common';// @Injectable类装饰器,用于定义提供者@Injectable()export class AppService { getHello(): string { return 'Hello World!'; }}常见命令
1、创建项目 new(n) [options] [name]
--directory 指定目标目录-d或--dry-run 不输出创建过程中的报告-g或--skip-git 不要初始化git仓库(默认是会在项目创建git仓库)-s或--skip-install 不要安装依赖哭-p或--package-manager [name] 指定包管理工具-l或--language [lang] 指定语言JS或者TS-c或--collection [name] 用特定的架构生成项目2、构建项目 build [options] [app] 默认会将TS文件构建到项目的dist目录中
-c或--config [path] 用cli构建时特定的配置文件-p或--path [path] tsconfig配置文件-w或--watch 实时重加载,观察模式--watchAssets 观察非ts文件模式--webpackPath [path] webpack的配置文件--tsc 使用tsc编译3、运行项目 start [options] [app]
-c或--config [path] 用cli构建时特定的配置文件-p或--path [path] tsconfig配置文件-w或--watch 实时重加载,观察模式--watchAssets 观察非ts文件模式-d或--debug [hostport] 调试模式--webpack用webpack编译--webpackPath [path] webpack的配置文件--tsc 使用tsc编译-e或--exec [binary] 以二进制运行(默认用node)--preserveWatchOutput tsc的观察模式4、更新项目 update(u) [options] 更新当前项目的依赖组件
-f或--force 强制重新安装依赖-t或--tag 升级被打上(latest | beta | rc | next tag)的组件5、生成 generate(g) [options] <schematic> [name] [path] 创建一个Nest的元素
6、其它
add [options] <library> 添加对外部库的支持
info(i) 获取当前Nest项目的详细情况
generate命令
nest generate命令可以快速生成项目、模块、控制器、服务、管道、拦截器、中间件等文件的基本结构
nest --help 查看nestjs所有的命令
Usage: nest <command> [options]
Options: -v, --version Output the current version. -h, --help Output usage information.
Commands: new|n [options] [name] Generate Nest application. build [options] [app] Build Nest application. start [options] [app] Run Nest application. info|i Display Nest project details. add [options] <library> Adds support for an external library to your project. generate|g [options] <schematic> [name] [path] Generate a Nest element. Schematics available on @nestjs/schematics collection: ┌───────────────┬─────────────┬──────────────────────────────────────────────┐ │ name │ alias │ description │ │ application │ application │ Generate a new application workspace │ │ class │ cl │ Generate a new class │ │ configuration │ config │ Generate a CLI configuration file │ │ controller │ co │ Generate a controller declaration │ │ decorator │ d │ Generate a custom decorator │ │ filter │ f │ Generate a filter declaration │ │ gateway │ ga │ Generate a gateway declaration │ │ guard │ gu │ Generate a guard declaration │ │ interceptor │ itc │ Generate an interceptor declaration │ │ interface │ itf │ Generate an interface │ │ library │ lib │ Generate a new library within a monorepo │ │ middleware │ mi │ Generate a middleware declaration │ │ module │ mo │ Generate a module declaration │ │ pipe │ pi │ Generate a pipe declaration │ │ provider │ pr │ Generate a provider declaration │ │ resolver │ r │ Generate a GraphQL resolver declaration │ │ resource │ res │ Generate a new CRUD resource │ │ service │ s │ Generate a service declaration │ │ sub-app │ app │ Generate a new application within a monorepo │ └───────────────┴─────────────┴──────────────────────────────────────────────┘nest g mo 生成模块
它会在src目录下创建一个user目录,包含user.module.ts,然后自动在app.module.ts中导入UserModule
nest g mo user CREATE src/user/user.module.ts (85 bytes) UPDATE src/app.module.ts (391 bytes)nest g co 生成控制器
会继续在user目录下创建user.controller.ts和user.controller.spec.ts,然后自动在user.module.ts中导入UserController
nest g co user CREATE src/user/user.controller.ts (101 bytes) CREATE src/user/user.controller.spec.ts (496 bytes) UPDATE src/user/user.module.ts (170 bytes)nest g s 生成服务(提供者、业务层)
会继续在user目录下创建user.service.ts和user.service.spec.ts,然后自动在user.module.ts中导入UserService
nest g s user CREATE src/user/user.service.ts (92 bytes) CREATE src/user/user.service.spec.ts (464 bytes) UPDATE src/user/user.module.ts (249 bytes)nest g res 可以代替上面三个命令,快速生成模块的标准结构,并且可选API风格,以及是否生成CRUD结构
nest g res list ? What transport layer do you use? REST API ? Would you like to generate CRUD entry points? No CREATE src/list/list.controller.ts (211 bytes) CREATE src/list/list.controller.spec.ts (576 bytes) CREATE src/list/list.module.ts (250 bytes) CREATE src/list/list.service.ts (92 bytes) CREATE src/list/list.service.spec.ts (464 bytes) UPDATE package.json (2049 bytes) UPDATE src/app.module.ts (380 bytes)RESTful API
RESTful 既不是标准也不是协议,只是一种风格。在RESTful中,一切都被认为是资源,每个资源有对应的URL标识,即使用HTTP动词来表示对资源的操作,如GET、POST、PUT、DELETE等。
以往POST一把梭哈的风格,表示对同一资源的不同操作需要多个URL
http://localhost:8080/api/get_list?id=1http://localhost:8080/api/delete_list?id=1http://localhost:8080/api/update_list?id=1RESTful使用不同的请求方式表示对同一资源的不同操作,只需要一个URL
http://localhost:8080/api/list/1查询GET,提交POST,更新 PUT PATCH,删除 DELETE使用nest g res命令生成的控制器默认是RESTful API风格
@Controller('list') // 路由前缀export class ListController { constructor(private readonly listService: ListService) {}
@Post() // 提交 create(@Body() createListDto: CreateListDto) { return this.listService.create(createListDto); }
@Get() // 查询所有 findAll() { return this.listService.findAll(); }
@Get(':id') // 查询单个 findOne(@Param('id') id: string) { return this.listService.findOne(+id); }
@Patch(':id') // 更新单个 update(@Param('id') id: string, @Body() updateListDto: UpdateListDto) { return this.listService.update(+id, updateListDto); }
@Delete(':id') // 删除单个 remove(@Param('id') id: string) { return this.listService.remove(+id); }}版本控制
在应用迭代更新时,可能会出现接口不兼容的情况,这时就需要对接口进行版本控制,保留或丢弃旧版本的接口,增加新版本的接口。
NestJS提供了版本控制的功能,通过app.enableVersioning()开启,文档
import { NestFactory } from '@nestjs/core';import { VersioningType } from '@nestjs/common';import { AppModule } from './app.module';
async function bootstrap() { const app = await NestFactory.create(AppModule); // 开启版本控制 app.enableVersioning({ // type指定版本控制的类型,接收VersioningType枚举类型, type: VersioningType.URI, // export declare enum VersioningType { // URI = 0, // 版本将在请求的 URI 中传递(默认) // HEADER = 1, // 通过请求头控制版本 // MEDIA_TYPE = 2, // 请求的Accept标头将指定版本 // CUSTOM = 3 // 自定义控制版本 // } }); await app.listen(3000);}bootstrap();在控制器中使用@Version()指定某个接口的版本,或在@Controller()中指定整个路由的版本
@Controller({ path: 'list', // 路由前缀 // 路径中会带上v+版本号,在路由前缀之前 version: '1', // 路由版本,如:/v1/list})export class ListController { // ...... @Get() @Version('2') // 控制单个接口的版本 // http://127.0.0.1:3000/v2/list/ findAll() { return this.listService.findAll(); }
@Get(':id') // http://127.0.0.1:3000/v1/list/123456 findOne(@Param('id') id: string) { return this.listService.findOne(+id); }}Code码规范
Code码分类:
- 1xx Informational(信息状态码)接受请求正在处理
- 2xx Success(成功状态码)请求正常处理完毕
- 3xx Redirection(重定向状态码)需要附加操作已完成请求
- 4xx Client Error(客户端错误状态码)服务器无法处理请求
- 5xx Server Error(服务器错误状态码)服务器处理请求出错
常用Code码:
- 200 OK
- 201 Created 创建成功
- 301 Moved Permanently 永久重定向至新URI
- 302 Found 临时重定向,URI不变,资源实际位置已改变
- 304 Not Modified 协商缓存了
- 400 Bad Request 参数错误
- 401 Unauthorized token错误
- 403 Forbidden 验证失败,无权限访问
- 404 Not Found 接口不存在
- 500 Internal Server Error 服务端错误
- 502 Bad Gateway 上游接口有问题或者服务器问题
- 503 Service Unavailable 服务不可用
Controller控制器
控制器是添加了@Controller装饰器的类,用于处理请求、控制路由、调用服务、返回响应。
Nest将Express中的Router抽象成为Controller
@Controller有多个重载:
Controller(): ClassDecorator;Controller(options: ControllerOptions): ClassDecorator;Controller(prefix: string | string[]): ClassDecorator;当传入string时(支持通配符),表示路由前缀,可以传入string数组,表示多个路由前缀。
全局前缀由 app.setGlobalPrefix() 设置
还可以传入ControllerOptions对象,进行更多配置
interface ControllerOptions extends ScopeOptions, VersionOptions { path?: string | string[]; // 路由前缀 // 要求传入请求的 HTTP 主机匹配某个特定值,如www.example.com host?: string | RegExp | Array<string | RegExp>;}interface VersionOptions { version?: VersionValue; // 版本控制}interface ScopeOptions { scope?: Scope; // 控制器的作用域 durable?: boolean; // 将常规提供程序转变为持久提供程序}处理路径和请求方法
控制器中使用请求方法装饰器来处理不同路径下的各种类型的请求
有:@Get @Post @Put @Delete @Patch @Options @All
装饰器仅接收path参数(支持通配符),表示路径,可以传入数组,表示多个路径。默认为根路径。
若传入:开头的字符串,则表示路由参数,如@Get(':id')
Get: (path?: string | string[]) => MethodDecorator;Post: (path?: string | string[]) => MethodDecorator;// .......**注意:**路径是直接定义在装饰器上的,而与引用关系无关
处理请求
Nest提供了参数装饰器,用于获取请求体、请求头、请求参数等
@Param(key?: string)获取路由参数,相当于req.params@Query(key?: string)获取查询参数,相当于req.query@Body(key?: string)获取请求体,相当于req.body@Headers(key?: string)获取请求头,相当于req.headers@Request()、@Req()获取请求对象@HostParam(property?: string | (Type | PipeTransform)与@Param类似,但是从主机参数中获取,需要与@Controller的host选项配合使用@Session()获取session,相当于req.session@Ip()获取客户端IP,相当于req.ip
1、@Param和@Query
获取的params和query都是对象,可以传入key,获取指定的参数
@Get('hello/:id')getHello( @Param() params, @Param('id') id: string, @Query() query, @Query('a') a: string,): string { console.log(params); // { id: '123' } console.log(query); // { a: '1', b: '2' } console.log(id); // 123 console.log(a); // 1 return 'Hello World!';}2、@Body获取请求体,可选传入key,获取指定的参数
@Post('list')getList(@Body() body: object, @Body('name') name: string): object { console.log(body); // { name: 'chuckle' } console.log(name); // chuckle return body;}3、@Headers获取请求头,可选传入key,获取指定的参数
@Post('headers')getHeaders( @Headers() headers: object, @Headers('token') token: string, @Ip() ip: string,): object { console.log(token); // 123456789 console.log(ip); // ::ffff:127.0.0.1 console.log(headers); // { // "token": "123456789", // "content-length": "23", // "accept-encoding": "gzip, deflate, br", // "accept": "*/*", // "user-agent": "Thunder Client (https://www.thunderclient.com)", // "content-type": "application/json", // "host": "127.0.0.1:3000", // "connection": "close" // } return headers;}4、@Request()、@Req()获取请求对象,拿到req后,操作就和Express一样了
@Post('req/:id')getReq(@Req() req: Request): string { console.log(req.url); // /req?a=1&b=2 console.log(req.query); // { a: '1', b: '2' } console.log(req.params); // { id: '123' } console.log(req.body); // { name: 'chuckle' } console.log(req.headers); // { 'content-length': ......} return 'req';}session案例
session用于会话控制,在服务器端保存当前访问用户的相关信息,并将session_id以cookie形式返回给浏览器
Nestjs是基于Express的,所以session的使用方式与其一样,同样使用express-session中间件
npm i express-session -Snpm i @types/express-session -D在man.ts中使用express-session中间件
import * as expressSession from 'express-session';app.use( expressSession({ name: 'sid', //设置cookie的name,默认值是:connect.sid secret: 'chuckle', //参与加密的字符串(又称签名) resave: true, //是否在每次请求时重新保存session,用于重置session过期时间 rolling: true, //是否在每次请求时重新设置cookie的过期时间 // 浏览器端cookie设置 cookie: { httpOnly: true, // 开启后前端无法通过 JS 操作 maxAge: 1000 * 60, // 不仅控制cookie,也控制session的生命周期 }, }),);简单测试:
@Get('session')getSession(@Session() session) { console.log(session.username); // chuckle if (!session.username) { session.username = 'chuckle'; // 设置session,就是把用户的基本信息传进去 } return session.username;}返回响应
Nest管理响应对象,会将添加了请求方法装饰器的函数的返回值作为响应体,并自动设置MIME等信息
提供了几个方法装饰器:
@Header(key: string, value: string)设置响应头,相当于res.set()@HttpCode(code: number)设置响应码,相当于res.status()@Redirect(url: string, statusCode?: number)重定向,相当于res.redirect()@Response(option?)、@Res(option?)获取响应对象@Next()获取next函数,相当于Express中的next()@Render()渲染模板,相当于res.render()
@Get('data')@Header('name', 'chuckle')@HttpCode(500)// @Redirect('https://api.github.com', 302)getData() { return 'data';}**注意:**如果使用了@Res()获取响应对象,则会失去依赖于 Nest 标准响应处理的功能。
如:响应返回值、拦截器、处理响应对象的装饰器等,这意味着一切响应操作都需要手动完成。
@Get('data')getData(@Res() res: Response) { res.set('name', 'chuckle'); res.status(500); // res.redirect(302, 'https://api.github.com'); res.json({ data: 'data', });}导入express的类型声明以获得智能提示
import { Request, Response } from 'express';如果希望手动操作Res后,仍然能让Nest接管Res,可以向 @Res(option) 传入配置对象,只有一个属性passthrough,默认为false,表示是否将响应对象交回给Nest处理(处理管道、发送响应)
@Get('data')getData(@Res({ passthrough: true }) res: Response) { res.set('name', 'chuckle'); return { data: 'data', };}
// 当然不嫌麻烦的话,也可以像下面那样,next到下一个处理方法,也能让Nest接管响应,但没人会想这么用吧@Get('data')getData(@Res() res: Response, @Next() next) { res.set('name', 'chuckle'); next();}@Get('data')reData() { return { data: 'data', };}渲染模板案例
Nest很好的继承了Express所支持的MVC模板,例如pug、hbs、ejs,相关文档,Express框架-模板引擎ejs
npm install hbs -S # hbs模板npm install pug -S # pug模板npm install ejs -S # pug模板在main.ts中设置视图目录、使用模板引擎
**注意:**Nest的核心工厂,在创建时一定要定义一个泛型NestExpressApplication,才能使用 useStaticAssets 等方法
import { NestExpressApplication } from '@nestjs/platform-express';import { join } from 'path';async function bootstrap() { // Nest的核心工厂,在创建时一定要定义一个泛型NestExpressApplication const app = await NestFactory.create<NestExpressApplication>(AppModule); app.useStaticAssets(join(__dirname, '..', 'public')); // 静态资源目录 app.setBaseViewsDir(join(__dirname, '..', 'views')); // 视图目录 app.setViewEngine('pug'); // 使用模板引擎}创建一个index.pug模板
html head title Pug模板 body h1 #{title} p #{message}最后使用@Render()渲染模板,传入模板名,处理方法的返回值必须是一个对象,对象的属性会传递给模板的变量
@Get('pug')@Render('index')renderPug() { return { title: 'chuckle', message: 'hello world', };}如果需要动态决定渲染的模板,则需要使用express的res.render()方法。
Provider提供者
Provider是一个具有@Injectable()装饰器的类,用于提供各种服务,如CURD、http请求等业务逻辑。
许多基本的 Nest 类都可能被视为 provider,如中间件、拦截器、管道等。它们都可以依赖注入到所需的地方。
Controller只处理路由和请求,处理业务逻辑的方法,由Provider提供,Nest动态地将其所需的Provider注入。
Nest借助TS的元数据,实现了按类型进行解析的依赖注入,通过TS提供的内置元数据,运行时可以拿到如类的构造函数的参数类型、数量、位置等信息。详见:TypeScript笔记-元数据
@Injectable()只接受一个可选的对象,用于配置作用域和持久化。
Injectable(options?: InjectableOptions): ClassDecorator;type InjectableOptions = ScopeOptions;interface ScopeOptions { scope?: Scope; // 作用域 durable?: boolean; // 将常规提供程序转变为持久提供程序}简单的Provider:
import { Injectable } from '@nestjs/common';@Injectable()export class AppService { getHello(): string { return 'Hello World!'; }}Nest的依赖注入非常强大,模块中的提供者不仅可以注入到该模块的控制器中,还可以注入到在模块中一切所依赖的地方,如:应用在控制器中的过滤器、拦截器和其它提供者等。
注册与自定义名称
@Module()的providers属性用于注册提供者。提供者通常需要在模块中注册,才能注入到其他模块、控制器。
而中间件、拦截器等特殊的提供者,实现了特殊的内置接口,则拥有各自不同的使用方式。
类型列表注册提供者较为方便,无需使用@Inject()显式注入,Nest会自动注入到所需的地方
@Module({ controllers: [AppController], // 注册控制器 providers: [AppService], // 注册提供者})自定义名称:对象的provide属性指定名称,useClass属性指定类,也称为类提供者
providers: [ { provide: 'AppService', useClass: AppService, },],若自定义了名称,则需要使用@Inject(token)显式注入到所需的地方
@Controller()export class AppController { constructor(@Inject('AppService') private readonly appService: AppService) {}}值提供者
useValue 属性指定值,值可以是任意类型的,最终会直接注入到所需的地方,这对于传入一些配置信息、外部库、常量值很有用。
providers: [ { provide: 'TestValue', useValue: [1, 2, 3], },],仍然需要使用@Inject(token)显式注入
有别于构造函数注入,下面使用了属性注入,实际上两者作用相同。
@Controller()export class AppController { @Inject('TestValue') value: number[];
@Get('value') getValue(): number[] { console.log(this.value); // [ 1, 2, 3 ] return this.value; }}工厂模式
工厂函数useFactory()允许动态创建提供者,返回实际的provider。
一个简单的工厂可能不依赖于其他的提供者。更复杂的工厂可以注入其他提供者的实例来计算结果。
- 工厂函数可以接受(可选)参数。
- inject 属性接受一个提供者数组,在实例化过程中,Nest 将解析该数组并将其作为参数传递给工厂函数。
- 这两个列表应该是相关的: Nest 将从 inject 列表中以相同的顺序将实例作为参数传递给工厂函数。
import { TestService } from './test.service';providers: [ TestService, { provide: 'TestService1', // inject的必须是providers中已注册的提供者 inject: [TestService], // useFactory接收的提供者已经实例化,可以直接使用 useFactory(testService: TestService) { // 在工厂中可以做一些逻辑处理 return testService.getHello(); }, },],
@Controller()export class AppController { constructor(@Inject('TestService1') private readonly testService1: string,) {}}工厂可以是异步的,允许返回Promise,Nest会等待Promise完成后再注入
useFactory: async (testService: TestService) => { return await new Promise((resolve) => { setTimeout(() => { resolve(testService.getHello()); }, 2000); });},别名提供者
useExisting 属性指定一个已经存在的提供者,为其设置别名,提供了多种访问同一提供者的方式
providers: [ TestService, { // 为TestService设置别名TestService2 provide: 'TestService2', useExisting: TestService, },],@Controller()export class AppController { constructor(@Inject('TestService2') private readonly testService2: TestService,) {}}获取请求对象
提供者可以通过注入 REQUEST 对象来访问原始请求对象的引用,而不必从控制器中传递它。
注意:一旦在提供者中注入了 REQUEST 对象,该提供者将变为请求作用域。
import { REQUEST } from '@nestjs/core';@Injectable()export class AppService { @Inject(REQUEST) res: Request;
getHello(): string { console.log(this.res.url); // /value return 'Hello World!'; }}作用域
实例共享是 Nest 的特点,单例对于Node应用程序是完全安全的,因为Node并不遵循请求/响应多线程无状态模型。
通常需要关注Provider和Controller的作用域,它们默认是单例的,即在整个应用程序中共享一个实例,有着同应用程序一样的生命周期。但也可以通过scope配置改变其作用域。文档-注入作用域
Nest的Scope提供三种作用域(注入范围):
- 默认(
DEFAULT):提供者的单个实例在整个应用程序中共享。实例生命周期直接绑定到应用程序生命周期。一旦应用程序启动,所有的单例提供者都已实例化。默认情况下使用单例作用域。 - 请求(
REQUEST):为每个传入的请求创建提供者的新实例。在请求完成处理后,对实例进行垃圾回收。 - 瞬态(
TRANSIENT):瞬态提供者不会在消费者之间共享。每个注入临时提供者的消费者将收到一个新的专用实例。
将枚举类型 Scope 的属性传递给装饰器的 scope 选项来指定作用域。在自定义提供者的情况下,必须设置一个额外的范围属性。
@Injectable({ scope: Scope.REQUEST,})@Controller({ scope: Scope.REQUEST,})// 在自定义提供者的情况下,必须设置一个额外的范围属性。providers: [ { provide: 'AppService', useClass: AppService, scope: Scope.REQUEST, },]请求控制器
控制器通常只配置为默认或请求作用域。对于请求作用域的控制器,将为每个入站请求创建一个新实例,并在请求完成处理后进行垃圾回收。
作用域链:作用域在注入链上冒泡。依赖于请求提供者的控制器本身也将是请求作用域的,因为控制器依赖于它们完成工作。即依赖方的作用域会等于被依赖方的作用域。
性能影响: 使用请求范围的控制器将对应用程序性能产生影响。虽然Nest尝试缓存尽可能多的元数据,但仍然需要在每个入站请求上动态创建各种实例。因此,这会减慢平均响应时间、影响总体基准测试结果。
若非必要,应该保持使用默认的单例作用域。
当控制器本身或其所依赖的提供者实例化耗时较长,使用请求作用域就会极大影响性能,需要等待提供者实例化完成后才能实例化控制器去处理请求。
{ provide: 'TestService1', inject: [TestService], useFactory: async (testService: TestService) => { return await new Promise((resolve) => { setTimeout(() => { resolve(testService.getHello()); }, 2000); // 该工厂模式提供者耗时较长 }); },},模块
Nestjs采用模块化来组织应用结构,整个应用由一个根模块(Application Module)和多个功能模块共同组成。
模块是具有 @Module() 装饰器的类。装饰器提供了元数据,Nest 用它来组织应用程序结构。
Module(metadata: ModuleMetadata): ClassDecorator;export interface ModuleMetadata { imports?: Array<Type<any> | DynamicModule | Promise<DynamicModule> | ForwardReference>; controllers?: Type<any>[]; providers?: Provider[]; exports?: Array<DynamicModule | Promise<DynamicModule> | string | symbol | Provider | ForwardReference | Abstract<any> | Function>;}Provider<T = any> = | Type<any> // 类型 | ClassProvider<T> // 类 | ValueProvider<T> // 值 | FactoryProvider<T> // 工厂 | ExistingProvider<T>; // 别名@module()接受一个描述模块的ModuleMetadata对象:
- providers 由 Nest 注入器实例化的提供者,并且可以至少在整个模块中共享
- controllers 控制器的集合,由 Nest 实例化并注册的
- imports 该模块所依赖的其它模块的列表
- exports 由本模块提供的在其他模块中可用的提供者,为providers的子集。
import { Module } from '@nestjs/common';@Module({ providers: [], imports: [], controllers: [], exports: [],})export class TestModule {}作用:
- controllers和providers是一个模块的基本组成部分,将模块的功能分为了控制和服务两部分。
- imports和exports处理模块之间的关系,imports声明本模块所依赖的其他模块,exports声明本模块可向外暴露的服务(提供者)
模块共享
本模块要使用某个服务有多种方式:
- **注册提供者:**在providers注册该服务为提供者。
- **模块共享:**在imports导入依赖并暴露了该服务的模块。
同一个服务在多个模块中注册为提供者,最终会产生多个实例,不符合单例模式。 而模块共享只会产生一个实例,将该实例共享给其他模块使用,符合单例模式。
**案例:**user模块暴露UserService
import { UserService } from './user.service';@Module({ providers: [UserService], exports: [UserService], // 将UserService(实例)导出,以便其他模块使用})export class UserModule {}在任意模块中导入UserModule,即可使用UserService
import { UserModule } from './user/user.module';@Module({ imports: [UserModule],})在控制器中使用:
import { UserService } from './user/user.service';@Controller()export class AppController { constructor(private readonly userService: UserService,) {} @Get('user/all') getUser(): string { return this.userService.findAll(); }}在服务中使用:
import { UserService } from './user/user.service';@Injectable()export class AppService { constructor(private readonly userService: UserService) {} getUsers(): string { const users = this.userService.findAll(); console.log(users); return users; }}全局模块
@Global()装饰器将模块注入到全局,其它模块无需导入即可使用
**注意:**全局模块还是必须要导入一次,通常在根模块中导入
@Global()@Module({ providers: [ { provide: 'Config', useValue: { baseUrl: '/api', }, }, ], exports: ['Config'],})export class ConfigModule {}@Module({ imports: [ListModule, UserModule, ConfigModule],})export class AppModule {}@Controller()export class AppController { constructor(@Inject('Config') private readonly config,) {} @Get('config') getConfig(): string { console.log(this.config); // { baseUrl: '/api' } return this.config; }}使一切全局化并不是一个好的解决方案。全局模块可用于减少必要模板文件的数量。imports 数组仍然是使模块 API 透明的最佳方式。
动态模块
动态模块也是一个类,至少有一个返回值为 DynamicModule 的静态方法(允许异步),通过该方法可以传入若干参数,实现动态创建自定义模块。
动态模块一定是全局或共享模块,需由其它模块导入时动态配置使用。
interface DynamicModule extends ModuleMetadata { module: Type<any>; // 模块类型 global?: boolean; // 是否为全局模块}interface ModuleMetadata { imports?: Array<Type<any> | DynamicModule | Promise<DynamicModule> | ForwardReference>; controllers?: Type<any>[]; providers?: Provider[]; exports?: Array<DynamicModule | Promise<DynamicModule> | string | symbol | Provider | ForwardReference | Abstract<any> | Function>;}定义动态模块:
import { DynamicModule, Module } from '@nestjs/common';@Module({})export class ConfigModule { static forRoot(path: string): DynamicModule { return { module: ConfigModule, providers: [ { provide: 'Config', useValue: { baseUrl: '/api' + path, }, }, ], exports: ['Config'], global: true, }; }}使用动态模块:调用静态方法,传入参数
@Module({ imports: [ConfigModule.forRoot('/qx')],})扩展模块元数据
动态模块实际上只是扩展了 @Module() 中定义的基本模块元数据。
配置了模块元数据后,若不调用静态方法,直接导入,就和之前一样,使用元数据中定义。
扩展元数据的规则:覆盖同名的、保留多余的、添加新的。
@Global()@Module({ providers: [ { provide: 'Config', useValue: { baseUrl: '/api', }, }, { provide: 'Test', useValue: 'test', }, ], exports: ['Config', 'Test'],})export class ConfigModule { static forRoot(path: string): DynamicModule { return { module: ConfigModule, // 会与元数据中的providers合并 providers: [ { provide: 'Config', useValue: { baseUrl: '/api' + path, }, }, ], // 会与元数据中的exports合并 exports: ['Config'], global: true, }; }}@Module({ imports: [ConfigModule.forRoot('/qx')], // 或 imports: [ConfigModule],})@Controller()export class AppController { constructor( @Inject('Config') private readonly config, @Inject('Test') private readonly test, ) {} @Get('config') getConfig(): string { console.log(this.config); // { baseUrl: '/api' } console.log(this.test); // test return this.config; }}中间件
中间件是在路由处理程序之前调用的函数。Nest的中间件基本上等同于Express。Express-中间件
中间件规则:
- 可以执行任意代码
- 操作request和response对象
- 终止请求响应过程
- 通过next()函数将控制权交给下一个中间件或路由处理程序
- 如果不是最后一个环节,则必须执行next()来传递控制权
和Express一样,Nest的中间件可以是一个函数,但实现了NestMiddleware接口的Provider也能作为中间件
Provider
nest g mi <name> 快速创建一个Provider中间件
该命令会创建同名文件夹,包含 <name>.middleware.ts,该文件向外暴露一个实现了NestMiddleware接口的类
import { Injectable, NestMiddleware } from '@nestjs/common';@Injectable()export class LoggerMiddleware implements NestMiddleware { use(req: any, res: any, next: () => void) { console.log('这是一个中间件') next(); }}NestMiddleware 接口规定了中间件必须具有 use() 方法,该方法接收三个参数:请求对象、响应对象、next函数
interface NestMiddleware<TRequest = any, TResponse = any> { use(req: TRequest, res: TResponse, next: (error?: Error | any) => void): any;}引入Express的类型声明以获得智能提示
import { Request, Response, NextFunction } from 'express';use(req: Request, res: Response, next: NextFunction){ ... }函数式
如果一个中间件非常简单,没有成员,没有额外的方法,没有依赖关系,那么可以将其简单地写成一个函数。
export function logger(req: Request, res: Response, next: NextFunction) { console.log('这是一个中间件') next();};路由中间件
由模块控制中间件应用到哪些控制器或路由路径上。
中间件不能在 @Module() 装饰器中列出,要使用中间件的模块需要实现 NestModule 接口。
interface NestModule { configure(consumer: MiddlewareConsumer): any;}NestModule 要求模块具有 configure() 方法。该方法使用 MiddlewareConsumer 类来连接中间件到特定的路由。
interface MiddlewareConsumer { apply(...middleware: (Type<any> | Function)[]): MiddlewareConfigProxy;}MiddlewareConsumer 的 apply() 方法接收若干中间件,返回 MiddlewareConfigProxy 对象,使用该对象的方法来配置哪些路由需要应用中间件。
exclude()接收若干路由,排除这些路由,不使用中间件。forRoutes()接收若干路由,这些路由使用中间件。- 可以链式排除或注册中间件。
interface MiddlewareConfigProxy { exclude(...routes: (string | RouteInfo)[]): MiddlewareConfigProxy; forRoutes(...routes: (string | Type<any> | RouteInfo)[]): MiddlewareConsumer; // 先调用排除,再调用应用}传入的参数:
- 字符串:表示该路径需要应用中间件(或排除)
- 类型(控制器):表示该控制器的所有路由都需要应用中间件
RouteInfo:可以更细致控制路径、版本、请求方法。
interface RouteInfo { path: string; // 路径 method: RequestMethod; // 请求方法,一个枚举类型 version?: VersionValue; // 版本}中间件无论是被注册在哪个module中,其效果都是一样的。因为都是通过forRoutes和exclude方法来设定需要应用的路由。可以统一注册在根模块中,也可以分散注册。
例子:
export class ListModule implements NestModule { configure(consumer: MiddlewareConsumer) { // consumer.apply(LoggerMiddleware).forRoutes(ListController); consumer .apply(LoggerMiddleware) .exclude({ path: '(.*)', method: RequestMethod.POST, version: '1', }) .forRoutes(ListController); }}Nest中,大部分情况下,使用string作匹配时,都允许插入正则表达式语法,如 (.*) 匹配若干任意字符
全局中间件
全局中间件绑定到所有路由上,在每个请求上先于路由中间件执行。
在 main.ts 中使用 app.use() 注册全局中间件。
function logger(req: Request, res: Response, next: NextFunction) { console.log('[全局中间件]' + req.url); next();}async function bootstrap() { const app = await NestFactory.create<NestExpressApplication>(AppModule); app.use(logger);}从中间件往后,就是Nest的一些高度封装的功能了,如拦截器、管道、守卫、过滤器等,大量使用了Rxjs的API
评论