缓存机

前言

前几天出了一个问题,我们的项目要进行对公演示,前一天都好好的,到了开会当天早上,打不开了,我只能说,"猿",真是不可言。领导直接炸毛了,一脚蹬开他的战斗机,在群里疯狂轰炸。

怎么回事呢,后来排查发现,一位其他业务线的大兄弟,恰好在前一天下班前提交了一行代码,这一行代码,恰好在第二天的时候把他们的一个服务器干爆了,直接内存溢出了,而这个服务器里面的服务又恰好是我们项目依赖的一个服务,这个服务一炸,导致我们自己的服务成片的报错了。就是这么回事。

好在是这位大哥排查了半天问题不见进展,灵光一闪,一手代码回滚,在开会之前给倒腾回去了。

然鹅事情并没有就此完结,开完会后领导的命令就下来了,"你们前端,最好搞一个预防机制,至少在这种项目演示的场景下不能让我什么都看不到,云云云,云云云..."。

好,这就是这篇文章要解决的问题了。

初步方案

其实最开始的时候呢,我走过一点弯路,选了一个最直接,但是最笨的实现方式,逻辑是这样的:我们新建一个目录 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.contextwebpack 的一个方法,也就是说,使用这个方法的前提条件是:项目打包编译必须使用 webpack。也就是说,在 vue-cli3+ 中你可以这么干,在 vite 中可能就失灵了。

关于 require.contextwebpack 官网中有更为详细的介绍,感兴趣的同学可以戳进去看看。

到这里,有的同学可能就会疑惑了,因为毕竟 vite 都普及这么久了,只限于 vue-cli 着实是有点让人接受不了。这个我承认,至于当初为什么我会偏执于此呢,一是恰好当时这个项目的环境允许,用的还是 vue2 + vue-cli 的配置,二是我个人觉得,这种使用者察觉不到,而构建者帮你自动完成的操作很微妙,像魔术一样,虽然眼前这种操作算只能得终究一皮毛,但还是令本小码蚁趋之如鹜。

接着,我们只需要再对封装的 api 做一些改造,当发现请求异常时我们便通过获取到的大 map 去拿对应的静态数据进行渲染即可。

问题

上面的策略,逻辑上是通的,实行起来却问题颇多,什么问题呢:

第一:一个查询接口可能会在不同的场景用到不同的参数来获取不同的数据,也就是说,接口path不能直接用来当作数据 mapkey

第二:无法解决动态参数问题,即当一个接口可以通过传入不同的参数来进行查询时,我们的这种方式是无法应对的。

第三:使用的数据一直是当时录入的数据,当数据过时时,重新录入异常麻烦。

新的思路

其实在看到问题三时,我就隐约感觉到,这种问题必须通过自动化的方式去处理,当静态形式的策略执行起来错综复杂、千丝万缕时,一定要考虑一下使用动态的形式。

什么是动态的形式呢,我们先来看一张图:

我们可以看到,该策略与第一种策略的最大区别在于:

  1. 使用了动态缓代替了静态文件
  2. 通过对接口数据的自动拾取代替了老版本的手动录入

这里还有一个比较重要的问题要说明一下,那就是缓存的策略,缓存文件的结构我设计成了下面这样子:

js 复制代码
{
   '/api/test': [
     data_1: {
       unikey,  // params + path 混合生成
       time, // 数据拾取时间
       response // 接口返回数据
     }
     ...
   ]
}

这里可能有的同学就会有疑问了,说你咋还是用的接口地址来做 mapkey?为什么还要这么干呢?我思考了一下,如果将 unikey 用来做 mapkey,在使用上没啥问题,但是因为使用动态参数查询的接口是普遍存在的,这样 map 上就会出现大量的 unikey 不同但是实际上是同一个接口的 key,导致 map 的映射链被污染。

为了优雅的解决这个问题,我们还是将 接口路径 当作 key 来使用,而将 key 映射的值定义成数组形式,并给这个数组设置一个缓存上限,当数组的缓存量超过 10 (自己定义)时,就舍弃掉数组最末尾那个数据,并将新数据添加到数组最开始那个位置。这么做有什么好处呢?

  1. map 结构清晰,每个接口对应每个接口应有的数据作用域
  2. 我们依然可以通过 unikey 准确的查询到同一接口不同参数的准确值
  3. 当缓存上限设置的恰当时,我们既能冗余掉同一接口不同参数的场景,又能够保证数据在一定时间区间内的新鲜度

结语

今天分享的这部分内容呢,可能有些大佬老早就处理过了,并且很有可能已经开发出了更优的解法,我今天呢在这里说是实话,是有点话多的,啰嗦讲了这么一大堆呢,其实是想更加贴切的把事情的来龙去脉,把我的有些想法的由来及理由去讲清楚,以方便大家能更好的理解。

同时呢,也希望我今天的分享能给大家在类似问题的处理上带来一点小小的启发,提供一点小小的帮助,也殷切欢迎各路大佬给不吝赐教,留下新思路和新想法同大家一起讨论。

最后呢,写文不易,还是弱弱的骗个赞。

后思与展望

整件事情,当最后再回过头来看时,其实我还是觉得并不完美,因为把这么大量的数据存在前端的缓存里,着实不太优雅。我有过一个想法,就是在服务端与业务端之间,我们用 node + koa + mongo 再搭一个中间服务层,将真正的数据请求、数据缓存、接口的预防机制等逻辑统统都放到中间层来进行处理,对于业务端来说,这种策略就达到了无感防崩。

相关推荐
一颗松鼠4 分钟前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
小远yyds24 分钟前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
吕彬-前端1 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱1 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai2 小时前
uniapp
前端·javascript·vue.js·uni-app
bysking2 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓3 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4113 小时前
无网络安装ionic和运行
前端·npm
理想不理想v3 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云3 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js