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);


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM