NestJS 12升级踩坑:从Webpack到Rspack,我折腾了一整个周末

作者:Flynt日期:2026/9/5

说实话,看到NestJS 12的changelog时我是有点兴奋的。 8月27号发布,ESM-first、Rspack替代Webpack、Standard Schema验证、@nestjs/observe原生可观测性——每一条都戳在痛点上。特别是Rspack替代Webpack那条,我心想:终于不用等turbopack慢吞吞地编译了。

然后我花了整个周末才把项目跑起来。 先说下项目情况:一个中等体量的B端后台系统,NestJS 11.2.0 + Webpack monorepo架构,用了NATS消息队列、GraphQL订阅、大约40个module。升级前我特意跑了nest upgrade --dry-run看了看报告——嗯,报告写得很漂亮,告诉我"大部分迁移会自动完成"。 大部分。注意这个词。

周六上午:Webpack到Rspack

升级命令跑得很顺利:

1npm i -g @nestjs/cli@latest
2nest upgrade
3

CLI自动帮我把nest-cli.json里的webpack配置改了,@nestjs/*包全升到v12兼容版本。本地nest build一跑——过了。 我心想这事也没网上说的那么麻烦嘛。 然后nest start。 报错。

1Error: Cannot find module '@nestjs/core'
2Require stack:
3- /path/to/project/dist/main.js
4

我盯着这个报错看了五分钟。@nestjs/core?刚才build不是过了吗? 周六下午三点,我第三次跑nest start,还是同样的报错。我甚至怀疑是不是npm缓存出了问题,清了缓存重装依赖,没用。翻了半天issue才发现——NestJS 12所有核心包现在是ESM,但我的项目还是CommonJS。Node.js 20.19+的require(esm)确实能兼容,但有个前提:你的build输出格式得配对。 Webpack时代我的tsconfig.json"module": "commonjs",Rspack默认行为不一样。翻了Rspack的NestJS集成文档,发现需要在rspack.config.js里显式指定输出格式:

1// rspack.config.js
2module.exports = {
3  output: {
4    library: { type: 'commonjs2' },
5  },
6  // 关键:告诉Rspack你的目标是Node.js
7  target: 'node',
8};
9

加上这两行,build过了,start也过了。 但别高兴太早——monorepo里的shared library又有问题。之前Webpack用的是ts-loader处理跨包引用,Rspack用的是SWC。我的shared包里有个循环依赖(Module A import Module B import Module A),Webpack时代居然能跑,Rspack直接炸了。 后来我查了下,循环依赖这问题其实madge --circular src一跑就能查出来,但我当时没跑。排查了两个小时,最后老老实实把循环依赖拆了。这不能怪Rspack,Webpack本来就"不应该"让循环依赖跑起来,只是它默默帮你兜住了。Rspack不惯着这毛病。

周六晚上:NATS换包的坑

搞完Rspack已经晚上八点了。我想着顺手把NATS也换了,毕竟官方说就换个包名的事。 NestJS 12把NATS从nats包换成了@nats-io/transport-node。官方migration guide只说了一句"run npm uninstall nats && npm install @nats-io/transport-node"。 问题在于:不只是换个包名。 原来的NATS客户端序列化是Buffer,新版是JSON字符串。如果你的自定义deserializer之前是直接读Buffer的——

1// v11 能跑的代码
2@ClientNATS('SERVICE_A')
3private client: ClientProxy;
4
5// 消息处理
6this.client.send('topic', payload).pipe(
7  // 之前这里拿到的是Buffer
8  map((response) => response.toString('utf-8')),
9);
10

升级后这里拿到的已经是解析好的对象了,你再[.toString()](function toString() { [native code] })会直接报错。 而且自定义deserializer的签名也变了。老版本接收的是Buffer,新版接收的是完整的NATS Message对象:

1// v12 新版deserializer
2deserializer(message: NatsMsg): any {
3  // message是完整的NatsMsg,不是payload
4  return JSON.parse(message.data.toString());
5}
6

我项目里有3个自定义deserializer,全得改。改完跑测试,又有两个mock数据的格式对不上——因为mock数据是按老版本的Buffer格式写的。 这块折腾到晚上十点多。后来我学乖了,先全局搜了一遍nats的import和所有deserializer实现,统一改完再跑测试。

周日:一个隐蔽的hooks顺序问题

周六搞到半夜,周日早上起来继续。这次是Lifecycle Hooks顺序变了。 这个breaking change在changelog里只占一行:"Lifecycle hooks are now invoked by component hierarchy level"。 翻译成人话就是:OnModuleInitOnApplicationBootstrap这些钩子的调用顺序变了。以前是按模块注册顺序来的,现在按组件层级(父模块→子模块)。 我的项目有个数据库连接初始化逻辑放在OnModuleInit里,另一个缓存预热放在OnApplicationBootstrap里。升级后这俩的执行顺序反了——缓存预热先跑了,但数据库还没连上。 启动直接报错。 我一开始根本没注意到这个变更。排查了半天看log才发现,OnApplicationBootstrap比OnModuleInit先执行了。解决方式是把缓存预热挪到一个独立的module里,用imports确保它在数据库module之后初始化。 如果你项目里依赖hooks执行顺序(比如"先连数据库再初始化缓存"这种隐式依赖),一定要提前检查。

新功能:Standard Schema确实香

周日晚上终于把坑都填完了,试了试新功能。Standard Schema验证确实好用。以前@Body()参数验证得写个class-validator的DTO类,现在直接用Zod:

1import { z } from 'zod';
2
3const CreateUserSchema = z.object({
4  name: z.string().min(2),
5  email: z.string().email(),
6  age: z.number().int().positive().optional(),
7});
8
9@Post()
10create(@Body({ schema: CreateUserSchema }) body: z.infer<typeof CreateUserSchema>) {
11  return this.usersService.create(body);
12}
13

配合StandardSchemaValidationPipe,连DTO类都省了。而且schema能直接喂给@nestjs/swagger生成OpenAPI文档——这点很关键,之前class-validator的装饰器和swagger的装饰器要写两遍,现在一份schema搞定。 **@nestjs/observe**试了一下,挺惊喜。不用接Jaeger、不用配OpenTelemetry collector,直接:

1import { createObserveModule, ObserveInstrument } from '@nestjs/observe';
2
3const { ObserveModule } = createObserveModule();
4
5const app = await NestFactory.create(AppModule, {
6  instrument: ObserveInstrument,
7});
8

启动后自动采集HTTP请求耗时、GraphQL解析时间、队列消费耗时。数据格式是OpenTelemetry标准的,后面要接Grafana或者Datadog都能直接用。 结果一跑,Fastify不支持,我人傻了。我们项目恰好用的是Fastify……所以暂时没上线,等官方支持了再说。

值不值得升?

你要问我值不值得升……我周末两天都搭进去了,你说呢? NestJS 12的ESM支持和Rspack确实能带来构建速度提升(我本地monorepo build时间从45秒降到12秒),Standard Schema验证也比class-validator优雅不少。但breaking change不少,特别是NATS用户和依赖hooks顺序的项目,升级成本不低。 我的建议是先在开发环境跑一遍。测试覆盖完整的可以放心升,覆盖不全的先补测试再说。

几个容易踩的坑提前说一下:

  • Node.js必须v20.19+或v22.12+,21.x不支持
  • 循环依赖先用madge --circular src查,Rspack不惯着这个
  • Rspack配置要加target: 'node'library.type
  • NATS不只是换包名,序列化格式变了
  • Lifecycle hooks顺序变了,检查有没有隐式依赖执行顺序
  • GraphQL订阅的subscriptions-transport-ws已移除,必须换graphql-ws

Rspack是真的快,这一点没骗我。


NestJS 12升级踩坑:从Webpack到Rspack,我折腾了一整个周末》 是转载文章,点击查看原文


相关推荐


别把豆包当聊天用了,现在的豆包和以前不一样了
程序员晓凡2026/8/28

大家好,我是晓凡。 豆包算是全民 AI 应用了,上班族、老人、小孩,手机里基本人手一个。我很早就给爸妈装上了,他们用得特别顺手,用他们的话说,豆包正好用,什么都懂。以前手机字体怎么调大、照片怎么发到家庭群、体检报告上的箭头是什么意思,这些事他们都要打电话来问我,现在直接跟豆包语音聊,问完自己就能搞定。 不过最近我才发现,豆包又悄悄更新了一大波东西:能操作本地电脑、使用浏览器,支持调用 Skills 技能和定时任务,内置了 Office 办公套件,还支持专业的图片视频设计。 不知道你是不是和我一样


Android 架构进阶:为什么项目越大,越需要把对象创建权拿走
潜龙勿用之化骨龙2026/8/20

刚开始写 Android 项目的时候,我其实不太理解 DI 有什么必要。 一个对象而已: val repository = UserRepositoryImpl() 直接创建不就完了吗? 如果只是一个小项目,确实没什么问题。 甚至我觉得,这时候为了 Hilt、Koin 再引入一套依赖注入体系,反而有点重。 真正让我改变想法的,是项目开始变大以后。 你会发现,项目变复杂以后,麻烦来了: 到处都是创建对象。 一、项目小的时候,直接创建对象没有问题 比如: class UserRepository


Java对象头Mark Word状态与锁膨胀过程剖析
wuminyu2026/8/6

Java对象头Mark Word状态与锁膨胀过程剖析 前言对象头Mark Word状态与锁膨胀过程剖析1. 64位 HotSpot JVM 下 Mark Word 内存布局64位 JVM Mark Word 位结构图表C++ 头文件位常量定义 (`markOop.hpp`) 2. 状态机迁移全景与锁演进路线3. 偏向锁(Biased Locking)的加锁与撤销源码解析3.1 偏向锁的快速进入(Fast Path)3.2 偏向锁的撤销(Revocation) 4. 轻量级锁(Lig


2026 哪个远程控制软件好用?国内海外 6 款主流远程软件深度测评(附评分表与选型建议)
禁止默2026/7/28

2026 年了,远程控制软件早就不是"偶尔帮老妈修电脑"的低频工具了——居家连公司工位改方案、出差路上用平板回邮件、周末秒回 OA、被控端跑训练任务、远程串流玩 3A,这些场景已经把远程软件变成了天天开的生产力底座。 但"哪个好用"这个问题在 2026 年变得更难回答了,原因是市场出现了三条暗线: 国内三巨头分层明显:UU 远程靠网易的网络底子杀进来,把 4K/144fps、端口映射、远程终端全做成了免费;ToDesk 免费版开始卡时长;向日葵免费版广告越塞越多,并且开始排队收费。


让家里网速有据可查:用MySpeed做一个24小时测速看板
是店小二呀2026/7/20

前言 很多人办理宽带时看到的是500M、1000M,可真正用起来却不是那么回事。晚上看视频卡顿,家里多人同时上网就变慢,联系运营商时,对方一句“后台检测正常”,问题往往就很难继续推进。 问题不一定出在你不会测速,而是普通测速方式很难留下连续证据。偶尔打开第三方测速网站测一次,只能说明当时那一刻的结果,不能反映一天内不同时段的波动,也很难判断是不是晚高峰、Wi-Fi环境或线路质量造成的影响。 如果家里本来就有群晖NAS,可以把它变成一台长期在线的测速服务器。MySpeed就是适合这种场景的轻


PDF解析实现
马里马里奥-2026/7/12

1. 项目背景与需求分析 在现代Web应用中,PDF 文档处理是一个常见但复杂的需求。无论是企业 OA 系统、在线教育平台还是知识管理工具,都需要能够高效解析 PDF 内容。本次作业要求实现一个中间件,专门处理前端上传的PDF文件,具体要求如下: 输入格式:接收 Base64 编码的 PDF 数据核心功能:将 Base64 转换为正常 PDF 文件并提取文本内容输出要求:将提取的文本内容完整放入 system_message 中,作为后续对话的上下文扩展目标:最终版本应支持上传或发送包含完整


【Agent 学习日记】从问题到答案:RAG系统完整处理流程与核心机制深度拆解
小假是真的2026/7/4

目录 🍬前言 🍬一、 RAG 系统全流程总览(宏观视角) 🍬二、 离线预处理:决定 RAG 效果的上限 🍬三、 在线推理第一步:问题是如何被“拆解”的?(Query 拆解) 🍬四、 向量检索与重排序:从“大海捞针”到“精准定位” 🍬五、 大模型(LLM)在 RAG 中到底负责什么? 🍬六、 最终输出:不仅是“答案” 🍬七、 关键痛点与优化方向(总结展望) 🍬八、面试回答 🍡RAG完整处理流程 🍡问题如何拆解? 🍡大模型负责什么? 🍡最终输出什么


GitHub 热榜项目 - 周榜(2026-06-21)
CoderJia_2026/6/26

GitHub 热榜项目 - 周榜(2026-06-21) 生成于:2026-06-21 统计摘要 共发现热门项目: 21 个 Token赞助:siliconflow 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。 本期热点趋势总结 本期 GitHub 热榜呈现出明显的 AI 工程化 与基础设施化 趋势:MCP 服务、Agent 技能、提示词安全扫描、RAG 压缩、代码知识图谱等项目集中爆发,说明开发重点已


《PyTorch 深度修炼》Dataset 和 DataLoader:数据如何喂给模型
闵孚龙2026/6/17

一、模型吃的不是文件,是 Batch Tensor 很多人刚学 PyTorch,会把数据加载理解成“读文件”。这个理解太浅。 训练模型时,真正进入模型的不是图片路径,不是 JSON,不是数据库记录,而是整理好的 Batch Tensor。 Dataset 负责回答一个问题:一个样本怎么取。DataLoader 负责回答另一个问题:怎样高效、稳定、成批地把样本送到训练循环。 所以 DataLoader 不是一个普通 for 循环。它是一条数据流水线。它管顺序、管批次、管拼接、管多进程、管预


Java Spring Data JPA 实战指南:Repository 查询、分页与实体映射
唐青枫2026/6/10

简介 Spring Data JPA 是 Spring Data 家族里专门用来简化 JPA 开发的模块。 它不是一个新的 ORM 规范。 更准确地说: JPA 是规范 Hibernate 是常见实现 Spring Data JPA 是 Spring 对 JPA Repository 的封装 在 Spring Boot 项目里,常见调用链大致是: Controller | v Service | v Repository | v Spring Data JPA |

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读