大家好我是Joney 好久没回来了,为了回归到Nest这个专题中,我最近在看Nest的 release 发现官方还是很活跃的,作为一个开源项目 迭代和维护是非常重要的事情, 这篇文章我们来唠嗑一下 Nest 24年的更新内容。本文梳理了从24年1月份到 24年底的所有官方更新内容,我不会一一罗列,挑重点讲(bugfix/feature)
release一览
以下统计是简单粗略的统计,后续文章会改进此方法。数据上可能不是100%准确
Nest 从 24年1月开发的24年第一次 release v10.3.1 直到年底 一共发布了13个release v10.4.15 ,到25年直接从v10.x 迈入 v11.x
这些不断地release和仓库迭代, 看得出作者和开发社区还是很用心的,至少是一个值得信赖的项目,不像其它项目做完KPI就跑路, 蛐蛐一下国内的某些大厂哈(......)
是开源是很重要,但是持续维护才能让开发者信任;另提一个事情 了解我的朋友都知道 我的职业重点一直都是App相关的领域,最近 Lynx 发布了他们的3.4新版本 ,对 HarmonyOS 生态有了一定的支持(比RN强哈哈 在这一点上 好像RN 对此并不是友好啊),持续观望中,后续有机会给大家叨叨。
总结来说,24年 Nest的工作分布如下
可以看到许多工作是依赖升级迭代支持相关的内容。bug修复和功能升级还是很快的,截止发文官方仓库的openIssue在29个

