專案用 httpVueLoader 在瀏覽器直接載入 .vue 檔,沒有 webpack、沒有建置步驟,改完存檔重新整理就看得到結果。方便是方便,但我很快撞到一個惱人的狀況:明明改了 SurveyPit.vue,按 F5 畫面還是舊的,連改了三次都一樣。

第一個念頭是去包 keep-alive,結果越包越糟。後來才弄懂,「快取」這個詞在 Vue 裡指兩種完全不同的東西,方向搞錯就會像我那樣白忙。所以先把這兩件事拆開講。

兩種「快取」

  • 瀏覽器檔案快取(這篇要解的):瀏覽器為了省流量,會把抓過的 .vue.js 暫存起來。你改了檔案、重新整理,瀏覽器拿出暫存的舊檔,於是看到舊內容。這是 HTTP 層的事。
  • 元件實例快取keep-alive 在做的事。它把元件實例留在記憶體裡,切走再切回來時不重建,保留表單輸入跟捲動位置。這是記憶體層的狀態保留,跟「檔案有沒有更新」沒關係。

把這兩者搞混的代價很實際。我當初想解「改完 .vue 不更新」卻跑去包 keep-alive,不但沒效,反而把舊實例牢牢釘在記憶體裡,新內容更難冒出來。

問題情境

當時專案有這幾個用 httpVueLoader 全域註冊的元件:

1
2
3
4
Vue.component('x-spit', httpVueLoader('DrainFacility/SurveyPit.vue')); // 陰井
Vue.component('x-shole', httpVueLoader('DrainFacility/SurveyHole.vue')); // 人孔
Vue.component('x-spiple', httpVueLoader('DrainFacility/SurveyPiple.vue')); // 連接管
Vue.component('x-spipeline', httpVueLoader('DrainFacility/SurveyPipeline.vue')); // 管線

改完 SurveyPit.vue 重新整理,畫面沒變。原因在 httpVueLoader 是用 XMLHttpRequest 去抓 .vue 檔,再丟給 Vue 編譯器處理。這個請求跟載入圖片、CSS 一樣會吃瀏覽器快取,瀏覽器看到同一個 URL,就直接給你暫存的版本。

解決方案

1. 加版本號或時間戳

最直接的辦法,是在檔案路徑後面接一個會變動的查詢參數。URL 一變,瀏覽器就當成新檔案重抓:

1
2
3
4
5
6
// 開發時用時間戳,每次載入都不同,等於完全不吃快取
Vue.component('x-spit', httpVueLoader('DrainFacility/SurveyPit.vue?v=' + Date.now()));

// 上線時用版本號,發版才換值,平時仍能吃到快取
const VERSION = '1.0.1';
Vue.component('x-spit', httpVueLoader(`DrainFacility/SurveyPit.vue?v=${VERSION}`));

注意 VERSION 要寫成字串 '1.0.1'1.0.1 直接當數字寫會是語法錯誤,因為 JavaScript 數字只能有一個小數點。

兩種寫法差在換值的頻率。Date.now() 每次重新整理都不一樣,快取等於失效,適合開發階段;上線後別這樣用,否則每個訪客每次都重抓,CDN 也快取不了。

如果生產環境要用手動維護的版本號,有幾個現實風險得先想清楚:多人協作容易忘記更版、版本號撞號、或是改了檔案但版本號沒跟著動。這做法是 httpVueLoader 沒有建置流程下的權宜之計,勉強能撐,但不該當成長期維護的標準。能引進建置工具的話(見方案 3),那才是穩的方向。

2. 從伺服器端關掉快取

換個角度,從伺服器下手,用 HTTP 標頭叫瀏覽器別快取特定路徑:

1
2
3
4
5
6
# Nginx 設定範例
location /components/ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "Thu, 01 Jan 1970 00:00:00 GMT";
}

這裡的引號不能省。add_header 只吃單一 value 參數,Cache-Control 的值含逗號跟空白,沒包引號 nginx -t 過不了。

Expires 標頭的值得是 RFC 1123 格式的 HTTP 日期字串。有人會圖省事寫 "0",但這不是合法日期格式。按 RFC 7234(以及取代它的 RFC 9111)的規定,快取收到非法格式的 Expires 日期,要把它當成「過去的時間」,也就是視為已過期,並不是忽略掉。所以與其賭各家瀏覽器怎麼解讀 "0",不如直接寫一個明確的過去時間點(像上面的 Unix epoch)。

還有一個容易踩的細節:同一個回應如果帶了 Cache-Control: max-age,瀏覽器會直接忽略 Expires,以 max-age 為準。上面範例用的是 no-cache, no-storeno-store 要求快取不得儲存回應,既然不儲存就不存在後續重用的新鮮度判斷,Expires 對此情境無實際作用。光靠 Cache-Control: no-store 就足夠禁止儲存,Expires 可省略。

