zoukankan      html  css  js  c++  java
  • localStorage兼容方案实现

    [2012-08-07 注]第二版组件已经写完,比此版组件完善不少,近两天会放出。不着急的可以等等^_^

    [2013-10-16 注]实在没有什么时间写博客了,需要的请移步

           https://github.com/machao/localStorage/

    这篇文章做了以下假设:

    1、  你知道什么是localStorage(不知道,猛击这里进行补课

    2、  你需要一个兼容IE系列的本地存储方案(不考虑低版本IE的请飘过或直接看二次包装

    兼容方案效果:

            所有主流浏览器支持以下方法和属性:

            window.localStorage 的 getItem/setItem/removeItem/clear/key 方法以及 length属性

            window.LS 的 get/set/remove/clear/each 方法

    一、引言

            Web Storage这个东西真正好(路人甲:一头大来一头小…):量大、永久存储、不用使用任何插件,不随http发送 … 用来保存一些用户非敏感的状态和信息是再合适不过的了。其浏览器兼容性如下:

           

            可以看到,由于除IE外的其他浏览器很早的版本都支持了,关键是我们可以不用考虑这些浏览器的更低版本(路人甲:为啥?答:由于升级策略以及使用人群不同而决定的,详细请咨询百度先生),所以基本上可以认为都已经支持了localStorage,而IE是个特例,虽然IE8就开始支持localStorage应该夸奖,但是其更低版本IE却都不支持,令人气馁的是,这些低版本浏览器在中国的占有率还TMD挺高… (路人甲:Seems like some sort of human rights violation)

            所以,我们需要一个兼容方案来处理IE,使得它们也能支持类似本地存储的能力,userData被发现了(路人甲:IE其实很牛逼,很早的版本就通过特有的filter、vml支持CSS3、Canvas等html5的特性了,但是就是因为TMD太超前,又不愿意遵循后生定制的标准,才导致如今混乱的局面)。

    二、语法

            localStorage语法很简单,而且我们不需要考虑具体的实现机制:

    1 window.localStorage.getItem( key );
    2 window.localStorage.setItem( key, value );
    3 window.localStorage.removeItem( key );
    4 window.localStorage.clear();
    5 window.localStorage.length;
    6 window.localStorage.key( i );

            上述五个方法和一个属性是最常用、也够用的localStorage的接口,也是所有实现得最为统一的接口。更多其他使用方法,请自行百度查找,这里不一一列举。

            userData语法略复杂,它需要依赖一个html元素来做代理,该元素需要设定addBehavior('#default#userData'),假设该元素为o,那么如此调用:

    1 o.load(dataFile);
    2 o.getAttribute(name);
    3 o.setAttribute(name, value);
    4 o.removeAttribute(name);
    5 o.save(dataFile);

            更多其他接口请参考官网MSDN资料

            兼容方案的目的是:通过对userData的封装,提供一个模拟localStorage对象,实现相同或者高度相似的接口。

    三、现状

             userData需要指定一个缓存文件(大小限制128k),然后读取指定节点的内容,但是无法遍历缓存文件或节点。网上的兼容方案有两种:

            1、  以文件名作为key,自定义一个节点,来保存value

            2、  确定一个文件,以节点名作为key,来保存value

            以文件名作为key的,虽然文件个数不受限制,但是同一域名下有总容量的限制(1024k),同时每个文件最小占用4k(多数系统下),实际文件数不会超过256个,虽然基本够用,但是总归不太好,浪费了不必要的空间(路人甲:哪怕存储一个字符,也要创建一个缓存文件,占用4k的空间,对吧?答:正解。)

            以节点名作为key的,其灵活性就比较好,一般情况下,单个的key/value都是小量数据,128k的文件上限足够存储数据之用了,所以本文最终采用了以节点名作为key的存储点。

            有了以上接口,setItem / getItem / removeItem 就很容易实现了,但是仍有两个问题比较棘手:

            1、  原生localStorage可以随时使用,但是网上流传的兼容版本必须等页面加载了document.body后才能使用

            2、  原生localStorage支持clear方法,以及通过length属性和key方法来实现遍历,网上流传的兼容版本都没有此功能

    四、解决方案

             首先,要解决访问时机的问题。

             网上流传的代码〔节选〕:

    1 UserData.o = document.createElement('INPUT');
    2 UserData.o.type = "hidden";
    3 UserData.o.style.display = "none";
    4 UserData.o.addBehavior ("#default#userData");
    5 document.body.appendChild(UserData.o);

            问题出在最后一句,如果document.body不存在,那么这个input也就插入失败,自然就不能继续使用userData,当然,如果你的js放在body标签内,那就可以使用了,但是我喜欢把js放在head内(路人甲:我也喜欢~~)――虽然与google的优化法相左,但是我控制不了自己的洁癖:我觉得放在body内就是污染了body标签,所以我就尝试把input插入到一个已经存在的标签内(哪怕是head),结果居然成功了(路人乙:那你的这个input不也是污染了head标签吗?答:那就不管了,哈哈):

    1 var box = document.body || document.getElementsByTagName("head")[0] || document.documentElement,
    2 o = document.createElement('input');
    3 o.type = "hidden";
    4 o.addBehavior ("#default#userData");
    5 box.appendChild(o);
    6 UserData.o = o;

            这样,只要是在组件js下面的任何地方任何时机都可以调用模拟的localStorage组件了,这爽了很多,至少在页面初始化的时候就可以读取一些数据来做一些事情,而不是必须等到页面加载完成才能访问。

            解决了第一个访问时机的问题后,那如何实现可遍历的功能呢?

            要遍历,我们就必须要知道以往存储的所有的key,既然userData没有接口提供,那只有我们自己来保存这些key了,存在哪儿?自然还是userData本身。之前提到,用户的key/value存储在一个缓存文件中,那么可以将key存在另外一个缓存文件中,当调用clear方法或者key方法的时候,读取这个keyCache列表就能知道已经存了哪些数据。

            什么时候更新这个keyCache?当然是 setItem/removeItem 的时候,同理,length属性也是在这两个方法被调用的时候更新的。

            怎么更新这个keyCache?当然是跟 setItem 一样的逻辑,只是load的文件不同而已。另外,由于只能存储字符串类型的数据,我们就需要把key拼成一个字符串和进行保存,这里又有两个方法:

            1、  保存成json串格式的数组。但是key中不能包括引号(可转义)和逗号;

            2、  保存成特定分隔符连成的字符串。但是key中不能包含分隔符字符(串);

            既然第一个处理那么复杂(转义引号、json转化序列化),那不如就用逗号来拼接key来得方便。这因为如此,key就了一个限制:不能包含半角逗号(此限制对一般开发人员来说几乎都是透明的,没听说过谁在变量名中包含逗号)。

            方法写好了:

     1     cacheKey : function( key, action ){
     2         if( !this.init() )return;
     3         var o = this.o;
     4         //加载keyCache
     5         o.load(this.keyCache);
     6         var str = o.getAttribute("keys") || "",
     7             list = str ? str.split(",") : [],
     8             n = list.length, i=0, isExist = false;
     9         //将key转化为小写进行查找和存储
    10         key = key.toLowerCase();
    11         for(; i<n; i++){
    12             if( list[i] === key ){
    13                 isExist = true;
    14                 if( action === 2 ){ //如果是删除
    15                     list.splice(i,1);
    16                     n--; i--;
    17                 }
    18             }
    19         }
    20         if( action === 1 && !isExist ) //如果是写
    21             list.push(key);
    22         //存储
    23         o.setAttribute("keys", list.join(","));
    24         o.save(this.keyCache);
    25     }

           至此,userData已经完成了最难的两个功能(至少是网上流传版本没有的),剩下的就是简单的setItem / getItem / removeItem功能了,一个函数就可以搞定了。

           这里把源码一并贴出来,并适当进行了一点修改和优化(代码略长,默认给折叠起来了):

    View Code
      1 (function(window, undefined){
      2 //如果已经支持了,则不再处理
      3 if( window.localStorage )
      4     return;
      5 /*
      6  * IE系列
      7  */
      8 var userData = {
      9     //存储文件名(单文件小于128k,足够普通情况下使用了)
     10     file : window.location.hostname || "localStorage",
     11     //key'cache
     12     keyCache : "localStorageKeyCache",
     13     //keySplit
     14     keySplit : ",",
     15     // 定义userdata对象
     16     o : null,
     17     //初始化
     18     init : function(){
     19         if(!this.o){
     20             try{
     21                 var box = document.body || document.getElementsByTagName("head")[0] || document.documentElement, o = document.createElement('input');
     22                 o.type = "hidden";
     23                 o.addBehavior ("#default#userData");
     24                 box.appendChild(o);
     25                 //设置过期时间
     26                 var d = new Date();
     27                 d.setDate(d.getDate()+365);
     28                 o.expires = d.toUTCString();
     29                 //保存操作对象
     30                 this.o = o;
     31                 //同步length属性
     32                 window.localStorage.length = this.cacheKey(0,4);
     33             }catch(e){
     34                 return false;
     35             }
     36         };
     37         return true;
     38     },
     39     //缓存key,不区分大小写(与标准不同)
     40     //action  1插入key 2删除key 3取key数组 4取key数组长度
     41     cacheKey : function( key, action ){
     42         if( !this.init() )return;
     43         var o = this.o;
     44         //加载keyCache
     45         o.load(this.keyCache);
     46         var str = o.getAttribute("keys") || "",
     47             list = str ? str.split(this.keySplit) : [],
     48             n = list.length, i=0, isExist = false;
     49         //处理要求
     50         if( action === 3 )
     51             return list;
     52         if( action === 4 )
     53             return n;
     54         //将key转化为小写进行查找和存储
     55         key = key.toLowerCase();
     56         for(; i<n; i++){
     57             if( list[i] === key ){
     58                 isExist = true;
     59                 if( action === 2 ){
     60                     list.splice(i,1);
     61                     n--; i--;
     62                 }
     63             }
     64         }
     65         if( action === 1 && !isExist )
     66             list.push(key);
     67         //存储
     68         o.setAttribute("keys", list.join(this.keySplit));
     69         o.save(this.keyCache);
     70     },
     71 //核心读写函数
     72     item : function(key, value){
     73         if( this.init() ){
     74             var o = this.o;
     75             if(value !== undefined){ //写或者删
     76                 //保存key以便遍历和清除
     77                 this.cacheKey(key, value === null ? 2 : 1);
     78                 //load
     79                 o.load(this.file);
     80                 //保存数据
     81                 value === null ? o.removeAttribute(key) : o.setAttribute(key, value+"");
     82                 // 存储
     83                 o.save(this.file);
     84             }else{ //
     85                 o.load(this.file);
     86                 return o.getAttribute(key) || null;
     87             }
     88             return value;
     89         }else{
     90             return null;
     91         }
     92         return value;
     93     },
     94     clear : function(){
     95         if( this.init() ){
     96             var list = this.cacheKey(0,3), n = list.length, i=0;
     97             for(; i<n; i++)
     98                 this.item(list[i], null);
     99         }
    100     }
    101 };
    102 //扩展window对象,模拟原生localStorage输入输出
    103 window.localStorage = {
    104     setItem : function(key, value){userData.item(key, value); this.length = userData.cacheKey(0,4)},
    105     getItem : function(key){return userData.item(key)},
    106     removeItem : function(key){userData.item(key, null); this.length = userData.cacheKey(0,4)},
    107     clear : function(){userData.clear(); this.length = userData.cacheKey(0,4)},
    108     length : 0,
    109     key : function(i){return userData.cacheKey(0,3)[i];},
    110     isVirtualObject : true
    111 };
    112 })(window);

    五、已知问题

            1、userData在存储节点的时候,不区分大小写,而localStorage区分;这个问题暂时想不到什么好的办法解决。

            2、模拟localStorage只能清理或遍历通过本组件设定的userData数据,其他方式保存的userData数据无法清理和遍历。

            3、模拟localStorage的使用和原生localStorage上仍有有效范围的差异。

    六、二次包装

             我是一个勤奋的懒人,我会为了以后能够偷懒省事儿,我会进一步优化和改善现有的代码,哪怕功能已经够用了,甚至是完备的。

             localStorage太长,那就用LS!

             setItem / getItem / removeItem 中的item都是废话(路人甲:是废词。),删掉!

             length / key 写循环太麻烦,增加each方法!

             部分原生的localStorage有bug,修改!

             不同浏览器的getItem返回值有细小差异,统一输出!

             经常用jQuery/Core(我们自己前端框架的主对象),扩展一个备份!

             于是,就有了下面的这个二次包装:

     1 (function(window,localStorage,undefined){
     2 var LS = {
     3     set : function(key, value){
     4         //在iPhone/iPad上有时设置setItem()时会出现诡异的QUOTA_EXCEEDED_ERR错误
     5         //这时一般在setItem之前,先removeItem()就ok了
     6         if( this.get(key) !== null )
     7             this.remove(key);
     8         localStorage.setItem(key, value);
     9     },
    10     //查询不存在的key时,有的浏览器返回undefined,这里统一返回null
    11     get : function(key){
    12         var v = localStorage.getItem(key);
    13         return v === undefined ? null : v;
    14     },
    15     remove : function(key){ localStorage.removeItem(key); },
    16     clear : function(){ localStorage.clear(); },
    17     each : function(fn){
    18         var n = localStorage.length, i = 0, fn = fn || function(){}, key;
    19         for(; i<n; i++){
    20             key = localStorage.key(i);
    21             if( fn.call(this, key, this.get(key)) === false )
    22                 break;
    23             //如果内容被删除,则总长度和索引都同步减少
    24             if( localStorage.length < n ){
    25                 n --;
    26                 i --;
    27             }
    28         }
    29     }
    30 },
    31 j = window.jQuery, c = window.Core;
    32 //扩展到相应的对象上
    33 window.LS = window.LS || LS;
    34 //扩展到其他主要对象上
    35 if(j) j.LS = j.LS || LS;
    36 if(c) c.LS = c.LS || LS;
    37 })(window,window.localStorage);
  • 相关阅读:
    程序员偷偷深爱的9个不良编程习惯
    JQuery实现放大镜
    ACM1995
    liubo.im
    Linux中的一些点
    EPOLL使用详解
    Elays'Blog
    c#数据库解析
    codeforces #332 div 2 D. Spongebob and Squares
    类型
  • 原文地址:https://www.cnblogs.com/zjcn/p/2575026.html
Copyright © 2011-2022 走看看