.net 如何强制硬刷新(ctrl+;F5)?

.net 如何强制硬刷新(ctrl+;F5)?,.net,asp.net-mvc,http,caching,.net,Asp.net Mvc,Http,Caching,我们正在积极开发一个使用.Net和MVC的网站,我们的测试人员正在尝试测试最新的东西。每次修改样式表或外部javascript文件时,测试人员都需要进行硬刷新(IE中的ctrl+F5)以查看最新内容 我是否可以强制他们的浏览器获取这些文件的最新版本,而不是依赖缓存版本?我们没有从IIS或任何东西进行任何类型的特殊缓存 一旦投入生产,就很难告诉客户需要硬刷新才能看到最新的更改 谢谢 您需要修改所引用的外部文件的名称。例如,在每个文件的末尾添加构建编号,如style-1423.css,并将编号作为构

我们正在积极开发一个使用.Net和MVC的网站,我们的测试人员正在尝试测试最新的东西。每次修改样式表或外部javascript文件时,测试人员都需要进行硬刷新(IE中的ctrl+F5)以查看最新内容

我是否可以强制他们的浏览器获取这些文件的最新版本,而不是依赖缓存版本?我们没有从IIS或任何东西进行任何类型的特殊缓存

一旦投入生产,就很难告诉客户需要硬刷新才能看到最新的更改


谢谢

您需要修改所引用的外部文件的名称。例如,在每个文件的末尾添加构建编号,如style-1423.css,并将编号作为构建自动化的一部分,以便每次使用唯一的名称部署文件和引用。

您可以在每次页面刷新时使用随机字符串调用JS文件。这样你就可以确信它总是新鲜的

您只需要这样称呼它“/path/to/your/file.js?


示例:jquery-min-1.2.6.js?234266在对CSS和Javascript文件的引用中,附加一个版本查询字符串。每次更新文件时都会碰撞它。这将被网站忽略,但web浏览器会将其视为新资源并重新加载

例如:

<link href="../../Themes/Plain/style.css?v=1" rel="stylesheet" type="text/css" />
<script src="../../Scripts/site.js?v=1" type="text/javascript"></script>

以编程方式将文件的最后修改日期作为querystring附加到URL,而不是生成号或随机数。这将防止您忘记手动修改查询字符串的任何事故,并允许浏览器在文件未更改时缓存该文件

示例输出可能如下所示:

<script src="../../Scripts/site.js?v=20090503114351" type="text/javascript"></script>


您可以编辑文件的http头,强制浏览器在每次请求时重新验证

因为您只提到测试人员的抱怨,您是否考虑过让他们关闭本地浏览器缓存,以便每次都检查新内容?这会让他们的浏览器慢一点。。。但是,除非您每次都在进行可用性测试,否则这可能比修复文件名、添加查询字符串参数或修改标题容易得多


这是我们测试环境中90%的案例。

我也遇到了这个问题,发现了我认为是一个非常令人满意的解决方案。

请注意,使用查询参数
../foo.js?v=1
可能意味着某些代理服务器显然不会缓存该文件。最好直接修改路径

我们需要浏览器在内容更改时强制重新加载。因此,在我编写的代码中,路径包含被引用文件的MD5散列。如果文件重新发布到web服务器,但内容相同,则其URL相同。更重要的是,使用无限期缓存也是安全的,因为URL的内容永远不会改变

该散列在运行时计算(并缓存在内存中以提高性能),因此无需修改构建过程。事实上,自从把这段代码添加到我的网站上以后,我就不用花太多心思了

您可以在此网站上看到它的运行:

在CSHTML/ASPX文件中 内容控制器 这节课相当长。它的关键很简单,但事实证明,为了强制重新计算缓存的文件哈希,您需要监视文件系统的更改。我通过FTP发布我的站点,例如,
bin
文件夹在
Content
文件夹之前被替换。在此期间请求站点的任何人(人或蜘蛛)都将导致旧哈希更新

