由於前端頁面每次在發佈一版的時候, JS 打包器會自動產生新的雜湊檔案( /assets/index-BYVyqdnB.css ),然後舊的雜湊檔案就會隨著部署而消失。當用戶還在瀏覽頁面的同時,如果有需要動態載入檔案,就會發生檔案不存在的情況。

Screenshot 2025-10-22 at 10.17.11.png

解決辦法

  • 對舊資源做短期保留,等用戶刷新後再清理
    • 優點:可同時兼容新舊版
    • 缺點:部署流程不容易設計,以及如何清除舊版本需要思考。
  • 定期檢查,是否有版本更新。若是則重新載入新版本。
    • 優點:只要稍微改動加入檢查即可
    • 缺點:檢查的週期太長,可能還是會遇到找不到檔案。太短,可能請求量太多。而且重新載入版本,會影響使用者的體驗。

應該選擇哪一個方案?

….

….

….

….

….

….

….

….

image.png

短期保留舊資源

今年 3 月的時候,AWS Amplify 發布了一個功能, Skew Protection

只需要簡單的從 Amplify 後台設定開啟即可。

選擇想要啟動的分支,然後把它啟用即可。

https://aws.amazon.com/tw/about-aws/whats-new/2025/03/aws-amplify-hosting-deployment-skew-protection-support/

工作原理

  • 開啟設定之後 Skew Protection 後續發布的每一個版本會進行保留,至多保留八個版本。(建議開啟之後,重新發佈一個新版)
  • 在請求網頁的時候,系統會設定一個名為 __dpl 的 Cookie ,上面記錄著最新的版本編號
  • 後續請求任何資源,帶上這個 cookie,會傳回對應版本的靜態內容。

按官方文件說明,第一次開啟時由於配合相關的 CDN 設定變更,大約十分鐘後才會開始生效。

檢查新版

首先為每一個版本產生版本檔案,可以在部署的時候,根據 git commit hash 建立。

底下修改 package.json 的 build 來實現

"-- 舊 --"
"build": "vite build",

"-- 新 --"
"build": "vite build && git rev-parse --short HEAD > dist/assets/version.txt",

然後在 main.js 的部分,加入檢查的邏輯

if ("production" === import.meta.env.VITE_APP_ENV) {
  const LOCAL_KEY = "app-version";

  async function checkVersion() {
    const res = await fetch("/assets/version.txt", {
	    cache: "no-store"
	   });
    const version = await res.text()
    if (!version) {
      return;
    }

      const current = sessionStorage.getItem(LOCAL_KEY);
      if (!current) {
        sessionStorage.setItem(LOCAL_KEY, version);
        return;
      }
      if (version !== current) {
        sessionStorage.setItem(LOCAL_KEY, version);
        location.reload(true);
      }
  }

  checkVersion();
  setInterval(checkVersion, 60000);
}

一般來說,上述的寫法就能達到檢查的作用。但是在開啟 Skew Protection 之後,每一個靜態資源的存取,只會拿到特定版本的內容。無法透過上述方法取得最新的版本(瀏覽器會自動帶上 cookie)。所以需要透過設定 credentials: 'omit' 避免 cookie 內容送出

    const res = await fetch("/assets/version.txt", {
	    cache: "no-store"
	    credentials: 'omit'
    });

管理系統登入的時候會需要填入第一道密碼,隨後填入的第一道密碼會透過瀏覽器自動設定 Authorization header

Authorization: Basic xxxxxxxxxxxxxxxx

但是當我們設定 credentials: 'omit' 的同時,也避免了 Authorization header 被送出。此時就會造成該靜態資源無法存取。所以我們需要建構一個新的 Header 包含原本的 Authorization,但是不要送出 Cookie。

首先瀏覽器的安全性設計,會有底下限制:

  • 你無法取得請求送出時由瀏覽器帶上的 Header,所以無法知道 Authorization header 是什麼。

  • 你無法修改一個被設置為 HttpOnly 的 Cookie

    Screenshot 2025-10-17 at 13.30.46.png

所以無奈只能移除 credentials: 'omit'

讓 Amplify 後端提供最新版本號到表頭

解決上述問題的思路,就是要能夠 動態 的取得新的版本號。 Amplify 當然有能力建構後端的 API,但最後我選擇一個比較輕量的解決方案。在回傳的表頭帶上最新的版本號。

customHttp.yml

Amplify 有提供一個功能可以自訂請求要回應的表頭內容,透過在根目錄放置 customHttp.yml

customHeaders:
  - pattern: '/assets/version.txt'
    headers:
    - key: 'X-Version'
      value: 'XXXXXXX'

但由於版本號每次都不同,所以需要動態的產生 customHttp.yml。

首先透過在根目錄建立 customHttp.template.yml

然後透過 amplify.yml 使用 envsubst 自動替換環境變數

customHeaders:
  - pattern: '/assets/version.txt'
    headers:
    - key: 'X-Version'
      value: '$AWS_AMPLIFY_DEPLOYMENT_ID'

amplify.yml

這邊透過精確的控制 amplify 建構過程來完成這事情。透過在根目錄放置 amplify.yml 可以完整的操作建置的每一個動作。

version: 1
frontend:
  phases:
    preBuild:
      commands:
        - envsubst < ./customHttp.template.yml > ./customHttp.yml
        - yarn install --frozen-lockfile
    build:
      commands:
        - yarn build
  artifacts:
    baseDirectory: dist
    files:
      - '**/*'
  cache:
    paths:
      - node_modules/**/*

最後透過修改原本的 JS 檢查版本邏輯,從 header 取得最新版本。

// 舊
async function checkVersion() {
    const res = await fetch("/assets/version.txt", { cache: "no-store" });
    const version = await res.text()
    if (!version) {
      return;
    }
}

// 新
async function checkVersion() {
    const res = await fetch("/assets/version.txt", { cache: "no-store" });
    const version = res.headers.get('X-Version')
    if (!version) {
      return;
    }
}

最後加上一些 GUI,提醒有版本的更新。

Screenshot 2025-10-17 at 14.34.29.png