再看 以月份为维度的分析:老外的年底也卷 哈哈哈
重要更新
接下来我们分析一下 24年 的重要功能更新和bug修复
功能增强
24年一共新增了 13个相关的内容,
01.为装饰器 @Header 添加可动态获取的值,
在以往的版本中 如果你想要下面的操作动态获取值是不行的,因为Nest只会在启动的时候初始化一次后续不在变化
ts
@Header('Last-Modified', new Date().toUTCString())
此次更新解决了这个问题(源代码如下新增了 set的方式和方法
ts
export function Header(
name: string,
value: string | (() => string),// value 可为函数
): MethodDecorator {
return (
target: object,
key: string | symbol,
descriptor: TypedPropertyDescriptor<any>,
) => {
extendArrayMetadata(HEADERS_METADATA, [{ name, value }], descriptor.value);
return descriptor;
};
}
02.可选择影响 OpenSSL 协议行为,添加了一个可以获取sourceOptions的方式
这个东西用的不算多,主要是和TSL安全相关,问题的来源是这样的: 在 NestJS(Node.js)里如何只允许安全的 TLS 密码套件,并禁用弱算法,如果想定义自定义的其他算法或者降级,那么是无法做到的因为Nest没有提供这个选项,翻了一下之前版本的源代码 实际上secureOptions是可选的 不能自定义,
ts
import { constants } from 'crypto';
import fs from 'fs';
import path from 'path';
import { HttpsOptions } from '@nestjs/common/interfaces/external/https-options.interface';
const keyPath = path.resolve(__dirname, 'cert', 'stock-tracker.key.pem');
const certPath = path.resolve(__dirname, 'cert', 'stock-tracker.cert.pem');
const ciphers = [
'ECDHE-ECDSA-AES256-GCM-SHA384',
'ECDHE-RSA-AES256-GCM-SHA384',
'ECDHE-ECDSA-CHACHA20-POLY1305',
'ECDHE-RSA-CHACHA20-POLY1305',
'ECDHE-ECDSA-AES128-GCM-SHA256',
'ECDHE-RSA-AES128-GCM-SHA256',
'ECDHE-ECDSA-AES256-SHA384',
'ECDHE-RSA-AES256-SHA384',
'ECDHE-ECDSA-AES128-SHA256',
'ECDHE-RSA-AES128-SHA256'
];
export const httpsOptions: HttpsOptions = {
key: fs.readFileSync(keyPath),
cert: fs.readFileSync(certPath),
ciphers: ciphers.join(';'),
secureOptions:
constants.SSL_OP_NO_TLSv1_1 |
constants.SSL_OP_NO_TLSv1 |
constants.SSL_OP_NO_SSLv3 |
constants.SSL_OP_NO_SSLv2
};
原文在:www.reddit.com/r/node/comm...
03 设置多 cookie 标头
cookie 标头,您可以有多个具有相同键的标头(两个 Set-Cookie )。在原来的版本中,只有 Fastify 处理了多次set的问题;使用现有的 setHeader 方法,Express 中的标头无法附加。 新功能是如下所示,Express 实现调用 response.append(name, value),Fastify 实现调用 'response.header(name, value你可以在 Express 中拥有多个 Set-Cookie 标头
tsresponse.append(name,
response.header(name, value)
04 对 RabbitMQ 功能针对性优化(ts类型声明的优化)
问题:RabbitMQ 微服务客户端只允许根据 client/client-rmq.ts:136(Nest源代码) 向底层 ampqlib 传递与套接字选项相关的属性。无法传递额外的连接选项(如 ssl 证书和客户端证书)或指定不同的身份验证方法(如 EXTERNAL)。
解决:对于底层 connectionOptions 的专递给出了新的Options和实现
ts
export interface AmqpConnectionManagerSocketOptions {
+++
connectionOptions?: AmqpConnectionOptions; // 从any 变成了具体的类型
+++
}
import { ConnectionOptions } from 'tls';
import { TcpSocketConnectOpts } from 'net';
type AmqpConnectionOptions = (ConnectionOptions | TcpSocketConnectOpts) & {
noDelay?: boolean;
timeout?: number;
keepAlive?: boolean;
keepAliveDelay?: number;
clientProperties?: any;
credentials?:
| {
mechanism: string;
username: string;
password: string;
response: () => Buffer;
}
| {
mechanism: string;
response: () => Buffer;
}
| undefined;
};
05.升级 @fastify/static 到v7版本
06.为buffer的 body 读取新增了一个 装饰器 @RawBody
ts
// 在没有这个装饰器前 我们需要从req写慢慢读取
@Get('/')
myRoute(@Req() req: RawBodyRequest<unknown>) {
const { rawBody } = req
// ...
}
// 功能新增之后可以直接读取了
@Get('/')
myRoute(@RawBody() rawBody: Buffer) {
// ...
}
官方文档使用指南: docs.nestjs.com/faq/raw-bod...
07.实现了一个方法 用于手动关闭ws链接
ts
// packages/platform-ws/adapters/ws-adapter.ts
public async close(server: any) {
const closeEventSignal = new Promise((resolve, reject) =>
server.close(err => (err ? reject(err) : resolve(undefined))),
);
for (const ws of server.clients) {
ws.terminate();
}
await closeEventSignal;
}
08.API一致性优化
如果我们在构造函数上使用 @Inject(),Nest 会认为这是由于某种循环(基本上与 @Inject(undefined)一样)。虽然我们应该在基于属性的注入上使用 @Inject(), 下面的代码会导致程序报错,但是这个报错的类型其实很令人费解
ts
import { Module, Inject } from '@nestjs/common';
class Foo {}
@Module({
providers: [Foo],
})
export class AppModule {
@Inject() foo1: Foo
constructor(
@Inject(Foo) readonly foo2: Foo,
@Inject() readonly foo3: Foo, // Fails
) {}
onModuleInit() {
console.log('foo1 instanceof Foo:', this.foo1 instanceof Foo)
console.log('foo1 === foo2:', this.foo1 === this.foo2)
console.log('foo1 === foo3:', this.foo1 === this.foo3)
}
}
shell
[Nest] 286235 - 04/12/2024, 12:15:25 PM ERROR [ExceptionHandler] Nest can't resolve dependencies of the AppModule (Foo, ?). Please make sure that the argument dependency at index [1] is available in the AppModule context.
Potential solutions:
- Is AppModule a valid NestJS module?
- If dependency is a provider, is it part of the current AppModule?
- If dependency is exported from a separate @Module, is that module imported within AppModule?
@Module({
imports: [ /* the Module containing dependency */ ]
})
它不应该是这样 期待的是 能够正常运行才是

现在这个PR 调整了这种判断
09. 优化多次MiddlewareConsumer
exclude逻辑补充
问题:看下面的代码
ts
configure(consumer: MiddlewareConsumer) {
consumer
.apply((req, res, next) => res.send(MIDDLEWARE_VALUE))
.exclude('test', 'overview/:id', 'wildcard/(.*)', { // 暂时叫做 exclude1 好了
path: 'middleware',
method: RequestMethod.POST,
}) // ignored
.exclude('multiple/exclude')
.forRoutes('*');
}
// 我们其实期待 是先 exclude1 在此基础上 还要把 multiple/exclude exclude掉,
// 但是实际上却是,会直接覆盖到 exclude1 里去
configure(consumer: MiddlewareConsumer) {
consumer
.apply((req, res, next) => res.send(MIDDLEWARE_VALUE))
.exclude('test', 'overview/:id', 'wildcard/(.*)', 'multiple/exclude', {
path: 'middleware',
method: RequestMethod.POST,
})
.forRoutes('*');
}
解决:在这个PR中 成功的解决了这个问题
10.forwardRef带来的问题修复
我们先来 说说啥是 forwardRef好了,文档在这里 docs.nestjs.com/fundamental... 我们以前总是会发生一些 循环依赖的问题,现在Nest提供新 forwardRef 的方法来解决这个问题,比如
ts
// 下面两个 service 如果相互依赖 那么可以使用 这个方法来解决循环依赖问题,遇到循环依赖问题,程序将run不起来
@Injectable()
export class CatsService {
constructor(
@Inject(forwardRef(() => CommonService))
private commonService: CommonService,
) {}
}
@Injectable()
export class CommonService {
constructor(
@Inject(forwardRef(() => CatsService))
private catsService: CatsService,
) {}
}
// 注意一点 实例化的顺序是不确定的,所以你的 constructor 中的实现最好不要用到这两个互相引用的玩意儿
// 要不然会出问题
// 对于 module 来说同样适用
@Module({
imports: [forwardRef(() => CatsModule)],
})
export class CommonModule {}
@Module({
imports: [forwardRef(() => CommonModule)],
})
export class CatsModule {}
但是之前的版本如果你这样写 就会有问题
ts
import { Module, forwardRef } from '@nestjs/common';
@Module({})
export class FooModule {}
@Module({
imports: [
forwardRef(() => FooModule),
],
exports: [forwardRef(() => FooModule)],
})
export class AppModule {}
如果我们导出来一个 dynamicModule 的时候也会有相同的问题

当我们使用 forwardRef 时,这会破坏 ConditionModule 的 @nestjs/config。这个PR就解决了这个问题,并且把错误的提示也优化成了警告。
11.一个与vite ssr 有关的功能
在 @nestjs/platform-fastify
的 FastifyAdapter
源码里,你会看到两行看起来"神秘"的赋值:
ts
this.skipMiddie = true;
this.isMiddieRegistered = true;
它们并不是 Fastify 本身的 API,而是 NestJS 为了**"到底要不要再用一次 middie 插件"自己维护的内部状态标记**。
我们先看 skipMiddie,Fastify 有一个官方插件 middie
,用来把 Express 风格的中间件(function (req, res, next) {}
)挂到 Fastify 的 onRequest
钩子上。 NestJS 的 FastifyAdapter
默认就会注册这个插件,从而让你可以:
ts
app.use(cors());
app.use(helmet());
isMiddieRegistered的意思是,标记我已经注册过 middie,后面别重复 若 FastifyAdapter 时手动传了 ignoreMiddie: true,或者 Nest 发现你给的实例已经注册过 middie,那么registerMiddie() 会直接return 避免重复创建,
两者配合,保证 middie 只被挂一次,避免重复报错,也让用户可以通过 ignoreMiddie 选项彻底关掉 Express 中间件兼容层。
这个PR的作者提到了这样的一个工程:github.com/nestjs/nest... 它实际上和我前面提及的Newegg中的基于Nest的React SSR 是同样的东西,但是使用了vite去build,在vite的集成到 FastifyAdapter 时 会写下面的代码
ts
async function bootstrapNestJs() {
const adapter = new FastifyAdapter({ logger: true });
await adapter.register(FastifyVite as any, {
...
});
// 告知Nest这个玩意儿已经注册过了 不要再注册---这个是由开发者自己写的 Nest 还没有提供这种包内的判断逻辑
(adapter as any).isMiddieRegistered = true;
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
adapter,
);
const server = app.getHttpAdapter().getInstance() as any;
// vite
await server.vite.ready();
await app.listen(3000);
}
之所以写这个代码
ts
(adapter as any).isMiddieRegistered = true;)
是因为 @fastify/vite & @nestjs/platform-fastify 都注册了 middie
. 然后Nest 就开始报错了....,为此这个作者想的一个方法是 在Nest中添加一个字段标识 表示 middle 已经注册。这个PR就是干这个事的,下面列举这个PR的核心改动
ts
// packages/platform-fastify/adapters/fastify-adapter.ts
if ((instanceOrOptions as FastifyAdapterBaseOptions)?.skipMiddie) {
this.isMiddieRegistered = true;
}
提个PR给Vite不好么?狗头保命
12. 为Application 的onReady 新增一个监听
很多时候 我们也许会用到下面的代码 onApplicationBootstrap
,但是这个东西是在 await app.listen
之前执行的, 我们希望能提供一个 在 app.listen 准备好之后再执行的 方法,为此这个 PR 提供了这个方法,当你在使用的时候可以这样:
ts
@Injectable()
export class AppService {
constructor(httpAdapterHost: HttpAdapterHost) {
// Can use it in the "onModuleInit" or "onApplicationBoostrap" hooks too
// Supports multiple subscribers (uses RxJS Subject under the hood)
httpAdapterHost.listen$.subscribe(() => console.log('HTTP server is listening'));
// Can also verify the current status using the "listening" getter
console.log(httpAdapterHost.listening)
}
}
虽然我们在不用这个PR的时候有别的解决方法比如,(docs.nestjs.com/techniques/...)
ts
await app.listen(...);
await app.get(EventEmitter2).emit(...);
nest作者 kamilmysliwiec 觉得这个方法应该就够用了,然后老哥 mckramer 搬来了Spring 的文档.... emm kamilmysliwiec 才合并了这个PR..... 好好好 有理有据
13.程序异常的延迟关闭策略
run nestjs 应用程序期间使用生命周期方法,并进行异步作,例如对数据库、消息/作业处理的调用,并且收到 SIGTERM 信号(发生了异常),则在执行这些异步作时,异步作所需的连接可能会关闭。有时候,这种情况时不时地发生在 K8s 集群中,该进程可以随时终止。这可能会导致意外错误。
这个PR 提供的一个解决方案是应用程序应延迟调用关闭hook,直到应用完全初始化。
大概的意思就是说,就算应用程序初始化失败了,也应该把Nest的生命周期完全的调用一遍,此PR核心代码如下
ts
packages/core/nest-application-context.ts
public async close(signal?: string): Promise<void> {
await this.initializationPromise; // 也要await 初始化的时候 的 promise 全部返回
await this.callDestroyHook();
await this.callBeforeShutdownHook(signal);
++++
}
14.能够在使用 Kafka 时使用 emit 函数发送多条消息
这个PR主要想实现下面的功能
ts
// reference code at [client-kafka] lib
// (https://github.com/nestjs/nest/blob/master/packages/microservices/client/client-kafka.ts)
protected async dispatchEventBatch(packets: OutgoingEvent[]): Promise < any > {
if(!packets.length) return;
const pattern = this.normalizePattern(packets[0].pattern);
const outgoingEvents = await Promise.all(
packets.map((packet) => {
return this.serializer.serialize(packet.data, { pattern }
});
);
const message = Object.assign(
{
topic: pattern,
messages: outgoingEvents,
},
this.options.send || {},
);
return this.producer.send(message);
}
这个PR合并之后 为kafka这个nest包新增了一个 emitBatch 方法
ts
ublic emitBatch<TResult = any, TInput = any>(
pattern: any,
data: { messages: TInput[] },
): Observable<TResult> {
if (isNil(pattern) || isNil(data)) {
return _throw(() => new InvalidMessageException());
}
const source = defer(async () => this.connect()).pipe(
mergeMap(() => this.dispatchBatchEvent({ pattern, data })),
);
const connectableSource = connectable(source, {
connector: () => new Subject(),
resetOnDisconnect: false,
});
connectableSource.connect();
return connectableSource;
}
public commitOffsets(
topicPartitions: TopicPartitionOffsetAndMetadata[],
): Promise<void> {
if (this.consumer) {
return this.consumer.commitOffsets(topicPartitions);
} else {
throw new Error('No consumer initialized');
}
}
protected async dispatchBatchEvent<TInput = any>(
packets: ReadPacket<{ messages: TInput[] }>,
): Promise<any> {
if (packets.data.messages.length === 0) {
return;
}
const pattern = this.normalizePattern(packets.pattern);
const outgoingEvents = await Promise.all(
packets.data.messages.map(message => {
return this.serializer.serialize(message as any, { pattern });
}),
);
const message = Object.assign(
{
topic: pattern,
messages: outgoingEvents,
},
this.options.send || {},
);
return this.producer.send(message);
}
bugfix
24年一共修复了 22 个bug,这里列举(算了不列举了 篇幅有点大 放下一篇文章)