代码看起来比读/写锁定要复杂得多

public sealed class ContentController : Controller
{
    #region Hash calculation, caching and invalidation on file change

    private static readonly Dictionary<string, string> _hashByContentUrl = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
    private static readonly Dictionary<string, ContentData> _dataByHash = new Dictionary<string, ContentData>(StringComparer.Ordinal);
    private static readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
    private static readonly object _watcherLock = new object();
    private static FileSystemWatcher _watcher;

    internal static string ContentHashUrl(string contentUrl, string contentType, HttpContextBase httpContext, UrlHelper urlHelper)
    {
        EnsureWatching(httpContext);

        _lock.EnterUpgradeableReadLock();
        try
        {
            string hash;
            if (!_hashByContentUrl.TryGetValue(contentUrl, out hash))
            {
                var contentPath = httpContext.Server.MapPath(contentUrl);

                // Calculate and combine the hash of both file content and path
                byte[] contentHash;
                byte[] urlHash;
                using (var hashAlgorithm = MD5.Create())
                {
                    using (var fileStream = System.IO.File.Open(contentPath, FileMode.Open, FileAccess.Read, FileShare.Read))
                        contentHash = hashAlgorithm.ComputeHash(fileStream);
                    urlHash = hashAlgorithm.ComputeHash(Encoding.ASCII.GetBytes(contentPath));
                }
                var sb = new StringBuilder(32);
                for (var i = 0; i < contentHash.Length; i++)
                    sb.Append((contentHash[i] ^ urlHash[i]).ToString("x2"));
                hash = sb.ToString();

                _lock.EnterWriteLock();
                try
                {
                    _hashByContentUrl[contentUrl] = hash;
                    _dataByHash[hash] = new ContentData { ContentUrl = contentUrl, ContentType = contentType };
                }
                finally
                {
                    _lock.ExitWriteLock();
                }
            }

            return urlHelper.Action("Get", "Content", new { hash });
        }
        finally
        {
            _lock.ExitUpgradeableReadLock();
        }
    }

    private static void EnsureWatching(HttpContextBase httpContext)
    {
        if (_watcher != null)
            return;

        lock (_watcherLock)
        {
            if (_watcher != null)
                return;

            var contentRoot = httpContext.Server.MapPath("/");
            _watcher = new FileSystemWatcher(contentRoot) { IncludeSubdirectories = true, EnableRaisingEvents = true };
            var handler = (FileSystemEventHandler)delegate(object sender, FileSystemEventArgs e)
            {
                // TODO would be nice to have an inverse function to MapPath.  does it exist?
                var changedContentUrl = "~" + e.FullPath.Substring(contentRoot.Length - 1).Replace("\\", "/");
                _lock.EnterWriteLock();
                try
                {
                    // if there is a stored hash for the file that changed, remove it
                    string oldHash;
                    if (_hashByContentUrl.TryGetValue(changedContentUrl, out oldHash))
                    {
                        _dataByHash.Remove(oldHash);
                        _hashByContentUrl.Remove(changedContentUrl);
                    }
                }
                finally
                {
                    _lock.ExitWriteLock();
                }
            };
            _watcher.Changed += handler;
            _watcher.Deleted += handler;
        }
    }

    private sealed class ContentData
    {
        public string ContentUrl { get; set; }
        public string ContentType { get; set; }
    }

    #endregion

