一、写在前面
上周测试同事给我提了个问题,说是运营系统的某个编辑页面中一个请求调用太多次了,看看怎么回事。我刚听说心里不屑一顾:能有多少次啊。结果测试环境打开页面一看,直呼好家伙!一个页面调用了30次请求,这真是捅了麻雀窝了。
那行吧,还是需要优化一下的,打开项目代码搜索这个请求,发现是在全局的一个 Upload 组件里面的初始化方法里面调用的,目的是获取图片上传oss系统的签名。这个页面一共有30个 Upload 组件,所以整个页面渲染完成后会调用30次接口!!我接着查看接口请求返回的数据发现签名的有效期是1小时,每次调用又重新刷新了这个签名。但是为什么最先调用接口的 Upload 组件还能上传图片成功,这我还不知道。
我灵机一动,把这个获取签名的方法单纯抽取出来,第一次调用后将返回数据缓存下来,后面请求时岂不美哉!但实际操作时发现事情没我想象的那么简单。。。
二、我的解决方案1.0
一开始我的方案是使用 Vuex 缓存接口返回的签名数据,Upload 组件每次都先从 Vuex 中 state 中查找签名数据 cosConfig,如果没找到再去请求接口。大致的流程如下图:
捋清楚后开始新写 Vuex 的 state 和对应的 mutation了,写完后一运行发现还是继续调用了30次请求,我很纳闷啊!!!无奈只好debugger语句开始一行行代码进行调试。问题也终于被我发现了,那就是:签名数据的异步获取。这个签名数据是通过调用后端接口返回给前端的,这个过程是异步的。那么当这个页面存在30个 Upload 组件时,每个组件都在自己的 creted 生命周期函数里先查找了 Vuex 中有没有缓存的签名数据。那页面第一次渲染时肯定是没有的,所以都会找不到签名数据,紧接着每个组件都会继续调用接口获取签名数据。等到获取到了签名之后,再缓存在 Vuex 中,根本就没有意义了。所以方案一失败!!
三、我的解决方案2.0
我需要承认的是平时困于重复性业务的开发中,很少去处理稍微复杂一点的问题,脑子容易混沌。我在发现方案1.0失败了之后,开始想其他的解决方案。通过 google 的无私帮助下,我找到了这篇文章([vue中多个相同组件重复请求的问题?]),完全就是和我一样的问题嘛。我进去看了第一个赞最多的回答,清晰透彻!主要的解决方案就是运用设计模式中的单例模式,把 Upload 组件中的获取签名的方案单独抽出来。这样子页面上不管有多少个 Upload 组件,调用的获取签名的方法都是同一个。这样子就可以在这个方法里面做文章了。
那么要做什么文章呢?我们假设这个获取上传图片签名的方法名叫做 getCosConfig,无论多少个 Upload 组件,都是调用同一个 getCosConfig 方法。那么在这个方法外部添加一个缓存对象 cacheConfig,每次先从这个缓存对象查找存不存在配置数据,若存在直接获取,不存在就调用接口获取。光是这样效果还是和方案1.0结果一样的,同样会调用30次接口,所以我们还需要加一个计数器变量 count。count 的初始值是0,每次请求都会加1。这样子当我们发现是第一次请求时就去调用接口,不是第一次的话就等待,直到第一次请求结束获得数据。逻辑流程图如下:
四、我的解决方案2.1
到此,本以为这个问题完美解决,但是我突然发现这个接口有入参的!这个页面调用的30个接口中,其中两个剩余的28个参数是不同的。我赶忙去查询了接口文档,发现这个接口是用于获取图片上传的签名,并且不同的业务模块的存储位置是不同的。那么自然返回的上传签名也是不同的,这也意味着原来的 cosConfig 的数据结构是不对的。因为原来的一级对象结构会导致不同业务模块的签名数据混乱了,搞不好弄成了p0级的线上bug。想到这里我心里一凉,感慨还好我细心多瞅了一眼。
&emsp既然问题已经定位到了,那么解决方案2.1自然而然也出来了,只要改造一下 co sConfig 和 count 的结构即可,增加一个key,变成二级的对象。最后我的代码成品如下:
import api from '../../api/system'
// 上传配置细分到了模块维度,数据结构为 {moduleKey1: {}, moduleKey2: {}} let cacheCosConfig = {} let countMap = {}
/**
- 存在时间戳接口返回秒为单位的,需要转换为毫秒为单位
- @param {Number} expiredTime 失效的时间戳
- @returns {Boolean} 是否过期 */ function checkCacheValid({ expiredTime }) { let endTime = expiredTime let nowTime = new Date().getTime()
if ((endTime + '').length === 10) { endTime = expiredTime * 1000 }
const isValid = endTime - nowTime > 0 console.log('---isValid', endTime, nowTime, isValid)
return isValid } /**
- 睡眠一段时间
- @param {Number} ms 延迟时间,单位毫秒
- @returns {Promise} */ async function delay (ms = 200) { return new Promise(resolve => setTimeout(resolve, ms)) }
/**
由于单个页面可能存在十几个upload组件,每个组件都会调用/store/policy/sts/get接口,造成资源的极大浪费。
基于此优化为首次调用接口后将获取到的配置对象缓存于 Vuex 中,后续需要时首先从 vuex 获取,若过期再调用接口重新获取 */ export async function getCosConfig(module) { try { // 初始化计数器重置为0 if (countMap[module] === undefined) { countMap[module] = 0 }
// 如果缓存中该模块的配置存在,那么从缓存中取配置 if (cacheCosConfig[module] && cacheCosConfig[module].expiredTime) { // 签名有效 if (checkCacheValid(cacheCosConfig[module])) { return { ...cacheCosConfig[module] } } else { // 签名失效,重置数据 cacheCosConfig[module] = undefined countMap[module] = 0 } }
// 如果不是第一次请求,循环等待 if (countMap[module]++) { while (!cacheCosConfig[module]) { await delay() } } else { const res = await api.getFileAuthorization({ module, }) const data = window.oc.serverReturn(res)
if (data) { cacheCosConfig[module] = {...data} } }
countMap[module]-- return { ...cacheCosConfig[module] } } catch (error) { console.error('---获取CosConfig出错:', error) countMap[module]-- } }
五、总结
最后总结一下,数据结构和设计原则的学习看似虚无缥缈,实际上能够帮助我们解决复杂度很高的问题。通过结合我们日常的开发工作,我们才能感受到这些知识的魅力,也会让我们更加有动力去提高我们的水平。