跳至主要内容

統一部署

本文檔說明如何使用 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 字串替換(hrefsrccontent
CSP nonce動態生成並注入不支援
配置端點GET /config
URL 替換source-urlurl(SEO、Sitemap)
GitLab 替換gitlab.source-basegitlab.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,並動態處理:

  1. 注入 <base href> — 根據 app.basename(或 context-path)注入,讓 SPA 路由正確運作
  2. 生成 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-basebasename

Docusaurus 建置時 (baseUrl: "/") 部署後 (basename: "/appfuse-docs/")
href="/server/..." → href="/appfuse-docs/server/..."
src="/assets/..." → src="/appfuse-docs/assets/..."
content="/" → content="/appfuse-docs/"

2. URL 替換source-urlurl

建置時 URL 部署後 URL
https://source.example.com → https://target.example.com/appfuse-docs

影響 canonical URL、Open Graph meta、sitemap.xml。

3. GitLab URL 替換gitlab.source-basegitlab.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.tsbaseUrl 一致,因為它是替換的「搜尋字串」。若兩者不一致,替換會失敗。這是建置時決定的值,不是 runtime 可調整的。 :::

設計原則:一次建置、多處部署

替換機制讓同一份建置產物可以部署到不同環境,只需調整配置:

環境basenameurlgitlab.url
開發/appfuse-docs/http://localhost:8080https://gitlab.com/...
生產/https://appfuse.leandev.iohttps://gitlab.com/...
私有/docs/https://docs.internal.comhttps://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 配置:

Headerapp-office-hostappfuse-docs-host
HSTSapp.security.hstsdocs.security.hsts
CSPapp.security.csp(含 nonce)docs.security.csp
Referrer-Policyapp.security.referrerdocs.security.referrer
Permissions-Policyapp.security.permissionsdocs.security.permissions
X-Frame-OptionsDENYSAMEORIGIN
X-Content-Type-Optionsnosniffnosniff

靜態資源快取

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 未定義。

排查步驟

  1. 開啟瀏覽器 DevTools → Network,檢查 JS/CSS 請求的 HTTP 狀態碼
  2. 若回傳 503/404,檢查請求路徑是否出現雙重前綴(如 /appfuse-docs/appfuse-docs/assets/...
  3. 確認 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

下一步