返回博客

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
正解:一个目录只跑一个 next dev

要么接受「web dev」和「桌面 app dev」互斥,同一时间只开其中一个;要么让桌面 devUrl 复用已有的 web dev server,beforeDevCommand 留空,通过 wait-on 保证顺序依赖。

排查步骤

  1. 看报错里有没有 Dir: 行——有就是目录锁,不是端口冲突。
  2. kill <PID> 杀掉旧进程后重试,第二个就能正常启动。
  3. 区分两种错误:出现 EADDRINUSE listen :::<port> 才是真正的端口被占,改端口有效。

一句话外卖

Next.js 16 dev server 的去重 key 是目录,不是端口——分端口跑不了两个 next dev,报错里的 Dir: 是关键线索。