使用HTML5 localStorage缓存GWT应用程序/小部件 [英] Cache in GWT app/widget with HTML5 localStorage

查看:90
本文介绍了使用HTML5 localStorage缓存GWT应用程序/小部件的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我试图为我的一个GWT小部件合并一个数据缓存。

我有一个数据源接口/类,它通过 RequestBuilder 和JSON。因为我多次显示小部件,所以我只想检索一次数据。

所以我试着带一个应用程序缓存。天真的方法是在单例对象中使用 HashMap 来存储数据。但是,如果支持,我也希望使用HTML5的localStorage / sessionStorage。

HTML5 localStorage仅支持字符串值。所以我必须将我的对象转换为JSON并以字符串形式存储。但不知何故,我不能拿出一个很好的干净的方式来做到这一点。这是我到目前为止。

我定义了一个接口有两个函数: fetchStatsList()获取列表的可以显示在窗口小部件中的统计信息,并且 fetchStatsData()获取实际数据。

  public interface DataSource {
public void fetchStatsData(Stat stat,FetchStatsDataCallback callback);
public void fetchStatsList(FetchStatsListCallback callback);

Stat 类是一个简单的Javascript Overlay类( JavaScriptObject ),带有一些getters(getName()等)
我有一个普通的不可缓存的实现我的DataSource的RequestBuilderDataSource 看起来如下所示:

  public class RequestBuilderDataSource implements DataSource {
@Override
public void fetchStatsList(final FetchStatsListCallback callback){
//创建RequestBuilderRequest,检索响应并解析JSON
callback.onFetchStatsList(stats);

$ b @Override
public void fetchStatsData(List< Stat> stats,final FetchStatsDataCallback callback){
String url = getStatUrl(stats);
//创建RequestBuilderRquest,检索响应并解析JSON
callback.onFetchStats(dataTable); // dataTable的类型为DataTable


我遗漏了大多数的代码,因为它非常简单。



这可以直接使用,但是统计数据列表以及数据每次都会被检索,即使数据很难处理在每个小部件实例中共享。

为了支持缓存,我添加了一个 Cache 接口和两个Cache实现(一个用于HTML5 localStorage和一个用于HashMap) :

  public interface Cache {
void put(Object key,Object value);
Object get(Object key);
void remove(Object key);
void clear();
}

我添加了一个新类 RequestBuilderCacheDataSource ,它扩展了 RequestBuilderDataSource 并在构造函数中使用了一个 Cache 实例。

  public class RequestBuilderCacheDataSource extends RequestBuilderDataSource {

private final Cache cache;

publlic RequestBuilderCacheDataSource(最终Cache缓存){
this.cache = cache;

$ b @Override
public void fetchStatsList(final FetchStatsListCallback callback){
Object value = cache.get(list);
if(value!= null){
callback.fetchStatsList((List< Stat>)value);
$ b $ else $ {
super.fetchStatsList(stats,new FetchStatsListCallback(){
@Override
public void onFetchStatsList(List< Stat> stats){
cache.put(list,stats);
callback.onFetchStatsList(stats);
}
});
super.fetchStatsList(callback);


$ b @Override
public void fetchStatsData(List< Stat> stats,final FetchStatsDataCallback callback){
String url = getStatUrl(stats) ;
Object value = cache.get(url);
if(value!= null){
callback.onFetchStatsData((DataTable)value);

else {
super.fetchStatsData(stats,new FetchStatsDataCallback(){
@Override
public void onFetchStatsData(DataTable dataTable){
cache。 put(url,dataTable);
callback.onFetchStatsData(dataTable);
}
});



$ / code $ / pre

基本上新的类将会查找在 Cache 中的值,如果没有找到它,它将调用父类中的获取函数并拦截回调以将其放入缓存中,然后调用实际的回调函数。
所以为了同时支持HTML5 localstorage和普通的JS HashMap存储,我创建了两个我的 Cache 接口的实现:



JS HashMap存储

  public class DefaultcacheImpl implements Cache {
private HashMap< Object,Object>地图;

public DefaultCacheImpl(){
this.map = new HashMap< Object,Object>();
}

@Override
public void put(Object key,Object value){
if(key == null){
throw new NullPointerException( key is null);
}
if(value == null){
throw new NullPointerException(value is null);
}
map.put(key,value);

$ b @Override
public Object get(Object key){
//检查null作为缓存不应存储空值/键
if (key == null){
throw new NullPointerException(key is null);
}
return map.get(key);
}

@Override
public void remove(Object key){
map.remove(key);
}

@Override
public void clear(){
map.clear();
}
}


$ b HTML5 localStorage

  public class LocalStorageImpl implements Cache {

public static enum TYPE {LOCAL,SESSION}
私人TYPE类型;
private存储cacheStorage = null;

public LocalStorageImpl(TYPE类型)抛出Exception {
this.type = type;
if(type == TYPE.LOCAL){
cacheStorage = Storage.getLocalStorageIfSupported();
}
else {
cacheStorage = Storage.getSessionStorageIfSupported();

if(cacheStorage == null){
throw new Exception(LocalStorage not supported);



$ b @Override
public void put(Object key,Object value){
//转换对象(可能是任何任意对象)转换为JSON
String jsonData = null;
if(value instanceof List){//如果它是Stat对象的列表
JSONArray array = new JSONArray();
int index = 0;
for(Object val:(List)value){
array.set(index,new JSONObject((JavaScriptObject)val));
index = index +1;
}
jsonData = array.toString();
}
else //如果它是一个DataTable
{
jsonData = new JSONObject((JavaScriptObject)value).toString();
}
cacheStorage.setItem(key.toString(),jsonData);
}

@Override
public Object get(Object key){
if(key == null){
throw new NullPointerException(key is空值);
}
String jsonDataString = cacheStorage.getItem(key.toString());
if(jsonDataString == null){
return null;
}
Object data = null;
Object jsonData = JsonUtils.safeEval(jsonDataString);
if(!key.equals(list))
data = DataTable.create((JavaScriptObject)data);
else if(jsonData instanceof JsArray){
JsArray< GenomeStat> jsonStats =(JsArray< GenomeStat>)jsonData;
列表< GenomeStat> stats = new ArrayList< GenomeStat>();
for(int i = 0; i< jsonStats.length(); i ++){
stats.add(jsonStats.get(i));
}
data =(Object)stats;
}
返回数据;
}

@Override
public void remove(Object key){
cacheStorage.removeItem(key.toString());
}

@Override
public void clear(){
cacheStorage.clear();
}

public TYPE getType(){
return type;


$ / code>

这篇文章有点长,但希望澄清什么我试图达到。它归结为两个问题:


  1. 对此方法的设计/体系结构的反馈(例如对RequestBilderDataSource进行子类化以获取缓存功能等)。这可以改善(这可能更多地涉及到一般设计比GWT)。

  2. 使用 DefaultCacheImpl 可以非常容易地存储和检索任意对象。我怎样才能实现与localStorage相同的东西,我必须转换和解析JSON?我正在使用 DataTable ,它需要调用 DataTable.create(JavaScriptObject jso)函数才能工作。我怎样才能解决这个问题,而没有多少if / else和检查实例?
  3. 我的解决方案

我的第一个想法:使它成为两层缓存,而不是两个不同的缓存。从内存映射开始,所以在读取给定对象时不需要序列化/反序列化,因此在一个地方更改对象会改变它。然后依靠本地存储为下一页加载保留数据,避免从服务器拉下数据的需要。



我倾向于说跳过会话存储,因为这不会持续很长时间,但它确实有它的好处。



为了存储/读取数据,我鼓励检查AutoBeans而不是使用JSO。通过这种方式,您可以支持任何类型的数据(可以存储为自动更新),并且可以将Class参数传入到获取器中,以指定您将从服务器/缓存中读取的数据类型,并将json解码为bean以同样的方式。作为额外的好处,autobeans更容易定义 - 不需要JSNI。一个方法可能看起来像这样(请注意,在DataSource及其impl中,签名不同)。 ; void fetch(Class< T> type,List< Stat> stats,Callback< T,Throwable>回调);

也就是说,什么是 DataTable.create ?如果它已经是JSO,那么您可以像从(通常情况下)从RequestBuilder数据读取时一样投射到DataTable。



我也鼓励不返回JSON数组直接来自服务器,但将其包装在一个对象中,作为保护用户数据不被其他站点读取的最佳做法。 (好吧,重读这些问题时,对象也不是很好)。与其在这里讨论它,请查阅 JSON安全最佳实践?



因此,所有这些都说,首先定义数据(不确定这些数据是如何工作的,所以只是按照我的意思去做)

  public interface DataTable {
String getTableName();
void setTableName(String tableName);
}
public interface Stat {//对于应该提供的内容不清楚
String getKey();
void setKey(String key);
String getValue();
String setValue(String value);
}
public interface TableCollection {
List< DataTable>的getTables();
void setTables(List< DataTable> tables);
int getRemaining(); //如果你有太多的话不会发送全部内容?





$ b

对于autobeans,我们定义了一个工厂,可以在给定的情况下创建我们的任何数据一个 Class 实例和一些数据。这些方法中的每一个都可以作为一种构造函数在客户端创建一个新实例,并且工厂可以传递给AutoBeanCodex来解码数据。

 接口DataABF扩展AutoBeanFactory {
AutoBean< DataTable>的dataTable();
AutoBean< Stat> STAT();
AutoBean< TableCollection> tableCollection();
}

将所有工作的String< => Object委派给AutoBeanCodex,但您可能想要一些简单的包装,使它可以轻松地从html5缓存和RequestBuilder结果中调用。快速示例:

  public class AutoBeanSerializer {
private final AutoBeanFactory factory;
public AutoBeanSerializer(AutoBeanFactory factory){
this.factory = factory;
}

public String< T> encodeData(T data){
//首先,将autobean映射到数据
//如果我们找不到它,可能会抛出一些东西
AutoBean< T> autoBean = AutoBeanUtils.getAutoBean(data);

//然后,将其编码
//由于AutoBean具有这些细节,因此无需工厂或类型,因此
返回AutoBeanCodex.encode(autoBean);
}
public< T> T decodeData(Class< T> dataType,String json){
AutoBean< T> bean = AutoBeanCodex.decode(factory,dataType,json);

//展开这个bean,并返回实际的数据
return bean.as();
}
}


I am trying to incorporate a data cache for one of my GWT widgets.
I have a datasource interface/class which retrieves some data from my backend via RequestBuilder and JSON. Because I display the widget multiple times I only want to retrieve the data once.

So I tried to come with an app cache. The naive approach is to use a HashMap in a singleton object to store the data. However I also want to make use of HTML5's localStorage/sessionStorage if supported.
HTML5 localStorage only supports String values. So I have to convert my object into JSON and store as a string. However somehow I can't come up with a nice clean way of doing this. here is what I have so far.

I define a interface with two functions: fetchStatsList() fetches the list of stats that can be displayed in the widget and fetchStatsData() fetches the actual data.

public interface DataSource {
    public void fetchStatsData(Stat stat,FetchStatsDataCallback callback);
    public void fetchStatsList(FetchStatsListCallback callback);
}

The Stat class is a simple Javascript Overlay class (JavaScriptObject) with some getters (getName(), etc) I have a normal non-cachable implementation RequestBuilderDataSource of my DataSource which looks like the following:

public class RequestBuilderDataSource implements DataSource {
    @Override
    public void fetchStatsList(final FetchStatsListCallback callback) {
         // create RequestBuilderRequest, retrieve response and parse JSON 
        callback.onFetchStatsList(stats);
    }

    @Override
    public void fetchStatsData(List<Stat> stats,final FetchStatsDataCallback callback) {
        String url = getStatUrl(stats);
        //create RequestBuilderRquest, retrieve response and parse JSON
        callback.onFetchStats(dataTable); //dataTable is of type DataTable
    }
}

I left out most of the code for the RequestBuilder as it is quite straightforward.

This works out of the box however the list of stats and also the data is retrieved everytime even tough the data is shared among each widget instance.

For supporting caching I add a Cache interface and two Cache implementations (one for HTML5 localStorage and one for HashMap):

public interface Cache {
    void put(Object key, Object value);
    Object get(Object key);
    void remove(Object key);
    void clear();
}

I add a new class RequestBuilderCacheDataSource which extends the RequestBuilderDataSource and takes a Cache instance in its constructor.

public class RequestBuilderCacheDataSource extends RequestBuilderDataSource {

    private final Cache cache;

    publlic RequestBuilderCacheDataSource(final Cache cache) {
        this.cache = cache;
    }

    @Override
    public void fetchStatsList(final FetchStatsListCallback callback) {
       Object value = cache.get("list");
       if (value != null) {
           callback.fetchStatsList((List<Stat>)value);
       }
       else {
           super.fetchStatsList(stats,new FetchStatsListCallback() {
               @Override
               public void onFetchStatsList(List<Stat>stats) {
                   cache.put("list",stats);
                   callback.onFetchStatsList(stats);
               }
           });
           super.fetchStatsList(callback);
       }
    }

    @Override
    public void fetchStatsData(List<Stat> stats,final FetchStatsDataCallback callback) {
        String url = getStatUrl(stats);
        Object value = cache.get(url);
        if (value != null) {
            callback.onFetchStatsData((DataTable)value); 
        }
        else {
            super.fetchStatsData(stats,new FetchStatsDataCallback() {
                @Override
                public void onFetchStatsData(DataTable dataTable) {
                    cache.put(url,dataTable);
                    callback.onFetchStatsData(dataTable);
                }
            });
        }
    }
}

Basically the new class will lookup the value in the Cache and if it is not found it will call the fetch function in the parent class and intercept the callback to put it into the cache and then call the actual callback.
So in order to support both HTML5 localstorage and normal JS HashMap storage I created two implementations of my Cache interface:

JS HashMap storage:

public class DefaultcacheImpl implements Cache {
    private HashMap<Object, Object> map;

    public DefaultCacheImpl() {
        this.map = new HashMap<Object, Object>();
    }

    @Override
    public void put(Object key, Object value) {
        if (key == null) {
            throw new NullPointerException("key is null");
        }
        if (value == null) {
            throw new NullPointerException("value is null");
        }
        map.put(key, value);
    }

    @Override
    public Object get(Object key) {
        // Check for null as Cache should not store null values / keys
        if (key == null) {
            throw new NullPointerException("key is null");
        }
        return map.get(key);
    }

    @Override
    public void remove(Object key) {
        map.remove(key);
    }

    @Override
    public void clear() {
       map.clear();
    }
}

HTML5 localStorage:

public class LocalStorageImpl implements Cache{

    public static enum TYPE {LOCAL,SESSION} 
    private TYPE type;
    private Storage cacheStorage = null;

    public LocalStorageImpl(TYPE type) throws Exception {
        this.type = type;
        if (type == TYPE.LOCAL) {
            cacheStorage = Storage.getLocalStorageIfSupported();
        }
        else {
            cacheStorage = Storage.getSessionStorageIfSupported();
        }
        if (cacheStorage == null) {
            throw new Exception("LocalStorage not supported");
        }
    }


    @Override
    public void put(Object key, Object value) {
        //Convert Object (could be any arbitrary object) into JSON
        String jsonData = null;
        if (value instanceof List) {   // in case it is a list of Stat objects
            JSONArray array = new JSONArray();
            int index = 0;
            for (Object val:(List)value) {
                array.set(index,new JSONObject((JavaScriptObject)val));
                index = index +1;
            }
            jsonData = array.toString();
        }
        else  // in case it is a DataTable
        {
            jsonData = new JSONObject((JavaScriptObject) value).toString();
        }
        cacheStorage.setItem(key.toString(), jsonData);
    }

    @Override
    public Object get(Object key) {
        if (key == null) {
            throw new NullPointerException("key is null");
        }
        String jsonDataString = cacheStorage.getItem(key.toString());
        if (jsonDataString == null) {
            return null;
        }
        Object data = null;
        Object jsonData = JsonUtils.safeEval(jsonDataString);
        if (!key.equals("list")) 
            data = DataTable.create((JavaScriptObject)data);
        else if (jsonData instanceof JsArray){
            JsArray<GenomeStat> jsonStats = (JsArray<GenomeStat>)jsonData;
            List<GenomeStat> stats = new ArrayList<GenomeStat>();
            for (int i = 0;i<jsonStats.length();i++) {
                stats.add(jsonStats.get(i));
            }
            data = (Object)stats;
        }
        return data;
    }

    @Override
    public void remove(Object key) {
        cacheStorage.removeItem(key.toString());
    }

    @Override
    public void clear() {
        cacheStorage.clear();
    }

    public TYPE getType() {
        return type;
    }
}

The post got a little bit long but hopefully clarifies what I try to reach. It boils down to two questions:

  1. Feedback on the design/architecture of this approach (for example subclassing RequestBilderDataSource for cache function, etc). Can this be improved (this is probably more related to general design than specifically GWT).
  2. With the DefaultCacheImpl it is really easy to store and retrieve any arbitrary objects. How can I achieve the same thing with localStorage where I have to convert and parse JSON? I am using a DataTable which requires to call the DataTable.create(JavaScriptObject jso) function to work. How can I solve this without to many if/else and instance of checks?

解决方案

My first thoughts: make it two layers of cache, not two different caches. Start with the in-memory map, so no serialization/deserialization is needed for reading a given object out, and so that changing an object in one place changes it in all. Then rely on the local storage to keep data around for the next page load, avoiding the need for pulling data down from the server.

I'd tend to say skip session storage, since that doesn't last long, but it does have its benefits.

For storing/reading data, I'd encourage checking out AutoBeans instead of using JSOs. This way you could support any type of data (that can be stored as an autobean) and could pass in a Class param into the fetcher to specify what kind of data you will read from the server/cache, and decode the json to a bean in the same way. As an added bonus, autobeans are easier to define - no JSNI required. A method could look something like this (note that In DataSource and its impl, the signature is different).

public <T> void fetch(Class<T> type, List<Stat> stats, Callback<T, Throwable> callback);

That said, what is DataTable.create? If it is already a JSO, you can just cast to DataTable as you (probably) normally do when reading from the RequestBuilder data.

I would also encourage not returning a JSON array directly from the server, but wrapping it in an object, as a best practice to protect your users' data from being read by other sites. (Okay, on re-reading the issues, objects aren't great either). Rather than discussing it here, check out JSON security best practices?

So, all of that said, first define the data (not really sure how this data is intended to work, so just making up as I go)

public interface DataTable {
    String getTableName();
    void setTableName(String tableName);
}
public interface Stat {// not really clear on what this is supposed to offer
    String getKey();
    void setKey(String key);
    String getValue();
    String setValue(String value);
}
public interface TableCollection {
    List<DataTable> getTables();
    void setTables(List<DataTable> tables);
    int getRemaining();//useful for not sending all if you have too much?
}

For autobeans, we define a factory that can create any of our data when given a Class instance and some data. Each of these methods can be used as a sort of constructor to create a new instance on the client, and the factory can be passed to AutoBeanCodex to decode data.

interface DataABF extends AutoBeanFactory {
    AutoBean<DataTable> dataTable();
    AutoBean<Stat> stat();
    AutoBean<TableCollection> tableCollection();
}

Delegate all work of String<=>Object to AutoBeanCodex, but you probably want some simple wrapper around it to make it easy to call from both the html5 cache and from the RequestBuilder results. Quick example here:

public class AutoBeanSerializer {
    private final AutoBeanFactory factory;
    public AutoBeanSerializer(AutoBeanFactory factory) {
        this.factory = factory;
    }

    public String <T> encodeData(T data) {
        //first, get the autobean mapped to the data
        //probably throw something if we can't find it
        AutoBean<T> autoBean = AutoBeanUtils.getAutoBean(data);

        //then, encode it
        //no factory or type needed here since the AutoBean has those details
        return AutoBeanCodex.encode(autoBean);
    }
    public <T> T decodeData(Class<T> dataType, String json) {
        AutoBean<T> bean = AutoBeanCodex.decode(factory, dataType, json);

        //unwrap the bean, and return the actual data
        return bean.as();
    }
}

这篇关于使用HTML5 localStorage缓存GWT应用程序/小部件的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