一个AJAX的缓存策略

来源:互联网 发布:狗爹域名 编辑:程序博客网 时间:2024/05/02 01:40
                                                                 一个AJAX的缓存策略
                                                                                 作者:Bruce Perry
                                                                                 May 03, 2006
 
AJAX应用有能力使得(我们)通过后台的HTTP连接获取小批量的信息,这是现代浏览器API必须装备的强大的工具。对于大多数的浏览器(例如,Firefox 1.5, Safari 2.0, Opera 8.5),这个任务背后的引擎是Windows对象的XMLHttpRequest (XHR)对象。Internet Explorer 5.5-6对于这个对象有不同的表述,例如,XMLHttp,但是IE 7有可能采用与其他浏览器相同的对象。
 
避免混乱
然而,在AJAX应用中混乱的使用HTTP请求从来就不是一个好的注意或者说好的设计决策。服务器端相对应的对象没有能力处理如潮水般的请求,而AJAX应用的客户端可能会有一些请求的超时或者中断,这会破坏用户体验,而用户体验却恰恰是AJAX的强大之处。
 
能源价格
我在最近的一篇文章里介绍的一个应用使用XHR来获得最新的石油、汽油和其他的能源的价格。这些数据,由美国能源信息秘书处发布,都是一些公开的领域。我的应用就是把它们从适当的网址获取或者挖出。更好的办法是和能源的Web服务连接(HTML的挖掘是笨拙和脆弱的),但是我没有找到一个开源的、免费的服务。我公开建议这样的服务。
EIA的Web页面满足了应用的目的,因为它毕竟是提供石油期货的最新数据。当用户登陆Web页面的时候,应用在浏览器窗口显示价格。图1-1显示了页面左边的样子。左边栏显示的是价格,这些价格当鼠标经过的时候会加亮。
图1-1:获取能源价格的Web页面的样子。(点击看大图)
页面上每一个区域或div标签都专注于一个独立的价格(例如石油),它们都有单独的刷新按钮。这赋予了用户获取最新价格的选择权。然而,EIA一个礼拜只将这些价格更新一次。因而,如果一个用户在一个小时之前已经登陆过了,再为处理这些请求而到EIA网页将是极大浪费。我们知道,这个价格在很大程度上是一样的,所以我们必须让当前显示的价格驻留下来。
 
为缓存数据指定一个时间期限
有大量的可行的方案来解决这一个问题或需求。我所选择的一个解决方案是,使用原型开源JavaScript库,创建一个对象。当一个价格被HTTP请求最新更新以后,保持它的轨迹。这个对象用一个属性来表示一个范围,例如24小时。如果一个价格在小于24小时之前被获取过,那么这个对象阻止请求被初始化,并且显示相同的数据。
这就是一个AJAX的缓存策略,它被设计来周期性的从服务器刷新数据,更新的条件是仅仅在客户端数据的版本被缓存了一个给定的周期以后。如果应用的用户离开了她的浏览器的页面超过24小时,这时她点击刷新按钮,XHR发送请求并且取得一个新的价格来显示。我们希望给与用户刷新价格而不用重新载入整个页面的能力,而且我们也希望砍掉那些不必要的请求。
 
引入JavaScript
Web应用在HTML文件里使用script标记来引入必要的JavaScript,包括原型库(Prototype library)。我写的第一篇关于这个应用的xml.com文章展示了怎么下载和安装原型(Prototype)
...
..
 
创建刷新按钮
首先,我展示来自eevapp.js的代码,它们被我用来获取能源价格。其次,我将展示这个对象,它就像一个请求过滤器。
//When the browser has finished loading the web page,
//fetch and display the energy prices
window.onload=function(){
    //generatePrice method takes two parameters:
    //the energy type (e.g., oil) and the id of the
    //span element where the price will be displayed
    generatePrice("oil","oilprice");
    generatePrice("fuel","fuelprice");
    generatePrice("propane",
            "propaneprice");
    
    //set up the Refresh buttons for    
    //getting updated oil, gasoline, and 
    //propane prices.
    //Use their onclick event handlers.    
    $("refresh_oil").onclick=function(){ 
        generatePrice("oil","oilprice"); }
    $("refresh_fuel").onclick=function(){ 
        generatePrice("fuel","fuelprice"); }
    $("refresh_propane").onclick=function(){ 
        generatePrice("propane",
            "propaneprice"); }
    
};
 
