[更新迭代 - 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,这里列举(算了不列举了 篇幅有点大 放下一篇文章)

相关推荐
在云端易逍遥2 小时前
前端必学的 CSS Grid 布局体系
前端·css
ccnocare2 小时前
选择文件夹路径
前端
艾小码2 小时前
还在被超长列表卡到崩溃?3招搞定虚拟滚动,性能直接起飞!
前端·javascript·react.js
闰五月2 小时前
JavaScript作用域与作用域链详解
前端·面试
泉城老铁2 小时前
idea 优化卡顿
前端·后端·敏捷开发
前端康师傅2 小时前
JavaScript 作用域常见问题及解决方案
前端·javascript
司宸2 小时前
Prompt结构化输出:从入门到精通的系统指南
前端
我是日安2 小时前
从零到一打造 Vue3 响应式系统 Day 9 - Effect:调度器实现与应用
前端·vue.js