我看到很多关于如何实现这一点的不同答案,所以我想在这里总结一下它们(并添加我自己发明的第四种方法):
(1) 向URL添加唯一的缓存破坏查询参数,例如:
newImage.src = "image.jpg?t=" + new Date().getTime();
优点:100%可靠,快速易懂且易于实现。
缺点:完全绕过缓存,这意味着当图像在视图之间
不更改时会出现不必要的延迟和带宽使用。将可能用许多完全相同的图像副本填充浏览器缓存(以及所有中间缓存)!此外,需要修改图像URL。
何时使用:当图像不断变化时使用,例如用于实时网络摄像头视频流。如果您使用此方法,请确保使用“
Cache-control: no-cache
”HTTP标头提供图像本身!!!(通常可以使用.htaccess文件设置)。否则,您将逐步填满旧版本图像的缓存!
(2) 将查询参数添加到URL中,仅在文件更改时更改,例如:
echo '<img src="image.jpg?m=' . filemtime('image.jpg') . '">';
(这是PHP服务器端代码,但这里重要的一点就是在文件名后附加了一个?m=[文件最后修改时间]查询字符串)。
优点: 100%可靠,快速易懂易实现,并且完美保留缓存优势。
缺点: 需要修改图像URL。此外,对服务器的工作量稍微增加——它必须访问文件最后修改时间。此外,需要服务器端信息,因此不适合仅用客户端解决方案来检查刷新的图像。
何时使用: 当您想缓存图像,但可能需要定期从服务器端更新它们而不更改文件名本身时。并且当您可以轻松确保在HTML中的每个图像实例中添加正确的查询字符串时。
(3) 使用头部Cache-control: max-age=0, must-revalidate
为您的图像提供服务,并向URL添加唯一的memcache破坏片段标识符,例如:
newImage.src = "image.jpg#" + new Date().getTime();
这里的思路是使用缓存控制头将图像放入浏览器缓存,但立即标记它们过时,以便每次重新显示时浏览器都必须与服务器检查它们是否已更改。这确保了浏览器的HTTP缓存始终返回图像的最新副本。然而,如果浏览器已经有一份内存中的图像副本,则通常会重复使用它,甚至不检查其HTTP缓存。为了防止这种情况发生,使用片段标识符:在内存中的图像
src
比较包括片段标识符,但在查询HTTP缓存之前被删除。(例如,
image.jpg#A
和
image.jpg#B
可能都会从浏览器的HTTP缓存中的
image.jpg
条目中显示,但
image.jpg#B
永远不会使用自上次显示
image.jpg#A
时保留的内存中的图像数据显示)。
优点:合理利用HTTP缓存机制,并在图像未更改时使用缓存的图像。适用于无法处理静态图像URL中添加查询字符串的服务器 (因为服务器从不看到片段标识符 - 它们仅供浏览器使用)。
缺点:依赖浏览器对带有URL片段标识符的图像的行为有些可疑(或至少是文档不全) (但是,我已经在FF27、Chrome33和IE11中成功测试过了)。每次查看图像都仍会向服务器发送重新验证请求,这可能是过度的,如果图像很少更改和/或延迟是一个大问题(因为即使缓存的图像仍然有效,您也需要等待重新验证响应)。需要修改图像URL。
何时使用:当图像可能经常更改或需要由客户端间歇性地刷新而无需服务器端脚本参与,但仍需要缓存优势时使用。例如,轮询更新不规则每隔几分钟更新一次的现场网络摄像头图像。或者,如果您的服务器不允许静态图像URL上的查询字符串,则使用它替代(1)或(2)。
编辑2021:不再适用于最新的Chrome和Edge:这些浏览器中的内部内存缓存现在会忽略片段标识符(可能自从切换到Blink引擎以来?)。但请参见下面的方法(4),在这两个特定的浏览器上现在要简单得多,因此考虑将该方法与(4)的简化版本结合使用以覆盖这两个浏览器。
(4)使用JavaScript强制刷新特定图像,首先将其加载到隐藏的<iframe>
中,然后在iframe的contentWindow
上调用location.reload(true)
。
步骤如下:
将要刷新的图片加载到一个隐藏的iframe中。 [编辑2021:对于Chrome和Edge,请加载带有<img>
标签的HTML页面,而不是原始图像文件]。这只是一个设置步骤 - 如果需要,可以提前长时间完成实际刷新。此时,即使图像未能成功加载也没有关系!
[编辑2021:最近的Chrome和Edge现在无需执行此步骤]。搞定后,清空您页面或任何DOM节点中所有拷贝的该图像(即使是存储在JavaScript变量中的虚拟节点)。这是必要的,因为浏览器可能会从旧的内存拷贝中显示图像(尤其是IE11):在刷新HTTP缓存之前,您需要确保所有内存中的拷贝都被清除。如果其他JavaScript代码正在异步运行,还可能需要防止该代码同时创建要刷新的图像的新副本。
调用iframe.contentWindow.location.reload(true)
。 true
强制绕过缓存,直接重新从服务器加载并覆盖现有的缓存副本。
[编辑2021:在最近的Chrome和Edge上不需要此步骤 - 在这些浏览器上,现有图像将在前面的步骤后自动更新!]完成重新加载后,恢复空白图片。它们应该现在显示来自服务器的新版本!
对于同域图像,可以直接将图像加载到iframe中。[编辑2021:不适用于Chrome、Edge]。对于跨域图像,您必须加载包含<img>
标签中图像的HTML页面来自您的域,否则尝试调用iframe.contentWindow.reload(...)
时会出现“访问被拒绝”错误。[在Chrome和Edge上也要这样做]。
优点:就像您希望DOM具有的image.reload()函数一样工作!允许通常缓存图像(如果您想要,在将来过期日期上甚至使用它们,从而避免频繁重新验证)。仅使用客户端代码,即可刷新特定图像,而无需更改当前页面或任何其他页面上该图像的URL。
缺点:依赖于JavaScript。不能保证在每个浏览器中完全正常工作(尽管我已成功在FF27、Chrome33和IE11中测试过)。与其他方法相比非常复杂。[编辑2021:除非您仅需要最近的Chrome和Edge支持,否则它非常简单。]
何时使用:当您拥有一组基本静态的图像需要缓存,但仍需要偶尔更新它们并获得立即的视觉反馈,以表明已进行更新时。特别是在刷新整个浏览器页面无法起作用的情况下,例如在某些基于AJAX构建的Web应用程序中。当方法(1)-(3)不可行时,因为由于某种原因您无法更改可能显示所需更新的图片的所有URL。(请注意,使用这三种方法会刷新图像,但如果另一个页面尝试显示该图像没有适当的查询字符串或片段标识符,则可能显示旧版本)。
以下提供了以相对健壮和灵活的方式实现此操作的详细信息:
假设您的网站包含位于URL路径/img/1x1blank.gif
处的空白1x1像素.gif,并且还具有以下单行PHP脚本(仅适用于将强制刷新应用于跨域图像,并且可以在任何服务器端脚本语言中重写)位于URL路径/echoimg.php
:
<img src="<?=htmlspecialchars(@$_GET['src'],ENT_COMPAT|ENT_HTML5,'UTF-8')?>">
那么,这是一个实际的Javascript实现方式,演示如何完成所有这些操作。看起来有点复杂,但有很多注释,重要的函数只有forceImgReload() - 前两个函数只是将图像置空和取消置空,并应根据您自己的HTML进行有效设计,因此请按照最适合您的方式编写代码;它们中的许多复杂性可能对您的网站来说是不必要的:
// This function should blank all images that have a matching src, by changing their src property to /img/1x1blank.gif.
//
// Optionally it may return an array (or other collection or data structure) of those images affected.
// This can be used by imgReloadRestore() to restore them later, if that's an efficient way of doing it (otherwise, you don't need to return anything).
// NOTE that the src argument here is just passed on from forceImgReload(), and MAY be a relative URI;
// However, be aware that if you're reading the src property of an <img> DOM object, you'll always get back a fully-qualified URI,
// even if the src attribute was a relative one in the original HTML. So watch out if trying to compare the two!
// NOTE that if your page design makes it more efficient to obtain (say) an image id or list of ids (of identical images) *first*, and only then get the image src,
// you can pass this id or list data to forceImgReload() along with (or instead of) a src argument: just add an extra or replacement parameter for this information to
// this function, to imgReloadRestore(), to forceImgReload(), and to the anonymous function returned by forceImgReload() (and make it overwrite the earlier parameter variable from forceImgReload() if truthy), as appropriate.
function imgReloadBlank(src)
{
//
//
//
var blankList = [],
fullSrc = /* Fully qualified (absolute) src - i.e. prepend protocol, server/domain, and path if not present in src */,
imgs, img, i;
for each (/* window accessible from this one, i.e. this window, and child frames/iframes, the parent window, anything opened via window.open(), and anything recursively reachable from there */)
{
// get list of matching images:
imgs = theWindow.document.body.getElementsByTagName("img");
for (i = imgs.length; i--;) if ((img = imgs[i]).src===fullSrc) // could instead use body.querySelectorAll(), to check both tag name and src attribute, which would probably be more efficient, where supported
{
img.src = "/img/1x1blank.gif"; // blank them
blankList.push(img); // optionally, save list of blanked images to make restoring easy later on
}
}
for each (/* img DOM node held only by javascript, for example in any image-caching script */) if (img.src===fullSrc)
{
img.src = "/img/1x1blank.gif"; // do the same as for on-page images!
blankList.push(img);
}
//
//
//
//
//
//
//
//
//
return blankList; // optional - only if using blankList for restoring back the blanked images! This just gets passed in to imgReloadRestore(), it isn't used otherwise.
}
// This function restores all blanked images, that were blanked out by imgReloadBlank(src) for the matching src argument.
// ##### You should code the actual contents of this function according to your page design, and what images there are on them, as well as how/if images are dimensioned, etc!!! #####
function imgReloadRestore(src,blankList,imgDim,loadError);
{
// ##### Everything here is provisional on the way the pages are designed, and what images they contain; what follows is for example purposes only!
// ##### For really simple pages containing just a single image that's always the one being refreshed, this function could be as simple as just the one line:
//
//
//
//
//
//
var i, img, width = imgDim&&imgDim[0], height = imgDim&&imgDim[1];
if (width) width += "px";
if (height) height += "px";
if (loadError) {/* If you want, do something about an image that couldn't load, e.g: src = "/img/brokenImg.jpg"; or alert("Couldn't refresh image from server!"); */}
// If you saved & returned blankList in imgReloadBlank(), you can just use this to restore:
for (i = blankList.length; i--;)
{
(img = blankList[i]).src = src;
if (width) img.style.width = width;
if (height) img.style.height = height;
}
}
// Force an image to be reloaded from the server, bypassing/refreshing the cache.
// due to limitations of the browser API, this actually requires TWO load attempts - an initial load into a hidden iframe, and then a call to iframe.contentWindow.location.reload(true);
// If image is from a different domain (i.e. cross-domain restrictions are in effect, you must set isCrossDomain = true, or the script will crash!
// imgDim is a 2-element array containing the image x and y dimensions, or it may be omitted or null; it can be used to set a new image size at the same time the image is updated, if applicable.
// if "twostage" is true, the first load will occur immediately, and the return value will be a function
// that takes a boolean parameter (true to proceed with the 2nd load (including the blank-and-reload procedure), false to cancel) and an optional updated imgDim.
// This allows you to do the first load early... for example during an upload (to the server) of the image you want to (then) refresh.
function forceImgReload(src, isCrossDomain, imgDim, twostage)
{
var blankList, step = 0, // step: 0 - started initial load, 1 - wait before proceeding (twostage mode only), 2 - started forced reload, 3 - cancelled
iframe = window.document.createElement("iframe"), // Hidden iframe, in which to perform the load+reload.
loadCallback = function(e) // Callback function, called after iframe load+reload completes (or fails).
{ // Will be called TWICE unless twostage-mode process is cancelled. (Once after load, once after reload).
if (!step) // initial load just completed. Note that it doesn't actually matter if this load succeeded or not!
{
if (twostage) step = 1; // wait for twostage-mode proceed or cancel; don't do anything else just yet
else { step = 2; blankList = imgReloadBlank(src); iframe.contentWindow.location.reload(true); } // initiate forced-reload
}
else if (step===2) // forced re-load is done
{
imgReloadRestore(src,blankList,imgDim,(e||window.event).type==="error"); // last parameter checks whether loadCallback was called from the "load" or the "error" event.
if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
}
}
iframe.style.display = "none";
window.parent.document.body.appendChild(iframe); // NOTE: if this is done AFTER setting src, Firefox MAY fail to fire the load event!
iframe.addEventListener("load",loadCallback,false);
iframe.addEventListener("error",loadCallback,false);
iframe.src = (isCrossDomain ? "/echoimg.php?src="+encodeURIComponent(src) : src); // If src is cross-domain, script will crash unless we embed the image in a same-domain html page (using server-side script)!!!
return (twostage
? function(proceed,dim)
{
if (!twostage) return;
twostage = false;
if (proceed)
{
imgDim = (dim||imgDim); // overwrite imgDim passed in to forceImgReload() - just in case you know the correct img dimensions now, but didn't when forceImgReload() was called.
if (step===1) { step = 2; blankList = imgReloadBlank(src); iframe.contentWindow.location.reload(true); }
}
else
{
step = 3;
if (iframe.contentWindow.stop) iframe.contentWindow.stop();
if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
}
}
: null);
}
然后,为了强制刷新与您的页面位于同一域中的图像,您只需要执行以下操作:
forceImgReload("myimage.jpg");
刷新来自其他地方(跨域)的图像:
forceImgReload("http://someother.server.com/someimage.jpg", true);
更高级的应用可能是在上传新版本到服务器后重新加载图像,同时准备重新加载过程的初始阶段,以最小化用户可见的重新加载延迟。如果您正在通过 AJAX 进行上传,并且服务器返回一个非常简单的 JSON 数组 [success, width, height],那么您的代码可能如下所示:
function uploadAndRefreshCache(fileForm, serverURL)
{
var xhr = new XMLHttpRequest(),
proceedWithImageRefresh = forceImgReload(serverURL, false, null, true);
xhr.addEventListener("load", function(){ var arr = JSON.parse(xhr.responseText); if (!(arr&&arr[0])) { proceedWithImageRefresh(false); doSomethingOnUploadFailure(...); } else { proceedWithImageRefresh(true,[arr[1],ar[2]]); doSomethingOnUploadSuccess(...); }});
xhr.addEventListener("error", function(){ proceedWithImageRefresh(false); doSomethingOnUploadError(...); });
xhr.addEventListener("abort", function(){ proceedWithImageRefresh(false); doSomethingOnUploadAborted(...); });
xhr.open("post","uploadImageToServer.php");
xhr.send(new FormData(fileForm));
}
最后提醒一下:虽然本文讨论的是图片,但也可能适用于其他类型的文件或资源。例如,防止使用过时的脚本或CSS文件,甚至刷新更新的PDF文档(仅在设置为在浏览器中打开时使用(4))。对于这些情况,方法(4)可能需要对上述Javascript进行一些更改。