Nestjs学习总结_3

日志模块:

程序错误或者操作错误

常见日志以及获取(记录)方式

第三方日志方案:

winston类比:wepack

pino类比vite

通用业务系统日志系统配置(学习定时任务)

按功能分类:

● 错误日志 -> 方便定位问题,给用户友好的提示

● 调试日志 -> 方便开发

● 请求日志-> 记录敏感行为

日志记录位置:

● 控制台日志->方便监看(调试用)

● 文件日志->方便回溯与追踪(24小时滚动)

● 数据库日志->敏感操作、敏感数据记录

nestjs中记录日志位置推荐

自带的日志模块:

● 局部模块声明

ts 复制代码
  private logger = new Logger(UserController.name);

第三方日志模块:

pino

pino - npm

http://getpino.io/#/docs/benchmarks

https://github.com/pinojs/pino/blob/HEAD/docs/web.md#nest

● pino不需要调用log方法,本身在安装之后只要有请求就会自动打印日志

● pino的日志全部挤在一起,官方提供一个中间件来解决该问题,去格式化日志:

复制代码
pnpm i pino-pretty

全局使用

winston

捕获全局 exception

● 只能有一个全局异常捕获模块

捕获局部

● 第二个命令即可,不需要日志

安装:

复制代码
nest g f filters/typeorm --flat --no-spec

修改文件:

局部注册:

Nestjs注解器为什么可以生效

我们使用的是ts的注解语法,最终运行的时候是commpn.js规范,也就是用node来运行,通过指定ts配置使得代码可以正确的解析:

tsconfig.json配置:

json 复制代码
{
  "compilerOptions": {
    // 指定生成哪个模块系统代码:'nodenext' 用于 Node.js 的 ES 模块支持
    "module": "nodenext",
    // 模块解析策略:'nodenext' 用于 Node.js 的模块解析算法
    "moduleResolution": "nodenext",
    // 是否解析 package.json 中的 exports 字段(Node.js 12+ 功能)
    "resolvePackageJsonExports": true,
    // 启用 ES 模块互操作性,允许从没有默认导出的模块进行默认导入
    "esModuleInterop": true,
    // 确保每个文件都可以安全地转译,而不依赖其他导入
    "isolatedModules": true,
    // 生成相应的 .d.ts 声明文件
    "declaration": true,
    // 移除编译输出中的注释
    "removeComments": true,
    // 为装饰器发出设计类型的元数据
    "emitDecoratorMetadata": true,
    // 启用实验性的装饰器支持(常用于 Angular、NestJS 等框架)
    "experimentalDecorators": true,
    // 允许从没有默认导出的模块进行默认导入(与 esModuleInterop 配合使用)
    "allowSyntheticDefaultImports": true,
    // 指定 ECMAScript 目标版本:ES2023
    "target": "ES2023",
    // 生成相应的 .map 源映射文件,便于调试
    "sourceMap": true,
    // 指定输出目录
    "outDir": "./dist",
    // 解析非相对模块名的基准目录
    "baseUrl": "./",
    // 启用增量编译,提高后续编译速度
    "incremental": true,
    // 跳过类型声明文件(.d.ts)的类型检查
    "skipLibCheck": true,
    // 启用严格的 null 检查
    "strictNullChecks": true,
    // 禁止对不一致的文件引用大小写报错
    "forceConsistentCasingInFileNames": true,
    // 禁用对隐含 any 类型的检查(设为 false 更宽松)
    "noImplicitAny": false,
    // 禁用对 bind、call、apply 的更严格检查
    "strictBindCallApply": false,
    // 禁用 switch 语句中贯穿情况的检查
    "noFallthroughCasesInSwitch": false
  }
}
关键信息:

@Get,这种注解器在后台去做了解析工作,使得可以正确解析出请求的路由

复制代码
    // 为装饰器发出设计类型的元数据    "emitDecoratorMetadata": true,    
    // 启用实验性的装饰器支持(常用于 Angular、NestJS 等框架)    "experimentalDecorators": true,
Turbo Console Log

快速生成console注释

ctrl + alt + L

快速创建模块:

● 快速创建Logs模块:

复制代码
nestjs g mo logs -d

TypeCLI的使用

● 使用ormconfig.ts去进行数据库配置