    public ActionResult Get(string hash)
    {
        _lock.EnterReadLock();
        try
        {
            // set a very long expiry time
            Response.Cache.SetExpires(DateTime.Now.AddYears(1));
            Response.Cache.SetCacheability(HttpCacheability.Public);

            // look up the resource that this hash applies to and serve it
            ContentData data;
            if (_dataByHash.TryGetValue(hash, out data))
                return new FilePathResult(data.ContentUrl, data.ContentType);

            // TODO replace this with however you handle 404 errors on your site
            throw new Exception("Resource not found.");
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }
}
公共密封类ContentController:Controller
{
#文件更改时的区域哈希计算、缓存和失效
私有静态只读字典_hashByContentUrl=新字典(StringComparer.OrdinalIgnoreCase);
私有静态只读字典_dataByHash=新字典(StringComparer.Ordinal);
私有静态ReaderWriterLockSlim _lock=new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
私有静态只读对象_watcherLock=new object();
私有静态文件系统监视程序_watcher;
内部静态字符串ContentHashUrl(字符串contentUrl、字符串contentType、HttpContextBase httpContext、UrlHelper UrlHelper)
{
确保重新观看(httpContext);
_lock.EnterUpgradeableReadLock();
尝试
{
字符串散列;
if(!\u hashByContentUrl.TryGetValue(contentUrl,out散列))
{
var contentPath=httpContext.Server.MapPath(contentUrl);
//计算并合并文件内容和路径的哈希值
字节[]contentHash;
字节[]urlHash;
使用(var hashAlgorithm=MD5.Create())
{
使用(var fileStream=System.IO.File.Open(contentPath,FileMode.Open,FileAccess.Read,FileShare.Read))
contentHash=hashAlgorithm.ComputeHash(文件流);
urlHash=hashAlgorithm.ComputeHash(Encoding.ASCII.GetBytes(contentPath));
}
var sb=新的StringBuilder(32);
for(var i=0;i<head>
  <link rel="stylesheet" type="text/css"
        href="/c/e2b2c827e84b676fa90a8ae88702aa5c" />
  <script src="/c/240858026520292265e0834e5484b703"></script>
</head>
<img src="/c/4342b8790623f4bfeece676b8fe867a9" />
routes.MapRoute(
    "ContentHash",
    "c/{hash}",
    new { controller = "Content", action = "Get" },
    new { hash = @"^[0-9a-zA-Z]+$" } // constraint
    );
public sealed class ContentController : Controller
{
    #region Hash calculation, caching and invalidation on file change

    private static readonly Dictionary<string, string> _hashByContentUrl = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
    private static readonly Dictionary<string, ContentData> _dataByHash = new Dictionary<string, ContentData>(StringComparer.Ordinal);
    private static readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
    private static readonly object _watcherLock = new object();
    private static FileSystemWatcher _watcher;

    internal static string ContentHashUrl(string contentUrl, string contentType, HttpContextBase httpContext, UrlHelper urlHelper)
    {
        EnsureWatching(httpContext);

        _lock.EnterUpgradeableReadLock();
        try
        {
            string hash;
            if (!_hashByContentUrl.TryGetValue(contentUrl, out hash))
            {
                var contentPath = httpContext.Server.MapPath(contentUrl);

                // Calculate and combine the hash of both file content and path
                byte[] contentHash;
                byte[] urlHash;
                using (var hashAlgorithm = MD5.Create())
                {
                    using (var fileStream = System.IO.File.Open(contentPath, FileMode.Open, FileAccess.Read, FileShare.Read))
                        contentHash = hashAlgorithm.ComputeHash(fileStream);
                    urlHash = hashAlgorithm.ComputeHash(Encoding.ASCII.GetBytes(contentPath));
                }
                var sb = new StringBuilder(32);
                for (var i = 0; i < contentHash.Length; i++)
                    sb.Append((contentHash[i] ^ urlHash[i]).ToString("x2"));
                hash = sb.ToString();

                _lock.EnterWriteLock();
                try
                {
                    _hashByContentUrl[contentUrl] = hash;
                    _dataByHash[hash] = new ContentData { ContentUrl = contentUrl, ContentType = contentType };
                }
                finally
                {
                    _lock.ExitWriteLock();
                }
            }

            return urlHelper.Action("Get", "Content", new { hash });
        }
        finally
        {
            _lock.ExitUpgradeableReadLock();
        }
    }

    private static void EnsureWatching(HttpContextBase httpContext)
    {
        if (_watcher != null)
            return;

        lock (_watcherLock)
        {
            if (_watcher != null)
                return;

            var contentRoot = httpContext.Server.MapPath("/");
            _watcher = new FileSystemWatcher(contentRoot) { IncludeSubdirectories = true, EnableRaisingEvents = true };
            var handler = (FileSystemEventHandler)delegate(object sender, FileSystemEventArgs e)
            {
                // TODO would be nice to have an inverse function to MapPath.  does it exist?
                var changedContentUrl = "~" + e.FullPath.Substring(contentRoot.Length - 1).Replace("\\", "/");
                _lock.EnterWriteLock();
                try
                {
                    // if there is a stored hash for the file that changed, remove it
                    string oldHash;
                    if (_hashByContentUrl.TryGetValue(changedContentUrl, out oldHash))
                    {
                        _dataByHash.Remove(oldHash);
                        _hashByContentUrl.Remove(changedContentUrl);
                    }
                }
                finally
                {
                    _lock.ExitWriteLock();
                }
            };
            _watcher.Changed += handler;
            _watcher.Deleted += handler;
        }
    }

    private sealed class ContentData
    {
        public string ContentUrl { get; set; }
        public string ContentType { get; set; }
    }

    #endregion

    public ActionResult Get(string hash)
    {
        _lock.EnterReadLock();
        try
        {
            // set a very long expiry time
            Response.Cache.SetExpires(DateTime.Now.AddYears(1));
            Response.Cache.SetCacheability(HttpCacheability.Public);

            // look up the resource that this hash applies to and serve it
            ContentData data;
            if (_dataByHash.TryGetValue(hash, out data))
                return new FilePathResult(data.ContentUrl, data.ContentType);

            // TODO replace this with however you handle 404 errors on your site
            throw new Exception("Resource not found.");
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }
}
public static class ContentHelpers
{
    [Pure]
    public static MvcHtmlString ScriptImportContent(this HtmlHelper htmlHelper, [NotNull, PathReference] string contentPath, [CanBeNull, PathReference] string minimisedContentPath = null)
    {
        if (contentPath == null)
            throw new ArgumentNullException("contentPath");
#if DEBUG
        var path = contentPath;
#else
        var path = minimisedContentPath ?? contentPath;
#endif

        var url = ContentController.ContentHashUrl(contentPath, "text/javascript", htmlHelper.ViewContext.HttpContext, new UrlHelper(htmlHelper.ViewContext.RequestContext));
        return new MvcHtmlString(string.Format(@"<script src=""{0}""></script>", url));
    }

    [Pure]
    public static MvcHtmlString CssImportContent(this HtmlHelper htmlHelper, [NotNull, PathReference] string contentPath)
    {
        // TODO optional 'media' param? as enum?
        if (contentPath == null)
            throw new ArgumentNullException("contentPath");

        var url = ContentController.ContentHashUrl(contentPath, "text/css", htmlHelper.ViewContext.HttpContext, new UrlHelper(htmlHelper.ViewContext.RequestContext));
        return new MvcHtmlString(String.Format(@"<link rel=""stylesheet"" type=""text/css"" href=""{0}"" />", url));
    }

    [Pure]
    public static string ImageContent(this UrlHelper urlHelper, [NotNull, PathReference] string contentPath)
    {
        if (contentPath == null)
            throw new ArgumentNullException("contentPath");
        string mime;
        if (contentPath.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
            mime = "image/png";
        else if (contentPath.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || contentPath.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase))
            mime = "image/jpeg";
        else if (contentPath.EndsWith(".gif", StringComparison.OrdinalIgnoreCase))
            mime = "image/gif";
        else
            throw new NotSupportedException("Unexpected image extension.  Please add code to support it: " + contentPath);
        return ContentController.ContentHashUrl(contentPath, mime, urlHelper.RequestContext.HttpContext, urlHelper);
    }
}