代码的注释解释了这段代码的功能,那些奇数的语法片断中需要解释的是$("refresh_oil").onclick形式的表达式。
原型(Prototype)使用$()方法来返回一个文档对象模式(DOM)的Element对象。Element和HTML的id属性相关联,代码给$()方法传递一个string参数。这个语法和document.getElementById("refresh_oil")是等价的,但是它对于开发者来说却更短、更加容易上手。"refresh_oil"指的是什么呢?它是Web页面上的刷新按钮的id。参见图1-1所显示的浏览器上的左边栏的刷新按钮。
 
代码给按钮的onclick事件处理器设置了一个方法,它用来选择性的获取一个新的能源价格。选择性的意思是,我们的过滤器决定是否发送一个HTTP请求。
 
首先是检测CacheDecider对象
generatePrice()方法首先检测一个叫CacheDecider的缓存关联对象,用来决定是否初始化一个新的用来获取能源价格的HTTP请求。每一个能源类别——石油、汽油和丙烷,都拥有自己的CacheDecider对象。代码在一个叫cacheHash的hash里存储这些对象。因此,表达式cacheHash["oil"]将会返回石油价格的CacheDecider对象。
如果CacheDecider或者过滤器允许代码创建一个新的HTTP请求(CacheDecider.keepData()返回false),那么代码将调用getEnergyPrice()来更新,即,原油价格。代码的注释解释了代码的功能。
/*Check the CacheDecider object to determine
if a new energy price should be fetched*/
function generatePrice(energyType, elementID){
    //set the local variable cacher to the CacheDecider
    //object associated with oil, gas, or propane
    var cacher = cacheHash[energyType];
    //If this object is null, then a CacheDecider object
    //has not yet been instantiated for the
    //specified energy type.
    if(! cacher) {
        //CacheDecider has a parameter that
        //specifies the number of seconds to keep
        //the data, here 24 hours
        cacher = new CacheDecider(60*60*24); 
        //store the new object in the hash with
        //the energy type, say "oil", as the key
        cacheHash[energyType]=cacher;
        //Fetch and display the new energy price
        getEnergyPrice(energyType, elementID);
        return;
    }
    //The CacheDecider has already been instantiated, so
    //check whether the currently displayed energy price
    //is stale and should be updated.
    if(! cacher.keepData()) { 
        getEnergyPrice(energyType, elementID);}
}
/*
Use Prototype's Ajax.Request object to fetch an energy price.
Parameters: 
energyType is oil, fuel, or propane.
elementID is the id of a span element on the web page;
the span will be updated with the new data.
*/
function getEnergyPrice(energyType, elementID){
 
 new Ajax.Request(go_url, {method: "get",
      parameters: "priceTyp="+energyType,
      onLoading:function(){ $(elementID).innerHTML=
                              "waiting...";},
      onComplete:function(request){
          if(request.status != 200) {
            $(elementID).innerHTML="Unavailable."
          }   else {
            $(elementID).innerHTML=
            request.responseText;}
      }});
 
}
 
原型的Ajax对象
与AJAX相关的代码使用原型的Ajax.Request对象,只有10行代码长。代码和服务器相连,从美国政府网站上取得新的能源价格。然后显示这些价格,而不用改变Web页面的其他部分。然而,代码有点容易混淆,这儿是解释。
你可能习惯于寻找被直接编程的XMLHttpRequest对象,然而,原型(Prototype)为你初始化了这个对象。包括针对各种浏览器需要做的处理。仅仅是产生一个新的AJAX对象,像新的Ajax.Request。设置HTTP请求的动作。prototype.js文件里包括了这个对象的定义。由于我们的Web页面引入了JavaScript文件,我们能够创建这个对象的一个新的实例,而不用越过其他的任何hoop。我们的代码不用处理XHR涉及的各种浏览器的不同。
Ajax.Request的第一个参数是它需要连接到的服务器组建的URL。例如http://www.eeviewpoint.com/energy.jsp。第二个参数指出这是一个GET HTTP类型请求。第三个参数提供了一个将要传递到服务器组件的查询串。因而,整个URL看起来像这样的样子:http://www.eeviewpoint.com/energy.jsp?priceTyp=oil,例如。
 