复制代码
npm install ts-node --save-dev
  1. 需要使用DataSource来包裹配置文件:

  2. 运行typeorm命令指定获取需要连接数据库的具体结构

    npx typeorm init --database mysql

使用typeorm在多个不同的环境下去读取不同的配置文件:

通过环境变量解析不同文件,通过dotENV解析不同配置

Control模块:

管路由的,可以定义全局的路由前缀,也可以定义模块的路由前缀

通过注解获取请求相关的参数

查询的两种方式:

  1. 使用typeorm的find方法

  2. 使用typeorm的queryBuilder方法

常见的几种数据库查询方式

我的理解是,图中的阴影代表查询的范围,不代表最终的查询结果

● left join也就是以一张主表为中心,去查询别的副表中也有并且符合条件的

● inner join 也就是查询的范围是两张表都有的部分,查询的只是范围

连续进行条件筛选
优化版本:

想法: 可以使用1=1来简写不存在的情况(如果存在就传递条件,否则就传递1=1)

实现,封装where函数

建立索引

难道说查询一个用户存不存在是靠传递过来username就查username这么low的方法吗?

不不不。

将username设置为唯一的键值,就比如新增用户,那么下次新增相同用户的时候就会主动报索引的错误了

唯一索引 = 对不可重复的字段进行索引值限定的数据库约束

实现: 索引+约束

复制代码
-- 唯一索引实际上是:索引(快速查找) + 约束(数据完整性) -- 就像字典的:- 索引功能:按字母顺序快速查找- 约束功能:不允许重复的单词条目
唯一索引和普通索引:
唯一索引和主键
唯一索引的底层原理

B+树结构:

复制代码
唯一索引通常使用B+树实现: 根节点    ↓中间节点(范围划分)    ↓  叶子节点(实际数据指针 + 唯一值) 查找过程:从根节点到叶子节点,确保快速定位

插入时的检查流程:

复制代码
-- 当插入新记录时的内部流程:1. 检查唯一索引树中是否存在相同值2. 如果存在 → 抛出唯一约束违反错误3. 如果不存在 → 插入新记录并更新索引

interface和type都在什么场景下使用?

js有两种不同规范,一种是export function,另一种是export class。

type更适合export function,interfance更适合export class

换句话,export class在扩展能力比export function扩展能力很强

同样export function的组合能力比export class能力很强

像extends和prototype就不一样,像consturctor运行和call/apply就不一样,同样ts中也不一样,class直接get/set达到实体类的效果

● 类型系统层级:type更灵活,interface更适合 OOP

● 编程范式差异:class面向对象,function函数式

● 语言特性:不同语法有不同的设计意图和适用场景

总结:

需要对已有类型进行组合或者推导就用type,单纯定义对象属性用interface/class

使用编辑器自带的debug程序调试

● 启动调试进程

● 发送一个需要测试的请求

● 在监视中去筛选需要监视的变量

对于规范方法名称

● Controller语义化命名

● Service和Repositories是对应关系,方法名称可以一样,有些情况下都不需要拆Service文件和Repositories,只有业务逻辑复杂的情况下才去考虑拆成两个文件

把Service和Repositories分开的好处就是,核心逻辑单独放,查询数据库的逻辑单独放,这样假如我们后续不想要使用TypeOrm去查询数据库,而去用SQL或者prisma的时候只需要去替换TypeOrm即可

存储库,一般来说一个存储库对应一个实体类

TypeORM中不同方法之间的区别,平时建议用remove 和insert

● remove可以触发typeorm上的钩子方法

不建议使用delete,因为delete是硬删除

remove触发实体监听器和订阅者相关的生命周期函数,允许我们在remove成功后做一些额外操作

注意,这些生命周期是需要注册在实体类的下面的,监听的是实体

总结:使用remove的好处就是可以触发生命周期,记录被删除的时间等相关信息,一般用于去处理敏感数据的时候使用,如果是不重要的直接使用delete也可以,但是delete没有生命周期,删除了之后无法找到相关痕迹哦

JSON Web Token(JWT)

即使请求头中的数据一样,如果不满足发送端和服务器的证书相匹配的话,服务器也是会拒绝该请求的

session是保存在服务器端,cookie保存在客户端

由于token是无状态的,所以中间人可能够劫持到数据,这一点是毋庸置疑的,但是呢为什么还说JWT还是非常安全的呢?

