[更新迭代 - 1] Nestjs 在24年底更新了啥?(功能篇)

大家好我是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;
  };
}

PR:github.com/nestjs/nest...

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...

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-fastifyFastifyAdapter 源码里,你会看到两行看起来"神秘"的赋值:

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 新增一个监听

PR:github.com/nestjs/nest...

很多时候 我们也许会用到下面的代码 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,这里列举(算了不列举了 篇幅有点大 放下一篇文章)

相关推荐
liangshanbo12151 天前
React 19 新特性:原生支持在组件中渲染 <meta> 与 <link>
前端·javascript·react.js
小小工匠1 天前
基于 Spring AI 的多平台多模型动态切换实战
后端
浩男孩1 天前
🍀发现个有趣的工具可以用来随机头像🚀🚀
前端
咸菜一世1 天前
scala中class的使用
后端
豆浆Whisky1 天前
反射还是代码生成?Go反射使用的边界与取舍|Go语言进阶(11)
后端·go
Penge6661 天前
MySQL 分页优化
后端
华仔啊1 天前
前后端防重复提交的 6 种落地实现:从按钮禁用到 AOP 全自动防护
java·后端
前端 贾公子1 天前
《Vuejs设计与实现》第 18 章(同构渲染)(下)
前端·javascript·html
程序新视界1 天前
MySQL的OR条件查询不走索引及解决方案
数据库·后端·mysql
U.2 SSD1 天前
ECharts 日历坐标示例
前端·javascript·echarts