面向微信的webapp本地化存储数据方案

webapp和一般的手机网页版相比,有两个最大的区别,一个是api交互,一个是本地化数据存储。今天主要来谈一谈本地化数据存储方案。

什么是本地化数据存储

webapp在构建界面时,需要数据,通过和api进行交互,获取数据后,即可利用这些数据进行页面的渲染。但是除了从api获取数据外,也需要将数据存放在本地,以备不时之需。本地数据包含两类,一类是客户端数据,一类是缓存。客户端数据包括app的设置项目、用户个人资料、与api交互时所需要的鉴权(认证)信息等。缓存数据时在和api交互完之后,把结果暂时保存在本地,当第二次请求同一个api接口的时候,根据一些条件判断,可以直接使用本地的缓存作为页面渲染的数据。

说的简单一点,就是webapp本身要在客户端(通常是手机)上保存数据,这就是本地化数据存储。通常,本地化数据存储有关系型数据和非关系型数据,关系型数据使用我们熟悉的sql类数据存储来实现,非关系型数据使用我们熟悉的key-value类数据存储来实现。

webapp应该如何选择本地化数据存储方式

首先,我们在对待webapp项目的时候,要把它和PC web网站区分开。虽然webapp也依托于web,但是从开发和架构上,它更像是native app,也就是原生应用。作为app,和web网站相比有一些局限:

  • 不能完善的实现基于浏览器的http请求和响应
  • 由于上面这点,不能像web网站一样,基于服务端session来保持用户登录
  • app向服务端的请求,没有浏览器所包含的host、referer等信息
  • 要完成http请求,必须借助第三方开发包

基于上面这些问题,实现app和api的数据交互时,必须一个一个的去解决。webapp借助的是浏览器,原本是可以绕过上面这些问题的,但是在设计api时,api的架构首先是面向native app的,所以api本身也在设计中要解决这些问题,而这个设计,反而让webapp失去了借助浏览器实现http交互的能力。

一旦出现上面这些情况,webapp的数据处理起来就比较麻烦,必须完全按照native app的设计风格来处理数据。

为了适应更多的使用场景,我们通过html5本身提供的一些本地化数据存储和cookie作为辅助来实现webapp的本地化数据存储。

  • cookie:保存在浏览器临时目录中的小颗粒数据存储模式,以文本文件的形式存储,可以通过记事本等编辑器打开查看,最大不超过5K,而且有生命周期(可以自己设置),也可以通过浏览器被清除。微信中,cookie可以被正常使用,但是每次微信重启,或者微信内部机制限制,cookie都会被清空。
  • localStorage:HTML5推崇的一种本地化数据存储形式,是HTML5时代的一种新机制,支持HTML5的大部分浏览器都支持该功能。localStorage可以实现本地数据的永久化存储,在没有调用localStorage.removeItem之前,这个数据可以一直存在。当然,如果浏览器本身崩溃,该值虽然大部分情况下保留,偶尔也会丢失。微信高版本里面该功能可以正常使用,重启微信该值仍然存在。
  • localSession:也是HTML5的新特性,在使用方法上和localStorage一样,在存储上和cookie很像,但是唯一一点不同的事,cookie可以用户自己设置生命周期,而localSession不能自己设置生命周期,它是一个会话级别的数据,就像服务端的session一样,当用户没有连续使用会话时,会话就会断开,localSession就没了,得重新去生成。
  • sqlite:原本HTML5加入的新功能,可以用javascript实现sqlite的操作,构建本地化关系数据库。可是在最近的一些反馈中,sqlite正在被移出HTML5的标准。当然,从目前浏览器的支持而言,大部分支持HTML5的浏览器,也支持sqlite,因此,我们可以在webapp中使用sqlite来保存一些必要的关系型数据。

在大部分情况下,我们主要使用localStorage和cookie来实现数据的本地化存储。它们都可以经过处理后,实现丰富的key-value数据存储。不过由于cookie的容量限制在5K,所以使用时还是有所限制。

javascript的cookie操作

和服务端语言不同,javascript和php保存的cookie不在同一个地方,因此javascript创建的cookie,PHP是读取不到的。javascript的cookie被用document.cookie存储起来。

下面,我们创建一个函数(基于jquery),用来操作cookie:

