首页 / NestJS 入门教程 / 高级技巧

NestJS 入门教程

高级技巧

本教程共 47 篇 · 第 43 篇 · 更新于 2026-08-09 · 约 15 分钟阅读

NestJS热重载REPLCQRSSSECookieSession

本节目标:学习NestJS的高级开发技巧,包括热重载、REPL调试、CQRS模式、Server-Sent Events、Cookie和Session管理,提升开发效率和代码质量。

写到这里,你已经掌握了NestJS的核心功能。这一章我们来聊几个实用的高级技巧,它们能让你的开发体验更爽,代码架构更优雅。

热重载(Hot Module Replacement)

开发时最烦的就是改一行代码就要等半天重启。webpack的热模块替换(HMR)能解决这个问题,改完代码自动生效,不用重启应用。

安装依赖

npm i --save-dev webpack-node-externals run-script-webpack-plugin webpack
Tip

如果你用的是Yarn Berry(不是经典版Yarn),装webpack-pnp-externals代替webpack-node-externals

配置webpack

在项目根目录创建webpack-hmr.config.js

const nodeExternals = require('webpack-node-externals');
const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin');

module.exports = function (options, webpack) {
  return {
    ...options,
    entry: ['webpack/hot/poll?100', options.entry],
    externals: [
      nodeExternals({
        allowlist: ['webpack/hot/poll?100'],
      }),
    ],
    plugins: [
      ...options.plugins,
      new webpack.HotModuleReplacementPlugin(),
      new webpack.WatchIgnorePlugin({
        paths: [/\.js$/, /\.d\.ts$/],
      }),
      new RunScriptWebpackPlugin({ 
        name: options.output.filename, 
        autoRestart: false 
      }),
    ],
  };
};

这个配置做了三件事:

  • 启用热模块替换插件
  • 忽略已编译的JS和类型声明文件
  • 自动运行编译后的代码

修改main.ts

在入口文件里加上HMR支持:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

declare const module: any;

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);

  if (module.hot) {
    module.hot.accept();
    module.hot.dispose(() => app.close());
  }
}
bootstrap();

module.hot.accept()告诉webpack接受模块更新,dispose()在模块被替换时关闭应用。

启动命令

package.json里加个脚本:

{
  "scripts": {
    "start:dev": "nest build --webpack --webpackPath webpack-hmr.config.js --watch"
  }
}

运行npm run start:dev,改代码试试,应该能立即生效。

Warning

webpack不会自动复制资源文件(比如GraphQL的schema文件)到dist目录。如果你的项目用到了这类资源,需要手动处理。

REPL交互式环境

REPL(Read-Eval-Print-Loop)是个交互式环境,能让你在终端里直接调用服务方法、查看依赖图。调试的时候特别好用。

创建repl.ts

在项目根目录创建repl.ts

import { repl } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  await repl(AppModule);
}
bootstrap();

启动REPL

npm run start -- --entryFile repl

启动后会看到:

LOG [NestFactory] Starting Nest application...
LOG [InstanceLoader] AppModule dependencies initialized
LOG REPL initialized

常用命令

获取服务实例

> get(AppService).getHello()
'Hello World!'

get()函数能从依赖容器里取出服务实例,直接调用它的方法。

查看可用方法

> methods(AppController)

Methods:
 ◻ getHello

methods()列出某个控制器或服务的所有公开方法。

查看依赖图

> debug()

AppModule:
 - controllers:
  ◻ AppController
 - providers:
  ◻ AppService

debug()打印所有注册的模块、控制器和提供者,帮你理清依赖关系。

查看帮助

> help()

列出所有可用的内置函数。

Tip

开发时可以加--watch参数运行REPL,代码改动会自动重载:

npm run start -- --watch --entryFile repl

保存历史记录

REPL重载后历史记录会丢失,可以这样保存:

async function bootstrap() {
  const replServer = await repl(AppModule);
  replServer.setupHistory(".nestjs_repl_history", (err) => {
    if (err) console.error(err);
  });
}

这样每次输入的命令都会保存到.nestjs_repl_history文件里。

CQRS模式

CQRS(Command Query Responsibility Segregation)把读写操作分开处理。写操作用Command,读操作用Query,各自有独立的处理器。

安装

npm install --save @nestjs/cqrs

基本用法

在根模块导入CqrsModule:

import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';

@Module({
  imports: [CqrsModule.forRoot()],
})
export class AppModule {}

Command(命令)

Command用来改变应用状态。比如”杀龙”这个操作:

// kill-dragon.command.ts
export class KillDragonCommand {
  constructor(
    public readonly heroId: string,
    public readonly dragonId: string,
  ) {}
}

创建Command Handler处理这个命令:

// kill-dragon.handler.ts
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { KillDragonCommand } from './kill-dragon.command';

