前言
前几天出了一个问题,我们的项目要进行对公演示,前一天都好好的,到了开会当天早上,打不开了,我只能说,"猿",真是不可言。领导直接炸毛了,一脚蹬开他的战斗机,在群里疯狂轰炸。
怎么回事呢,后来排查发现,一位其他业务线的大兄弟,恰好在前一天下班前提交了一行代码,这一行代码,恰好在第二天的时候把他们的一个服务器干爆了,直接内存溢出了,而这个服务器里面的服务又恰好是我们项目依赖的一个服务,这个服务一炸,导致我们自己的服务成片的报错了。就是这么回事。
好在是这位大哥排查了半天问题不见进展,灵光一闪,一手代码回滚,在开会之前给倒腾回去了。
然鹅事情并没有就此完结,开完会后领导的命令就下来了,"你们前端,最好搞一个预防机制,至少在这种项目演示的场景下不能让我什么都看不到,云云云,云云云..."。
好,这就是这篇文章要解决的问题了。
初步方案
其实最开始的时候呢,我走过一点弯路,选了一个最直接,但是最笨的实现方式,逻辑是这样的:我们新建一个目录 mockFiles
,目录里面一个接口对应一个文件往里添加,文件的结构如下:
js
export default {
path: '/test/getlist',
date: '2023-11-13', // 数据录入的日期
data: { // 数据接口返回的内容
...
}
}
这样,我们在外层再写一个通用方法,将原来的请求封装再包装一下。
为了偷懒不去写映射文件,我使用了 vue.js 提及的自动化注册的一种手法,详细的内容在这里,生疏了的同学可以戳进去复习复习。
具体怎么做的呢 ?
js
// 新建一个文件 mock.js
function getMockApi () {
const files = require.context(
'@/mockFiles/',
false,
/\w+\.js$/
)
const mockApi = Object.fromEntries(
files.keys().map(filename => {
const { default, default: { path } } = requireComponent(filename)
return [ path, default]
})
)
return mockApi
}
这个时候,我们就会得到这样的一个大 map
:
js
{
'/test/getlist': {
path: '/test/getlist',
date: '2023-11-13',
data: {
...
}
}
}
这里我们还是先简单介绍一下,require.context
方法一共接收四个参数:
js
// directory: 相对路径,路径写到目标目录即可
// useSubdirectories: 是否深度查找。简单点说就是是否递归匹配子文件
// regExp: 文件名匹配正则式
// mode: 获取模式,同步或异步
require.context(
directory,
(useSubdirectories = true),
(regExp = /^./.*$/),
(mode = 'sync')
)
这里 directory
依然支持别名(如@
等),另外多讲一句,regExp
参数的运用颇有门道,用好了可以解决实用过程中的大部分问题。
我们要注意的是,require.context
是 webpack
的一个方法,也就是说,使用这个方法的前提条件是:项目打包编译必须使用 webpack
。也就是说,在 vue-cli3+
中你可以这么干,在 vite
中可能就失灵了。
关于 require.context
,webpack
官网中有更为详细的介绍,感兴趣的同学可以戳进去看看。
到这里,有的同学可能就会疑惑了,因为毕竟 vite
都普及这么久了,只限于 vue-cli
着实是有点让人接受不了。这个我承认,至于当初为什么我会偏执于此呢,一是恰好当时这个项目的环境允许,用的还是 vue2 + vue-cli
的配置,二是我个人觉得,这种使用者察觉不到,而构建者帮你自动完成的操作很微妙,像魔术一样,虽然眼前这种操作算只能得终究一皮毛,但还是令本小码蚁趋之如鹜。
接着,我们只需要再对封装的 api
做一些改造,当发现请求异常时我们便通过获取到的大 map
去拿对应的静态数据进行渲染即可。
问题
上面的策略,逻辑上是通的,实行起来却问题颇多,什么问题呢:
第一:一个查询接口可能会在不同的场景用到不同的参数来获取不同的数据,也就是说,接口path
不能直接用来当作数据 map
的 key
。
第二:无法解决动态参数问题,即当一个接口可以通过传入不同的参数来进行查询时,我们的这种方式是无法应对的。
第三:使用的数据一直是当时录入的数据,当数据过时时,重新录入异常麻烦。
新的思路
其实在看到问题三时,我就隐约感觉到,这种问题必须通过自动化的方式去处理,当静态形式的策略执行起来错综复杂、千丝万缕时,一定要考虑一下使用动态的形式。
什么是动态的形式呢,我们先来看一张图:
我们可以看到,该策略与第一种策略的最大区别在于:
- 使用了动态缓代替了静态文件
- 通过对接口数据的自动拾取代替了老版本的手动录入
这里还有一个比较重要的问题要说明一下,那就是缓存的策略,缓存文件的结构我设计成了下面这样子:
js
{
'/api/test': [
data_1: {
unikey, // params + path 混合生成
time, // 数据拾取时间
response // 接口返回数据
}
...
]
}
这里可能有的同学就会有疑问了,说你咋还是用的接口地址来做 map
的 key
?为什么还要这么干呢?我思考了一下,如果将 unikey
用来做 map
的 key
,在使用上没啥问题,但是因为使用动态参数查询的接口是普遍存在的,这样 map
上就会出现大量的 unikey
不同但是实际上是同一个接口的 key
,导致 map
的映射链被污染。
为了优雅的解决这个问题,我们还是将 接口路径
当作 key
来使用,而将 key
映射的值定义成数组形式,并给这个数组设置一个缓存上限,当数组的缓存量超过 10 (自己定义)时,就舍弃掉数组最末尾那个数据,并将新数据添加到数组最开始那个位置。这么做有什么好处呢?
map
结构清晰,每个接口对应每个接口应有的数据作用域- 我们依然可以通过
unikey
准确的查询到同一接口不同参数的准确值 - 当缓存上限设置的恰当时,我们既能冗余掉同一接口不同参数的场景,又能够保证数据在一定时间区间内的新鲜度
结语
今天分享的这部分内容呢,可能有些大佬老早就处理过了,并且很有可能已经开发出了更优的解法,我今天呢在这里说是实话,是有点话多的,啰嗦讲了这么一大堆呢,其实是想更加贴切的把事情的来龙去脉,把我的有些想法的由来及理由去讲清楚,以方便大家能更好的理解。
同时呢,也希望我今天的分享能给大家在类似问题的处理上带来一点小小的启发,提供一点小小的帮助,也殷切欢迎各路大佬给不吝赐教,留下新思路和新想法同大家一起讨论。
最后呢,写文不易,还是弱弱的骗个赞。
后思与展望
整件事情,当最后再回过头来看时,其实我还是觉得并不完美,因为把这么大量的数据存在前端的缓存里,着实不太优雅。我有过一个想法,就是在服务端与业务端之间,我们用 node + koa + mongo 再搭一个中间服务层,将真正的数据请求、数据缓存、接口的预防机制等逻辑统统都放到中间层来进行处理,对于业务端来说,这种策略就达到了无感防崩。