如果你是在app目录下开发 Next.js 13/14/15 的全栈应用,用 VS Code 调试这件事,我建议你趁早放弃“用 console.log 打天下”的念头。
我自己就是从console.log重度用户转过来的,早期写 Next.js 全栈项目,前端组件里一个状态不对,就在useEffect里打三四个 log,后端 API 返回数据不对,又在route.ts里把request、params、body各打印一遍。日志本身没问题,但当页面交互链路变长、服务端组件和客户端组件交错执行、数据库查询和第三方 API 调用混在一起时,console.log 的输出顺序完全跟不上代码真实的执行顺序。你会看到服务端日志和浏览器控制台日志各打各的,中间还隔着红字警告和 favicon 404,想靠这些日志还原调用链路,基本是在做阅读理解。
后来我认真把 VS Code 的调试器完整配置了一遍,才发现整个过程没有想象中复杂。Next.js 官方文档里其实给了明确的调试方案,只是解释得比较简略,网上很多教程又只讲了前端页面的断点,没人告诉你 API Route、服务端组件、Edge Runtime 这些还要分开处理。这篇文章就把我踩过的坑和完整的配置过程写出来,尽量做到让一个刚接触 Next.js 的开发者照着做,也能顺利打断点、看调用栈、查变量。
这个指南适配的场景很简单:你在本机用 VS Code 开发 Next.js 全栈项目,想用一个统一的调试工具同时搞定浏览器端代码(客户端组件、事件处理函数、状态逻辑)和 Node.js 端代码(API Route、Server Actions、服务端渲染逻辑),而不是在浏览器 DevTools 和终端之间来回切换。
1. 调试前的准备与整体认知
1.1 全栈调试为什么会这么绕
很多人在 Next.js 项目里第一次尝试调试时,会遇到一个很困惑的现象:在页面的Button的onClick回调里打断点,VS Code 能正常命中,但在app/api/user/route.ts的GET函数里打断点,启动调试后根本不停下来。
这不是你的配置有问题,而是因为 Next.js 本身是一个“同构”框架。你写的代码有一部分是浏览器端 JavaScript,由 Chrome 的 V8 引擎执行;有一部分是 Node.js 端代码,由 Node.js 进程执行。VS Code 的调试器同一个时刻只连接一个调试目标,要么连浏览器渲染进程,要么连 Node.js 进程。所以当你只配置了前端调试时,API Route 里的断点自然不会被监听到。
理解了这一点,你就会明白全栈调试的核心思路其实是同时启动两个调试会话,让 VS Code 在浏览器端和 Node.js 端之间自动切换。而这一切都可以通过launch.json里的复合配置(compounds)来实现。
1.2 环境准备:Node、VS Code 与一个干净的 Next.js 项目
建议先把环境整理干净,这样后边排查问题时能少很多干扰项。
- Node.js 版本:Next.js 15 要求 Node.js 18.18.0 或更高版本,建议直接用 20.x LTS,实测最稳。
- VS Code 版本:1.85 及以上都行,老版本对调试面板的一些 UI 支持不够好。
- 扩展插件:不用额外装任何调试扩展。VS Code 内置的 JavaScript Debugger(即
vscode.js-debug)已经完全够用。如果你想看 React 组件状态,倒是可以装一个 React DevTools,但跟本文的调试关系不大。
创建项目不需要重新初始化,直接用你现有的 Next.js 项目就好。但如果你只是拿来练手,我还是建议新建一个干净的create-next-app项目先跑通流程:
npx create-next-app@latest debug-next-demo注意在交互选项里把 TypeScript、ESLint、App Router 都选上,Tailwind CSS 按需选择。项目创建完成后,先跑一次npm run dev,确认开发服务器能正常启动、浏览器能打开http://localhost:3000,再继续往下做。
2. VS Code 调试 Next.js 的核心原理与选型
2.1 别急着写配置,先搞懂这三个关键概念
调试配置之所以让人头疼,是因为launch.json里的每一项背后都对应一个实际动作。先花两分钟理解三个概念,后面配什么都不会慌。
第一个是launch(启动模式)和attach(附加模式)的区别。launch模式下,调试器会帮你启动一个进程,比如帮你跑next dev,然后自动挂上去;attach模式下,进程已经跑起来了,调试器只负责连接。Next.js 官方提供的方案两种都有,但更推荐先理解launch,因为它的环境更干净、可复现性更高。
第二个是“调试目标”(debug target)的概念。VS Code 里的 JavaScript Debugger 可以同时管理多个调试目标。比如你启动一个 Node.js 进程,它是一个目标;浏览器打开页面后,页面里的主线程是另一个目标;如果页面里还有 Web Worker,又会有独立的目标。调试器顶部有个目标选择器,你真正常用的是页面主线程(Frontend)和 Node.js 进程(Backend)。
第三个是sourceMap。Next.js 开发模式下,通过编译器把.tsx、.ts文件编译成浏览器和 Node 能执行的 JavaScript,同时生成 source map 文件。调试器是靠着 source map 才能在你的源码.tsx文件里命中断点的。如果 source map 没生成或者配置不对,断点就会显示为“未绑定”状态。
2.2 官方的“Full Stack”配置到底做了什么
Next.js 官方给了一套专门用于 VS Code 的调试方式,本质上是把两个配置用compounds组合成一个。我先把完整配置贴出来,再拆开逐项解释:
{ "version": "0.2.0", "configurations": [ { "name": "Next.js: Launch Full Stack", "type": "node-terminal", "request": "launch", "command": "npm run dev", "serverReadyAction": { "pattern": "started server on .+, url: (https?://.+)?", "uriFormat": "%s", "action": "debugWithEdge" } }, { "name": "Next.js: Attach to existing dev server", "type": "node-terminal", "request": "attach", "command": "", "serverReadyAction": { "pattern": "started server on .+, url: (https?://.+)?", "uriFormat": "%s", "action": "debugWithEdge" } } ], "compounds": [ { "name": "Next.js: Full Stack", "configurations": [ "Next.js: Launch Full Stack", "Next.js: Attach to existing dev server" ] } ] }这里最核心的是type: "node-terminal"。它不是直接调试一个 Node.js 脚本,而是在 VS Code 的内置终端里启动npm run dev,同时把终端里输出的一切都接进调试器。serverReadyAction的作用是监听终端输出,一旦发现 Next.js 打印出“started server on ...”的行,就自动解析出 URL 并用 Edge(这里指的是基于 V8 的浏览器核心,而不是 Edge Runtime)打开调试浏览器。action: "debugWithEdge"意味着会自动用内置的无头 Chrome/Edge 调试会话打开页面,这样你在浏览器里点击页面时,前端断点就能命中。
单独运行第一个配置,就只能调后端 Node.js;单独运行第二个配置,必须先手动npm run dev,它再去附加,也只能调后端。只有用compounds把两个组合起来,一个负责启动服务器并附加到 Node 进程,一个负责打开一个带调试能力的浏览器,才能同时看到前后端目标。
2.3 dev、start、edge 三种运行模式的调试差异
这块是很多人忽略的。Next.js 应用的运行方式不同,调试配置也应该不同。
next dev:开发模式,组件和路由都是按需编译。调试体验最好,因为编译有缓存、source map 全、断点响应快。绝大多数日常开发场景都用这个。next start:生产模式,必须先next build。构建后的代码经过压缩和优化,变量名全部被改写,如果没有在构建时保留 source map,断点很难命中。一般不建议在生产模式做源码级调试,但可以调试性能问题,比如看内存占用、CPU 火焰图。next dev --turbo:Webpack 换成 Turbopack 的模式,编译速度更快,但调试器对--turbo的支持在不同版本上有波动。如果你的断点经常不命中,先把它换成普通的next dev试试。
此外还有一个容易被误解的 Edge Runtime。如果你在 Next.js 里配置了export const runtime = 'edge',这个 API Route 或中间件不运行在 Node.js 进程里,而是运行在一个更接近浏览器的 Edge 沙箱里。此时node-terminal类型附加不到这个代码的执行环境,断点大概率无法命中。处理办法要么把该路由的 runtime 改回nodejs调试,要么只在中间件和少量边缘函数里用日志排查。
3. 从零配置完整调试环境的实操过程
3.1 写一份能用的 launch.json
先在你的项目根目录下打开.vscode文件夹,如果没有就新建一个,然后创建launch.json。在 VS Code 里可以按Ctrl+Shift+D打开“运行和调试”面板,点击“创建 launch.json 文件”,选“Node.js”模板,会自动生成一个基础配置。
把上边贴出的完整配置直接覆盖进去,保存。此时“运行和调试”面板的顶部应该出现一个 “Next.js: Full Stack” 的下拉选项。这就是我们主用的方案。
如果你更愿意手动管理,不想让调试器自动打开浏览器,也可以在serverReadyAction里把action改为"openExternally",这样只启动外部浏览器,再用 VS Code 的“Attach to Chrome”配置附加调试前端。但说实话,自动调试浏览器更省事,页面一打开就能直接打断点,推荐直接用默认方案。
3.2 启动调试并理清调试面板
在“运行和调试”面板里选择“Next.js: Full Stack”,点击绿色播放按钮启动。
踩过的关键点是:第一次启动会创建一个内置于 VS Code 的浏览器窗口(类似无头调试浏览器)。这个窗口和我们日常用的 Chrome 不一样,地址栏是隐藏的,需要靠地址栏导航时可以直接在“调试控制台”的过滤框输入 URL,或者直接修改webServerReadyAction的uriFormat。我通常的用法是:启动后等内置浏览器自动打开http://localhost:3000,然后用它的地址栏手动跳转。
启动成功后,你会看到两个调试会话在“调用堆栈”区域并列显示:一个标记为 Node.js 进程,一个标记为浏览器页面调试器。前端断点命中时,“调用堆栈”区域顶部选中浏览器目标;后端断点命中时自动切到 Node 目标。如果不小心把两个目标挂反了,可以通过调试会话下拉框手动切换。
3.3 前端断点实战:页面组件与客户端交互
打开项目里的app/page.tsx,找到渲染部分的代码。在 JSX 里把鼠标移到某一行上,点击行号左侧空白区域加一个断点,或者直接用F9快捷键。
然后回到调试内置浏览器,刷新页面。你会看到 VS Code 自动聚焦到page.tsx文件,断点所在行高亮,左侧“变量”面板展示当前组件作用域内的所有变量。这一步走通,说明浏览器端调试链路没问题。
接着在页面里添加一个带use client的组件,比如一个计数器按钮,在onClick回调中打一个断点。放一个debugger语句也行,不过断点比debugger强在不需要改代码。点击按钮,断点命中后,你可以在“监视”面板手动输入count、event等表达式,实时查看变化。
这里有一个真正的全栈体验:当你在一个客户端组件的事件处理函数里调用fetch('/api/user'),你可以在fetch的那一行打断点,按F11步入。调试器会直接带你跳转到浏览器的 fetch 实现代码里,继续按F11,甚至会进入 Node.js 端app/api/user/route.ts的GET函数。两个不同语言运行时之间的跳转,在这一刻被统一了。
3.4 后端断点实战:API Route 与服务端组件
现在处理我开头说的场景——API Route 的调试。
在app/api/user/route.ts里给GET函数第一行加一个断点。回到内置浏览器,刷新页面或者手动通过fetch触发一次请求。如果当前页面没有调用这个 API,最简单的办法是在地址栏直接输入http://localhost:3000/api/user,然后回车。
断点命中后,你可以查看request对象的所有属性、params、searchParams,也可以展开request.headers查看每个 header。对调试来说,这一步的价值远远大于在代码里写console.log(req)再等终端输出,因为你可以直接在上面的“监视”面板里写表达式,比如await request.json(),在不污染代码的前提下看请求体内容。
服务端组件(Server Component)的调试和 API Route 类似。在app/page.tsx里如果有一段服务端渲染逻辑,比如async function getData()里的数据请求,同样会运行在 Node.js 进程。直接打断点,然后在浏览器里刷新页面或导航到该页面即可命中。注意,服务端组件的代码不会出现在浏览器“源码”里,所以只能在 Node 调试会话中看到。
4. 常见问题与排查技巧实录
4.1 断点不命中:先看“未绑定”和“灰色断点”
这是我遇到最多的问题。打断点后,断点位置是灰色圆圈,或者悬停显示“未绑定断点”,多半是 source map 没有正确生成。
在 Next.js 开发模式下,通常不需要额外配置就能生成 source map,但如果你修改了next.config.js里的productionBrowserSourceMaps或者自定义了 webpack 配置,就可能影响调试。检查办法:在“运行和调试”面板的“断点”区域看状态,如果是空心圆说明未绑定;如果是实心红点说明正常。
另一个常见原因是断点打在了错误的代码位置。比如在.next/server/app下的编译文件里打断点,而不是在项目源码文件里打断点。记住,一定去src/app或app目录下的源文件打,不要跑到.next目录下去打。
如果你用了 Turbopack(即next dev --turbo)并且断点不稳定,也有一个已知的 workaround:在next.config.js的experimental.turbo配置里手动打开 source map 支持,或者干脆换回 webpack 模式。
4.2 workspace 下有多层项目或子目录的路径问题
很多全栈项目不会是单一 Next.js 目录。比如你用的 monorepo,前端在apps/web,后端在apps/api。此时直接把launch.json放在根目录.vscode下,默认的${workspaceFolder}解析到整个仓库的根目录,会导致npm run dev找不到包。
解决办法有两种:
- 在根目录的
.vscode/launch.json明确指定cwd字段:
{ "type": "node-terminal", "request": "launch", "command": "npm run dev", "cwd": "${workspaceFolder}/apps/web" }- 或者把
.vscode/launch.json直接放到apps/web目录下,这样 VS Code 会把它作为该子项目的调试配置入口。
如果项目里还嵌套了 ESModule 路径别名(比如@/*指向src/*),记得检查tsconfig.json的baseUrl和paths,调试器会依赖这些配置解析源码映射。
4.3 端口冲突与 Windows 环境的特殊坑
默认 Next.js 跑在 3000 端口,如果你同时开了其他服务抢占 3000,next dev会自动切到 3001 甚至 3002。serverReadyAction的正则模式能匹配端口变化,但如果你用的是类似"pattern": "Ready in"这样的自带正则,可能匹配不到 URL,导致调试浏览器没有自动打开。
有个简单的处理方式:在package.json里给 dev 脚本固定端口:
"scripts": { "dev": "next dev -p 3000" }这样至少保证每次调试都在同一个端口,不会因为随机端口影响浏览器自动打开和调试会话的 URL 解析。
在 Windows 上还有一个容易忽略的问题:防火墙或杀毒软件可能会拦截 Node.js 进程的调试端口(默认 9229)。如果你发现Next.js: Launch Full Stack在终端里能正常打印启动日志,但 VS Code 迟迟没有检测到调试会话,检查一下 Windows Defender 是否把 Node.js 隔离了。处理办法是把项目目录加进白名单,或者放行 Node.js 对局域网/回环地址的监听。
4.4 热更新与断点失效的处理
Next.js 开发模式自带 Fast Refresh,你改完代码保存,页面自动更新,但这个过程中调试器可能会失联一小段时间。
比较常见的现象是:断点原本是红色实心(已绑定),代码保存后变成了空圈(未绑定),几秒后又变回红色。这是正常的,因为 Fast Refresh 触发了模块重新编译,调试器需要重新解析新的 source map。如果长时间停在未绑定状态,可以手动点击“重新连接”或者重启调试会话。
另外很重要的一点:不要修改.next目录下的任何文件,也不要手动去移动、删除.next里的编译产物,否则 source map 与源码的对应关系会被彻底弄乱。如果你必须清缓存,先停掉调试会话,删除.next目录,再重启。
5. 更进阶的调试姿势
5.1 异步流程调试:Promise 链与微任务的断点逻辑
全栈项目里最难的调试场景是异步流程。服务端fetch数据库,返回之后处理数据再渲染;客户端useEffect里发请求,拿到响应后更新状态,还有一个setTimeout做防抖。如果代码里全是await,想通过简单断点理顺逻辑,很容易在await处被调试器“跳过”或者“来回乱跳”。
我自己的习惯是在不确定的await行前打一个断点,然后在“监视”面板里写下new Promise(resolve => { setTimeout(() => resolve("done"), 1000) })这样的表达式,用调试器自带的求值能力模拟下一步的数据。这个方式在调试 Next.js 服务端组件的嵌套异步调用时尤其好用。
另外,当你按F10跳过某个await表达式时,如果发现调试器跳到了特别奇怪的位置(比如跳到模块底部),不要紧张,这是异步断点的一种表现。你可以按Ctrl+K然后Ctrl+Shift+F8断开所有断点,在“调用堆栈”里往下翻,找到真正的入口栈帧。
5.2 网络请求与 fetch 的追踪
在 Next.js 的全栈调试里,最容易绕晕的是请求链路。前端发fetch('/api/xxx'),Next.js server 接收到请求后转发到外部 API,外部 API 返回后,中间件还有可能做一次 rewrite,最后才回到前端。
这时候光靠断点不够,需要配合调试控制台。VS Code 的“调试控制台”不仅能看console.log输出,还能直接求值表达式。你可以在断点命中后输入performance.getEntriesByType('resource')查看浏览器端所有资源加载时间,也可以在 Node 端输入process.memoryUsage()看内存状态。
如果你想查看某个fetch请求的完整 headers、body 和响应,可以在 fetch 调用处打断点,展开变量面板找到Request对象,逐项展开它的headers和body。不过 Node 端 fetch 的 response body 是一个可读流,直接展开往往看不到正文,我的办法是在调试终端手动再发一次请求:
curl -X GET http://localhost:3000/api/user这样可以直观地确认 API 是否正常响应。注意,生产模式下如果启用了output: 'standalone',路径的解析方式可能会和开发模式不同,排查时别只盯着开发环境。
5.3 React DevTools 与 VS Code 联动
VS Code 调试器自带的变量面板能看 JavaScript 变量,但如果你想看 React 组件的 props、state、hooks 状态,它就无能为力了。这时候建议在调试内置浏览器里装一个 React DevTools 扩展,或者直接在外部 Chrome 里调试。
不过有一个简单的折中方案:在组件内部打断点,然后在“监视”面板中手动输入表达式查看。比如你想看某组件的 props,可以先在组件函数第一行打断点,然后在“监视”里输入props,调试器会完整展开。如果你想看某个 state,输入state也是一样的。这种操作比打开 React DevTools 更直接,而且不离开调试上下文。
还有一个常用技巧:由于 Next.js 的客户端组件在开发模式下会经过 HMR 的包装,你可能看到额外的__REACT_DEVTOOLS_GLOBAL_HOOK__之类的全局变量。不要理会它们,直接关心你自己的业务代码即可。
5.4 服务端组件(RSC)调试的细节
App Router 的项目里,默认的页面组件大部分是服务端组件。它们的特点是:只会在服务端执行一次,不会出现在浏览器源码里,也不能使用useState、useEffect这类客户端 hooks。
在调试服务端组件时,我提醒三点:
- 断点确实能命中,因为整个组件函数运行在 Node.js 进程里,你只需要在函数体任意位置打断点。
- 如果这个服务端组件被其他组件引用,并且你还开启了
loading.tsx、error.tsx等嵌套文件,调试器的执行顺序可能会和你的预期不一致。因为 Next.js 会把多个组件打包进同一个模块,执行顺序取决于渲染树。 - 服务端组件里的
async/await天然支持调试,只要await后面的 Promise resolve 了,断点就能在下一条语句命中。但如果 Promise 一直 pending,比如数据库连接超时或者外部请求一直不返回,调试器会一直停在await那一行。此时需要检查是不是第三方服务的问题,而不是调试配置的问题。
最后分享一个我自己觉得最有价值的操作习惯:调试器里按Ctrl+Shift+I可以随时打开内置浏览器的 DevTools,这里能看到 VS Code 调试器没展示的 console 错误和 network 请求。我在全栈项目里真正排查问题时,往往是“ VS Code 调试器 + 内置浏览器 DevTools ”两个面板同时开,前端断点看渲染逻辑,DevTools Network 看请求状态,Node 调试器再看服务端数据处理。三个视角同时推进,比单靠一个面板效率高很多。
这套流程你跑通之后,再遇到 Next.js 的边界场景,就不会再靠打日志碰运气了。调试器给了你一个上帝视角,你能看到前端到后端、状态到网络、渲染到数据的完整链路。刚开始可能会觉得多开两个窗口很麻烦,但用熟之后你会觉得比什么都顺手。