开发者

Programmatically check and update only if an image has changed

I have an application which updates an image from time to time. The update interval is not predictable. The image itself is updated atomically on the web server via rename(). That is all this application does and there shall be no change on the Apache side such that the webserver can continue to only serve static files.

There is some AJAX script which displays the content and updates this image when it is changed. This is done using polling. The naive JavaScript version used a counter and updated pulled the image each second or so by adding a query timestamp. However 99% of the time this pulls the image unchanged.

The current not so naive version uses XMLHttpRequest aka. AJAX to check the If-Modified-Since-header, and if a change is detected the update is invoked.

The question now is, is there a better way to archive this effect? Perhaps look at the last paragraph of this text before you dive into this ;)

Here are the core code snippets of the current version. Please note that the code is edited for brevity, so var initialization left away and some lines removed which are not of interest.

First the usual, slightly extended AJAX binding:

// partly stolen at http://snippets.dzone.com/posts/show/2025
function $(e){if(typeof e=='string')e=document.getElementById(e);return e};
ajax={};
ajax.collect=function(a,f){var n=[];for(var i=0;i<a.length;i++){var v=f(a[i]);if(v!=null)n.push(v)}return n};
ajax.x=function(){try{return new XMLHttpRequest()}catch(e){try{return new ActiveXObject('Msxml2.XMLHTTP')}catch(e){return new ActiveXObject('Microsoft.XMLHTTP')}}};
ajax.send=function(u,f,m,a,h){var x=ajax.x();x.open(m,u,true);x.onreadystatechange=function(){if(x.readyState==4)f(x.responseText,x,x.status==0?200:x.status,x.getResponseHeader("Last-Modified"))};if(m=='POST')x.setRequestHeader('Content-type','application/x-www-form-urlencoded');if(h)h(x);x.send(a)};
ajax.get=function(url,func){ajax.send(url,func,'GET')};
ajax.update=function(u,f,lm){ajax.send(u,f,'GET',null,lm?function(x){x.setRequestHeader("If-Modified-Since",lm)}:lm)};
ajax.head=function(u,f,lm){ajax.send(u,f,'HEAD',null,lm?function(x){x.setRequestHeader("If-Modified-Since",lm)}:lm)};

The basic HTML part, it includes 2 images which are flipped after loading, and a third one (not referenced in the code snippets) to display archived versions etc., which prevents flipping the updates as well:

</head><body onload="init()">
<div id="shower"><img id="show0"/><img id="show1"/><img id="show2"/></div>

The initial part includes the timer. It is a bit more to it, to compensate for network delays on slow links, reduce the polling rate etc.:

function init()
{
window.setInterval(timer,500);
for (var a=2; --a>=0; )
  {
    var o=$("show"+a);
    o.onload  = loadi;
  }
disp(0);
}

function disp(n)
{
shown=n;
window.setTimeout(disp2,10);
}

function disp2()
{
hide("show0");
hide("show1");
hide("show2");
show("show"+shown);
}

function hide(s)            
{                           
$(s).style.display="none";  
}                           
function show(s)            
{                           
$(s).style.display="inline";
}                           

function timer(e)
{
if (waiti && !--waiti)
  dorefresh();
nextrefresh();
}

function nextrefresh()
{
if (sleeps<0)
  sleeps = sleeper;
if (!--sleeps)
  pendi = true;
if (pendi && !waiti)
  dorefresh();
}

From time to time dorefresh() is called to pull the HEAD, tracking If-Modified-Since:

function dorefresh()
{
waiti = 100; // allow 50s for this request to take
ajax.head("test.jpg",checkrefresh,lm);
}

function checkrefresh(e,x,s,l)
{
if(!l)
  {
    // not modified
    lmc++;
    waiti = 0;
  }
else
  {
    lmc=0;
    lm=l;

    $("show"+loadn).src = "test.jpg?"+stamp();

    waiti=100;
  }
pendi=false;
sleeper++;
if (sleeper>maxsleep)
  sleeper = maxsleep;
sleeps=0;
nextrefresh();
}

function stamp()
{
return new Date().getTime();
}

When the image is loaded it is flipped into vie开发者_JS百科w. shown usually is 0 or 1:

function loadi()
{
waiti=0;
$("show"+loadn).style.opacity=1;
if (shown<2)
  disp(loadn);
loadn=1-loadn;
}

Please note that I only tested this code with Webkit based browsers yet. Sorry, I cannot provide a working example, as my update source is non-public. Also please excuse that the code is somewhat quick-n-dirty quality.

Strictly speaking HEAD alone is enough, we could look at the Last-Modified header of course. But this recipe here also works for GET requests in a non-image situation.

AJAX GET in combination with images makes less sense, as this pulls the image as binary data. I could convert that into some inline image, of course, but on bigger images (like mine) this will exceed the maximum URL length.

One thing which possibly can be done is using the browser cache. That is pull the image using an ajax.update and then re-display the image from the cache.

However this depends on the cache strategy of a browser. On mobile devices the image might be too big to be cached, in that case it is transferred twice. This is wrong as usually mobile devices have slow and more important expensive data links.

We could use this method if the webserver would write a text file, like JSON or a JS snippet, which then is used to display the image. However the nice thing about this code here is, that you do not need to provide additional information. So no race conditions, no new weird states like in disk full cases, just works.

So one basic idea is to not alter the code on the webserver which generates the picture, just do it on the browser side. This way all you need is a softlink from the web tree to the image and make sure, the image is atomically updated.

The downside of AJAX is the same origin policy, so AJAX can only check the HEAD of resources from the host which provided the running JavaScript code. Greasemonkey or Scriptlets can circumvent that, but these cannot be deployed to a broad audience. So foreign resources (images) sadly cannot be efficiently queried if they were updated or not. At my side luclily both, the script and the image, originate from the same host.

Having said this all, here are the problems with this code:

The code above adds to the delay. First the HEAD is checked and if this shows that something has changed the update is done. It would be nice to do both in one request, such that the update of the image does not require an additional roundtrip.

GET can archive that with If-Modified-Since, and it works on my side, however I found no way to properly display the result as an inlined image. It might work for some browsers, but not for all.

The code also is way too complex for my taste. You have to deal with possible network timeouts, not overwhelming limited bandwidth, trying to be friendly to the webserver, being compatible to as many browsers as possible, and so on.

Also I would like to get rid of the hack to use a query parameter just to pull an updated image, as this slowly fills/flushes the cache.

Perhaps there is an - unknown to me - way to "re-trigger" image refresh in the browser? This way the browser could check with If-Modified-Since directly and update the image. With JavaScript this could trigger a .load event then or similar. At my side I even do not need that at all, all I want is to keep the image somewhat current.

I did not experiment with CANVAS yet. Perhaps somebody has an idea using that.

So my question just is, is there any better way (another algorithm) than shown above, except from improving code quality?


From what I understand, you have 2 sources of information on the server: the image itself and time of last update. Your solution polls on both channels and you want to push, right?

Start here: http://en.wikipedia.org/wiki/Comet_(programming), there should be a simple way to let the server update the client on a new image url. In case server and client support websockets it's a shortcut.

However, most simple solution assumes no image url change and runs

image.src = "";
image.src = url;

by using setInterval() and let the browser deal with cache and headers.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