因为一般来说我们会对通信信道去进行加密,让中间人无法劫持到这个请求,即使截取到了,没有对应的SSL证书,也无法获取到加密的数据,通过这种形式我们可以保证数据传递到安全性

鉴权与守卫

shell 复制代码
npm install @nestjs/jwt passport-jwtnpm install @types/passport-jwt --save-devnpm install passport nest g s auth -d nest g mo auth

通过管道做数据校验

vaildator

管道有两个典型的应用场景:

● 转换:管道将输入数据转换为所需的数据输出(例如,将字符串转换为整数)

● 验证:对输入数据进行验证,如果验证成功继续传递;验证失败则抛出异常

在这两种情况下,管道参数(arguments) 会由控制器(controllers)的路由处理程序进行处理。Nest会在调用这个方法之前插入一个管道,管道会先拦截方法的调用参数,进行转换或是验证处理,然后用转换好或是验证好的参数调用原方法。

Nest自带很多开箱即用的内置管道。你还可以构建自定义管道。本章将介绍先内置管道以及如何将其绑定到路由处理程序(route handlers)上,然后查看一些自定义管道以展示如何从头开始构建自定义管道。

管道的类型:

Nestjs中创建管道的过程

  1. 全局配置管道

  2. 创建class类,即Entity,DTO

  3. 设置校验规则

  4. 使用该实体类或者DTO

安装依赖:

复制代码
npm i --save class-validator class-transformer

定义校验规则:

● 不使用if else去判断,使用管道去校验

路由鉴权 Jwt Strategy或者Local Strategy作为策略,或者说为什么能够实现路由鉴权,其实内部也是去实现了validate方法,而vaildate方法最后调用也是去查询数据库,变成了原子化操作。

所以我们可以进一步理解其为什么叫做策略,其实就是将很多重复操作去集成为某一"策略"帮助我们去简化代码

Guards内部使用策略,在校验成功后自动返回一个变量,这里我们只需要接受即可,也就是将返回值赋值给了 req.user

return req.user

解释:

● login过程是需要签名的,走的是本地 Local Strategy,去查询数据库校验的UserService

● profile过程是需要jsonwebtoken签名后的token(Header中的 Authorization toekn)去请求后端接口的,而这个签名的token是否安全由Jwt Strategy来决定的,通过Jwt Strategy校验payload(图中最下面的部分)

● 两者是相反的策略(Strategydd)

login和profile都是在AuthController模块中的,细细品味这个。。。

测试:

测试不是被动触发的,而是主动去执行测试用例

json 复制代码
{
  // 不写测试的"隐藏成本"
  "不写测试的隐藏成本": {
    "线上Bug修复": "2-8小时/次",
    "紧急发布": "团队加班",
    "客户投诉": "商誉损失",
    "技术债务": "代码越来越难改"
  },
  
  // 写测试的"前期投入"
  "写测试的前期投入": {
    "编写测试": "+30% 开发时间",
    "维护测试": "+10% 维护成本",
    "收益": {
      "提前发现Bug": "节约 80% 线上问题",
      "重构信心": "代码持续健康",
      "新人上手": "测试即文档"
    }
  }
}
技术门槛:
ts 复制代码
// 写业务代码
function createUser(userData) {
  return db.users.create(userData); // 相对简单
}

// 写测试代码
describe('用户创建', () => {
  it('应该验证必填字段', async () => {
    // 需要理解测试框架、异步、mock 等概念
    const mockDb = { create: jest.fn() };
    const service = new UserService(mockDb);
    
    await expect(service.createUser({}))
      .rejects.toThrow('缺少必填字段');
  });
});```

#### 测试方法:

##### 1.  单元测试:

● 直接调用:就像 service.createUser()这样直接执行

● 同步执行:立即得到结果

● 内存操作:不启动服务器,不发送 HTTP 请求

```ts
// user.service.spec.ts - 单元测试
import { UserService } from './user.service'; // 直接导入代码

describe('UserService', () => {
  let service: UserService;
  
  beforeEach(() => {
    service = new UserService(); // 直接创建实例
  });

  it('应该创建用户', () => {
    // 🎯 直接调用函数,就像普通代码一样
    const result = service.createUser('张三', 'zhang@example.com');
    // ❌ 这里会报错:缺少 phone 参数
  });
});
2. E2E测试:

