本文档覆盖 web/(Next.js 前端)的生产部署流程与一次由构建时环境变量缺失引发的全站 API 请求失败事故的复盘。文中不包含任何服务器地址、口令与密钥,部署机相关细节一律以 <占位符> 表示。
next start 运行。web/src/lib/api.ts 的 API_BASE_URL,该值在构建时由 NEXT_PUBLIC_API_BASE_URL 注入,会编译进产物(客户端 JS 与服务端 JS)。https://<站点>/api/... 路径,由反向代理把 /api 前缀转发到后端;后端自身是根路径、不感知 /api 前缀。NEXT_PUBLIC_API_BASE_URL,会把代码默认值 http://localhost:8090/api 编进线上产物(该默认值仅适用于本地开发),浏览器端与 SSR 端会同时失效(机制见 §3.4)。⚠️ 所有
NEXT_PUBLIC_*变量都必须在构建机上正确注入;next build会在构建期将它们内联进产物。运行时的进程重启不能改变已编译进产物的值。
.env 文件不进仓库,生产部署目录中保存一份权威副本。每次发版前以部署目录中的 .env 为准下载到 web/.env:
scp <部署主机>:<部署目录>/.env web/.env
校验关键变量:
grep -E "NEXT_PUBLIC_API_BASE_URL|NEXT_PUBLIC_NEXT_API_BASE_URL|NEXT_PUBLIC_PICTURE_BASE_URL" web/.env
必含(以实际生产域名为准):
NEXT_PUBLIC_API_BASE_URL=https://<站点>/api
NEXT_PUBLIC_NEXT_API_BASE_URL=https://<站点>/next-api
NEXT_PUBLIC_PICTURE_BASE_URL=https://<文件服务域名>
pnpm --filter web build
构建完成后,在产物里确认环境变量真的编进去了:
cd web
# 1) 客户端静态产物不得出现本地开发默认值
grep -rl "localhost:8090" .next/static/ 2>/dev/null | grep -v "\.map$" || true # 应无输出
# 2) 生产 API 基址应被编入
grep -rl "https://<站点>/api" .next/static/chunks/ | wc -l # 应 > 0
# 3) 运行时 JS 全量检查(排除 sourcemap 与构建缓存)
grep -rl "localhost:8090" .next/ 2>/dev/null | grep -vE "\.map$|/cache/" | head || true # 应无输出
预期:运行时 JS 均不含 http://localhost:8090/api;否则不要上传,回到 §2.0 检查 .env。
cd web
# .next 排除缓存目录(历史/开发构建缓存可达数 GB,不属于生产运行时)
tar -cf /tmp/web-next.tar --exclude='.next/cache' .next
tar -cf /tmp/web-public.tar public
# 1) 备份现有目录(务必先备份,便于回滚)
ssh <部署主机> "cd <部署目录> && cp -a .next .next.bak_$(date +%Y%m%d-%H%M) && cp -a public public.bak_$(date +%Y%m%d-%H%M)"
# 2) 上传
scp /tmp/web-next.tar /tmp/web-public.tar <部署主机>:<部署目录>/
# 3) 替换(先删旧目录再解压,避免旧 chunk 残留被清单引用)
ssh <部署主机> "cd <部署目录> && rm -rf .next public && tar -xf web-next.tar -C . && tar -xf web-public.tar -C . && rm -f web-next.tar web-public.tar"
说明:
next build产物 chunk 名带内容哈希,替换时必须整体删除旧.next再解压。
ssh <部署主机> "cd <部署目录> && pm2 restart <前端进程名>"
上线验证(从公网):
# 1) 首页 SSR 数据已渲染(统计数字来自后端,证明 SSR 链路打通)
curl -sS https://<站点>/ | grep -oE "blogCount[^,]*" | head -3
# 2) 客户端产物基址
CH=$(curl -sS https://<站点>/ | grep -oE 'src="/_next/static/chunks/[^"]+\.js' | head -1 | sed 's/src="//;s/"$//')
curl -sS "https://<站点>$CH" | grep -c "localhost:8090" # 应为 0
# 3) 后端接口直连
curl -sS -o /dev/null -w "%{http_code}\n" https://<站点>/api/site-stats # 应为 200
# 4) 进程日志无新 404
ssh <部署主机> "tail -20 ~/.pm2/logs/<前端进程名>-error.log"
curl -sS -o /dev/null -w "%{http_code}\n" https://<站点>/api/site-stats # 200 即后端正常
后端全 200,问题大概率在前端链路。
# 出现 http://localhost:8090/api 即命中"产物编错基址"
curl -sS https://<站点>/_next/static/chunks/<chunk>.js \
| grep -oE "https://<站点>/api|http://localhost:8090/api" | sort | uniq -c
同时对比问题文件的 Last-Modified,若恰好等于某次发版时间,即为该次发版引入。
看服务端(SSR)日志:若出现大量 Failed to fetch xxx: Error: HTTP error! status: 404,说明 SSR 抓到了后端不存在的路径(详见 §3.4)。
/api/...,转发到后端时剥离 /api 前缀;/api 能通”完全由反向代理保证;任何绕过代理直达后端的请求(如服务器本机 SSR 直连 http://localhost:<后端端口>/api/...)都会 404。本次发版构建机未注入 NEXT_PUBLIC_API_BASE_URL,api.ts 的默认回退值 http://localhost:8090/api 被编译进客户端与服务端全部产物:
http://localhost:8090/api/...,触发浏览器混合内容(Mixed Content)拦截 / 访问访客本机端口 → 全部失败;http://localhost:<后端端口>/api/...,而后端是根路径、/api 前缀只由代理剥离 → 全部 404(与进程日志吻合)。后端、数据库、反向代理均正常,故障完全由「构建期环境变量缺失」造成。
.env 到 web/.env;pnpm --filter web build;localhost:8090、生产基址正常编入;.next(排除 cache)与 public → 删除旧目录后解压替换;.env 不进仓库导致构建机差异难以察觉,建议:
NEXT_PUBLIC_*(作为工程变量);web/.env 含必填变量,缺失即失败。/api 接口」。.next/public 前先备份;确认新版正常后再清理旧包与备份,防止不可回滚。| 项 | 说明 |
|---|---|
| 前端运行 | next start(进程管理,工作目录为部署目录) |
| 前端端口 | <next-port>(内部监听) |
| 后端 | Spring Boot,监听本机端口,API 为根路径(无 /api 前缀) |
| 反向代理 | 对外暴露 /api/...,转发到后端时剥离 /api 前缀 |
| 构建注入变量 | NEXT_PUBLIC_API_BASE_URL、NEXT_PUBLIC_NEXT_API_BASE_URL、NEXT_PUBLIC_PICTURE_BASE_URL、NEXT_PUBLIC_SITE_URL、NEXT_PUBLIC_FILE_DOMAIN 等 |
| 代码默认值 | web/src/lib/api.ts:http://localhost:8090/api(仅本地开发) |