現象: 客戶反饋某彈窗保存按鈕點擊無響應。本地、測試環境均無法復現。登入客戶服務器系統復現成功。
控制台報錯: TypeError: Cannot read property 'length' of undefined — 代碼試圖遍歷 response.data.length,但 response.data 不是陣列。
預期返回:[{...}, {...}] (JSON 陣列)。
實際返回:{"success":false, "msg":"<script>top.location.href='/login'</script>"} — 這是未登錄/無權限時的攔截器返回!
對比正常與異常請求 URL:
https://domain:port/${basePath}/api/save → 解析為 /actual-context/api/savehttps://domain:port/projectName/api/save — 硬編碼了項目名稱!server.xml 虛擬目錄配置客戶服務器 conf/server.xml 配置:
<Host name="localhost" appBase="webapps" ...>
<Context path="" docBase="/opt/app/my-project" reloadable="true" />
</Host>
path="":應用掛載在**根路徑** /,訪問 URL **不帶**項目名。docBase:指向項目實際部署目錄。後果:
https://domain:port/api/save (無項目名)。/projectName/api/save → 請求路徑變為 https://domain:port/projectName/api/save。projectName 應用 → 404 或被默認 Servlet 處理 → 安全攔截器判定為未登錄請求 → 返回登錄頁腳本 → 前端 JS 解析報錯。path="/projectName" (或自動部署到 webapps/projectName),訪問**必須帶**項目名。/projectName/... 恰好匹配本地路徑 → 偽裝成「正常」。// JS 硬編碼
const url = '/my-project/api/save';
// JSP 硬編碼
<form action="/my-project/api/save">
// JSP
<% String basePath = request.getContextPath(); %>
<form action="<%=basePath%>/api/save">
// Thymeleaf
<form th:action="@{/api/save}">
// FreeMarker
<form action="${base}/api/save"> <!-- 需配置 SharedVariable 或 Filter 注入 -->
baseURL:
// axios 配置 (main.js / api.js)
import axios from 'axios';
// 方式 A: 打包時注入 (Webpack DefinePlugin / Vite env)
const baseURL = import.meta.env.VITE_API_BASE || '/api';
// 方式 B: 運行時從 meta 讀取 (index.html 注入)
const baseURL = document.querySelector('meta[name="base-url"]').content;
axios.defaults.baseURL = baseURL;
// 業務代碼只寫相對路徑
axios.post('/save', data);
<base href="${basePath}/"> + 相對路徑:
<head>
<base href="<%=request.getContextPath()%>/">
</head>
<body>
<form action="api/save"> <!-- 自動解析為 /context/api/save -->
| 核心教訓 | 具體行動 |
|---|---|
| 絕不在前端硬編碼項目名/上下文路徑 | 任何環境部署變更 (虛擬目錄、網關路由、域名遷移) 都會導致失效 |
| 上下文路徑由服務端模板或構建工具注入 | JSP 用 request.getContextPath(),現代框架用環境變量/運行時注入 |
| 環境一致性原則 | 開發、測試、生產 Tomcat server.xml Context path 應保持一致 (建議統一非根路徑 /app-name 或統一根路徑 /,二選一全環境同步) |
| 網關/反向代理層統一路徑轉發 | Nginx/Zuul/Spring Cloud Gateway 統一剝離/添加前綴,應用層無感 |
| 攔截器/過濾器返回類型一致性 | Ajax 請求攔截到未登錄應返回 401 JSON,而非 HTML 登錄頁腳本,前後端分離架構強制要求 |
血案啟示:
${basePath}不是模板變量,而是環境無關性的契約。凡是涉及 URL 路徑拼接的地方,若無${basePath}(或等價機制),皆為技術債,終將在環境變更時爆雷。