进度指示器
另外的一对参数用来让代码开发者决定在一个不同的请求处理情形下,应用该怎么做。例如,onLoaded代表的是这样一个请求序列的情形:当XMLHttpRequest.send()方法被调用的时候,一些HTTP响应头被获得,同时也有HTTP的响应状态。在这个情形下,我们的应用显示“等待…”,一个新的价格将随后出现。onComplete代表的是能源价格请求完成时的情形。这个方法和onComplete关联起来,因此,它决定了将会对请求的返回值做什么操作。代码自动的提供这些方法,它的request参数代表XHR使用HTTP request的一个实例。
onComplete:function(request){...
因此,在这个例子中,代码通过语句request.responseText取得返回值。如果一切正常,这个属性将要返回一个新的价格,例如66.07。
Ajax.Request对象也允许代码基于特定的HTTP响应状态值做出一定的动作。这是通过检测XHR的状态属性完成的(request.status)。如果状态值不是200(例如,可能是404,这是“Not Found”的意思),这个值表示响应是成功的,那么应用就在能源价格显示的地方显示“unavailable”。
我们也可以改变上面的行为,使得如果是一个不成功的请求的话,仅仅让当前显示的价格不变。
new Ajax.Request(go_url, {method: "get",
    parameters: "priceTyp="+energyType,
    onLoading:function(){ $(elementID).innerHTML=
       waiting...";},
    onComplete:function(request){
        if(request.status == 200) {
           $(elementID).innerHTML=
           request.responseText;}
}});
 
CacheDecider对象
最终价格的客户端难点是CacheDecider对象,图1-2是描述这个对象的UML图,我使用原型(Prototype)库写出了这个JavaScript对象。
图1-2,CacheDecider的UML类图描述
 
下面是这个对象的代码,在eevapp.js文件中:
var CacheDecider=Class.create();
CacheDecider.prototype = {
    initialize: function(rng){
        //How long do we want to
        //keep the associated data, in seconds?
        this.secondsToCache=rng;
        this.lastFetched=new Date();
 
    },
    keepData: function(){
        var now = new Date();
        //Get the difference in seconds between the current time
        //and the lastFetched property value
        var secondsDif = parseInt((now.getTime() - 
        this.lastFetched.getTime()) / 1000);
        //If the prior computed value is less than
        //the specified number of seconds to keep
        //a value cached, then return true
        if (secondsDif < this.secondsToCache) { return true;}
        //the data in the cache will be refreshed or re-fetched,
        //therefore reset the lastFetched value to the current time
        this.lastFetched=new Date();
        return false;
    }
}
 
prototype.js文件中包含了一个对Class的对象定义,调用Class.create()返回一个包含initialize()方法的对象,这像其他语言的构造器方法。JavaScript在代码每一次调用一个相关对象的新的实例的时候,都会调用这个方法。
我们的CacheDeciderinitialize()方法初始化了两个属性:secondsToCache,它代表在被缓存的值的下一个值被获取到之间的以秒为单位的时间数量。lastFetched,它代表的是,无论被缓存对象什么时候有了更新,最后一次更新的被缓存值的JavaScript的Date对象。
CacheDecider没有保存一个被缓存对象的引用。其他的对象使用CacheDecider来存储和检测时间限制或范围。
如果你还不清楚,请查阅副标题为“Check the CacheDecider Object Firs”的解释。
CacheDecider里还有一个keepData()方法,这个方法决定,在由于lastFetched超过缓存值所定义的时间段,是否这个过去的时间数被支持保留。
在我们的应用中,我们保持能源价格值在24小时之内,直到它被一个HTTP请求刷新。
cacher = new CacheDecider(60*60*24);
如果这个时间段没有被超过,那么keepData()返回true。否则,lastFetched属性被重新设置到当前时间(由于缓存值现在不能通过HTTP请求被更新),并且方法返回false。
用户能够更新数据,无论什么时候,只要她想这样,他只需重新载入整个web页面。这个动作将重新初始化我们所讨论到的所有对象。
 
服务器组件
应用使用了一种叫做“cross-domain proxy”的模式,这在http://ajaxpatterns.org/Cross-Domain_Proxy相关网站上有讨论。即使我的应用是从U.S. EIA网页上获取信息,这种获取由一个源自相同域(eeviewpoint.com)的组件初始化,然后作为我们应用的web页面。
获取web页面信息的服务器组件可以选择你的语言来编写:PHP, Ruby, Python, ASP.NET, Perl, Java。我选择Java,servlet/JSP编程和Tomcat容器。Ajax.Request对象和JSP相联接,使用Java工具类从U.S. EIA网站上获取能源信息。请参阅“Resources”栏,以便知道从哪里得到我们所讨论的那些类。
 
资料
Prototype:
http://script.aculo.us/
JavaScript code: http://www.parkerriver.com/ajaxhacks/xml_article_js.zip
Java code:
http://www.parkerriver.com/ajaxhacks/xml_article_java.zip
 
 


原创粉丝点击