[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site-settings":3,"note-nest-js":12,"notes-adjacent":23},{"siteName":4,"siteDescription":5,"authorName":6,"copyright":7,"logoSymbol":8,"createdAt":9,"updatedAt":10,"id":11},"DICE","前端工程师的技术日志，记录 AI 探索与独立开发的思考","dice","Copyright © 2026 Dice. All rights reserved.","https:\u002F\u002Fstatic.diceio.com\u002Ff\u002Fb263d41b758729f8a32abdfeed19f620b85b22e9f4e2a126b2bd016f88a4df41",1782794723207,1783653454363,"6a4349e3ab9e79d68cae07a4",{"id":13,"slug":14,"title":15,"summary":16,"content":17,"category":18,"tags":19,"date":21,"cover":22},"6a682cc526acfd3536ebf305","nest-js","系统学习Nest.js基础","学习Nest.js的基本语法、结构，如何编写接口。","今天学习Nest.js的基础，Nest.js 是一个用于构建企业级 Node.js 后端应用的框架，这个架构借鉴了 Angular 的架构思想，底层运行在 Express 或 Fastify 之上。\n\n学习前的思考：\n### 1. 为什么使用Express而不用Koa呢？\nNest 诞生时 Express 是 Node.js 最大生态，企业应用大量依赖 Express，而 Nest 的目标是给 Express 增加企业级架构能力。\n\n### 2. 为什么还要支持Fastify呢？\nExpress在大型项目会存在性能问题，而Fastify在高并发场景的性能更好。\n而最重要的Nest 想**解决的是“应用架构问题”，而不是“HTTP 底层问题**。所以最初底层设计就有一个适配器：Express适配器，后面Fastify出来，也对Fastify进行适配。\nNest.js采用的可拔插的架构。\n\n### 常用命令\n\n#### 创建项目\nnest new &lt;name&gt;\n\n#### 生成类的 generate（nest g）\nnest g xx ...\n\n| 完整名 | 简写 |  说明 |\n|--------|--------|--------|\n| module | mo | 模块（@Module）|\n| controller | co | 控制器（路由\u002F接口） |\n| service |s | 服务（业务逻辑，@Injectable） |\n| resource | res | CRUD 一整套（module + controller + service + dto 等）|\n| guard | gu | 守卫（鉴权、权限拦截）|\n|interceptor| itc | 拦截器（请求\u002F响应前后处理）|\n| pipe| pi | 管道（参数校验\u002F转换）|\n| filter| f | 异常过滤器 |\n|middleware |mi |中间件 |\n|decorator |d  |自定义装饰器 |\n|gateway |ga | WebSocket 网关 |\n|provider | pr | 普通 Provider |\n|class | cl | 普通类 |\n| interface | i |TypeScript interface |\n|dto | — | DTO（数据传输对象）|\n|exception |— | 自定义异常类 |\n\n\n\n### 基础知识\n\n#### 导出模块：export    \n\n####  全局模块共享：Global\n当有一个模块是存在通用的，任意模块能使用，则使用global。\n@Global：全局模块，可以在其他模块中注入\n这时还需要配合将此模块导出：exports\n```\n\n@Global() \u002F\u002F 全局模块，可以在其他模块中注入\n@Module({\n  providers: [\n      {\n        provide: 'Config',\n        useValue: {\n          baseUrl: '\u002Fapi',\n        },\n      },\n    ],\n\n    exports: [\n      {\n        provide: 'Config',\n        useValue: {\n          baseUrl: '\u002Fapi',\n        },\n      },\n    ],\n})\nexport class ConfigModule {}\n\n\n\n@Controller('list')\nexport class ListController {\n  constructor(\n    private readonly listService: ListService,\n    \n    @Inject('Config') private readonly config: any,\n  ) {}\n\n  @Get()\n  findAll() {\n    return this.config.baseUrl;\n  }\n}\n```\n\n\n#### 动态模块\n```\n\nimport { DynamicModule, Global, Module } from '@nestjs\u002Fcommon';\n\ninterface Options {\n  path: string;\n}\n\n@Global() \u002F\u002F 全局模块，可以在其他模块中注入\n@Module({ })\nexport class ConfigModule {\n  \u002F\u002F 动态模块，可以动态注入配置\n  static forRoot(options: Options): DynamicModule {\n    return {\n      module: ConfigModule,\n      providers: [\n        {\n          provide: 'Config',\n          useValue: {\n            baseUrl: '\u002Fapi' + options.path,\n          },\n        }, \n      ],\n      exports: [\n        {\n          provide: 'Config',\n          useValue: {\n            baseUrl: '\u002Fapi' + options.path,\n          },\n        },\n      ],\n    };\n  }\n}\n\n\n\nimport { Module } from '@nestjs\u002Fcommon';\nimport { AppController } from '.\u002Fapp.controller';\nimport { AppService } from '.\u002Fapp.service';\nimport { ListModule } from '.\u002Flist\u002Flist.module';\nimport { ConfigModule } from '.\u002Fconfig\u002Fconfig.module';\n \n@Module({\n  imports: [\n    ListModule,\n    \u002F\u002F 动态注入配置\n    ConfigModule.forRoot({ path: 'yanghesu' }),\n  ],\n  controllers: [AppController],\n  providers: [AppService],\n})\nexport class AppModule {}\n\n\n```\n\n\n#### 中间件\n\n命令： nest g mi xxx。创建中间件模块\n\n##### 应用到具体路由\n\n```\n\nimport { Injectable, NestMiddleware } from '@nestjs\u002Fcommon';\nimport { Request, Response, NextFunction } from 'express';\n\n@Injectable()\nexport class Logger implements NestMiddleware {\n\n  use(req: Request, res: Response, next: NextFunction) {\n    console.log('我来了～～～');\n    next();\n  }\n}\n\n\n\nimport {\n  Module,\n  NestModule,\n  MiddlewareConsumer,\n  RequestMethod,\n} from '@nestjs\u002Fcommon';\nimport { UserService } from '.\u002Fuser.service';\nimport { UserController } from '.\u002Fuser.controller';\n\nimport { Logger } from '..\u002Fmiddleware';\n\n@Module({\n  controllers: [UserController],\n  providers: [UserService],\n})\nexport class UserModule implements NestModule {\n  \u002F\u002F 配置中间件\n  configure(consumer: MiddlewareConsumer) {\n    \u002F\u002F 三种方式应用中间件\n    \u002F\u002F 1.应用中间件\n    \u002F\u002F consumer.apply(Logger).forRoutes('user');\n    \u002F\u002F 2.应用到整个控制器\n    \u002F\u002F consumer.apply(Logger).forRoutes(UserController);\n    \u002F\u002F 3.应用到单个路由\n    consumer\n      .apply(Logger)\n      \u002F\u002F 只对get请求应用中间件\n      .forRoutes({ path: 'user', method: RequestMethod.GET });\n  }\n}\n\n```\n\n##### 全局中间件\n```\n\n\nimport { NestFactory } from '@nestjs\u002Fcore';\nimport { AppModule } from '.\u002Fapp.module';\nimport { Request, Response, NextFunction } from 'express';\nimport cors from 'cors';\n\n\u002F\u002F 全局中间件\nfunction MiddlewareAll(req: Request, res: Response, next: NextFunction) {\n  console.log('我是全局中间件===', req.originalUrl);\n  next();\n}\n\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n  \u002F\u002F 使用cors中间件-允许跨域\n  app.use(cors());\n  \u002F\u002F 使用全局中间件\n  app.use(MiddlewareAll);\n  await app.listen(process.env.PORT ?? 3000);\n}\nbootstrap();\n\n````\n\n\n### 静态文件上传：multer\n搭配安装：@types\u002Fmulter \n搭配使用(内置)：@nestjs\u002Fplatform-express\n```\n\nimport { Module } from '@nestjs\u002Fcommon';\nimport { UploadService } from '.\u002Fupload.service';\nimport { UploadController } from '.\u002Fupload.controller';\n\u002F\u002F 导入multer模块\nimport { MulterModule } from '@nestjs\u002Fplatform-express';\n\u002F\u002F 导入diskStorage模块\nimport { diskStorage } from 'multer';\n\u002F\u002F 导入path模块\nimport { extname, join } from 'path';\n\n@Module({\n  imports: [\n    \u002F\u002F 注册multer模块， \n   \u002F\u002F register同步注册，固定路径\n  \u002F\u002F  registerAsync异步注册，适用依赖环境变量、配置中心、数据库等运行时才能拿到的值。\n    MulterModule.register({\n      \u002F\u002F 配置存放路径\n      storage: diskStorage({\n        \u002F\u002F 配置 文件存放路径目录\n        destination: join(__dirname, '..\u002Fimage'),\n        \u002F\u002F 配置文件名\n        filename: (req, file, cb) =&gt; {\n          const fileName = `${Date.now() + extname(file.originalname)}`;\n          return cb(null, fileName);\n        },\n      }),\n    }),\n  ],\n  controllers: [UploadController],\n  providers: [UploadService],\n})\nexport class UploadModule {}\n\n\nimport {\n  Controller,\n  Post,\n  UseInterceptors,\n  UploadedFile,\n} from '@nestjs\u002Fcommon';\nimport { UploadService } from '.\u002Fupload.service';\nimport { CreateUploadDto } from '.\u002Fdto\u002Fcreate-upload.dto';\nimport { UpdateUploadDto } from '.\u002Fdto\u002Fupdate-upload.dto';\n\u002F\u002F FileInterceptor 单个文件\n\u002F\u002F FilesInterceptor 多个文件\nimport { FileInterceptor, FilesInterceptor } from '@nestjs\u002Fplatform-express';\n\n@Controller('upload')\nexport class UploadController {\n  constructor(private readonly uploadService: UploadService) {}\n\n  @Post('album')\n  \u002F\u002F 处理文件上传\n \u002F\u002F 解析并保存文件\n  @UseInterceptors(FileInterceptor('file'))\n \u002F\u002F @UploadedFile() 把文件对象塞进file\n  upload(@UploadedFile() file) {\n    console.log('file===', file);\n    return '上传成功了';\n  }\n}\n\n\n\n\u002F\u002F upload.module.ts\nimport { Module } from '@nestjs\u002Fcommon';\nimport { MulterModule } from '@nestjs\u002Fplatform-express';\nimport { memoryStorage } from 'multer';\nimport { UploadController } from '.\u002Fupload.controller';\nimport { UploadService } from '.\u002Fupload.service';\n\n@Module({\n  imports: [\n    MulterModule.register({\n      storage: memoryStorage(), \u002F\u002F 先不落盘，方便处理\n    }),\n  ],\n  controllers: [UploadController],\n  providers: [UploadService],\n})\nexport class UploadModule {}\n\n\n\u002F\u002F 进行文件校验\nimport {\n  Controller,\n  Post,\n  UploadedFile,\n  UseInterceptors,\n  ParseFilePipe,\n  MaxFileSizeValidator,\n  FileTypeValidator,\n} from '@nestjs\u002Fcommon';\nimport { FileInterceptor } from '@nestjs\u002Fplatform-express';\nimport { UploadService } from '.\u002Fupload.service';\n\n@Controller('upload')\nexport class UploadController {\n  constructor(private readonly uploadService: UploadService) {}\n\n  @Post('image')\n  @UseInterceptors(FileInterceptor('file'))\n  uploadImage(\n    @UploadedFile(\n     \u002F\u002F Nest 内置的“文件校验管道”\n    \u002F\u002F \u002F\u002F 在进入方法体之前先跑 validators；任一失败会直接抛异常，不会继续执行后面的业务代码\n      new ParseFilePipe({ \n        \u002F\u002F validators：校验器列表，按顺序执行\n        validators: [\n          new MaxFileSizeValidator({ maxSize: 5 * 1024 * 1024 }), \u002F\u002F 5MB\n          new FileTypeValidator({ fileType: \u002F(jpg|jpeg|png|webp)$\u002F }), \u002F\u002F 用正则匹配允许的类型\n        ],\n      }),\n    )\n    file: Express.Multer.File,\n  ) {\n    return this.uploadService.saveImage(file);\n  }\n}\n\n\n\u002F\u002F 压缩转格式后再存\nimport { Injectable } from '@nestjs\u002Fcommon';\nimport { join } from 'path';\nimport { writeFile, mkdir } from 'fs\u002Fpromises';\nimport sharp from 'sharp';\n@Injectable()\nexport class UploadService {\n  async saveImage(file: Express.Multer.File) {\n    const dir = join(process.cwd(), 'uploads', 'images');\n    await mkdir(dir, { recursive: true });\n    const filename = `${Date.now()}.webp`;\n    const filepath = join(dir, filename);\n    \u002F\u002F 图片处理：压缩 + 转 webp\n    const buffer = await sharp(file.buffer)\n      .rotate()          \u002F\u002F 按 EXIF 纠正方向\n      .resize({ width: 1920, withoutEnlargement: true })\n      .webp({ quality: 80 })\n      .toBuffer();\n    await writeFile(filepath, buffer);\n    \u002F\u002F 生产环境这里通常改为：await oss.put(filename, buffer)\n    return {\n      url: `\u002Fstatic\u002Fimages\u002F${filename}`,\n      size: buffer.length,\n    };\n  }\n}\n\n\n\n```\n\n#### 下载文件\n```\nimport {\n  Controller,\n  Get,\n  Post,\n  Body,\n  Patch,\n  Param,\n  Delete,\n  UseInterceptors,\n  UploadedFile,\n  Res,\n} from '@nestjs\u002Fcommon';\nimport { UploadService } from '.\u002Fupload.service';\nimport { CreateUploadDto } from '.\u002Fdto\u002Fcreate-upload.dto';\nimport { UpdateUploadDto } from '.\u002Fdto\u002Fupdate-upload.dto';\n\u002F\u002F FileInterceptor 单个文件\n\u002F\u002F FilesInterceptor 多个文件\nimport { FileInterceptor, FilesInterceptor } from '@nestjs\u002Fplatform-express';\nimport { join } from 'path';\nimport type { Response } from 'express';\nimport { zip } from 'compressing';\n\n@Controller('upload')\nexport class UploadController {\n  constructor(private readonly uploadService: UploadService) {}\n\n  \u002F\u002F 1. 直接下载文件\n  @Get('export')\n  download(@Res() res: Response) {\n    const filePath = join(__dirname, '..\u002Fimage\u002F1785242445615.jpeg');\n    res.download(filePath);\n  }\n\n  \u002F\u002F 2. 下载文件 - 流的方式（压缩后再下载）\n  @Get('stream')\n  async down(@Res() res: Response) {\n    \u002F\u002F 拼接本地文件绝对路径（编译后 __dirname 为 dist\u002Fupload，..\u002Fimage 指向 dist\u002Fimage）\n    const filePath = join(__dirname, '..\u002Fimage\u002F1785242445615.jpeg');\n    \u002F\u002F 创建一个 zip 压缩流（边压缩边输出，不先写完整 zip 到磁盘）\n    const tarStream = new zip.Stream();\n    \u002F\u002F 把指定文件加入压缩流（可多次调用以打包多个文件）\n    await tarStream.addEntry(filePath);\n    \u002F\u002F 告诉浏览器：响应体是二进制流（通用下载类型）\n    res.setHeader('Content-Type', 'application\u002Foctet-stream');\n    \u002F\u002F 告诉浏览器：以附件形式下载，并指定默认保存的文件名\n    res.setHeader(\n      'Content-Disposition',\n      'attachment; filename=\"1785242445615.jpeg\"',\n    );\n    \u002F\u002F 把压缩流管道接到 HTTP 响应，数据会持续推给客户端\n    tarStream.pipe(res);\n  }\n\n}\n\n```\n\n#### 全局响应拦截\n```\n\nimport {\n  CallHandler,\n  ExecutionContext,\n  Injectable,\n  NestInterceptor,\n} from '@nestjs\u002Fcommon';\nimport { Observable } from 'rxjs';\nimport { map } from 'rxjs\u002Foperators';\n\n\u002F\u002F 统一响应体里 data 字段的类型（T 为业务数据的实际类型）\ninterface Data&lt;T&gt; {\n  data: T;\n}\n\n\u002F\u002F 全局\u002F局部响应拦截器：把控制器返回值包装成统一 JSON 结构\n@Injectable()\nexport class GlobalResponse&lt;T&gt; implements NestInterceptor {\n  \u002F\u002F Nest 在请求进入控制器前后会调用此方法\n  intercept(\n    context: ExecutionContext, \u002F\u002F 当前请求上下文（可取 req\u002Fres、handler 等信息）\n    next: CallHandler&lt;any&gt;, \u002F\u002F 调用链：用来继续执行后续拦截器\u002F控制器\n  ): Observable&lt;Data&lt;T&gt;&gt; {\n    \u002F\u002F next.handle()：执行控制器方法，得到原始返回值的 Observable\n    return next.handle().pipe(\n      \u002F\u002F map：在响应流出前，把原始 data 改造成统一格式\n      map((data) =&gt; {\n        return {\n          data, \u002F\u002F 业务数据（控制器 return 的内容）\n          status: 0, \u002F\u002F 业务状态码（0 表示成功，可按项目约定调整）\n          success: true, \u002F\u002F 是否成功\n          message: 'success', \u002F\u002F 提示信息\n        };\n      }),\n    );\n  }\n}\n\n\n\nimport { GlobalResponse } from '.\u002Fcommon\u002Fresponse';\n\u002F\u002F 使用全局拦截器\n  app.useGlobalInterceptors(new GlobalResponse());\n\n```\n\n\n","study-skill",[20],"nest.js","2026-07-28","\u002Fcovers\u002Fpost-2.webp",[24,27,41],{"id":13,"slug":14,"title":15,"summary":16,"content":25,"category":18,"tags":26,"date":21,"cover":22},"",[20],{"id":28,"slug":29,"title":30,"summary":31,"content":25,"category":32,"tags":33,"date":39,"cover":40},"6a4a8dade2c37b4b335c52ea","open-spec-and-spec-kit","学习如何项目上运用OpenSpec+Spec Kit进行开发","学习如何项目上运用OpenSpec+Spec Kit进行开发,学习中","ai",[34,35,36,37,38],"Open Spec","Spec Kit","AI","TDD","SDD","2026-07-05","\u002Fcovers\u002Fpost-1.webp",{"id":42,"slug":43,"title":44,"summary":45,"content":25,"category":46,"tags":47,"date":39,"cover":52},"6a4a87e9e2c37b4b335c525c","agent-skill-lighthouse-performance-audit","AI Agent 如何做网页性能审计？我的 Lighthouse Skill 实践笔记","封装了一个 Lighthouse 性能审计 Agent Skill：阶段 A 只出报告不改代码，AskQuestion 确认后再进阶段 B 出优化清单。支持 MCP\u002FCLI 双通道，内置 URL 重试、device 锁定、本地\u002F线上回归分流。Cursor \u002F Claude Code 通用，Skill 按阶段拆成 7 个 md 渐进加载。","Skills",[48,49,50,51],"Agent Skill","Lighthouse","性能优化","Cursor","\u002Fcovers\u002Fpost-3.webp"]