專案用 httpVueLoader 在瀏覽器直接載入 .vue 檔,沒有 webpack、沒有建置步驟,改完存檔重新整理就看得到結果。方便是方便,但我很快撞到一個惱人的狀況:明明改了 SurveyPit.vue,按 F5 畫面還是舊的,連改了三次都一樣。
第一個念頭是去包 keep-alive,結果越包越糟。後來才弄懂,「快取」這個詞在 Vue 裡指兩種完全不同的東西,方向搞錯就會像我那樣白忙。所以先把這兩件事拆開講。
兩種「快取」
- 瀏覽器檔案快取(這篇要解的):瀏覽器為了省流量,會把抓過的
.vue、.js暫存起來。你改了檔案、重新整理,瀏覽器拿出暫存的舊檔,於是看到舊內容。這是 HTTP 層的事。 - 元件實例快取:
keep-alive在做的事。它把元件實例留在記憶體裡,切走再切回來時不重建,保留表單輸入跟捲動位置。這是記憶體層的狀態保留,跟「檔案有沒有更新」沒關係。
把這兩者搞混的代價很實際。我當初想解「改完 .vue 不更新」卻跑去包 keep-alive,不但沒效,反而把舊實例牢牢釘在記憶體裡,新內容更難冒出來。
問題情境
當時專案有這幾個用 httpVueLoader 全域註冊的元件:
1 | Vue.component('x-spit', httpVueLoader('DrainFacility/SurveyPit.vue')); // 陰井 |
改完 SurveyPit.vue 重新整理,畫面沒變。原因在 httpVueLoader 是用 XMLHttpRequest 去抓 .vue 檔,再丟給 Vue 編譯器處理。這個請求跟載入圖片、CSS 一樣會吃瀏覽器快取,瀏覽器看到同一個 URL,就直接給你暫存的版本。
解決方案
1. 加版本號或時間戳
最直接的辦法,是在檔案路徑後面接一個會變動的查詢參數。URL 一變,瀏覽器就當成新檔案重抓:
1 | // 開發時用時間戳,每次載入都不同,等於完全不吃快取 |
注意 VERSION 要寫成字串 '1.0.1'。1.0.1 直接當數字寫會是語法錯誤,因為 JavaScript 數字只能有一個小數點。
兩種寫法差在換值的頻率。Date.now() 每次重新整理都不一樣,快取等於失效,適合開發階段;上線後別這樣用,否則每個訪客每次都重抓,CDN 也快取不了。
如果生產環境要用手動維護的版本號,有幾個現實風險得先想清楚:多人協作容易忘記更版、版本號撞號、或是改了檔案但版本號沒跟著動。這做法是 httpVueLoader 沒有建置流程下的權宜之計,勉強能撐,但不該當成長期維護的標準。能引進建置工具的話(見方案 3),那才是穩的方向。
2. 從伺服器端關掉快取
換個角度,從伺服器下手,用 HTTP 標頭叫瀏覽器別快取特定路徑:
1 | # Nginx 設定範例 |
這裡的引號不能省。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-store;no-store 要求快取不得儲存回應,既然不儲存就不存在後續重用的新鮮度判斷,Expires 對此情境無實際作用。光靠 Cache-Control: no-store 就足夠禁止儲存,Expires 可省略。
這招好在不必改任何前端程式碼,缺點是該路徑下的檔案完全不快取,每次都重抓。比較務實的折衷,是只對 .vue 開發檔下 no-cache,其餘靜態資源照常快取。
3. 上線改走有建置步驟的流程
開發時用 httpVueLoader 沒問題,但要正式上線、想要穩定的快取破壞機制,比較好的方向是引進有建置步驟的打包工具,像 webpack 或 Vite。
這裡得先講清楚一個我自己一開始也誤會的點。不是把這行:
1 | Vue.component('x-spit', httpVueLoader('DrainFacility/SurveyPit.vue')); |
直接換成這行就好:
1 | // 注意:這需要有建置流程才能跑,不是把字串換掉就行 |
httpVueLoader 的存在意義,就是「沒有建置步驟、瀏覽器執行期直接抓 .vue」。() => import('./...') 是另一套世界,它要 webpack 或 Vite 在打包時用 vue-loader 把 .vue 編譯成普通 JS,瀏覽器本身不認識 .vue。沒有建置流程,直接寫 import('./SurveyPit.vue') 跑不起來。順帶一提,import() 的路徑也是字串,引號不能漏。
而且 import() 這行語法本身不負責解快取。建置工具的快取破壞,是靠輸出檔名帶上內容雜湊:
1 | // webpack.config.js |
檔名裡的 [contenthash] 會跟著檔案內容跑。內容一改,雜湊變了,檔名也變,瀏覽器看到新檔名自然重抓;內容沒改,檔名不變,就能放心長期快取。從 httpVueLoader 搬到建置流程,解快取的是 [contenthash] 這個機制,不是 import() 那行字。
4. keep-alive 解的是另一個問題
keep-alive 不是用來解「改完 .vue 不更新」的。它做的是切換元件時把實例留在記憶體裡,保留狀態。
它真正派得上用場的場景是這樣:你有一個多分頁表單,或一個帶搜尋條件的列表,使用者切到別的分頁再切回來,希望剛填的內容、捲到的位置都還在:
1 | <keep-alive :include="['SurveyPit', 'SurveyHole']"> |
include 比對的是元件的 name。Vue 2 跟 Options API 在 script 裡宣告 name: 'SurveyPit' 就行。Vue 3 加 <script setup> 的情況稍微繞一點:
- Vue 3.2.34 以上:SFC 會自動把檔名當作
name(SurveyPit.vue對應的 name 就是SurveyPit),多數情況不用額外宣告。 - 要明確覆寫、或版本比較舊時:用
defineOptions巨集(Vue 3.3 起正式支援):
1 | <script setup> |
name 對不上,include 就比不到這個元件,快取當然也不會生效。
要強調的還是那句:用了 keep-alive,元件實例被釘在記憶體裡,反而更不會去重抓更新後的 .vue。目標如果是「看到改過的內容」,這招方向相反,別用,我已經親自驗證過一次了。
5. 開發時最省事的做法
邊改邊看,最快的方式是讓瀏覽器當下不快取,連設定都不用動:
- 開 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,這個惱人狀況就再沒出現過。




