為 js/css 加上自動版本參數防止 Cache 惹禍 - WebForm 版
8 |
.js/.css 換版後,想防止網頁讀取 Cache 強迫改用新版,最無敵的做法是在網址加個 ?v=XXX 參數,每次換版一併更新,URL 參數不同讓原 Cache 失效,即可確保萬無一失。
但手工維護 ?v=... 參數意味著每次換版必須叫出所有引用該 .js/.css 的網頁,一一修改 URL ?v= 參數,算不上什麼高明的主意。有個巧妙解法是由系統讀取 .js/.css 內容產生雜湊當成 v 參數,如此只要檔案內容異動,v 參數便自動更新,永遠不必擔心換版後還讀到 Cache 這種鳥事。
要實現自動版本參數,ASP.NET Core 有內建 asp-append-version Tag,針對 ASP.NET MVC 我也寫過類似機制:
最近維護古蹟踩到相同問題,卻發現軍火庫沒有兵器可用,少了 WebForm 版的自動版本參數機制。老系統說不定還得再戰十年,還是花點時間解決吧!
於是我寫了一顆 WebControl 來解決問題,網站還是 ASP.NET 3.5 無 MemoryCache 可用,改用 System.Web.Cache。為提升效率,以檔案絕對路徑當 Key 快取內容雜湊值,避免重複運算。System.Web.Cache 老歸老,還是有 CacheDependency(string filename) 可針對檔案建立依賴,能自動偵測檔案修改觸發快取更新,十分方便。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Web;
using System.Web.Hosting;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace FrontEndPack
{
public class IncludeResWithVersion : WebControl
{
public string Src
{
get { return (string)ViewState["src"]; }
set { ViewState["src"] = value; }
}
public string GetFileHash(string src)
{
var cache = HttpContext.Current.Cache;
var path = HttpContext.Current.Server.MapPath(src);
if (!File.Exists(path)) return string.Empty;
string cacheKey = "_res_hash__" + path;
if (cache[cacheKey] != null) return cache[cacheKey] as string;
using (SHA256 sha256 = SHA256.Create())
{
var hash = HttpServerUtility.UrlTokenEncode(
sha256.ComputeHash(File.ReadAllBytes(path)));
cache.Add(cacheKey, hash,
// 指定快取依賴,檔案異動時自動更新快取
new System.Web.Caching.CacheDependency(path),
DateTime.MaxValue, TimeSpan.FromMinutes(10),
System.Web.Caching.CacheItemPriority.Normal, null);
return hash;
}
}
protected override void Render(HtmlTextWriter writer)
{
var src = Src.Split('?').First();
var hash = GetFileHash(src);
var ext = Path.GetExtension(src);
writer.Write("\n");
switch (ext) {
case ".js":
writer.Write(string.Format(@"<script src=""{0}?v={1}""></script>", src, hash));
break;
case ".css":
writer.Write(string.Format(@"<link href=""{0}?v={1}"" rel=""stylesheet"" />", src, hash));
break;
default:
throw new Exception("File type " + ext + " not supported.");
}
writer.Write("\n");
}
}
}
將 IncludeResWithVersion.cs 放進 App_Code 目錄,要使用的網頁先加上 <%@Register Namespace="FrontEndPack" TagPrefix="fep" %>
註冊,接著將原本的 <script src="test.js"></script>
改成 <fep:IncludeResWithVersion runat="server" Src="./test.js" />
即可。
寫個簡單網頁驗證。用 <fep:IncludeResWithVersion runat="server" Src="./test.js" />
載入 .js,寫一小段 JavaScript 顯示其產生的 URL 及 test.js 內容:
<%@Page Language="C#" %>
<%@Register Namespace="FrontEndPack" TagPrefix="fep" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<fep:IncludeResWithVersion runat="server" Src="./test.js" />
</head>
<body style="width: 400px;">
<span></span>
<pre style="background: #ccc; padding: 6px;">
</pre>
<script>
let src = document.head.querySelector('script').src;
document.querySelector('span').innerHTML = src;
this.fetch(src)
.then(response => response.text())
.then(text => {
document.querySelector('pre').innerHTML = text;
});
</script>
</body>
</html>
將 test.js 第一行註解 Ver 1.0 改成 Ver 1.1,重新整理 ?v= 自動修改,成功!
Adding ?v= parameter is a good way to prevent cache, this article provide an example to generate the hash parameter based on file content.
Comments
# by 小黑
謝黑哥
# by yoyo
請問為什麼不是設定 HTTP Cache,而是採用此workaround呢?
# by noke
我是直接拿檔案最後修改時間當做版本(類似v=2311211146),也方便識別目前載入的檔案是什麼時候修改的。
# by Jeffrey
to yoyo,你是指設 No-Cache 或 ETag/If-None-Match 機制嗎?
# by yoyo
to Jeffrey, 是的,靜態檔案應該利用此機制即可
# by longer
無法使用在Web Application?? 出現剖析器錯誤<fep:IncludeResWithVersion.....
# by longer
還是乖乖的寫成assembly dll參考後,web.config註冊,這樣引用頁面也不必<@Register....。且Web網站和Web Application都可使用 <system.web> <pages> <controls> <add tagPrefix="fep" Assembly="FrontEndPack" namespace="FrontEndPack"> </add> </controls> </pages> </system.web>
# by longer
還是乖乖的寫成assembly,參考dll後,web.config註冊tagPrefix,這樣web網站和web 應用程式,都可使用,且引用頁面不必再Register。