什么是localstorage
前几天在老项目中发现有对cookie的操作觉得很奇怪,咨询下来是要缓存一些信息,以避免在URL上面传递参数,但没有考虑过cookie会带来什么问题:
① cookie大小限制在4k左右,不适合存业务数据
② cookie每次随HTTP事务一起发送,浪费带宽
我们是做移动项目的,所以这里真实适合使用的技术是localstorage,localstorage可以说是对cookie的优化,使用它可以方便在客户端存储数据,并且不会随着HTTP传输,但也不是没有问题:
① localstorage大小限制在500万字符左右,各个浏览器不一致
② localstorage在隐私模式下不可读取
③ localstorage本质是在读写文件,数据多的话会比较卡(firefox会一次性将数据导入内存,想想就觉得吓人啊)
④ localstorage不能被爬虫爬取,不要用它完全取代URL传参
瑕不掩瑜,以上问题皆可避免,所以我们的关注点应该放在如何使用localstorage上,并且是如何正确使用。
localstorage的使用
基础知识
localstorage存储对象分为两种:
① sessionStrage: session即会话的意思,在这里的session是指用户浏览某个网站时,从进入网站到关闭网站这个时间段,session对象的有效期就只有这么长。
② localStorage: 将数据保存在客户端硬件设备上,不管它是什么,意思就是下次打开计算机时候数据还在。
两者区别就是一个作为临时保存,一个长期保存。
这里来一段简单的代码说明其基本使用:
XML/HTML Code复制内容到剪贴板 <divid="msg"style="margin:10px0;border:1pxsolidblack;padding:10px;width:300px; height:100px;"> </div> <inputtype="text"id="text"/> <selectid="type"> <optionvalue="session">sessionStorage</option> <optionvalue="local">localStorage</option> </select> <buttononclick="save();"> 保存数据</button> <buttononclick="load();"> 读取数据</button> <scripttype="text/javascript"> varmsg=document.getElementById('msg'), text=document.getElementById('text'), type=document.getElementById('type'); functionsave(){ varstr=text.value; vart=type.value; if(t=='session'){ sessionStorage.setItem('msg',str); }else{ localStorage.setItem('msg',str); } } functionload(){ vart=type.value; if(t=='session'){ msg.innerHTML=sessionStorage.getItem('msg'); }else{ msg.innerHTML=localStorage.getItem('msg'); } } </script>
真实场景
实际工作中对localstorage的使用一般有以下需求:
① 缓存一般信息,如搜索页的出发城市,达到城市,非实时定位信息
② 缓存城市列表数据,这个数据往往比较大
③ 每条缓存信息需要可追踪,比如服务器通知城市数据更新,这个时候在最近一次访问的时候要自动设置过期
④ 每条信息具有过期日期状态,在过期外时间需要由服务器拉取数据
XML/HTML Code复制内容到剪贴板 define([],function(){ varStorage=_.inherit({ //默认属性 propertys:function(){ //代理对象,默认为localstorage this.sProxy=window.localStorage; //60*60*24*30*1000ms==30天 this.defaultLifeTime=2592000000; //本地缓存用以存放所有localstorage键值与过期日期的映射 this.keyCache='SYSTEM_KEY_TIMEOUT_MAP'; //当缓存容量已满,每次删除的缓存数 this.removeNum=5; }, assert:function(){ if(this.sProxy===null){ throw'notoverridesProxyproperty'; } }, initialize:function(opts){ this.propertys(); this.assert(); }, /* 新增localstorage 数据格式包括唯一键值,json字符串,过期日期,存入日期 sign为格式化后的请求参数,用于同一请求不同参数时候返回新数据,比如列表为北京的城市,后切换为上海,会判断tag不同而更新缓存数据,tag相当于签名 每一键值只会缓存一条信息 */ set:function(key,value,timeout,sign){ var_d=newDate(); //存入日期 varindate=_d.getTime(); //最终保存的数据 varentity=null; if(!timeout){ _d.setTime(_d.getTime()+this.defaultLifeTime); timeout=_d.getTime(); } // this.setKeyCache(key,timeout); entity=this.buildStorageObj(value,indate,timeout,sign); try{ this.sProxy.setItem(key,JSON.stringify(entity)); returntrue; }catch(e){ //localstorage写满时,全清掉 if(e.name=='QuotaExceededError'){ //this.sProxy.clear(); //localstorage写满时,选择离过期时间最近的数据删除,这样也会有些影响,但是感觉比全清除好些,如果缓存过多,此过程比较耗时,100ms以内 if(!this.removeLastCache())throw'本次数据存储量过大'; this.set(key,value,timeout,sign); } console&&console.log(e); } returnfalse; }, //删除过期缓存 removeOverdueCache:function(){ vartmpObj=null,i,len; varnow=newDate().getTime(); //取出键值对 varcacheStr=this.sProxy.getItem(this.keyCache); varcacheMap=[]; varnewMap=[]; if(!cacheStr){ return; } cacheMap=JSON.parse(cacheStr); for(i=0,len=cacheMap.length;i<len;i++){ tmpObj=cacheMap[i]; if(tmpObj.timeout<now){ this.sProxy.removeItem(tmpObj.key); }else{ newMap.push(tmpObj); } } this.sProxy.setItem(this.keyCache,JSON.stringify(newMap)); }, removeLastCache:function(){ vari,len; varnum=this.removeNum||5; //取出键值对 varcacheStr=this.sProxy.getItem(this.keyCache); varcacheMap=[]; vardelMap=[]; //说明本次存储过大 if(!cacheStr)returnfalse; cacheMap.sort(function(a,b){ returna.timeout-b.timeout; }); //删除了哪些数据 delMap=cacheMap.splice(0,num); for(i=0,len=delMap.length;i<len;i++){ this.sProxy.removeItem(delMap[i].key); } this.sProxy.setItem(this.keyCache,JSON.stringify(cacheMap)); returntrue; }, setKeyCache:function(key,timeout){ if(!key||!timeout||timeout<newDate().getTime())return; vari,len,tmpObj; //获取当前已经缓存的键值字符串 varoldstr=this.sProxy.getItem(this.keyCache); varoldMap=[]; //当前key是否已经存在 varflag=false; varobj={}; obj.key=key; obj.timeout=timeout; if(oldstr){ oldMap=JSON.parse(oldstr); if(!_.isArray(oldMap))oldMap=[]; } for(i=0,len=oldMap.length;i<len;i++){ tmpObj=oldMap[i]; if(tmpObj.key==key){ oldMap[i]=obj; flag=true; break; } } if(!flag)oldMap.push(obj); //最后将新数组放到缓存中 this.sProxy.setItem(this.keyCache,JSON.stringify(oldMap)); }, buildStorageObj:function(value,indate,timeout,sign){ varobj={ value:value, timeout:timeout, sign:sign, indate:indate }; returnobj; }, get:function(key,sign){ varresult,now=newDate().getTime(); try{ result=this.sProxy.getItem(key); if(!result)returnnull; result=JSON.parse(result); //数据过期 if(result.timeout<now)returnnull; //需要验证签名 if(sign){ if(sign===result.sign) returnresult.value; returnnull; }else{ returnresult.value; } }catch(e){ console&&console.log(e); } returnnull; }, //获取签名 getSign:function(key){ varresult,sign=null; try{ result=this.sProxy.getItem(key); if(result){ result=JSON.parse(result); sign=result&&result.sign } }catch(e){ console&&console.log(e); } returnsign; }, remove:function(key){ returnthis.sProxy.removeItem(key); }, clear:function(){ this.sProxy.clear(); } }); Storage.getInstance=function(){ if(this.instance){ returnthis.instance; }else{ returnthis.instance=newthis(); } }; returnStorage; });
这段代码包含了localstorage的基本操作,并且对以上问题做了处理,而真实的使用还要再抽象:
XML/HTML Code复制内容到剪贴板 define(['AbstractStorage'],function(AbstractStorage){ varStore=_.inherit({ //默认属性 propertys:function(){ //每个对象一定要具有存储键,并且不能重复 this.key=null; //默认一条数据的生命周期,S为秒,M为分,D为天 this.lifeTime='30M'; //默认返回数据 //this.defaultData=null; //代理对象,localstorage对象 this.sProxy=newAbstractStorage(); }, setOption:function(options){ _.extend(this,options); }, assert:function(){ if(this.key===null){ throw'notoverridekeyproperty'; } if(this.sProxy===null){ throw'notoverridesProxyproperty'; } }, initialize:function(opts){ this.propertys(); this.setOption(opts); this.assert(); }, _getLifeTime:function(){ vartimeout=0; varstr=this.lifeTime; varunit=str.charAt(str.length-1); varnum=str.substring(0,str.length-1); varMap={ D:86400, H:3600, M:60, S:1 }; if(typeofunit=='string'){ unitunit=unit.toUpperCase(); } timeout=num; if(unit)timeout=Map[unit]; //单位为毫秒 returnnum*timeout*1000; }, //缓存数据 set:function(value,sign){ //获取过期时间 vartimeout=newDate(); timeout.setTime(timeout.getTime()+this._getLifeTime()); this.sProxy.set(this.key,value,timeout.getTime(),sign); }, //设置单个属性 setAttr:function(name,value,sign){ varkey,obj; if(_.isObject(name)){ for(keyinname){ if(name.hasOwnProperty(key))this.setAttr(k,name[k],value); } return; } if(!sign)sign=this.getSign(); //获取当前对象 obj=this.get(sign)||{}; if(!obj)return; obj[name]=value; this.set(obj,sign); }, getSign:function(){ returnthis.sProxy.getSign(this.key); }, remove:function(){ this.sProxy.remove(this.key); }, removeAttr:function(attrName){ varobj=this.get()||{}; if(obj[attrName]){ deleteobj[attrName]; } this.set(obj); }, get:function(sign){ varresult=[],isEmpty=true,a; varobj=this.sProxy.get(this.key,sign); vartype=typeofobj; varo={'string':true,'number':true,'boolean':true}; if(o[type])returnobj; if(_.isArray(obj)){ for(vari=0,len=obj.length;i<len;i++){ result[i]=obj[i]; } }elseif(_.isObject(obj)){ result=obj; } for(ainresult){ isEmpty=false; break; } return!isEmpty?result:null; }, getAttr:function(attrName,tag){ varobj=this.get(tag); varattrVal=null; if(obj){ attrVal=obj[attrName]; } returnattrVal; } }); Store.getInstance=function(){ if(this.instance){ returnthis.instance; }else{ returnthis.instance=newthis(); } }; returnStore; });
我们真实使用的时候是使用store这个类操作localstorage,代码结束简单测试:
存储完成,以后都不会走请求,于是今天的代码基本结束 ,最后在android Hybrid中有一后退按钮,此按钮一旦按下会回到上一个页面,这个时候里面的localstorage可能会读取失效!一个简单不靠谱的解决方案是在webapp中加入:
XML/HTML Code复制内容到剪贴板 window.onunload=function(){};//适合单页应用,不要问我为什么,我也不知道
结语
localstorage是移动开发必不可少的技术点,需要深入了解,具体业务代码后续会放到git上,有兴趣的朋友可以去了解