MVCアプリケーションがあり、次のようにCSSファイルをレンダリングするためにStyleBundle
クラスを使用しています。
bundles.Add(new StyleBundle("~/bundles/css").Include("~/Content/*.css"));
私の問題は、Debug
モードでは、CSSのURLが個別にレンダリングされ、これらのURLを積極的にキャッシュするWebプロキシがあることです。 Release
モードでは、各リリースのキャッシュを無効にするために、クエリ文字列が最終URLに追加されることを知っています。
StyleBundle
を設定してDebug
モードでランダムなクエリ文字列を追加し、次の出力を生成してキャッシュの問題を回避することはできますか?
<link href="/stylesheet.css?random=some_random_string" rel="stylesheet"/>
これを行うには、カスタムIBundleTransformクラスを作成できます。次に、ファイルの内容のハッシュを使用してv = [filehash]パラメータを追加する例を示します。
public class FileHashVersionBundleTransform: IBundleTransform
{
public void Process(BundleContext context, BundleResponse response)
{
foreach(var file in response.Files)
{
using(FileStream fs = File.OpenRead(HostingEnvironment.MapPath(file.IncludedVirtualPath)))
{
//get hash of file contents
byte[] fileHash = new SHA256Managed().ComputeHash(fs);
//encode file hash as a query string param
string version = HttpServerUtility.UrlTokenEncode(fileHash);
file.IncludedVirtualPath = string.Concat(file.IncludedVirtualPath, "?v=", version);
}
}
}
}
次に、バンドルのTransformsコレクションに追加してクラスを登録できます。
new StyleBundle("...").Transforms.Add(new FileHashVersionBundleTransform());
バージョン番号は、ファイルの内容が変更された場合にのみ変更されます。
一意の文字列が必要です。ハッシュである必要はありません。ファイルのLastModified日付を使用して、そこからティックを取得します。 @Toddが指摘したように、ファイルのオープンと読み取りにはコストがかかります。ティックは、ファイルが変更されると変化する一意の番号を出力するのに十分です。
internal static class BundleExtensions
{
public static Bundle WithLastModifiedToken(this Bundle sb)
{
sb.Transforms.Add(new LastModifiedBundleTransform());
return sb;
}
public class LastModifiedBundleTransform : IBundleTransform
{
public void Process(BundleContext context, BundleResponse response)
{
foreach (var file in response.Files)
{
var lastWrite = File.GetLastWriteTime(HostingEnvironment.MapPath(file.IncludedVirtualPath)).Ticks.ToString();
file.IncludedVirtualPath = string.Concat(file.IncludedVirtualPath, "?v=", lastWrite);
}
}
}
}
そしてそれを使用する方法:
bundles.Add(new StyleBundle("~/bundles/css")
.Include("~/Content/*.css")
.WithLastModifiedToken());
そしてこれはMVCが書くものです:
<link href="bundles/css/site.css?v=635983900813469054" rel="stylesheet"/>
スクリプトバンドルでも正常に動作します。
このライブラリは、デバッグモードでバンドルファイルにキャッシュ無効化ハッシュを追加できるほか、他のいくつかのキャッシュ無効化アイテムを追加できます。 https://github.com/kemmis/System.Web.Optimization.HashCache
すべてのバンドルがコレクションに追加された後、BundlesCollectionインスタンスでApplyHashCache()拡張メソッドを実行します。
BundleTable.Bundles.ApplyHashCache();
HashCacheTransformのインスタンスを作成し、HashCacheを適用するバンドルインスタンスに追加します。
var myBundle = new ScriptBundle("~/bundle_virtual_path").Include("~/scripts/jsfile.js");
myBundle.Transforms.Add(new HashCacheTransform());
私は同じ問題を抱えていましたが、アップグレード後にクライアントブラウザのキャッシュバージョンに問題がありました。私の解決策は、次のようにクエリ文字列にバージョン番号を追加する独自のレンダラーで@Styles.Render("~/Content/css")
の呼び出しをラップすることです。
public static IHtmlString RenderCacheSafe(string path)
{
var html = Styles.Render(path);
var version = VersionHelper.GetVersion();
var stringContent = html.ToString();
// The version should be inserted just before the closing quotation mark of the href attribute.
var versionedHtml = stringContent.Replace("\" rel=", string.Format("?v={0}\" rel=", version));
return new HtmlString(versionedHtml);
}
そして、ビューでは次のようにします:
@RenderHelpers.RenderCacheSafe("~/Content/css")
現在はありませんが、すぐに追加される予定です(現在1.1安定版リリースの予定です。この問題はここで追跡できます: Codeplex
これはスクリプト用に書かれているが、スタイルにも機能することに注意してください(これらのキーワードを変更するだけです)
@ヨハンの答えに基づいて構築:
public static IHtmlString RenderBundle(this HtmlHelper htmlHelper, string path)
{
var context = new BundleContext(htmlHelper.ViewContext.HttpContext, BundleTable.Bundles, string.Empty);
var bundle = System.Web.Optimization.BundleTable.Bundles.GetBundleFor(path);
var html = System.Web.Optimization.Scripts.Render(path).ToString();
foreach (var item in bundle.EnumerateFiles(context))
{
if (!html.Contains(item.Name))
continue;
html = html.Replace(item.Name, item.Name + "?" + item.LastWriteTimeUtc.ToString("yyyyMMddHHmmss"));
}
return new HtmlString(html);
}
public static IHtmlString RenderStylesBundle(this HtmlHelper htmlHelper, string path)
{
var context = new BundleContext(htmlHelper.ViewContext.HttpContext, BundleTable.Bundles, string.Empty);
var bundle = System.Web.Optimization.BundleTable.Bundles.GetBundleFor(path);
var html = System.Web.Optimization.Styles.Render(path).ToString();
foreach (var item in bundle.EnumerateFiles(context))
{
if (!html.Contains(item.Name))
continue;
html = html.Replace(item.Name, item.Name + "?" + item.LastWriteTimeUtc.ToString("yyyyMMddHHmmss"));
}
return new HtmlString(html);
}
使用法:
@Html.RenderBundle("...")
@Html.RenderStylesBundle("...")
交換
@Scripts.Render("...")
@Styles.Render("...")
利点:
また、Bundlerを迅速に回避する必要がある場合:
public static MvcHtmlString ResolveUrl(this HtmlHelper htmlHelper, string url)
{
var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
var resolvedUrl = urlHelper.Content(url);
if (resolvedUrl.ToLower().EndsWith(".js") || resolvedUrl.ToLower().EndsWith(".css"))
{
var localPath = HostingEnvironment.MapPath(resolvedUrl);
var fileInfo = new FileInfo(localPath);
resolvedUrl += "?" + fileInfo.LastWriteTimeUtc.ToString("yyyyMMddHHmmss");
}
return MvcHtmlString.Create(resolvedUrl);
}
使用法:
<script type="text/javascript" src="@Html.ResolveUrl("~/Scripts/jquery-1.9.1.min.js")"></script>
交換:
<script type="text/javascript" src="@Url.Content("~/Scripts/jquery-1.9.1.min.js")"></script>
(他の多くの代替ルックアップも置き換えます)