$.cookie = function(key,value,options) {
    var defaults = {
        expires: 7*24*3600,
        path: '/',
        domain: ''
    };
    // get all
    if(key === undefined) {
        var result = null;
        var cookies = document.cookie ? document.cookie.split('; ') : [];

        for(var i = 0, l = cookies.length; i < l; i++) {
            var parts = cookies[i].split('=');
            var name = decodeURIComponent(parts.shift());
            var cookie = parts.join('=');

            if(cookie.indexOf('"') === 0) {
                cookie = cookie.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
            }

            cookie = decodeURIComponent(cookie.replace(/\+/g, ' '));

            result[name] = JSON.parse(cookie);
        }

        return result;
    }
    // clear
    else if(key === null) {
        var cookies = document.cookie.split(";");

        for (var i = 0; i < cookies.length; i++) {
            var cookie = cookies[i];
            var eqPos = cookie.indexOf("=");
            var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie;
            document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT";
        }
    }
    // read
    else if(value === undefined) {
        var result = null;
        var cookies = document.cookie ? document.cookie.split('; ') : [];

        for(var i = 0, l = cookies.length; i < l; i++) {
            var parts = cookies[i].split('=');
            var name = decodeURIComponent(parts.shift());
            var cookie = parts.join('=');

            if (key && key === name) {
                if(cookie.indexOf('"') === 0) {
                    cookie = cookie.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
                }

                cookie = decodeURIComponent(cookie.replace(/\+/g, ' '));
                result = JSON.parse(cookie);

                break;
            }
        }

        return result;
    }
    // delete
    else if(value === null) {
        $.cookie(key,'',{expires: '-1'});
    }
    // add or update
    else {
        if(typeof options == 'string' || typeof options == 'number') options = {expires: options}; // 如果第三个参数是数字
        options = $.extend(defaults,options);

        if(typeof options.expires === 'number') {
            var seconds = options.expires, t =  new Date();
            options.expires = t;
            options.expires.setTime(+ t + seconds / 24 / 3600 * 864e+5);
        }

        return (document.cookie = [
            encodeURIComponent(key) + '=' + encodeURIComponent(JSON.stringify(value)),
            options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
            options.path    ? '; path=' + options.path : '',
            options.domain  ? '; domain=' + options.domain : '',
            options.secure  ? '; secure' : ''
        ].join(''));
    }
}

在使用的时候,有三个参数,各自都有作用。

var cookies = $.cookie(); // 获取所有cookie
$.cookie(null); // 清空所有cookie
var value = $.cookie('name'); // 获取键名为'name'的值
$.cookie('name',null); // 删除键名为'name'的cookie
$.cookie('name','value'); // 添加或更新键名name=>value
$.cookie('name','value',3600); // 第三个参数表示生命周期,以秒为单位
$.cookie('name','value',{path:window.location.href,doamin:window.location.host,expires:3600}); // 第三个参数可以通过这种形式来设置cookie生效的域名和路径

这样,我们就可以非常方便的操作cookie了。

基于jquery的localStorage操作

和cookie不同,localStorage的操作方便得多,只需要用几个get,set,remove方法就可以完成操作。

但是,在一些特殊情况下,localStorage会失效,例如浏览器开启了隐私模式,禁止在本地存储localStorage数据,例如iPhone上的Safari默认是不支持localStorage,需要用户开启设置。但是一半而言,用户并不知道怎么开启,因此,在这种情况下,我们可以暂时用cookie进行代替。不过在微信中正常使用,无需担心。

下面我们写一个jquery的方法来操作:

$.storage = function(key,value,expires) {
    var Storage = function(){
        try {
            localStorage.setItem('storage','');
            localStorage.removeItem('storage');
            return localStorage;
        }
        catch(e) { // 浏览器隐私模式下,例如iphone的Safari默认情况下,localstorage无法使用,所以使用cookie来代替
            var cookies = document.cookie.split(";");
            return {
                length : function(){
                    return cookies.length;
                }(),
                key : function(i){
                    var parts = cookies[i].split('=');
                    var name = decodeURIComponent(parts.shift());
                    return name;
                },
                getItem : function(key) {
                    return $.cookie(key);
                },
                setItem : function(key,value) {
                    $.cookie(key,value,{expires:expires || 365*24*3600});
                },
                removeItem : function(key) {
                    $.cookie(key,null);
                },
                clear : function() {
                    $.cookie(null);
                }
            };
        }
    }();

    var data = {},time = parseInt((new Date()).getTime() / 1000);
    // 获取全部数据
    if(key === undefined) {
        var k;
        for(var i = 0;i < Storage.length;i ++) {
            k = Storage.key(i);
            value = $.kv(k);
            if(!value) continue;
            data[k] = value;
        }
        return data;
    }
    // clear
    else if(key === null) {
        Storage.clear();
    }
    // read
    else if(value === undefined) {
        data = Storage.getItem(key);
        if(!data) return false;

        data = JSON.parse(data);
        if(data.expires && time > data.expires) { // 如果过期
            $.kv(key,null);
            return false;
        }
        data = data.value;
        return data;
    }
    // delete
    else if(value === null) {
        Storage.removeItem(key);
    }
    // set:add or update
    else {
        data = {
            time: time,
            expires: expires ? time + expires : 0,
            value: value
        };
        data = JSON.stringify(data);

        Storage.setItem(key,data);
    }
}

这操作中,已经通过try来进行处理,保证在localStorage被禁用的情况下,可以使用cookie来代替。localStorage没有domain, path选项,因此第三个参数是数字,用来表示生命周期。

var values = $.storage(); // 获取全部数据
$.storage(null); // 清空全部数据
var value = $.storage('name'); // 获取键名为name的值
$.storage('name',null); // 删除键名name的值
$.storage('name','value'); // 添加或设置值
$.storage('name','value',3600); // 在设置值的时候,给一个生命周期值

通过上面这些方法,我们就可以方便的在webapp中设置、取出、删除本地化存储的数据,供webapp使用(或缓存)。

而且,在上面两个方法中,我们都加入了JSON方法,即把数据进行JSON格式化后存储,取出的时候,再进行JSON反解析。这样可以保证我们可以直接存储字典型数据,比如:

var cat = {
    'name' : 'kitty',
    'age' : 10
}
$.storage('cat',cat);

这样我们在取出'cat'之后,可以直接使用对象属性的方式获取cat.name, cat.age这些值。

2016-04-08