● 通过执行测试文件批量测试(触发测试用例中的接口)

● 需要提前配置注入测试环境

● HTTP 请求:像浏览器一样发送请求

● 完整应用:测试运行中的真实服务器

○ 当然测试环境,我是指真正的可以配置一个专门用来测试的环境

● 网络通信:通过 localhost 端口通信

ts 复制代码
// user.e2e-spec.ts - E2E 测试
describe('用户管理 API', () => {
  it('创建用户', async () => {
    // 🎯 发送 HTTP 请求到运行中的服务器
    const response = await global.pactum()
      .post('/users')
      .withJson({
        name: '张三',
        email: 'zhang@example.com'
        // ❌ 缺少 phone 参数
      })
      .expectStatus(400); // 期望返回 400 错误
    
    expect(response.json.error).toBe('缺少 phone 参数');
  });
});
测试执行时刻
shell 复制代码
# 当你运行测试时:
npm test user.e2e-spec.ts

# 时间线:
[14:30:00] 开始执行测试
[14:30:01] 测试代码发送 POST /users 请求  ← 主动触发!
[14:30:01] 服务器处理请求并返回响应
[14:30:01] 测试验证响应
[14:30:01] 测试结束
降低测试门槛:

方案1:测试代码生成工具

ts 复制代码
// 工具自动生成测试骨架

// 输入:业务代码
function createUser(name, email) {
  if (!name) throw new Error('姓名必填');
  return repository.save({ name, email });
}

// 输出:基础测试用例
describe('createUser', () => {
  it('should create user with valid data', () => {
    // 自动生成的基础测试
  });

  it('should throw error when name is empty', () => {
    // 自动生成的边界测试
  });
});

方案2:重点测试,非全面测试

ts 复制代码
// 只测试核心业务逻辑,不追求100%覆盖率

// ✅ 值得测试的(高ROI)
describe('支付流程', () => {
  it('用户支付后订单状态更新', () => { ... });
  it('库存不足时阻止支付', () => { ... });
});

// ❌ 可以不测试的(低ROI)
describe('工具函数', () => {
  it('add函数应该能加数字', () => { ... }); // 过度测试
});

方案3:测试模板和规范

ts 复制代码
// 团队统一的测试模板
// tests/templates/service.test.template.js
describe('{{serviceName}}', () => {
  beforeEach(() => {
    // 标准化的setup
  });

  it('should handle valid input', () => {
    // 标准化的测试结构
  });

  it('should throw error for invalid input', () => {
    // 标准化的错误测试
  });
});
实际的团队渐进式策略
ts 复制代码
// 只要求测试"赚钱"和"赔钱"的流程
const CRITICAL_FLOWS = [
  '用户注册',     // 影响获客
  '支付流程',     // 影响收入
  '数据导出',     // 影响客户信任
  '管理员操作'    // 影响安全
];
基础设置支持
复制代码
# 团队提供的测试工具
devDependencies:
  test-utils: '^1.0.0'  # 封装复杂测试逻辑
  mock-server: '^2.0.0'  # 简化外部依赖模拟
  coverage-check: '^1.0.0'  # 自动覆盖率检查
相关推荐
yogalin19931 小时前
如何实现一个简化的响应式系统
前端
kyriewen112 小时前
项目做了一半想重写?这套前端架构让你少走3年弯路
前端·javascript·chrome·架构·ecmascript·html5
HashTang2 小时前
我用 Cloudflare Workers + GitHub Actions 做了个 2.5 刀/月的 AI 日报,代码开源了
前端·ai编程·aiops
老王以为2 小时前
前端重生之 - 前端视角下的 Python
前端·后端·python
饭后一颗花生米2 小时前
2026 AI加持下前端学习路线:从入门到进阶,高效突破核心竞争力
前端·人工智能·学习
五号厂房2 小时前
TypeScript 类型导入详解:import type 与 import {type}
前端
果然_2 小时前
为什么你的 PR 总是多出一堆奇怪的 commit?90% 的人都踩过这个 Git 坑
前端·git
xpyjs2 小时前
零依赖、链式调用、可变对象:重新设计 JavaScript 颜色处理体验
前端
WayneYang2 小时前
Node.js 全栈知识点详细整理(含代码示例 + 前端结合实战)
前端·node.js