Next.js 16 按目录去重 dev 服务器,换端口没用
在一个 pnpm + Turborepo monorepo 里,我需要同时跑两个开发场景:一个是纯浏览器访问的 web 端,另一个是 Tauri 桌面壳通过 beforeDevCommand 拉起的同一份 Next.js 应用。两边用了不同的端口——理论上这应该够了。结果第二个进程直接挂掉,而且报错方式非常迷惑。
现象
package.json 里两条 dev 命令:
"dev": "next dev -p 5512", // 浏览器 web 端
"dev:desktop": "next dev -p 5514", // Tauri beforeDevCommand 拉起先跑 pnpm dev(web 端,5512 上),再启动 Tauri 的 app(它会触发 dev:desktop),第二个进程输出:
✓ Ready in 807ms
⨯ Another next dev server is already running.
- Local: http://localhost:5514
- PID: 77365
- Dir: /…/apps/www
- Run kill 77365 to stop it.
ELIFECYCLE Command failed with exit code 1.
奇怪:它先打印了 Ready,然后立刻报错退出;端口是 5514,跟 5512 毫不相干;报错里没有 EADDRINUSE,而是多了一行 Dir:。
根因
Next.js 16 新增了一个目录级去重 guard:它允许同一个项目目录下同时只能存在一个 next dev 进程。检测机制是在 .next/ 下写入一份锁文件 / PID 记录,只要目录相同就触发,与端口无关。
这和传统的 TCP 端口冲突是两码事:
| 错误类型 | 报错特征 |
|---|---|
| TCP 端口冲突 | EADDRINUSE listen :::<port>,无 Dir: |
| Next 16 目录锁 | Another next dev server is already running + Dir: <path> |
Next.js 16 的去重 key 是项目目录,不是端口。给 next dev 加 -p 5514 只能避免 TCP 端口冲突,但当同一目录下已有另一个 next dev 存活时,第二个依然会被 guard 直接杀掉。不要用"分端口 = 可以同时跑"作为依据。
正解
方案一:接受现实,只跑一个
桌面壳(Tauri / Electron)的 beforeDevCommand 本质上是为了帮你启动 web dev server,当你已经手动开了一个时,二者服务的是同一份 Next.js 应用。让二者并存本就没有意义——在桌面窗口里你已经能看到 web 页面了。
方案二:桌面 app 复用已有的 web dev server
如果确实需要同时打开浏览器 + 桌面两个界面,只跑一个 next dev,然后把桌面的 devUrl 指向这个端口,不设 beforeDevCommand:
// tauri.conf.json(示意)
{
"build": {
"devUrl": "http://localhost:5512",
"beforeDevCommand": "" // 留空,不让 Tauri 再起一个 next dev
}
}代价是单独跑 pnpm tauri dev 时必须先手动保证 web dev server 已在跑,否则 webview 会空白。可以在 app 的 npm script 里加一步 wait-on 检测端口:
wait-on http://localhost:5512 && pnpm tauri dev要么接受「web dev」和「桌面 app dev」互斥,同一时间只开其中一个;要么让桌面 devUrl 复用已有的 web dev server,beforeDevCommand 留空,通过 wait-on 保证顺序依赖。
排查步骤
- 看报错里有没有
Dir:行——有就是目录锁,不是端口冲突。 kill <PID>杀掉旧进程后重试,第二个就能正常启动。- 区分两种错误:出现
EADDRINUSE listen :::<port>才是真正的端口被占,改端口有效。
一句话外卖
Next.js 16 dev server 的去重 key 是目录,不是端口——分端口跑不了两个
next dev,报错里的Dir:是关键线索。