高级技巧
本教程共 47 篇 · 第 43 篇 · 更新于 2026-08-09 · 约 15 分钟阅读
本节目标:学习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,改代码试试,应该能立即生效。
Warningwebpack不会自动复制资源文件(比如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。
TipCQRS适合复杂业务场景,简单的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('客户端断开连接'))
);
}
TipSSE适合单向推送场景,比如实时通知、股票行情。如果需要双向通信,用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:即使没修改也强制保存sessionsaveUninitialized:强制保存未初始化的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);
}
TipCookie适合存储少量不敏感数据(如用户偏好),Session适合存储敏感信息(如登录状态)。JWT Token则是无状态方案,适合分布式系统。
小结
这一章学了几个实用技巧:
热重载让代码改动立即生效,不用反复重启应用,开发效率翻倍。
REPL提供交互式环境,能直接调用服务方法、查看依赖图,调试特别方便。
CQRS把读写操作分离,Command处理写,Query处理读,Event通知变化,Saga编排流程。适合复杂业务场景。
SSE实现服务器向客户端的单向推送,简单轻量,适合实时通知场景。
Cookie和Session管理用户状态,Cookie存浏览器,Session存服务端,各有适用场景。
这些技巧不是每个项目都要用,但了解它们能在合适的场景派上用场。下一章我们聊单元测试。