@CommandHandler(KillDragonCommand)
export class KillDragonHandler implements ICommandHandler<KillDragonCommand> {
  constructor(private repository: HeroesRepository) {}

  async execute(command: KillDragonCommand) {
    const { heroId, dragonId } = command;
    const hero = this.repository.findOneById(+heroId);
    
    hero.killEnemy(dragonId);
    await this.repository.persist(hero);
  }
}

在Service里通过CommandBus发送命令:

@Injectable()
export class HeroesGameService {
  constructor(private commandBus: CommandBus) {}

  async killDragon(heroId: string, dragonId: string) {
    return this.commandBus.execute(
      new KillDragonCommand(heroId, dragonId)
    );
  }
}

Query(查询)

Query用来读取数据,不改变状态:

// get-hero.query.ts
export class GetHeroQuery {
  constructor(public readonly heroId: string) {}
}

创建Query Handler:

// get-hero.handler.ts
import { QueryHandler, IQueryHandler } from '@nestjs/cqrs';
import { GetHeroQuery } from './get-hero.query';

@QueryHandler(GetHeroQuery)
export class GetHeroHandler implements IQueryHandler<GetHeroQuery> {
  constructor(private repository: HeroesRepository) {}

  async execute(query: GetHeroQuery) {
    return this.repository.findOneById(query.heroId);
  }
}

通过QueryBus执行查询:

const hero = await this.queryBus.execute(new GetHeroQuery(heroId));

Event(事件)

事件用来通知其他模块状态发生了变化:

// hero-killed-dragon.event.ts
export class HeroKilledDragonEvent {
  constructor(
    public readonly heroId: string,
    public readonly dragonId: string,
  ) {}
}

在模型里触发事件:

import { AggregateRoot } from '@nestjs/cqrs';

export class Hero extends AggregateRoot {
  constructor(private id: string) {
    super();
  }

  killEnemy(enemyId: string) {
    // 业务逻辑
    this.apply(new HeroKilledDragonEvent(this.id, enemyId));
  }
}

创建Event Handler处理事件:

// hero-killed-dragon.handler.ts
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { HeroKilledDragonEvent } from './hero-killed-dragon.event';

@EventsHandler(HeroKilledDragonEvent)
export class HeroKilledDragonHandler 
  implements IEventHandler<HeroKilledDragonEvent> {
  
  handle(event: HeroKilledDragonEvent) {
    // 处理事件,比如更新读模型、发送通知等
    console.log(`英雄${event.heroId}杀死了龙${event.dragonId}`);
  }
}
Note

别忘了把所有Handler注册到模块的providers里。

Saga(长流程编排)

Saga是长期运行的进程,监听事件并触发新的命令。比如英雄杀龙后掉落装备:

// heroes-game.saga.ts
import { Injectable } from '@nestjs/common';
import { Saga, ofType } from '@nestjs/cqrs';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { HeroKilledDragonEvent } from './hero-killed-dragon.event';
import { DropAncientItemCommand } from './drop-ancient-item.command';

@Injectable()
export class HeroesGameSagas {
  @Saga()
  dragonKilled = (events$: Observable<any>): Observable<any> => {
    return events$.pipe(
      ofType(HeroKilledDragonEvent),
      map((event) => new DropAncientItemCommand(event.heroId, 'ancient-sword')),
    );
  };
}

Saga监听HeroKilledDragonEvent,自动发出DropAncientItemCommand

Tip

CQRS适合复杂业务场景,简单的CRUD应用没必要用。读写分离能让系统更易扩展,但也增加了复杂度。

Server-Sent Events(SSE)

SSE是服务器向客户端推送实时数据的技术。客户端建立连接后,服务器可以持续发送更新。

创建SSE端点

@Sse()装饰器标记路由:

import { Controller, Sse } from '@nestjs/common';
import { Observable, interval } from 'rxjs';
import { map } from 'rxjs/operators';

@Controller()
export class AppController {
  @Sse('sse')
  sse(): Observable<MessageEvent> {
    return interval(1000).pipe(
      map((_) => ({ data: { time: new Date().toISOString() } }))
    );
  }
}

@Sse()MessageEvent都从@nestjs/common导入。SSE路由必须返回Observable流。

MessageEvent结构

export interface MessageEvent {
  data: string | object;  // 必填,消息内容
  id?: string;            // 可选,消息ID
  type?: string;          // 可选,事件类型
  retry?: number;         // 可选,重连间隔(毫秒)
}

客户端接收

浏览器用EventSource API接收:

const eventSource = new EventSource('/sse');

eventSource.onmessage = ({ data }) => {
  console.log('收到消息:', JSON.parse(data));
};

EventSource会自动维持连接,收到消息时触发onmessage回调。

处理客户端断开