這招好在不必改任何前端程式碼,缺點是該路徑下的檔案完全不快取,每次都重抓。比較務實的折衷,是只對 .vue 開發檔下 no-cache,其餘靜態資源照常快取。

3. 上線改走有建置步驟的流程

開發時用 httpVueLoader 沒問題,但要正式上線、想要穩定的快取破壞機制,比較好的方向是引進有建置步驟的打包工具,像 webpack 或 Vite。

這裡得先講清楚一個我自己一開始也誤會的點。不是把這行:

1
Vue.component('x-spit', httpVueLoader('DrainFacility/SurveyPit.vue'));

直接換成這行就好:

1
2
// 注意:這需要有建置流程才能跑,不是把字串換掉就行
Vue.component('x-spit', () => import('./DrainFacility/SurveyPit.vue'));

httpVueLoader 的存在意義,就是「沒有建置步驟、瀏覽器執行期直接抓 .vue」。() => import('./...') 是另一套世界,它要 webpack 或 Vite 在打包時用 vue-loader.vue 編譯成普通 JS,瀏覽器本身不認識 .vue。沒有建置流程,直接寫 import('./SurveyPit.vue') 跑不起來。順帶一提,import() 的路徑也是字串,引號不能漏。

而且 import() 這行語法本身不負責解快取。建置工具的快取破壞,是靠輸出檔名帶上內容雜湊:

1
2
3
4
5
6
// webpack.config.js
module.exports = {
output: {
filename: '[name].[contenthash].js', // 內容變,雜湊變,檔名就變
},
};

檔名裡的 [contenthash] 會跟著檔案內容跑。內容一改,雜湊變了,檔名也變,瀏覽器看到新檔名自然重抓;內容沒改,檔名不變,就能放心長期快取。從 httpVueLoader 搬到建置流程,解快取的是 [contenthash] 這個機制,不是 import() 那行字。

4. keep-alive 解的是另一個問題

keep-alive 不是用來解「改完 .vue 不更新」的。它做的是切換元件時把實例留在記憶體裡,保留狀態。

它真正派得上用場的場景是這樣:你有一個多分頁表單,或一個帶搜尋條件的列表,使用者切到別的分頁再切回來,希望剛填的內容、捲到的位置都還在:

1
2
3
<keep-alive :include="['SurveyPit', 'SurveyHole']">
<component :is="currentComponent"></component>
</keep-alive>

include 比對的是元件的 name。Vue 2 跟 Options API 在 script 裡宣告 name: 'SurveyPit' 就行。Vue 3 加 <script setup> 的情況稍微繞一點:

  • Vue 3.2.34 以上:SFC 會自動把檔名當作 nameSurveyPit.vue 對應的 name 就是 SurveyPit),多數情況不用額外宣告。
  • 要明確覆寫、或版本比較舊時:用 defineOptions 巨集(Vue 3.3 起正式支援):
1
2
3
<script setup>
defineOptions({ name: 'SurveyPit' })
</script>

name 對不上,include 就比不到這個元件,快取當然也不會生效。

要強調的還是那句:用了 keep-alive,元件實例被釘在記憶體裡,反而更不會去重抓更新後的 .vue。目標如果是「看到改過的內容」,這招方向相反,別用,我已經親自驗證過一次了。

5. 開發時最省事的做法

邊改邊看,最快的方式是讓瀏覽器當下不快取,連設定都不用動:

  1. 開 Chrome 開發者工具:
    • 在 Network 頁籤勾 Disable cache(只有開著 DevTools 才生效)
    • 或對重新整理按鈕按右鍵,選「清空快取並強制重新載入」

這是臨時用的招,不必改任何設定,也不必裝建置工具。如果專案已經有 Vue CLI(代表已經有建置流程了),vue.config.js 的 devServer 可以預設關掉開發伺服器的快取,但那是另一個情境,跟 httpVueLoader 無關。

怎麼挑

  • 開發階段、沒有建置工具:勾 DevTools 的 Disable cache,或路徑接 ?v=Date.now(),最省事。
  • 生產環境、還在用 httpVueLoader:手動維護版本號是權宜之計,能用但有維護風險;規模稍大就認真考慮搬到 webpack 或 Vite。
  • 已有建置流程:用 [contenthash] 自動破快取,最穩。
  • 想保留切換時的元件狀態(不是要更新檔案):才考慮 keep-alive,Vue 3 加 <script setup> 記得確認 name 有對上。

我繞的這趟彎路,說穿了就是把「檔案沒更新」當成「狀態沒保留」在解。快取存在是為了省流量、加快載入,目標從來不是把它整個關掉,而是在「改完看得到新內容」跟「平常載入夠快」之間抓平衡。後來專案上線版改走 Vite 的 [contenthash],開發機則固定勾 Disable cache,這個惱人狀況就再沒出現過。

參考資料