系統上線再踩到 js/css 換版但使用者瀏覽器續用 Cache 舊版出錯的坑,嚴格說來是低級錯誤,最簡單做法是 URL 加上 ?v=版本參數,每次換版就改參數,即可保證不會讀到 Cache 舊版惹禍。ASP.NET MVC ScriptBundle 內建依檔案內容計算 SHA256 雜湊碼產生 v 參數機制,檔案一異動 v 參數便會同步更新;ASP.NET Core 則有 asp-append-version TagHelper,也是依據檔案內容改變參數,提供強大的阻絕舊版 Cache 防護。

我比較喜歡 asp-append-version 的點子,直接用在 js、css 上,不必跟打包壓縮功能綁在一起,運用起來較簡便彈性,但我的專案是 ASP.NET MVC5,沒法用 ASP.NET Core 的新發明。找到幾個現成 NuGet Package,但對 Cache 採用的逾時更新機制不滿意,覺得結合 FileChangeMonitor 檔案一修改就更新才是王道,一來可節省不必要的雜湊運算,二則由檔案異動觸發重算不會有任何延遲更高級。反正原理很簡單,不如自己寫一個當成練功好了。

就醬,花了 20 分鐘,40 行程式碼搞定:

using System.IO;
using System.Web;
using System.Web.Hosting;
using System.Web.Mvc;
using System.Runtime.Caching;
using System.Security.Cryptography;

namespace AssetVersioning
{
    public static class AssetVersioningExt
    {
        public static MvcHtmlString Script(this HtmlHelper html, string src) 
            => MvcHtmlString.Create($@"<script src=""{GetPathWithHash(src)}""></script>");

        public static MvcHtmlString Css(this HtmlHelper html, string href)
            => MvcHtmlString.Create($@"<link href=""{GetPathWithHash(href)}"" rel=""stylesheet"" />");

        public static string GetPathWithHash(string path)
            => $"{VirtualPathUtility.ToAbsolute(path)}?v={GetFileHash(path)}";

        static MemoryCache cache = MemoryCache.Default;
        public static string GetFileHash(string path)
        {
            var physicalPath = HostingEnvironment.MapPath(path);
            if (!File.Exists(physicalPath)) return string.Empty;
            string cacheKey = $"__asset_hash__{path}";
            if (cache.Contains(cacheKey)) return cache[cacheKey] as string;
            using (SHA256 sha256 = SHA256.Create())
            {
                var hash = HttpServerUtility.UrlTokenEncode(
                    sha256.ComputeHash(File.ReadAllBytes(physicalPath)));
                var policy = new CacheItemPolicy();
                policy.ChangeMonitors.Add(new HostFileChangeMonitor(new string[] { physicalPath }));
                cache.Add(cacheKey, hash, policy);
                return hash;
            }
        }
    }
}

使用方法如下:(Index.cshtml)

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
    @Html.Css("~/Css/main.css")
</head>
<body>
    <div>
        ASP.NET MVC Rocks!
    </div>
    @Html.Script("~/Scripts/main.js")
</body>
</html>

簡單測一下,main.css 與 main.js 後方自動加上了 ?v=ZuTN8S... 參數:

修改 main.js 內容再重新整理網頁,可以看到 ?v= 參數已改變:

成功!

I like the idea of asp-append-version tag helper, so I write a HtmlHelper extesion method to provide similiar feature in MVC5.


Comments

# by Chris

為什麼不是一開始(開台)下no-store去防止client的cache問題? 畢竟cache本來的用意就是降低server端的流量的⋯

# by Jeffrey

to Chris, no-store 是指完全停用 Cache? 靜態檔案更新通常不會很頻繁,停用 Cache 將耗用無謂頻寬,拖累網頁載入速度,為了避免用到舊版動用如此激烈手段,代價高了點。

# by 卡比

我把 version 寫在 config,有需要才手動修改

# by Johnson

to 卡比, 我們的 MVC 5 專案目前也是將 version 寫成共用變數,當有修改JS、CSS檔案時才手動更新 但缺點是改動單一檔案會讓所有其他檔案的快取失效 另外想請問使用 Memory cache 在專案有這麼多檔案的情況下,是否會造成耗費大量記憶體影響效能?

Post a comment