客户端关闭连接时,NestJS会自动取消订阅。可以用finalize操作符做清理工作:

import { finalize } from 'rxjs/operators';

@Sse('sse')
sse(): Observable<MessageEvent> {
  return interval(1000).pipe(
    map((_) => ({ data: { hello: 'world' } })),
    finalize(() => console.log('客户端断开连接'))
  );
}
Tip

SSE适合单向推送场景,比如实时通知、股票行情。如果需要双向通信,用WebSocket(第36章)。

Cookie管理

Cookie是存储在浏览器的小数据,每次请求会自动带上。常用于存储会话ID、用户偏好等。

Express适配器

安装cookie-parser:

npm i cookie-parser
npm i -D @types/cookie-parser

在main.ts里启用:

import * as cookieParser from 'cookie-parser';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(cookieParser());
  await app.listen(3000);
}

读取Cookie

import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';

@Controller()
export class AppController {
  @Get()
  findAll(@Req() request: Request) {
    console.log(request.cookies);
    // 或者读取特定cookie
    const token = request.cookies['token'];
  }
}

设置Cookie

import { Controller, Get, Res } from '@nestjs/common';
import { Response } from 'express';

@Controller()
export class AppController {
  @Get()
  findAll(@Res({ passthrough: true }) response: Response) {
    response.cookie('token', 'abc123', {
      httpOnly: true,
      maxAge: 3600000, // 1小时
    });
  }
}
Warning

@Res()时要设置passthrough: true,否则NestJS不会自动处理响应。

自定义Cookie装饰器

为了更方便,可以创建自定义装饰器:

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const Cookies = createParamDecorator(
  (data: string, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return data ? request.cookies?.[data] : request.cookies;
  }
);

使用时:

@Get()
findAll(@Cookies('token') token: string) {
  console.log(token);
}

Fastify适配器

Fastify用@fastify/cookie

npm i @fastify/cookie
import fastifyCookie from '@fastify/cookie';

const app = await NestFactory.create(AppModule, new FastifyAdapter());
await app.register(fastifyCookie, {
  secret: 'my-secret', // 用于签名cookie
});

读取和设置方式类似,只是API略有不同:

// 读取
request.cookies['token']

// 设置
response.setCookie('token', 'abc123')

Session管理

Session在服务端存储用户会话信息,通过Cookie里的Session ID关联。适合存储登录状态、购物车等。

Express适配器

安装express-session:

npm i express-session
npm i -D @types/express-session

在main.ts里配置:

import * as session from 'express-session';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  app.use(
    session({
      secret: 'my-secret-key',
      resave: false,
      saveUninitialized: false,
    })
  );
  
  await app.listen(3000);
}

参数说明:

  • secret:签名cookie的密钥
  • resave:即使没修改也强制保存session
  • saveUninitialized:强制保存未初始化的session
Warning

默认的session存储是内存,不适合生产环境。生产环境要用Redis等外部存储。

使用Session

import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';

@Controller()
export class AppController {
  @Get()
  findAll(@Req() request: Request) {
    // 读取session
    const visits = request.session.visits;
    
    // 设置session
    request.session.visits = visits ? visits + 1 : 1;
  }
}

或者用@Session()装饰器:

import { Controller, Get, Session } from '@nestjs/common';

@Controller()
export class AppController {
  @Get()
  findAll(@Session() session: Record<string, any>) {
    session.visits = session.visits ? session.visits + 1 : 1;
  }
}

Fastify适配器

Fastify用@fastify/secure-session

npm i @fastify/secure-session
import secureSession from '@fastify/secure-session';

const app = await NestFactory.create(AppModule, new FastifyAdapter());
await app.register(secureSession, {
  secret: 'averylongphrasebiggerthanthirtytwochars',
  salt: 'mq9hDxBVDbspDR6n',
});

使用方式:

@Get()
findAll(@Req() request: FastifyRequest) {
  const visits = request.session.get('visits');
  request.session.set('visits', visits ? visits + 1 : 1);
}
Tip

Cookie适合存储少量不敏感数据(如用户偏好),Session适合存储敏感信息(如登录状态)。JWT Token则是无状态方案,适合分布式系统。

小结

这一章学了几个实用技巧:

热重载让代码改动立即生效,不用反复重启应用,开发效率翻倍。

REPL提供交互式环境,能直接调用服务方法、查看依赖图,调试特别方便。

CQRS把读写操作分离,Command处理写,Query处理读,Event通知变化,Saga编排流程。适合复杂业务场景。

SSE实现服务器向客户端的单向推送,简单轻量,适合实时通知场景。

Cookie和Session管理用户状态,Cookie存浏览器,Session存服务端,各有适用场景。

这些技巧不是每个项目都要用,但了解它们能在合适的场景派上用场。下一章我们聊单元测试。