把前后端放进同一个项目后,本地能跑通并不代表部署能跑通。部署时真正要回答的是两个独立问题:构建命令最终产出了哪些文件、落在哪个路径;启动命令读的是哪个目录、是构建产物还是源码。这两件事分别对应“构建入口”和“部署入口”,通常不在同一份配置里,混在一起看就容易出现“页面能开、接口 404”或“接口正常、页面 500”的情况。下面按目录边界、构建产物、部署指向、最小验证四步走一遍,每一步都可以用命令或页面行为确认,不依赖猜测。
判断方法:先用目录树锁定前端、服务端、共享代码三个物理根,再从构建脚本确认每条命令的产物路径,最后检查进程启动命令的工作目录与静态目录是否指向构建产物而不是源码。适用场景是单仓库同项目的前后端部署;风险边界在于不同框架的默认产物目录差异较大,产物路径以构建脚本和实际文件为准,部署前建议以生产模式启动后分别访问一个接口和一张页面确认,不要仅凭本地开发服务器表现下结论。
在项目根目录列出两层目录树,标出前端、服务端、共享代码各自的根
先确定物理边界,因为后面所有构建和部署配置都会引用这些路径,靠记忆或约定容易错。在项目根目录执行:
find . -maxdepth 2 -type d \
-not -path '*/node_modules*' \
-not -path '*/.git*' | sort
输出示例(不同项目名称会变,结构类似):
.
./apps
./apps/web
./apps/web/src
./apps/web/public
./apps/server
./apps/server/src
./packages
./packages/shared
./packages/shared/src
./scripts
逐处标注:apps/web 是前端根,页面、组件、静态资源都在这里,构建产物一般也落在这个目录下;apps/server 是服务端根,被进程直接启动,通常不需要前端构建步骤;packages/shared 是共享代码根,被两侧同时引用,它本身不产生独立部署单元,只能作为依赖被打进各自的产物里。
共享代码这一项需要单独注意:如果服务端启动时直接读 packages/shared/src 的 TypeScript 源码,而运行环境只能执行编译后的 JavaScript,部署就会在这里断掉。可以先确认共享包是被构建后引用,还是被源码路径引用,再决定要不要在服务端构建步骤里带上它。
读构建脚本,确认一条命令会产出哪些文件、落在哪个路径
把“构建”从概念落到具体路径。打开根目录 package.json 的 scripts,或等价的配置文件,按执行顺序看每一步的输入与产物:
{
"scripts": {
"build:shared": "tsc -p packages/shared/tsconfig.json",
"build:web": "vite build `--outDir` apps/web/dist",
"build:server": "tsc -p apps/server/tsconfig.json",
"build": "npm run build:shared && npm run build:web && npm run build:server"
}
}
按顺序解读这份片段:build:shared 的输入是共享代码源码,产物通常落在 packages/shared/dist;build:web 的输入是前端源码与静态资源,产物落在 apps/web/dist,其中包含 index.html 和带哈希的 JS/CSS;build:server 的输入是服务端源码,产物落在 apps/server/dist,入口文件常见为 dist/index.js。
顺序有意义:前端和服务端都依赖共享代码,所以 build:shared 必须排在最前。执行完一次完整构建后,可以直接用 ls apps/web/dist apps/server/dist 核对产物是否真的存在,而不是只看命令退出码。如果你的构建命令是单条(例如某个框架内置的一体化构建),同样要找出它写出的目录,方法一致。
检查部署入口指向的是构建产物还是源码
部署配置里最常见的问题是:进程启动命令读的是没被构建过的源码目录,或者静态目录指到了前端源码而不是产物目录。可以先用下面这份最小骨架把三件事写清楚——工作目录、启动命令、静态目录:
{
"workdir": "/srv/app",
"command": "node apps/server/dist/index.js",
"env": {
"NODE_ENV": "production",
"PORT": "3000",
"STATIC_DIR": "/srv/app/apps/web/dist"
}
}
三个字段各自对应一个判断:workdir 决定相对路径从哪算起,写错会让后面两个路径全部失效;command 指向的是构建产物 dist/index.js,不是 src/index.ts;STATIC_DIR 指向前端产物目录,不是 apps/web 或 apps/web/public。
验证方式是以生产模式启动后分别访问两类地址:先请求一个服务端接口(例如 curl -i http://127.0.0.1:3000/api/health),确认返回 JSON 或预期的状态码;再请求一个页面路径(例如 curl -I http://127.0.0.1:3000/),确认返回 200 且内容类型是 HTML,并检查静态资源能否正常取到。如果接口通、页面 404,多半是静态目录指错;如果页面通、接口 404,多半是启动命令或路由前缀不对。启动日志里通常会打印监听端口和静态目录,可以先对照日志确认进程实际读取的路径。
用一次最小改动同时验证两套入口没有串
这一步的目的不是回归测试,而是确认两个入口的依赖是分开的。做法是各改一处,改动小到一眼能看出效果:
- 前端改一处:把首页某个可见文案改掉,例如把标题从
Demo改为Demo-v2。 - 服务端改一处:在健康检查接口的返回里加一个字段,例如
{ "ok": true, "buildTag": "v2" }。 - 重新构建:执行
npm run build,不要只重启进程,因为前端改动不重新构建不会进入静态产物。 - 重启服务,再分别验证。
curl -s http://127.0.0.1:3000/api/health
curl -s http://127.0.0.1:3000/ | grep -o 'Demo-v2'
判断标准很直接:接口返回里应出现 buildTag,页面 HTML 里应出现 Demo-v2。如果只改了前端却必须重启服务端才生效,说明静态文件可能被打进了服务端产物,或者服务端在读源码目录;如果只改了服务端却导致页面报错,说明两侧共用了不该共用的构建产物。出现这两种情况,先回到第 2 步核对产物路径,再调整部署配置里的静态目录和启动命令。
还有一个容易被忽略的点:共享代码改动会同时影响两侧,这属于正常现象,不要用它来判断“串了”。真正要区分的是前端资源是否只经由静态目录提供、服务端入口是否只经由进程命令启动。把这两条线在配置里写死并用上述两个请求验证一遍,后续改目录或换部署方式时也能快速定位问题。