統一部署
本文檔說明如何使用 Host 模組將靜態前端與 Spring Boot 統一打包部署。
架構概述
AppFuse 提供兩種 Host 模組,將靜態產物封裝為 WAR 部署:
app-office (React SPA) appfuse-docs (Docusaurus)
│ │
│ npm run build │ npm run build
▼ ▼
app-office/dist/ appfuse-docs/build/
│ │
│ copyFrontendBuild │ copyDocsBuild
▼ ▼
app-office-host appfuse-docs-host
├── SpaController ├── StaticSiteController
│ (base href 注入) │ (baseUrl 字串替換)
├── CSP nonce 注入 ├── URL / GitLab URL 替換
└── /config 端點 └── Sitemap 處理
│ │
▼ ▼
app-office.war appfuse-docs.war
兩種 Host 的差異
| 特性 | app-office-host (SPA) | appfuse-docs-host (Docusaurus) |
|---|---|---|
| 路由處理 | 轉發至 index.html | 依檔案路徑回應對應 HTML |
| 路徑適配 | <base href> 注入 | HTML 字串替換(href、src、content) |
| CSP nonce | 動態生成並注入 | 不支援 |
| 配置端點 | GET /config | 無 |
| URL 替換 | 無 | source-url → url(SEO、Sitemap) |
| GitLab 替換 | 無 | gitlab.source-base → gitlab.url |
app-office-host
建置流程
cd app-office-host
# 一鍵建置(前端 + WAR)
./gradlew clean build
# 或分步執行
./gradlew -p ../app-office npm_run_build # 建置前端
./gradlew copyFrontendBuild # 複製到 static/
./gradlew war # 打包 WAR
切換托管的前端應用
編輯 gradle.properties:
# 托管 app-office(生產版)
hostedApp=app-office
# 或托管 app-office-mockup(Prototype 版)
# hostedApp=app-office-mockup
SpaController 機制
所有非靜態請求轉發至 index.html,並動態處理:
- 注入
<base href>— 根據app.basename(或context-path)注入,讓 SPA 路由正確運作 - 生成 CSP nonce — 每次請求產生唯一 nonce,注入
<script>標籤並更新 CSP header
<!-- 原始 index.html -->
<head><title>App</title></head>
<script src="/assets/main.js"></script>
<!-- SpaController 處理後 -->
<head><base href="/app-office/"><title>App</title></head>
<script src="/assets/main.js" nonce="abc123xyz"></script>
配置端點
GET /config 提供前端可讀取的應用配置(自動排除 app.security.*):
{
"app.name": "花店管理系統",
"app.version": "1.0.0",
"app.basename": "/app-office"
}
配置說明
app-office.yaml:
app:
basename: /app-office # 部署路徑(覆蓋 context-path)
msw:
enabled: false # MSW Mock 開關
security:
hsts: "max-age=31536000; includeSubDomains"
csp: "default-src 'self'; ..."
referrer: "strict-origin-when-cross-origin"
appfuse-docs-host
建置流程
cd appfuse-docs-host
# 一鍵建置(Docusaurus + WAR)
./gradlew clean build
# 產生 WAR
./gradlew war
StaticSiteController 機制
攔截所有 GET 請求,對 HTML 進行三層替換:
1. baseUrl 替換(source-base → basename)
Docusaurus 建置時 (baseUrl: "/") 部署後 (basename: "/appfuse-docs/")
href="/server/..." → href="/appfuse-docs/server/..."
src="/assets/..." → src="/appfuse-docs/assets/..."
content="/" → content="/appfuse-docs/"
2. URL 替換(source-url → url)
建置時 URL 部署後 URL
https://source.example.com → https://target.example.com/appfuse-docs
影響 canonical URL、Open Graph meta、sitemap.xml。
3. GitLab URL 替換(gitlab.source-base → gitlab.url)
建置時 GitLab URL 部署後 GitLab URL
https://gitlab.com/org/repo → https://git.internal.com/org/repo
影響「編輯此頁」等 GitLab 連結。
配置說明
appfuse-docs.yaml:
docs:
# 建置時的值(對齊 docusaurus.config.ts)
source-url: http://localhost:2000 # docusaurus.config.ts 的 url(dev-friendly 預設)
source-baseUrl: / # docusaurus.config.ts 的 baseUrl(dev-friendly 預設)
# 部署時的目標值
basename: /appfuse-docs/ # 與 server.servlet.context-path 對齊
siteUrl: https://appfuse.leandev.io # 部署網域(支援含可選 nginx 專案前綴)
# GitLab URL 替換(兩者相同時不替換)
gitlab:
source-base: https://gitlab.com/appfuse/webapp
url: https://gitlab.com/appfuse/webapp
# 安全 Headers
security:
hsts: "max-age=31536000; includeSubDomains; preload"
csp: "default-src 'self'; ..."
referrer: "no-referrer-when-downgrade"
:::caution source-base 不可隨意修改
source-base 必須與 docusaurus.config.ts 的 baseUrl 一致,因為它是替換的「搜尋字串」。若兩者不一致,替換會失敗。這是建置時決定的值,不是 runtime 可調整的。
:::
設計原則:一次建置、多處部署
替換機制讓同一份建置產物可以部署到不同環境,只需調整配置:
| 環境 | basename | url | gitlab.url |
|---|---|---|---|
| 開發 | /appfuse-docs/ | http://localhost:8080 | https://gitlab.com/... |
| 生產 | / | https://appfuse.leandev.io | https://gitlab.com/... |
| 私有 | /docs/ | https://docs.internal.com | https://git.internal.com/... |
Nginx 反向代理配置
使用 Nginx 反向代理到 Tomcat 時,需注意 proxy_pass 路徑與 basename 的配合。
配置原則
basename 應與瀏覽器看到的路徑一致,而非 Tomcat 的 context-path。
情境一:對外 URL 不帶路徑前綴
瀏覽器存取 https://appfuse.leandev.io/,Tomcat context-path 為 /appfuse-docs:
location / {
proxy_pass http://192.168.26.218:8080/appfuse-docs/;
}
此時 Nginx 已將 / 映射到 Tomcat 的 /appfuse-docs/,basename 須設為 /,不做替換:
# $APP_HOME/conf/appfuse-docs.yaml
docs:
basename: /
:::danger 常見陷阱:雙重路徑
若 basename 設為 /appfuse-docs/,Controller 會將 HTML 中的 src="/assets/..." 替換為 src="/appfuse-docs/assets/..."。瀏覽器請求 /appfuse-docs/assets/...,Nginx 再加上前綴轉發到 Tomcat 的 /appfuse-docs/appfuse-docs/assets/...,導致 404 或 503。
:::
情境二:對外 URL 帶路徑前綴
瀏覽器存取 https://example.com/appfuse-docs/,Tomcat context-path 為 /appfuse-docs:
location /appfuse-docs/ {
proxy_pass http://192.168.26.218:8080/appfuse-docs/;
}
此時瀏覽器路徑與 Tomcat context-path 一致,basename 設為 /appfuse-docs/:
docs:
basename: /appfuse-docs/
情境三:無 Nginx,直接存取 Tomcat
瀏覽器存取 http://localhost:8080/appfuse-docs/:
docs:
basename: /appfuse-docs/ # 與 context-path 一致
這是預設配置,不需要額外覆蓋。
快速判斷
basename = 瀏覽器網址列中,主機名之後、第一個路由路徑之前的部分
https://appfuse.leandev.io/guides/... → basename: /
https://example.com/appfuse-docs/guides/... → basename: /appfuse-docs/
http://localhost:8080/appfuse-docs/guides/... → basename: /appfuse-docs/
外部配置覆蓋
Spring Boot 支援從 $APP_HOME/conf/ 載入外部配置,覆蓋 WAR 內建的設定。適用於不重新建置就調整部署路徑的場景。
以 appfuse-docs-host 為例:
# Tomcat 的應用目錄下建立配置
$APP_HOME/conf/appfuse-docs.yaml
# 僅覆蓋需要修改的屬性
docs:
# 部署路徑(與瀏覽器實際存取的 base path 一致)
# 若經由 nginx 反向代理,可能與 server.servlet.context-path 不同
basename: /
安全 Headers
兩種 Host 都提供安全 Headers 配置:
| Header | app-office-host | appfuse-docs-host |
|---|---|---|
| HSTS | app.security.hsts | docs.security.hsts |
| CSP | app.security.csp(含 nonce) | docs.security.csp |
| Referrer-Policy | app.security.referrer | docs.security.referrer |
| Permissions-Policy | app.security.permissions | docs.security.permissions |
| X-Frame-Options | DENY | SAMEORIGIN |
| X-Content-Type-Options | nosniff | nosniff |
靜態資源快取
app-office-host
| 路徑 | 快取時間 | 說明 |
|---|---|---|
/assets/** | 365 天 | 含版本 hash,可長期快取 |
| 其他靜態檔案 | 1 天 | 一般檔案 |
appfuse-docs-host
| 路徑 | 快取時間 | 說明 |
|---|---|---|
/assets/** | 365 天 | 含版本 hash |
/img/** | 7 天 | 圖片資源 |
| 其他靜態檔案 | 1 小時 | 一般檔案 |
故障排除
Docusaurus 顯示 baseUrl 錯誤 Banner
症狀:頁面頂部顯示紅色 "Your Docusaurus site did not load properly" 警告。
原因:Docusaurus JS 未成功載入,window.docusaurus 未定義。
排查步驟:
- 開啟瀏覽器 DevTools → Network,檢查 JS/CSS 請求的 HTTP 狀態碼
- 若回傳 503/404,檢查請求路徑是否出現雙重前綴(如
/appfuse-docs/appfuse-docs/assets/...) - 確認
basename是否與瀏覽器實際存取的 base path 一致
SPA 路由返回 404
原因:SpaController 未攔截到該路徑。
解決:確認路徑不在靜態資源排除清單中。
CSP 阻擋腳本執行
原因:nonce 未正確注入,或 CSP 策略過嚴。
解決:檢查瀏覽器 Console 的 CSP 違規報告,必要時暫時放寬(僅開發環境):
app:
security:
csp: "default-src 'self'; script-src 'self' 'unsafe-inline'"
靜態資源 404
原因:前端建置輸出未複製到 src/main/resources/static/。
解決:
./gradlew clean build