Nest(NestJS)基础复习 - 速通版 02
- 2026-10-13 06:09:15
1. Nest 装饰器原理
装饰器 =一个普通函数,在类定义的那一刻自动执行,接收被装饰的目标(类、方法、属性、参数),然后给它加标记、加元数据、或改它的行为。
1.1)类装饰器
@Decorator 只是语法糖,等价于"在类定义完后立刻调用 Decorator(Target)"。
类装饰器接收一个参数:类构造函数本身。
// ① 写一个装饰器函数function Component(target) {console.log('装饰器执行了!收到的类是:', target.name);target.__role = 'component'; // 给类加个静态属性(标记)}// ② 用 @ 挂到类上@Componentclass UserService {}// ③ 运行时输出:// 装饰器执行了!收到的类是:UserServiceconsole.log(UserService.__role); // 'component'
@Component 在 class UserService 定义时就执行了,不是 new 的时候。
它收到的 target 就是 UserService 这个类本身(构造函数)。
可以在 target 上随便加属性——这就是"NestJS 靠装饰器存元数据"的原理
1.2)装饰器工厂:带参数的装饰器
@Controller('users') 里那个 'users' 是怎么传进去的?——装饰器外面再包一层函数。
// 装饰器工厂:外层函数接收参数,返回真正的装饰器函数function Controller(prefix) {console.log('工厂被调用,prefix =', prefix);// 返回真正的装饰器return function(target) {console.log('装饰器执行,给', target.name, '加路由前缀', prefix);target.__prefix = prefix;};}@Controller('users') // 先调工厂('users'),拿到真正的装饰器class UserController {}// 输出:// 工厂被调用,prefix = users// 装饰器执行,给 UserController 加路由前缀 usersconsole.log(UserController.__prefix); // 'users'
执行顺序:@Controller('users') = 先执行 Controller('users')(工厂)→ 拿到返回的装饰器函数 → 再把它作用到类上。
1.3) 方法装饰器:能改方法行为
方法装饰器收到 3 个参数:目标对象、方法名、属性描述符。
function Log(target, key, descriptor) {const original = descriptor.value; // 拿到原方法descriptor.value = function(...args) {console.log(`调用 ${key},参数:`, args);const result = original.apply(this, args); // 调原方法console.log(`${key} 返回:`, result);return result;};}class Calculator {@Logadd(a, b) { return a + b; }}const c = new Calculator();c.add(1, 2);// 输出:// 调用 add,参数:[1, 2]// add 返回:3
这就是 NestJS 拦截器/守卫的底层:方法装饰器能包住原方法,在前后加逻辑。NestJS 的 @Get()、@UseGuards() 本质都是在类/方法上写元数据,运行时 Nest 再根据这些元数据决定怎么处理请求。
1.4) 属性装饰器 & 参数装饰器
// 属性装饰器:目标.prototype, 属性名function Max(limit) {return function(target, key) {// 把校验规则记到类上const rules = target.__rules || (target.__rules = {});rules[key] = { max: limit };};}// 参数装饰器:第三个参数是参数索引function Param(name) {return function(target, methodName, paramIndex) {// 先记录/存储下来,然后nest等http请求过来时,// 再匹配对应的参数拿到对应的值console.log(`参数装饰器:${methodName} 的第 ${paramIndex} 个参数来自 ${name}`);};}class UserController {@Max(100)name = '';getUser(@Param('id') id) {}}// 输出:参数装饰器:getUser 的第 0 个参数来自 id
1.5)总结
@Module()@Controller() | ||
@Get()@UseGuards() | ||
@MaxLength() | ||
@Param()@Body()@Headers() |
1.6)NestJS 的装饰器到底干了什么?
以 @Module() 和 @Injectable() 为例:
@Injectable()classUserService{constructor(private db: Database) {}}
↓ Nest 在背后做了什么
1. Injectable() 给 UserService 打上标记:"这个类可以被 DI 容器管理"
2. TS 编译时把构造函数的参数类型(Database)保存到 UserService 上
3. Nest 启动时扫描所有 @Module,看到 providers: [UserService]
4. DI 容器读 UserService 的参数类型 → 发现要 Database → 先 new Database()
5. 再 new UserService(databaseInstance),缓存为单例
关键:步骤 2 依赖 emitDecoratorMetadata 这个 TS 编译选项——它让 TS 自动把"构造函数参数类型"写入元数据。没有这个选项,Nest 不知道要注入什么。
1.7) 手写一个迷你 DI/IoC 容器
用原生 JS 实现 NestJS 的核心:装饰器标记 + 容器自动注入。
// ===== ① 全局容器 =====const container = new Map();// ===== ② @Injectable() 装饰器:把类注册到容器 =====function Injectable() {return function(Target) {container.set(Target, null); // 先占位,实例化时再填};}// ===== ③ 业务类 =====class Database {query(sql) { return `执行 ${sql}`; }}@Injectable()class UserService {constructor(db) { // 真实 Nest 里靠 TS 元数据自动识别 db: Databasethis.db = db;}getUser() { return this.db.query('SELECT * FROM users'); }}// ===== ④ 手动注册 Database(真实 Nest 里在 @Module providers 里)=====container.set(Database, new Database());// ===== ⑤ 解析容器:递归创建所有依赖 =====function resolve(Target) {if (container.get(Target)) return container.get(Target); // 已实例化,返回单例// 真实 Nest:这里读构造函数参数类型元数据,自动决定依赖谁// 我们简化:手动写死 UserService 依赖 Databaseconst deps = Target === UserService ? [Database] : [];const depInstances = deps.map(resolve);const instance = new Target(...depInstances);container.set(Target, instance);return instance;}// ===== ⑥ 使用 =====const userService = resolve(UserService);console.log(userService.getUser());// 输出:执行 SELECT * FROM users
@Injectable()只是往 Map 里塞了个占位符。 resolve()递归地 new 依赖,缓存为单例。 NestJS 多出来的部分:用 TS 元数据(@Module()装饰器在providers中存储)自动发现 构造函数参数类型(不用手写 deps)、模块作用域、循环依赖检测。
1.8 )它做三件事:
标记: 给类/方法打标签(@Injectable、@Controller)
存元数据: 把路由前缀、参数来源、校验规则写到类上
改行为: 包住原方法,前后加逻辑(@Log、拦截器)
NestJS 在启动时扫描这些元数据,决定怎么路由、怎么注入依赖、怎么执行守卫和管道。
1.9)常见误区:
❌ 装饰器在 new Class()时执行——错,在 class 定义时就执行了。❌ 装饰器是"注解"——它是真实函数,可以执行任意逻辑。 ❌ @Controller('users')里的 'users' 是字符串传参——对,但它先经工厂函数返回真正的装饰器。
TS 装饰器代码执行顺序,不会影响 Nest 请求流水线的执行顺序!这里要分开两件事:
- 编译阶段:TS 装饰器(元数据写入)
只是打标记、存元数据 - 运行阶段:HTTP 请求到达时
Nest 读取元数据,固定按请求链路执行 Pipe / Guard / Interceptor,和装饰器书写先后无关
注意装饰器的执行顺序:1) 装饰器表达式(工厂函数)从上到下求值;2)装饰器函数调用从下到上。
2. 中间件 Middleware
在守卫之前执行,处理更"底层"的事:
CORS、请求体解析、静态资源、简单的请求日志。
类中间件必须实现 NestMiddleware 接口(use() 方法)。
// src\fundamentals\common\middleware\logger.middleware.tsimport { Injectable, Logger, NestMiddleware } from '@nestjs/common'import { Request, Response } from 'express'@Injectable()export class LoggerMiddleware implements NestMiddleware {use(req: Request, res: Response, next: () => void) {const { method, path } = reqLogger.log(`${method}${path}`)next()}}// src\app.module.ts// 在模块里配置(只能函数式配置在模块,或用 configure())export class AppModule {configure(consumer: MiddlewareConsumer) {// 为所有路由应用 LoggerMiddleware 中间件,排除 hello 路由的 POST 请求, 其他路由都应用 LoggerMiddleware 中间件consumer.apply(LoggerMiddleware).exclude({ path: 'hello', method: RequestMethod.POST }).forRoutes('*')}}
3. 守卫 Guard:认证 & 授权
"请求能不能进来?" 返回 true 放行,false/抛错拒绝。
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';@Injectable()export class AuthGuard implements CanActivate {canActivate(context: ExecutionContext): boolean {const req = context.switchToHttp().getRequest();const token = req.headers['authorization'];if (!token) throw new UnauthorizedException('未登录');return true;}}
// 用法:控制器级 / 方法级 / 全局@UseGuards(AuthGuard)@Controller('users')export classUserController{}// 全局守卫(main.ts 或 provider 注册)app.useGlobalGuards(new AuthGuard());
守卫 vs 中间件:守卫能拿到 ExecutionContext(含路由元数据),能用 Reflector 读自定义装饰器(如 @Roles('admin'))做角色鉴权——中间件做不到。这是守卫存在的最大理由。
4. 管道 Pipe:校验 + 转换
作用有两个:① 转换(string → number);② 校验(不合法直接抛 400)。
import {Controller, Post, Body, UsePipes,ValidationPipe, ParseIntPipe} from '@nestjs/common';import { IsString, IsEmail, MinLength } from 'class-validator';export class CreateUserDto {@IsString()name: string;@IsEmail()email: string;@MinLength(6)password: string;}@Controller('users')export class UserController {// 局部管道:只对这个 handler 生效@Post()@UsePipes(new ValidationPipe())create(@Body() dto: CreateUserDto) { ... }// 参数级管道:把 '1' 转成数字 1@Get(':id')findOne(@Param('id', ParseIntPipe) id: number) { ... }}
5. 控制器
controller 前面可能还有请求拦截器,然后再到 控制器
import { Controller, Get, Post, Body, Param, Query, Headers, HttpCode } from '@nestjs/common';@Controller('users') // 路由前缀:/usersexport class UserController {@Get() // GET /usersfindAll(@Query() query: any) { ... }@Get(':id') // GET /users/1findOne(@Param('id') id: string) { ... }@Post() // POST /users@HttpCode(201)create(@Body() body: CreateUserDto) { ... }@Get('header')readHeader(@Headers('authorization') auth: string) { ... }}
参数装饰器速记
@Body() | |
@Param('id') | |
@Query('page') | |
@Headers('token') | |
@Req() / @Res() |
参数装饰器自己拿不到任何请求数据。它只是在类定义那一刻,把"第 N 个参数来自 req 的哪个字段"记到元数据里;等真实请求到来时,Nest 才读这条笔记、从 req 里取值、拼成参数数组传给方法。
参数装饰器的签名:只能"记",不能"改"
参数装饰器收到 3 个参数,注意它没有返回值、没有描述符——它天生就不是用来改行为的,只是用来记录信息:
function Param(data) {// 返回真正的参数装饰器return function (target, propertyKey, parameterIndex) {// target : 类的 prototype(UserController.prototype)// propertyKey : 方法名('getUser')// parameterIndex: 参数下标(0 表示第 0 个参数)console.log(`记录:${propertyKey} 的第 ${parameterIndex} 个参数来自 params 的 ${data}`);};}class UserController {getUser(@Param('id') id) {}}// 输出:记录:getUser 的第 0 个参数来自 params 的 id
6. 拦截器 Interceptor:AOP 切面
"请求进来前、响应出去前,都能插一手"。
import {Injectable, NestInterceptor,ExecutionContext, CallHandler} from '@nestjs/common';import { map, tap } from 'rxjs';@Injectable()export class LoggingInterceptor implements NestInterceptor {intercept(context: ExecutionContext, next: CallHandler) {console.log('请求进来了');return next.handle().pipe(tap(() => console.log('响应出去了')),map((data) => ({ code: 0, data })), // 统一包装响应体);}}
经典用途:统一响应包装 {code, data, msg}、日志、缓存、超时控制(timeout())、接口耗时统计。基于 RxJS Observable,所以能拿到响应流做异步变换——这是拦截器比中间件强的点。
7. 异常过滤器 Exception Filter
"所有抛出的异常,统一在这里变成响应"。
import {ExceptionFilter, Catch, ArgumentsHost, HttpException} from '@nestjs/common';@Catch(HttpException) // 只捕获 HTTP 异常;@Catch() 捕获所有export class HttpExceptionFilter implements ExceptionFilter {catch(exception: HttpException, host: ArgumentsHost) {const ctx = host.switchToHttp();const res = ctx.getResponse();const status = exception.getStatus();res.status(status).json({code: status,message: exception.message,timestamp: new Date().toISOString(),});}}
生产用法:定义统一响应格式({code, message, timestamp}),把 HttpExceptionFilter 设为全局,所有错误都长一个样。业务代码里直接 throw new BadRequestException('xxx') 即可。
// src/main.tsimport { NestFactory } from '@nestjs/core'import { AppModule } from './app.module'import { HttpExceptionFilter } from './fundamentals/common/filters/http-exception.filter'async function bootstrap() {const app = await NestFactory.create(AppModule)// 全局过滤器app.useGlobalFilters(new HttpExceptionFilter())app.setGlobalPrefix('api')await app.listen(8080)}bootstrap()
HTTP请求进来↓Middleware(中间件)↓Guard 守卫(鉴权/权限)↓【Interceptor 拦截器:入站逻辑(intercept 里面,next.handle() 之前的代码)】↓Pipe 管道(参数解析、转换、DTO校验 ValidationPipe)↓Controller 控制器方法↓【Interceptor 拦截器:出站逻辑(next.handle()返回Observable,pipe/map处理响应)】↓ExceptionFilter 异常过滤器(任何环节抛异常都会跳到这里)↓返回HTTP响应