第一部分:认识Nest.js的「骨架」
1.1 框架定位
想象你要盖房子:
Express/Fastify = 给你砖头和水泥(基础工具)
Nest = 预制钢结构框架+施工图纸(企业级架构方案)
核心价值:用Angular风格的架构解决 Node.js 后端松散问题
1.2 核心三件套
// 典型结构示例 @Module({ controllers: [CatsController], // 接待处:处理请求 providers: [CatsService], // 工人:实际干活的 imports: [DatabaseModule] // 工具箱:其他模块的工具 }) export class CatsModule {}
1.3 依赖注入(DI)
场景:咖啡店点单
传统方式:你自己去后厨找牛奶、咖啡豆(紧密耦合)
DI模式:告诉服务员「我要拿铁」,后厨自动配齐(解耦)
代码体现:
// 错误示范:直接实例化(自己冲进后厨) classCatController{ constructor(){ this.catService =newCatService();// 耦合性强 } } // 正确做法:声明依赖(优雅点单) @Controller() classCatController{ constructor(privatereadonly catService: CatService){}// Nest自动注入 }
1.4 第一个Hello World(动手验证)
步骤分解:
安装脚手架:
npm i -g @nestjs/cli
创建项目:
nest new hello-world
核心文件结构:
src/ ├─ main.ts // 程序入口(点火装置) ├─ app.module.ts // 主模块(大脑) └─ app.controller.ts // 路由控制器(接线员)
修改app.controller.ts:
@Get() getHello(): string { return '你好,这里是自定义问候语!'; // 修改官方默认响应 }
1.5 典型目录结构
以下是一份典型的Nest.js项目目录结构及注释说明,适用于中大型项目开发:
├── src/ │ ├── common/ # 通用共享模块 │ │ ├── decorators/ # 自定义装饰器(如@Roles) │ │ ├── filters/ # 异常过滤器(全局异常处理) │ │ ├── interceptors/ # 拦截器(响应格式化/日志) │ │ └── middleware/ # 全局中间件(CORS/日志) │ │ │ ├── config/ # 配置相关 │ │ ├── app.config.ts # 应用基础配置 │ │ └── env/ # 环境配置校验 │ │ │ ├── database/ # 数据库模块 │ │ ├── entities/ # TypeORM实体类 │ │ ├── migrations/ # 数据库迁移脚本 │ │ └── database.module.ts │ │ │ ├── auth/ # 认证授权模块 │ │ ├── strategies/ # Passport策略(JWT/Local) │ │ ├── guards/ # 守卫(角色验证) │ │ ├── dto/ # 数据传输对象(登录/注册) │ │ └── auth.module.ts │ │ │ ├── user/ # 用户模块 │ │ ├── user.controller.ts │ │ ├── user.service.ts │ │ ├── dto/ # DTO(创建/更新用户) │ │ └── user.module.ts │ │ │ ├── app.module.ts # 根模块 │ └── main.ts # 应用入口文件 │ ├── test/ # 测试目录 │ ├── e2e/ # 端到端测试 │ └── unit/ # 单元测试 │ ├── public/ # 静态资源目录 ├── uploads/ # 文件上传存储目录 │ ├── .env # 本地环境变量 ├── .env.prod # 生产环境变量 ├── docker-compose.yml # Docker容器编排 ├── Dockerfile # Docker构建配置 ├── nest-cli.json # Nest CLI配置 ├── package.json # 依赖管理 └── tsconfig.json # TypeScript配置
第二部分:控制器——你的业务「接线员」
2.1 控制器的本质
想象你有一个快递分拣中心:
包裹(请求)进入传送带
分拣机器人(控制器)根据:
目的地地址(路由路径)
包裹类型(HTTP方法)
决定送到哪个处理区(处理方法)
2.2 创建业务控制器
# 命令行生成控制器(最佳实践) nest generate controller user
生成文件结构:
src/ └─ user/ ├─ user.controller.ts ├─ user.service.ts └─ user.module.ts
2.3 路由方法实战(Restful风格)
@Controller('users')// 基础路径 /users exportclassUserController{ // GET /users/profile @Get('profile') getProfile(){ return'用户张三的个人页面'; } // POST /users/register @Post('register') registerUser(@Body() userData:any){ console.log('收到注册数据:', userData); return{ success:true, userId:123}; } // 动态路由参数 /users/orders/789 @Get('orders/:orderId') getOrder(@Param('orderId') orderId:string){ return`查询订单${orderId}的详细信息`; } }
2.4 参数获取三大招
路径参数:
@Param('id')
// GET /product/iphone-15 @Get(':model') getProduct(@Param('model') model: string) { return `正在查询${model}型号`; }
查询参数:@Query()
// GET /search?q=nestjs&page=2 @Get('search') search(@Query() params: { q: string; page: number }) { return `搜索关键词:${params.q},第${params.page}页`; }
请求体:@Body()
// POST /login @Post('login') login(@Body() credentials: { username: string; password: string }) { // 实际开发中这里会调用验证服务 return `用户${credentials.username}正在尝试登录`; }
2.5 DTO模式(重要防御层)
场景:用户提交的数据就像未安检的行李
// 创建user.dto.ts exportclassCreateUserDto{ @IsString() @MinLength(5) username:string; @IsEmail() email:string; @IsStrongPassword() password:string; } // 在控制器中使用 @Post() createUser(@Body() createUserDto: CreateUserDto){ // 自动验证通过后才会执行到这里 returnthis.userService.create(createUserDto); }
2.6 常见问题现场诊断
Q:什么时候用@Param,什么时候用@Query?
用@Param:/users/123 (标识具体资源)
用@Query:/users?role=admin (过滤条件)
Q:DTO和普通的TypeScript接口有什么区别?
接口:只是类型声明
DTO:携带验证规则,实际参与运行时校验
第三部分:服务层——你的业务「后厨团队」
3.1 服务层定位
控制器 = 服务员(接收点单)
服务层 = 厨师团队(实际烹饪)
模块 = 餐厅部门划分(中厨/西厨)
3.2 创建标准服务(用户注册流程)
# 快速生成服务(附带测试文件) nest generate service user
基础服务结构:
@Injectable()// 标记为可注入类 exportclassUserService{ // 模拟数据库 private users:any[]=[]; // 实际业务操作 asyncregister(userDto: CreateUserDto){ // 检查用户名是否重复 const exist =this.users.find(u => u.username === userDto.username); if(exist){ thrownewConflictException('用户名已存在'); } // 模拟入库操作 const newUser ={ id: Date.now(), ...userDto, createdAt:newDate() }; this.users.push(newUser); return{ success:true, data: _.omit(newUser,['password'])// 使用lodash过滤敏感字段 }; } }
3.3 控制器与服务联动
@Controller('users') exportclassUserController{ // 依赖注入服务实例 constructor(privatereadonly userService: UserService){} @Post('register') asyncregister(@Body() dto: CreateUserDto){ // 控制器只做参数传递 returnawaitthis.userService.register(dto); } }
3.4 模块化拆分(数据库模块案例)
// database.module.ts @Module({ providers:[ { provide:'DATABASE_CONNECTION', useFactory:async()=>{ returnawaitcreateConnection({ type:'mysql', host:'localhost', port:3306, username:'root', password:'root', database:'test' }); } } ], exports:['DATABASE_CONNECTION']// 暴露给其他模块 }) exportclassDatabaseModule{} // 在用户模块引入 @Module({ imports:[DatabaseModule],// 导入数据库工具 controllers:[UserController], providers:[UserService] }) exportclassUserModule{}
3.5 异常处理艺术(错误分类)
// 自定义业务异常 exportclassInsufficientBalanceExceptionextendsHttpException{ constructor(){ super('账户余额不足',402);// 特定状态码 } } // 在服务中使用 chargeUser(userId:number, amount:number){ const balance =this.getBalance(userId); if(balance < amount){ thrownewInsufficientBalanceException(); } // 扣款逻辑... } // 全局过滤器(进阶用法) @Catch(InsufficientBalanceException) exportclassBalanceFilterimplementsExceptionFilter{ catch(exception: InsufficientBalanceException, host: ArgumentsHost){ const ctx = host.switchToHttp(); const response = ctx.getResponse(); response.status(402).json({ code:402, message:'请先充值后再进行操作', rechargeUrl:'/payment' }); } }
3.6 实战问答环节
Q:服务层必须用@Injectable装饰器吗?
必须,这是Nest识别可注入类的标记
Q:什么时候应该创建新模块?
当功能组件需要复用时(如数据库、第三方API)
业务领域明确独立时(如用户模块、订单模块)
Q:为什么不用全局try-catch?
Nest的异常过滤器机制更统一
可针对不同异常类型精细化处理
第四部分:管道——你的数据「安检通道」
4.1 管道核心作用
验证
:检查行李是否合规(数据格式)
转换
:把外币兑换成本地货币(数据格式化)
过滤
:没收违禁物品(清理不必要字段)
4.2 内置管道实战
// 单个路由使用 @Get(':id') findOne( @Param('id', ParseIntPipe) id:number// 自动转换数字 ){ return`查询ID为${id}的用户`; } // 测试案例: // 访问 /users/abc 将自动返回400错误 // 正确访问 /users/123 得到数字类型的123
4.3 自定义管道开发(手机号格式验证)
// phone.pipe.ts @Injectable() exportclassPhoneValidationPipeimplementsPipeTransform{ transform(value:any){ const valid =/^1[3-9]\d{9}$/.test(value); if(!valid){ thrownewBadRequestException('手机号格式错误'); } return value.replace(/(\d{3})\d{4}(\d{4})/,'$1****$2');// 自动脱敏 } } // 在控制器中使用 @Post('bind-phone') bindPhone( @Body('phone', PhoneValidationPipe) phone:string ){ return`绑定成功:${phone}`; }
4.4 全局管道配置(推荐方案)
// main.ts 启动文件 asyncfunctionbootstrap(){ const app =await NestFactory.create(AppModule); // 全局启用数据验证 app.useGlobalPipes( newValidationPipe({ whitelist:true, // 自动过滤DTO外的字段 transform:true, // 自动类型转换 disableErrorMessages: process.env.NODE_ENV==='production'// 生产环境隐藏错误详情 }) ); await app.listen(3000); }
4.5 Class Validator深度集成(企业级方案)
安装必要包:
npm install class-validator class-transformer
DTO增强:
// create-user.dto.ts exportclassCreateUserDto{ @IsString() @Length(2,20,{ message:'用户名长度需在2-20字符之间'}) username:string; @IsEmail({},{ message:'必须是有效的邮箱格式'}) email:string; @Matches(/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/,{ message:'密码必须包含字母和数字,至少8位' }) password:string; @Type(()=> Number)// 配合class-transformer转换类型 @IsInt({ message:'年龄必须是整数'}) @Min(18,{ message:'未满18岁不得注册'}) age:number; }
错误响应示例:
// 当验证失败时自动返回 { "statusCode":400, "message":[ "用户名长度需在2-20字符之间", "必须是有效的邮箱格式", "未满18岁不得注册" ], "error":"Bad Request" }
4.6 常见问题排雷
Q:管道和中间件有什么区别?
中间件:更底层,处理HTTP请求/响应(跨域、日志)
管道:专注处理请求参数(路由参数、查询参数、请求体)
Q:为什么要用class-transformer?
处理原始请求数据到类实例的转换
解决嵌套对象、数组等复杂结构的类型转换
Q:生产环境为什么要禁用错误详情?
防止敏感信息泄露(如数据库字段结构)
避免给攻击者提供线索
第五部分:守卫——你的系统「安检门禁」
5.1 守卫核心职责
身份核验
:检查登机牌(是否登录)
权限确认
:检查舱位等级(用户角色)
访问控制
:拦截危险人员(黑名单IP)
5.2 创建JWT守卫(实战认证方案)
// jwt.guard.ts @Injectable() exportclassJwtAuthGuardimplementsCanActivate{ constructor(private jwtService: JwtService){} canActivate(context: ExecutionContext):boolean{ const request = context.switchToHttp().getRequest(); const token =this.extractToken(request); if(!token){ thrownewUnauthorizedException('请先登录'); } try{ const payload =this.jwtService.verify(token); request.user = payload;// 注入用户信息到请求对象 returntrue; }catch{ thrownewUnauthorizedException('登录凭证已过期'); } } privateextractToken(request: Request):string|null{ return request.headers.authorization?.split(' ')[1]||null; } }
5.3 角色权限守卫(RBAC实现)
// roles.decorator.ts(自定义装饰器) exportconstRoles=(...roles:string[])=>SetMetadata('roles', roles); // roles.guard.ts @Injectable() exportclassRolesGuardimplementsCanActivate{ constructor(private reflector: Reflector){} canActivate(context: ExecutionContext):boolean{ const requiredRoles =this.reflector.get<string[]>( 'roles', context.getHandler() ); if(!requiredRoles)returntrue; const{ user }= context.switchToHttp().getRequest(); return requiredRoles.some(role => user.roles?.includes(role)); } } // 控制器使用示例 @Post('admin/reports') @UseGuards(JwtAuthGuard, RolesGuard) @Roles('admin','superadmin') generateReport(){ return'生成管理员专属报表'; }
5.4 守卫注册方式(灵活配置)
// 全局注册(main.ts) app.useGlobalGuards(newJwtAuthGuard()); // 控制器级别 @Controller('orders') @UseGuards(JwtAuthGuard) exportclassOrderController{} // 路由方法级别 @Get('secret') @UseGuards(RolesGuard) getSecretData(){}
5.5 第三方登录集成(Github OAuth示例)
// github.strategy.ts(Passport集成) @Injectable() exportclassGithubStrategyextendsPassportStrategy(Strategy,'github'){ constructor(){ super({ clientID: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_SECRET, callbackURL:'/auth/github/callback', scope:['user:email'] }); } asyncvalidate(accessToken:string, _refreshToken:string, profile:any){ return{ provider:'github', uid: profile.id, email: profile.emails[0].value, username: profile.username }; } } // 在控制器中使用 @Get('auth/github') @UseGuards(AuthGuard('github')) githubLogin(){} @Get('auth/github/callback') @UseGuards(AuthGuard('github')) githubCallback(@Req() req){ returnthis.authService.handleOAuthLogin(req.user); }
5.6 安全防护常见问题
Q:守卫和中间件的执行顺序?
中间件 -> 守卫 -> 拦截器 -> 管道 -> 控制器方法
Q:如何实现权限分级管理?
// 角色定义建议 enum UserRole { TOURIST=1, // 游客 USER=10, // 普通用户 EDITOR=20, // 内容编辑 ADMIN=99 // 系统管理员 } // 权限检查时使用数字比对 hasPermission(userRole:number, requiredRole:number){ return userRole >= requiredRole; }
Q:如何处理多设备登录问题?
// 生成带设备类型的JWT createToken(userId:string, deviceType:string){ returnthis.jwtService.sign({ sub: userId, device: deviceType // web/mobile/iot }); } // 在守卫中检查设备合法性 if(payload.device !=='web'){ thrownewForbiddenException('该设备无访问权限'); }
第六部分:拦截器——你的数据「物流中心」
6.1 拦截器核心能力(快递分拣系统比喻)
请求预处理
:包裹分类贴标签(添加全局请求头)
响应格式化
:统一包装盒规格(标准化响应结构)
异常捕获
:破损包裹拦截(错误日志记录)
性能监控
:运输时效统计(接口耗时计算)
6.2 创建基础拦截器(响应标准化)
// response.interceptor.ts @Injectable() exportclassResponseInterceptorimplementsNestInterceptor{ intercept(context: ExecutionContext, next: CallHandler): Observable<any>{ const ctx = context.switchToHttp(); const response = ctx.getResponse(); // 统一设置响应头 response.setHeader('X-Powered-By','NestJS'); return next.handle().pipe( map(data =>({ code:200, success:true, timestamp:newDate().toISOString(), data: _.isObject(data)? data :{ result: data } })) ); } } // 使用效果: // 原始返回: 'Hello World' // 处理后返回: // { // code: 200, // success: true, // timestamp: "2023-08-20T08:00:00Z", // data: { result: "Hello World" } // }
6.3 多场景实战应用
案例一:API耗时监控
intercept(context: ExecutionContext, next: CallHandler){ const start = Date.now(); return next.handle().pipe( tap(()=>{ const duration = Date.now()- start; console.log(`接口耗时:${duration}ms`); }) ); }
案例二:敏感数据脱敏
map(data => { if (data?.phone) { data.phone = data.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'); } return data; })
案例三:请求日志记录
const request = context.switchToHttp().getRequest(); logger.info(`[${request.method}] ${request.url} - Body: ${JSON.stringify(request.body)}`); return next.handle().pipe( catchError(err => { logger.error(`请求失败:${err.stack}`); return throwError(() => err); }) );
6.4 全局拦截器配置
// main.ts app.useGlobalInterceptors( new ResponseInterceptor(), new TimeoutInterceptor(5000) // 请求超时设置 );
6.5 高阶应用:缓存拦截器
// cache.interceptor.ts @Injectable() exportclassCacheInterceptorimplementsNestInterceptor{ private cache =newMap<string, any>(); intercept(context: ExecutionContext, next: CallHandler){ const request = context.switchToHttp().getRequest(); const cacheKey = request.originalUrl; if(request.method !=='GET')return next.handle(); const cached =this.cache.get(cacheKey); if(cached){ returnof(cached);// 直接返回缓存 } return next.handle().pipe( tap(response =>{ this.cache.set(cacheKey, response); setTimeout(()=>this.cache.delete(cacheKey),60000);// 60秒后清除 }) ); } }
6.6 拦截器使用原则
职责单一:每个拦截器专注一个任务
执行顺序:
请求 -> 全局拦截器 -> 控制器拦截器 -> 路由方法拦截器
性能注意:避免在拦截器中做重型操作
6.7 常见问题解答
Q:拦截器 vs 中间件 vs 过滤器?
中间件:处理原始HTTP请求(跨域、压缩)
拦截器:处理控制器输入输出(业务相关)
过滤器:捕获未处理的异常(兜底处理)
Q:如何跳过某些路由的拦截器?
// 使用自定义元数据装饰器 exportconstSkipInterceptor=()=>SetMetadata('skipInterceptor',true); // 在拦截器中判断 const shouldSkip =this.reflector.get<boolean>('skipInterceptor', context.getHandler()); if(shouldSkip)return next.handle()
Q:如何处理异步操作?
// 超时控制示例 intercept(context: ExecutionContext, next: CallHandler){ return next.handle().pipe( timeout(5000),// 5秒超时 catchError(err =>{ if(err instanceofTimeoutError){ thrownewRequestTimeoutException(); } returnthrowError(()=> err); }) ); }
第七部分:中间件——你的请求「预处理车间」
7.1 中间件定位
执行阶段
:控制器之前处理原始请求
典型场景
:
请求日志记录
跨域头设置
用户身份预校验
请求体压缩处理
7.2 创建日志中间件(实战演示)
// logger.middleware.ts import{ Request, Response, NextFunction }from'express'; exportfunctionLoggerMiddleware(req: Request, res: Response, next: NextFunction){ const start = Date.now(); res.on('finish',()=>{ const duration = Date.now()- start; console.log( `[${new Date().toISOString()}] ${req.method} ${req.originalUrl} - ${res.statusCode} (${duration}ms)` ); }); next();// 必须调用next继续传递 }
7.3 中间件注册方式(三种绑定策略)
// 全局中间件(main.ts) app.use(LoggerMiddleware); // 模块中间件(app.module.ts) exportclassAppModuleimplementsNestModule{ configure(consumer: MiddlewareConsumer){ consumer .apply(LoggerMiddleware) .forRoutes({ path:'user*', method: RequestMethod.ALL});// 匹配/user开头的所有路由 } } // 路由排除法 consumer .apply(AuthMiddleware) .exclude( { path:'auth/login', method: RequestMethod.POST}, { path:'docs', method: RequestMethod.GET} ) .forRoutes('*');
7.4 文件上传处理(Multer集成方案)
// 安装依赖 npm install @nestjs/platform-express multer @types/multer // 文件上传控制器 @Post('avatar') @UseInterceptors(FileInterceptor('file')) uploadAvatar( @UploadedFile() file: Express.Multer.File, @Body() userId:string ){ returnthis.userService.updateAvatar(userId, file); } // 配置文件存储(自定义配置) const storage = multer.diskStorage({ destination:(req, file, cb)=>{ cb(null,'./uploads'); }, filename:(req, file, cb)=>{ const ext = path.extname(file.originalname); cb(null,`${Date.now()}${ext}`); } }); // 在模块中注册 @Module({ imports:[ MulterModule.register({ storage: storage, limits:{ fileSize:1024*1024*5}// 5MB限制 }) ] }) exportclassFileModule{}
7.5 第三方中间件集成(安全防护)
// 安装安全中间件 npm install helmet compression // main.ts配置 import*as helmet from'helmet'; import*as compression from'compression'; asyncfunctionbootstrap(){ const app =await NestFactory.create(AppModule); // 安全头设置 app.use(helmet({ contentSecurityPolicy:false// 根据需求配置 })); // 响应压缩 app.use(compression({ level:6})); await app.listen(3000); }
7.6 中间件开发准则
功能纯净
:避免在一个中间件中做多件不相关的事
性能优先
:不要阻塞事件循环(避免同步耗时操作)
错误处理:必须捕获并传递错误
app.use((err, req, res, next) => { console.error(err.stack); res.status(500).send('服务器开小差了!'); });
7.7 常见问题破解
Q:中间件和过滤器的区别?
中间件:处理请求/响应周期中的原始数据
过滤器:处理控制器抛出的异常
Q:如何实现CSRF防护?
// 安装csrf防护 npm install csurf @types/csurf // 使用中间件 import*as csurf from'csurf'; app.use(csurf()); app.use((req, res, next)=>{ res.cookie('XSRF-TOKEN', req.csrfToken()); next(); }); // 前端需要在请求头携带 headers:{ 'X-XSRF-TOKEN':getCookie('XSRF-TOKEN') }
Q:处理大文件上传优化策略?
// 分片上传中间件 app.post('/upload',(req, res)=>{ const chunk = req.body.chunk; const index = req.body.index; const total = req.body.total; // 将分片暂存到临时目录 fs.writeFileSync(`./temp/${index}`, chunk); if(index === total){ // 合并所有分片 mergeFiles(total); } res.sendStatus(200); });
第八部分:微服务——你的业务「集团分部」
8.1 微服务核心特征(连锁餐厅)
独立运营
:每家分店自主管理(服务自治)
专业分工
:中央厨房负责食材供应(服务拆分)
统一协调
:总部调度各分店资源(服务发现)
容错机制
:某分店停业不影响整体(熔断降级)
8.2 创建基础微服务(TCP通信示例)
# 创建用户服务(独立项目) nest new user-service --strict # 创建订单服务(独立项目) nest new order-service --strict
用户服务配置:
// main.ts asyncfunctionbootstrap(){ const app =await NestFactory.createMicroservice<MicroserviceOptions>( AppModule, { transport: Transport.TCP, options:{ host:'localhost', port:3001} } ); await app.listen(); }
订单服务调用配置:
// order.module.ts @Module({ imports:[ ClientsModule.register([ { name:'USER_SERVICE', transport: Transport.TCP, options:{ host:'localhost', port:3001} } ]) ] })
8.3 跨服务通信模式(实战演示)
场景:创建订单时验证用户
// order.service.ts @Injectable() exportclassOrderService{ constructor( @Inject('USER_SERVICE')private userClient: ClientProxy ){} asynccreateOrder(userId:string, items:string[]){ // 调用用户服务验证 const userValid =awaitthis.userClient .send<boolean>('validateUser', userId) .toPromise(); if(!userValid){ thrownewBadRequestException('用户不存在'); } // 创建订单逻辑... return{ orderId: Date.now().toString()}; } } // user.controller.ts(用户服务) @MessagePattern('validateUser') validateUser(@Payload() userId:string){ returnthis.userService.exists(userId);// 返回布尔值 }
8.4 消息队列集成(RabbitMQ案例)
// 安装依赖 npm install @nestjs/microservices amqplib amqp-connection-manager // 邮件服务配置 const app =await NestFactory.createMicroservice<MicroserviceOptions>( AppModule, { transport: Transport.RMQ, options:{ urls:['amqp://localhost:5672'], queue:'email_queue', queueOptions:{ durable:true} } } ); // 订单服务发送邮件通知 this.emailClient.emit('send_order_email',{ email:'user@example.com', orderId:'12345' });
8.5 网关统一入口(API网关模式)
// api-gateway项目配置 @Controller() exportclassApiController{ constructor( @Inject('USER_SERVICE')private userClient: ClientProxy, @Inject('ORDER_SERVICE')private orderClient: ClientProxy ){} @Get('user/:id/orders') asyncgetUserOrders(@Param('id') userId:string){ // 并行调用两个服务 const[user, orders]=awaitPromise.all([ this.userClient.send('getUser', userId).toPromise(), this.orderClient.send('getUserOrders', userId).toPromise() ]); return{...user, orders }; } }
8.6 分布式系统常见问题
Q:如何保证数据一致性?
// 使用Saga模式示例 asynccreateOrder(){ try{ // 阶段1:扣减库存 await inventoryClient.emit('reduce_stock', items); // 阶段2:创建订单 const order =awaitthis.create(orderData); // 阶段3:扣款 await paymentClient.emit('deduct_balance', userId); return order; }catch(error){ // 补偿操作 await inventoryClient.emit('rollback_stock', items); await paymentClient.emit('rollback_balance', userId); throw error; } }
Q:如何实现服务发现?
// 使用Consul配置示例 transport: Transport.CONSUL, options:{ host:'consul-server', port:8500, service:{ name:'user-service', check:{ interval:'10s', http:'http://localhost:3001/health' } } }
Q:如何监控微服务健康?
// 健康检查端点 @Get('health') @HealthCheck() checkHealth(){ returnthis.health.check([ ()=>this.db.ping().then(()=> HealthCheckResult.healthy()), ()=>this.cache.ping().then(()=> HealthCheckResult.healthy()) ]); }
第九部分:MySQL集成——你的数据「智能仓库」
9.1 ORM核心概念(仓库管理员比喻)
实体(Entity) = 货物清单(数据结构定义)
仓库(Repository) = 货架管理员(数据存取操作)
迁移(Migration) = 仓库改造记录(数据结构变更)
9.2 环境配置(.env标准方案)
# .env DB_HOST=localhost DB_PORT=3306 DB_USER=root DB_PASSWORD=your_password DB_NAME=nest_demo DB_SYNC=false # 生产环境必须关闭!
9.3 实体定义规范(用户模型案例)
// user.entity.ts @Entity() exportclassUser{ @PrimaryGeneratedColumn() id:number; @Column({ length:30, comment:'登录账号'}) username:string; @Column({ select:false,// 查询时默认排除 comment:'加密后的密码' }) password:string; @Column({ type:'enum', enum: UserRole, default: UserRole.USER }) role: UserRole; @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date; }
9.4 数据库模块注册(动态配置)
// database.module.ts @Module({ imports:[ TypeOrmModule.forRootAsync({ imports:[ConfigModule], useFactory:(config: ConfigService)=>({ type:'mysql', host: config.get('DB_HOST'), port: config.get('DB_PORT'), username: config.get('DB_USER'), password: config.get('DB_PASSWORD'), database: config.get('DB_NAME'), entities:[__dirname +'/../**/*.entity{.ts,.js}'], synchronize: config.get('DB_SYNC'),// 仅开发环境开启 logging:['error','warn']// 生产环境建议关闭 }), inject:[ConfigService] }) ] }) exportclassDatabaseModule{}
9.5 数据操作实战(CRUD演示)
// user.service.ts @Injectable() exportclassUserService{ constructor( @InjectRepository(User) private userRepository: Repository<User> ){} // 创建用户 asynccreate(userDto: CreateUserDto){ const user =this.userRepository.create({ ...userDto, password: bcrypt.hashSync(userDto.password,10) }); returnawaitthis.userRepository.save(user); } // 分页查询 asyncpaginate(page =1, limit =10){ const[data, total]=awaitthis.userRepository.findAndCount({ skip:(page -1)* limit, take: limit, order:{ id:'DESC'} }); return{ data, meta:{ total, page, last_page: Math.ceil(total / limit)} }; } // 事务操作 asynctransferMoney(fromId:number, toId:number, amount:number){ awaitthis.userRepository.manager.transaction(async manager =>{ const from =await manager.findOne(User,{ where:{ id: fromId }}); const to =await manager.findOne(User,{ where:{ id: toId }}); if(from.balance < amount)thrownewError('余额不足'); await manager.update(User, fromId,{ balance: from.balance - amount }); await manager.update(User, toId,{ balance: to.balance + amount }); }); } }
9.6 复杂查询构建(QueryBuilder实战)
// 多表联查示例 asyncgetUserWithOrders(userId:number){ returnthis.userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.orders','orders') .where('user.id = :id',{ id: userId }) .andWhere('orders.status = :status',{ status:'paid'}) .orderBy('orders.createdAt','DESC') .getOne(); } // 原生SQL执行 asynccountActiveUsers(){ returnthis.userRepository.query(` SELECT COUNT(*) as count FROM users WHERE last_login_at > DATE_SUB(NOW(), INTERVAL 30 DAY) `); }
9.7 性能优化方案
索引优化:
@Index('IDX_USERNAME', ['username'], { unique: true }) @Entity() export class User { ... }
查询缓存:
const users =awaitthis.userRepository.find({ where:{ role:'admin'}, cache:{ id:'admin_users', milliseconds:60000// 1分钟缓存 } });
连接池配置:
// database.module.ts options:{ ..., extra:{ connectionLimit:20,// 最大连接数 waitForConnections:true, queueLimit:0 } }
9.8 数据迁移管理(生产环境必须)
# 生成迁移文件 npx typeorm migration:create ./migrations/InitSchema # 执行迁移 npx typeorm migration:run # 回滚迁移 npx typeorm migration:revert
迁移文件示例:
export classInitSchemaimplementsMigrationInterface{ publicasyncup(queryRunner: QueryRunner):Promise<void>{ await queryRunner.query(` CREATE TABLE users ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; `); } publicasyncdown():Promise<void>{ await queryRunner.dropTable('users'); } }
9.9 安全注意事项
SQL注入防护:
// 危险! query(`SELECT * FROM users WHERE name = '${name}'`); // 安全✅ query('SELECT * FROM users WHERE name = ?', [name]);
始终使用参数化查询
避免直接拼接SQL语句
敏感数据处理:
// 密码加密处理 asyncfunctionhashPassword(password:string){ return bcrypt.hash(password,10); } // 敏感字段排除 @Entity() exportclassUser{ @Column({ select:false}) password:string; }
9.10 常见问题解答
Q:实体和DTO有什么区别?
实体:描述数据库表结构(与数据库直接交互)
DTO:定义接口传输格式(请求/响应结构)
Q:如何高效处理批量插入?
const users =[...];// 大量用户数据 awaitthis.userRepository .createQueryBuilder() .insert() .into(User) .values(users) .orIgnore()// 忽略重复 .execute();
Q:事务处理的正确方式?
使用
transaction
方法包裹原子操作避免在事务中执行长时间操作
注意事务隔离级别设置
第十部分:测试与部署——你的系统「质检&物流」
10.1 测试金字塔模型(质量检测体系)
- 单元测试(70%):单个函数/类测试 - 集成测试(20%):模块间协作测试 - E2E测试(10%):完整业务流程测试
10.2 单元测试实战(用户服务案例)
// user.service.spec.ts describe('UserService',()=>{ let service: UserService; let mockRepository: jest.Mocked<Repository<User>>; beforeEach(async()=>{ // 模拟数据库操作 mockRepository ={ findOne: jest.fn(), save: jest.fn().mockImplementation((user)=> user) }asany; const module: TestingModule =await Test.createTestingModule({ providers:[ UserService, { provide:getRepositoryToken(User), useValue: mockRepository } ] }).compile(); service = module.get<UserService>(UserService); }); it('创建用户时应加密密码',async()=>{ const dto ={ username:'test', password:'123456'}; await service.create(dto); expect(mockRepository.save).toBeCalledWith( expect.objectContaining({ password: expect.not.stringMatching(dto.password) }) ); }); });
10.3 E2E测试配置(完整流程验证)
// app.e2e-spec.ts describe('用户模块 (e2e)',()=>{ let app: INestApplication; beforeAll(async()=>{ const moduleFixture =await Test.createTestingModule({ imports:[AppModule], }).compile(); app = moduleFixture.createNestApplication(); await app.init(); }); it('/user/register (POST)',()=>{ returnrequest(app.getHttpServer()) .post('/user/register') .send({ username:'e2e_test', password:'Test@123', email:'e2e@test.com' }) .expect(201) .expect(res =>{ expect(res.body.data.username).toBe('e2e_test'); expect(res.body.data.password).toBeUndefined(); }); }); afterAll(async()=>{ await app.close(); }); });
10.4 Docker容器化(生产部署标准)
# Dockerfile 三阶段构建 # 阶段一:构建环境 FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # 阶段二:生产依赖 FROM node:18-alpine AS deps WORKDIR /app COPY package*.json ./ RUN npm ci --production # 阶段三:最终镜像 FROM node:18-alpine WORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=deps /app/node_modules ./node_modules COPY .env.prod . EXPOSE 3000 CMD ["node", "dist/main.js"]
10.5 部署优化策略(生产环境配置)
# .env.prod 生产配置 NODE_ENV=production DB_HOST=mysql-prod DB_PORT=3306 DB_USER=prod_user DB_PASSWORD=${DB_PASSWORD}# 从安全仓库注入 DB_NAME=prod_db DB_SYNC=false # docker-compose.yml version: '3.8' services: app: build: . ports: - "3000:3000" environment: - DB_PASSWORD=${DB_PASSWORD} depends_on: - mysql mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} MYSQL_DATABASE: prod_db volumes: - mysql_data:/var/lib/mysql command: --default-authentication-plugin=mysql_native_password volumes: mysql_data:
10.6 监控与维护(生产必备)
// 健康检查端点 @Get('health') @HealthCheck() checkHealth(){ returnthis.health.check([ ()=>this.db.ping().then(()=> HealthCheckResult.healthy()), ()=>this.redis.ping().then(()=> HealthCheckResult.healthy()) ]); } // 日志切割配置(PM2) module.exports ={ apps:[{ name:'nest-app', script:'dist/main.js', instances:'max', autorestart:true, watch:false, max_memory_restart:'1G', error_file:'/var/log/nest-app/error.log', out_file:'/var/log/nest-app/out.log', log_date_format:'YYYY-MM-DD HH:mm:ss', merge_logs:true, env_production:{ NODE_ENV:'production' } }] };
10.7 持续集成实战(GitHub Actions)
# .github/workflows/deploy.yml name: Deploy Production on: push: tags: -'v*' jobs: build-and-deploy: runs-on: ubuntu-latest steps: -name: Checkout code uses: actions/checkout@v3 -name: Setup Node.js uses: actions/setup-node@v3 with: node-version:18 -name: Install Dependencies run: npm ci -name: Build Project run: npm run build -name: Run Tests run: npm run test:ci -name: Build Docker Image run: docker build -t ${{ secrets.DOCKER_USER }}/nest-app:${{ github.ref_name }} . -name: Push to Docker Hub run:| echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USER }} --password-stdin docker push ${{ secrets.DOCKER_USER }}/nest-app:${{ github.ref_name }} -name: SSH Deploy uses: appleboy/ssh-action@v0.1.10 with: host: ${{ secrets.PROD_HOST }} username: ${{ secrets.PROD_USER }} key: ${{ secrets.PROD_SSH_KEY }} script:| docker pull ${{ secrets.DOCKER_USER }}/nest-app:${{ github.ref_name }} docker stop nest-app || true docker rm nest-app || true docker run -d \ --name nest-app \ --env-file .env.prod \ -p 3000:3000 \ ${{ secrets.DOCKER_USER }}/nest-app:${{ github.ref_name }}
10.8 生产环境安全清单
1. [强制] 禁用Swagger文档(生产环境)
2. [推荐] 配置HTTPS证书(Let's Encrypt)
3. [重要] 定期备份数据库(每日全量+增量)
4. [必须] 使用防火墙限制端口访问
5. [建议] 实施速率限制(防止暴力破解)
10.9 故障排查指南
# 查看容器日志 docker logs nest-app --tail100-f # 进入容器调试 dockerexec-it nest-app sh # 性能分析 node--inspect=0.0.0.0:9229 dist/main.js # 使用Chrome DevTools连接调试 # 内存泄漏检测 NODE_OPTIONS=--max-old-space-size=4096node dist/main.js
全系列总结路线图
1. 项目初始化 -> 基础架构搭建
2. 路由定义 -> 业务功能开发
3. 数据库集成 -> 数据持久化
4. 鉴权体系 -> 安全保障
5. 测试覆盖 -> 质量保障
6. 容器化 -> 部署标准化
7. 监控告警 -> 运维保障
8. 持续交付 -> 自动化流程
上一篇:Javascript实现多类继承