在开发web应用的时候,性能是一个必不可少的话题。对于webpack打包的单页应用,我们可以通过很多方式来优化性能,比如tree-shaking,模块的懒加载,以及使用extrens网络CDN来加速这些常规的优化。甚至在vue-cli项目中,我们也可以使用--modern命令生成新旧浏览器代码来优化程序。事实上,缓存一定是改善网络应用的有效方式之一,尤其是当用户受到网络速度限制时。提高系统响应能力并降低网络消耗。当然,内容离用户越近,缓存就越快,缓存也就越有效。就客户端而言,我们有很多缓存数据和资源的方法,比如标准的浏览器缓存和目前流行的Serviceworker。但是,它们更适合缓存静态内容。比如html、js、css和图片等文件。对于缓存系统数据,我使用了另一种解决方案。那么现在就介绍一下我在项目中应用的各种api请求方案,从简单到复杂。方案一、数据缓存简单的数据缓存,第一次请求时获取数据,然后使用数据,不再请求后端api。代码如下:constdataCache=newMap()asyncgetWares(){letkey='wares'//从数据缓存中获取数据letdata=dataCache.get(key)if(!data){//没有数据请求服务器constres=awatrequest.get('/getWares')//其他操作...data=...//设置数据缓存dataCache.set(key,data)}returndata}第一行代码使用了es6以上的Map,如果map不是很好理解,可以参考ECMAScript6GettingStartedwithSetandMap或ExploringES6对map和set的介绍,可以理解为键值对存储结构。之后代码使用了async函数,异步操作更加方便。可以参考ECMAScript6GettingStartedwithasyncFunctions来学习或巩固知识。代码本身很容易理解。它使用Map对象来缓存数据,然后调用Map对象来获取数据。对于极其简单的业务场景,可以直接使用这段代码。调用方式:getWares().then(...)//第二次调用获取之前的数据getWares().then(...)方案二Promise缓存方案一不足。因为如果考虑同时调用这个api超过两次,第二次请求api会因为请求没有返回。当然,如果在系统中加入vuex、redux这样的单一数据源框架,这样的问题是不太可能遇到的,但是有时候我们希望在各个复杂的组件中单独调用API,不想组件之间进行数据通信,你会遇到这种情况。constpromiseCache=newMap()getWares(){constkey='wares'letpromise=promiseCache.get(key);//当前promise缓存中没有这个promiseif(!promise){promise=request.get('/getWares').then(res=>{//操作res...}).catch(error=>{//请求回来后,如果有问题,从缓存中删除promise,防止第二次请求fromcontinuingfailSpromiseCache.delete(key)returnPromise.reject(error)})}//returnpromisereturnpromise}这段代码避免了方案1中同时请求多个的问题,同时promise也被删除在后台出错的情况下,这样就不会出现缓存错误的promise一直报错的问题。调用方式:getWares().then(...)//第二次调用获取之前的promisegetWares().then(...)方案三、多promise缓存该方案需要同时进行多个api请求接下来,如果某个API出现错误,则同时返回数据。两者都不会返回正确的数据。constquerys={wares:'getWares',skus:'getSku'}constpromiseCache=newMap()asyncqueryAll(queryApiName){//判断传入数据是否为数组constqueryIsArray=Array.isArray(queryApiName)//统一数据处理,是否是字符串还是数组,就当做数组。constapis=queryIsArray?queryApiName:[queryApiName]//获取所有请求服务constpromiseApi=[]apis.forEach(api=>{//usepromiseletpromise=promiseCache.get(api)if(promise){//如果有的话缓存,直接pushpromise.push(promise)}else{promise=request.get(querys[api]).then(res=>{//operateonres...}).catch(error=>{//请求返回后,如果有问题,从缓存中删除promise.all(promiseApi).then(res=>{//根据输入是字符串还是数组返回数据,因为是数组操作//如果输入是字符串,则需要取出操作returnqueryIsArray?res:res[0]})}这种解决方案是同时从多个服务器获取数据的一种方式。可以同时获取多个数据进行运算,不会因为单个数据的问题而出错。调用方法:queryAll('wares').then(...)//第二次调用不会取ware,只会取skusqueryAll(['wares','skus']).then(...)方案四:添加与时间相关的缓存通常是有害的。如果我们知道数据被修改了,我们可以直接删除缓存。这时候我们就可以调用方法来请求服务器了。这样,我们就避免了在前端显示旧数据。但是我们可能有一段时间不对数据进行操作,那么这个时候旧的数据会一直存在,所以我们最好定个时间清除数据。该解决方案使用类持久数据进行数据缓存,并添加过期时间数据和参数化。代码如下:首先定义持久化类,可以存储promise或者dataclassItemCache(){construct(data,timeout){this.data=data//设置超时时间,设置多少秒this.timeout=timeout//创建对象的时间,大致设置为获取数据的时间this.cacheTime=(newDate()).getTime}}然后我们定义数据缓存。我们使用Map基本相同的apiclassExpriesCache{//定义静态数据map作为缓存池staticcacheMap=newMap()//数据是否超时staticisOverTime(name){constdata=ExpriesCache.cacheMap.get(name)//没有数据musttimeoutif(!data)returntrue//获取系统当前时间戳constcurrentTime=(newDate()).getTime()//获取当前时间和存储时间的过去秒数constoverTime=(currentTime-data.cacheTime)/1000//如果过去的秒数大于当前超时,也返回null让它去服务器端取数据if(Math.abs(overTime)>data.timeout){//这个代码可以没有,不会有问题,但是如果有这个代码,再进入这个方法可以减少判断。ExpriesCache.cacheMap.delete(name)returntrue}//没有超时returnfalse}//缓存中的当前数据是否超时statichas(name){return!ExpriesCache.isOverTime(name)}//删除缓存中的数据statichas(name){return!ExpriesCache.isOverTime(name)}//删除缓存中的数据staticdelete(name){returnExpriesCache.cacheMap.delete(name)}//getstaticget(name){constisDataOverTiem=ExpriesCache.isOverTime(name)//如果数据超时,则返回null,如果没有超时,则返回数据而不是ItemCache对象returnisDataOverTiem?null:ExpriesCache.cacheMap.get(name).data}//默认存储20分钟staticset(name,data,timeout=1200){//设置itemCacheconstitemCache=mewItemCache(data,timeout)//CacheExpriesCache.cacheMap.set(name,itemCache)}}这时候数据类和操作类已经定义好了,我们可以在api层这样定义//生成键值错误constgenerateKeyError=newError("Can'tgeneratekeyfromnameandargument")//生成key值functiongenerateKey(name,argument){//从arguments中获取数据并转intoanarrayconstparams=Array.from(argument).join(',')try{//返回字符串,函数名+函数参数return`${name}:${params}`}catch(_){//ReturngeneratedkeyerrorreturngenerateKeyError}}asyncgetWare(params1,params2){//生成keyconstkey=generateKey('getWare',[params1,params2])//获取数据letdata=ExpriesCache.get(key)if(!data){constres=awaitrequest('/getWares',{params1,params2})//使用10s缓存,10s后再次获取会获取null,继续请求ExpriesCache.set(key,res,10)}returndata}该方案采用不同的过期时间和api参数进行缓存,可以满足大部分业务场景。调用方法:getWares(1,2).then(...)//第二次调用获取前面的promisegetWares(1,2).then(...)//参数不同,不带前面的promisegetWares(1,3).then(...)方案五、基于装饰器的方案四和方案四的解决方案是一样的,不过是基于装饰器来做的。代码如下://生成键值错误constgenerateKeyError=newError("Can'tgeneratekeyfromnameandargument")//生成键值函数generateKey(name,argument){//从arguments中获取数据,然后变成数组constparams=Array.from(argument).join(',')try{//返回字符串return`${name}:${params}`}catch(_){returngenerateKeyError}}functiondecorate(handleDescription,entryArgs){//判断当前是否lastdata是Descriptor,如果是描述符,直接使用//装饰器如logif(isDescriptor(entryArgs[entryArgs.length-1])){returnhandleDescription(...entryArgs,[])}else{//Ifnot//例如add(1)plus(20)装饰器returnfunction(){returnhandleDescription(...Array.protptype.slice.call(arguments),entryArgs)}}}functionhandleApiCache(target,name,descriptor,...config){//获取函数体并保存constfn=descriptor.value//修改函数体descriptor.value=function(){constkey=generateKey(name,arguments)//无法生成key,直接请求serverdataif(key===generateKeyError){//使用刚刚保存的函数体请求returnfn.apply(null,arguments)}letpromise=ExpriesCache.get(key)if(!promise){//设置promisepromise=fn.apply(null,arguments).catch(error=>{//请求返回最后,如果有问题,从缓存中删除promiseExpriesCache.delete(key)//返回错误returnPromise.reject(error)})//使用10s缓存,10s后再次get,会得到null并继续向服务端请求ExpriesCache.set(key,promise,config[0])}returnpromise}returndescriptor;}//指定装饰器函数ApiCache(...args){returndecorate(handleApiCache,args)}此时我们会使用类到apiCacheclassApi{//Cache10s@ApiCache(10)//此时不要使用默认值,因为当前装饰器获取不到getWare(params1,params2){returnrequest.get('/getWares')}}因为函数存在Ascension,所以没办法把函数当作装饰器。例如:varcounter=0;varadd=function(){counter++;};@addfunctionfoo(){}代码的本意是执行后计数器等于1,但实际结果是计数器等于0因为提升了函数,实际执行的代码如下@addfunctionfoo(){}varcounter;varadd;counter=0;add=function(){counter++;};所以没有办法在函数上使用装饰器。具体可以参考ECMAScript6Decorator入门方法简单,对业务层影响不大。但是不能动态修改缓存时间。调用方法getWares(1,2).then(...)//第二次调用获取前面的promisegetWares(1,2).then(...)//参数不同,不要拿前面的promisegetWares(1,3).then(...)总结了api的缓存机制和场景。基本介绍到这里。基本可以完成大部分数据业务的缓存。我也想在这里求教。各位,有没有更好的解决办法,或者本博客有什么不对的地方,请大家指正,在此谢谢大家。同时,这里还有很多未完成的工作,可能会在后续的博客中继续完善。
