【PaperFlow】前端部署到子路径之后,怎么解决路径问题 如果前端是直接部署在域名根路径下很多配置确实容易“默认工作”。但我们当时一把前端挂到子路径问题就一下子多起来了。比如https://your-domain.example/paperflow/问题就会突然变多浏览器直接访问/时该跳去哪打包后的静态资源到底从/assets/...取还是/paperflow/assets/...取刷新/paperflow/posts会不会变成 404React Router 到底要不要配basename/api/...是让前端直连还是让 Nginx 代理。先说明一下这篇里不会放真实公网域名、服务器 IP、管理后台地址、密钥、邮箱账号之类的信息。像端口、上游地址这类内容只保留“怎么接”的结构不保留可以直接拿去扫机器的细节。我们最后能把这套路径跑顺不是因为哪一层特别高深而是因为这几层终于开始说同一种路径语言了。1. 我们先把前端入口统一成/paperflow/前端的vite.config.ts里最关键的其实就是base这一层。为了公开发帖不暴露不必要的部署细节我这里只保留结构exportdefaultdefineConfig({base:/paperflow/,plugins:[react()],server:{open:/paperflow/,proxy:{/api:{target:process.env.VITE_API_BASE??local-api-base,changeOrigin:true}}}});这里的base: /paperflow/决定了两件事打包后的静态资源路径以/paperflow/为前缀开发态打开页面时也优先从/paperflow/进入。我们当时就是先把这一步钉住因为它相当于先声明这个前端应用不是部署在网站根路径而是部署在/paperflow/下面。只要这件事先说清楚后面 Nginx 和路由层才有共同的参照物。2. React Router 这层不能凭感觉必须跟base一起走在apps/paperflow-web/src/main.tsx里我们没有把路由前缀写死而是直接从import.meta.env.BASE_URL派生const rawBaseUrl import.meta.env.BASE_URL; const routerBasename rawBaseUrl.endsWith(/) ? rawBaseUrl.slice(0, -1) : rawBaseUrl; const normalizedBasename routerBasename routerBasename ! / ? routerBasename : ; const currentPath window.location.pathname; const pathWithSlash ${normalizedBasename}/; if (normalizedBasename currentPath ! normalizedBasename !currentPath.startsWith(pathWithSlash)) { const nextPath currentPath / ? pathWithSlash : ${normalizedBasename}${currentPath}; window.location.replace(${nextPath}${window.location.search}${window.location.hash}); } BrowserRouter basename{routerBasename || /} App / /BrowserRouter我们后来回头看最省心的一点就在这儿前端路由前缀不是手工写两份而是直接复用Vite base的结果。这能避免一个很常见的问题打包配置是/paperflow/但BrowserRouter还在按/解释路由最后跳转、刷新、资源加载全乱套。另外这段代码里还有一个我们自己觉得挺有用的小处理如果当前地址没有落在basename下就主动重定向过去。这意味着用户即使从根路径或者别的裸路径进入也能被收回到统一入口。3. Nginx 这边不只是放静态文件它还在帮我们把入口收住docker/nginx/paperflow.conf里最关键的几个location大概是这个结构。这里同样省略了不必要的真实部署细节只保留路径逻辑location / { return 302 /paperflow/posts; } location /api/ { proxy_pass http://gateway-upstream; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location /paperflow/assets/ { rewrite ^/paperflow/(.*)$ /$1 break; try_files $uri 404; } location /paperflow/ { rewrite ^/paperflow/(.*)$ /$1 break; try_files $uri $uri/ /index.html; }我们那时候就是靠这几段硬把几个入口关系拉直的。它们其实分别在解决三件不同的事。第一件根路径跳转。用户访问/时不是直接给一个空白首页而是明确跳到/paperflow/posts。第二件接口代理。所有/api/请求都先经过 Nginx再转给网关这一层。公开文章里不需要写出真实上游地址知道“浏览器不直接碰后端服务”这件事就够了。第三件子路径静态资源和 SPA 刷新。/paperflow/assets/解决打包后资源路径问题/paperflow/下的try_files ... /index.html则保证前端路由刷新不会被 Nginx 当成真实文件查找失败。这三层缺一层都不行。4. 为什么/paperflow/assets/还要单独拎出来我们第一次看到这段配置时其实也会疑惑既然/paperflow/已经有try_files为什么/paperflow/assets/还要单独写一段。后来真正踩到白屏问题之后这个原因就很现实了静态资源和前端路由虽然都挂在/paperflow/下但语义完全不同。/paperflow/posts、/paperflow/login这类路径是 SPA 路由/paperflow/assets/index-xxxxx.js这类路径是真实静态文件。如果不把资源目录单独拿出来最糟的情况就是资源请求没命中真实文件又被 fallback 到/index.html浏览器收到的是 HTML却以为自己在加载 JS页面直接白屏。所以资源路径必须明确按真实文件处理不能和前端页面路由混在一起。5./api为什么不并到/paperflow/api下面我们最后保留的是这种结构location /api/ { proxy_pass http://gateway-upstream; }而不是/paperflow/api/我们最后保留/api/...这条独立路径是因为这样更容易把前端入口和后端入口拆开理解/paperflow/...属于前端页面和静态资源入口/api/...属于后端接口入口两者都由同一个 Nginx 暴露给浏览器但语义上不混在一起。这也和前端开发态是一致的。vite.config.ts里本地开发代理本质上也是这个结构proxy:{/api:{target:process.env.VITE_API_BASE??local-api-base,changeOrigin:true}}也就是说无论开发态还是生产态前端认的都是同一个接口前缀/api/...这对我们这种学生项目特别重要因为开发态和部署态如果连接口前缀都不一样后面排查的时候特别容易把自己绕晕。6. 后端这边也得继续守住/api/v1这层边界前端和 Nginx 路径收住了后端也要继续保持一致。user-service和content-service的application.yml都定义了server:servlet:context-path:/api/v1而网关这边又按/api/v1/...这套路径做路由分发-Path/api/v1/auth/**-Path/api/v1/users/**,/api/v1/public/users/**-Path/api/v1/posts,/api/v1/posts/**-Path/api/v1/comments,/api/v1/comments/**-Path/api/v1/pathfinder/sessions,/api/v1/pathfinder/sessions/**这说明整条链路的路径语义其实是一致的浏览器页面入口走/paperflow/...浏览器接口入口走/api/...网关和业务服务内部继续统一到/api/v1/...路径层级一旦这样固定下来后面无论联调还是部署脑子里至少不会同时打两三套路径。7. 我们最后发现最怕的不是配置多而是只有一层记得自己在子路径下我们最后发现这类问题最容易出事故的地方不是某个单独配置项写错而是不同层对“自己到底是不是部署在子路径下”理解不一致。典型错误一般有四种Vitebase配了/paperflow/但 React Router 还按/解释React Router 配了basename但 Nginx 没处理刷新 fallbackNginx 处理了/paperflow/但静态资源还在按/assets/取前端页面走子路径接口也被错误地改成/paperflow/api/...。这些问题单看都不复杂但叠在一起就特别像大学生项目里最常见的那种情况:每一层都觉得自己差不多对了最后整体就是跑不顺。因为你会看到首页能开某些页面刷新就 404某些资源偶尔又能加载接口调用路径还不统一。我们后来最有用的方法不是继续乱试而是老老实实把四层边界按顺序对一遍Vite baseBrowserRouter basenameNginx location /paperflow/Nginx location /api/8. 回头看这其实不只是前端细节而是一次完整的部署排坑一开始我们也把它当成前端小问题后来才发现它其实是一次完整的全链路排坑。因为它同时要求前端构建工具理解部署位置前端路由理解部署位置Nginx 理解页面和资源的区别网关理解接口前缀边界。只要这几层没有用同一套语义系统就会看起来“好像差不多”但总有一处在漏水。PaperFlow 现在这套方案其实不算复杂但它至少有一个很朴素的优点每一层都明确知道自己面对的是/paperflow/还是/api/。9. 最后如果你也是类似的大学生项目准备把 React 前端挂到一个子路径下真的不要只改一处配置就觉得结束了。至少把下面这几项一起核对掉vite.config.ts的baseBrowserRouter的basenameNginx 的静态资源路径处理Nginx 的 SPA fallback/api是否继续保持独立入口