diff --git a/config/nginx/conf.d/xuqm.conf b/config/nginx/conf.d/xuqm.conf index 493b5d8..33cb167 100644 --- a/config/nginx/conf.d/xuqm.conf +++ b/config/nginx/conf.d/xuqm.conf @@ -138,5 +138,8 @@ server { sub_filter 'https://dev.xuqinmin.com' 'https://$host'; sub_filter_once off; sub_filter_types text/html text/javascript application/javascript; + proxy_hide_header Cache-Control; + proxy_hide_header Expires; + add_header Cache-Control $xuqm_web_cache_control always; } } diff --git a/config/nginx/nginx.conf b/config/nginx/nginx.conf index 21d08bd..6ba8fb9 100644 --- a/config/nginx/nginx.conf +++ b/config/nginx/nginx.conf @@ -7,6 +7,13 @@ http { proxy_read_timeout 300s; proxy_send_timeout 300s; + # 入口 HTML/manifest 必须每次再验证;只有版本目录和历史迁移期的哈希 assets + # 可以永久缓存。外层 nginx 统一覆盖上游头,避免私有部署代理层重新引入旧入口。 + map $uri $xuqm_web_cache_control { + default "no-cache, no-store, must-revalidate"; + ~^/(?:releases/[^/]+/assets|assets)/ "public, max-age=31536000, immutable"; + ~^/docs/(?:releases/[^/]+/assets|assets)/ "public, max-age=31536000, immutable"; + } + include /etc/nginx/conf.d/*.conf; } - diff --git a/docs/configuration.md b/docs/configuration.md index de8ce4a..ba7b42a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -114,6 +114,11 @@ SDK_TENANT_SERVICE_URL: "http://tenant-service:9001" nginx 容器属于 `base` 必启服务,直接绑定宿主机 `0.0.0.0:80`,统一处理所有内部路由。配置文件为 `config/nginx/conf.d/xuqm.conf`,使用 `resolver 127.0.0.11 valid=10s` + 变量化 `proxy_pass`,**容器重建后 nginx 无需 reload**,自动通过 Docker DNS 获取新 IP。 +Web 代理会覆盖上游缓存头:入口 HTML、manifest 和普通路由使用 +`no-cache, no-store, must-revalidate`;`releases/<版本>/assets/` 及迁移期旧 +`assets/` 使用一年 `immutable`。tenant-web 镜像保留当前与上一版本资源,因此发布后 +旧页面仍可完成 lazy chunk 加载,不会回退到 SPA HTML 或返回 404。 + 详见 [runbook.md](runbook.md#nginx-配置)。 --- diff --git a/docs/runbook.md b/docs/runbook.md index e4eaefd..fc58e09 100644 --- a/docs/runbook.md +++ b/docs/runbook.md @@ -170,6 +170,23 @@ bash scripts/reset.sh --no-pull --- +### 发布后页面资源 404 + +**现象**:发布后已打开的页面跳转功能时,请求旧哈希 JavaScript/CSS 返回 404。 +**原因**:旧入口 HTML 仍引用上一版本 lazy chunk,但旧镜像已被替换。 +**当前保障**: + +- 入口 HTML 与 manifest 禁止持久缓存; +- 哈希资源使用版本目录并声明 `immutable`; +- 新 tenant-web 镜像会保留上一版本资源目录; +- 哈希资源不存在时返回真实 404,禁止回退为 `index.html`。 + +若升级后仍出现问题,先确认 tenant-web 镜像与本仓库 nginx 配置来自同一发布版本,再 +执行 `bash scripts/reset.sh --no-pull` 重建容器;不要通过清空所有浏览器缓存掩盖发布 +配置错误。 + +--- + ### 服务 crash-loop / 启动失败 **现象**:`docker compose ps` 显示服务 `Restarting`,日志反复出现启动异常。 diff --git a/scripts/verify.sh b/scripts/verify.sh index 398d622..ab75136 100644 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -113,6 +113,11 @@ http_body() { curl -sL --noproxy '*' --max-time "$timeout" "$url" 2>/dev/null || true } +http_headers() { + local url="$1" timeout="${2:-8}" + curl -sS --noproxy '*' -D - -o /dev/null --max-time "$timeout" "$url" 2>/dev/null || true +} + http_body_auth() { local url="$1" token="$2" timeout="${3:-8}" curl -sL --noproxy '*' --max-time "$timeout" \ @@ -242,6 +247,28 @@ else fail "控制台前端不可访问 (HTTP $WEB_CODE)" fi +WEB_CACHE="$(http_headers "$BASE_URL/" | tr -d '\r' | awk 'BEGIN{IGNORECASE=1} /^Cache-Control:/{value=$0} END{print value}')" +if printf '%s' "$WEB_CACHE" | grep -qi 'no-store'; then + pass "入口 HTML 禁止持久缓存" +else + fail "入口 HTML 缺少 no-store" "${WEB_CACHE:-无 Cache-Control}" +fi + +WEB_HTML="$(http_body "$BASE_URL/")" +WEB_ASSET="$(printf '%s' "$WEB_HTML" | grep -oE '/releases/[^/]+/assets/[^"[:space:]]+\.(js|css)' | head -1 || true)" +if [ -n "$WEB_ASSET" ]; then + ASSET_HEADERS="$(http_headers "$BASE_URL$WEB_ASSET" | tr -d '\r')" + ASSET_CODE="$(printf '%s' "$ASSET_HEADERS" | awk '/^HTTP/{code=$2} END{print code}')" + ASSET_CACHE="$(printf '%s' "$ASSET_HEADERS" | awk 'BEGIN{IGNORECASE=1} /^Cache-Control:/{value=$0} END{print value}')" + if [ "$ASSET_CODE" = "200" ] && printf '%s' "$ASSET_CACHE" | grep -qi 'immutable'; then + pass "版本化哈希资源可访问且 immutable" + else + fail "版本化资源缓存契约异常" "HTTP ${ASSET_CODE:-000}, ${ASSET_CACHE:-无 Cache-Control}" + fi +else + fail "入口 HTML 未引用版本化 releases assets" +fi + DOCS_CODE=$(http_get "$BASE_URL/docs/") if echo "$DOCS_CODE" | grep -qE '^(200|301|302)$'; then pass "文档站可访问 (HTTP $DOCS_CODE)"