小程序oss图片本地化缓存爬坑记

业务背景

小程序某个页面采用的oss地址的图片访问量很大,导致oss流量告急,所以想加个本地化缓存,用户只有第一次来加载网图,之后每次都访问本地路径。

思路

加缓存要先思考,缓存到哪里缓存满了删除逻辑又是什么 ,前端第一时间想到的肯定是localStorage,这个小程序是有Storage的但是这个只能存字符串,导致要想存图片就只能存储base64,遇到大图在10m的限制下就显得捉襟见肘了,所以大容量存储会想到indexDB 但是小程序又不支持。还好小程序提供了文件管理器wx.getFileSystemManager以下简称fs

我们要用到的几个核心api:

1、wx.downloadFile 这个api会下载图片并返回临时下载的地址,这个地址是临时文件会随着小程序关闭进行清理

2、fs.saveFile 这个是把上边返回的临时文件进行保存,保存为持久文件调用此api后上边临时路径将不可用

3、fs.getSavedFileList 获取当前本地文件list,用来查缓存大小

4、fs.accessSync 检测本地路径是否有文件,用来查缓存是否存在


好了解了以上知识点,就可以做设计了,通过传入网图地址我们要获取他的本地地址,这要我们去存一个关联着本地地址和网络地址的的描述文件,此时用storage正合适。 做的比较匆忙当时设计的数据结构是数组

js 复制代码
imgStore: [{cacheId:'', webPath: '', localPath: ''}]

现在回过头来想可能对象的形式把cachId拿出来当key会好一点

js 复制代码
import Taro from '@tarojs/taro'

const fs = Taro.getFileSystemManager();
function checkImgStore(data) {
  
  return new Promise((resolve, reject) => {
    let imgStore = Taro.getStorageSync('imgStor') || []
    let cacheIndex = imgStore.findIndex(item => {
      return item.cacheId === data.cacheId
    })
    // 如果缓存中没有 将原地址返回 然后进行缓存 缓存前进行溢出判断
    if (cacheIndex === -1) {
      resolve(data)
      isFull(data).then(res => {
        imgStore.push({
          ...res
        });
        Taro.setStorageSync("imgStor", imgStore);
      })
    } else {
      // 缓存中有
      let localPath = imgStore[cacheIndex].localPath || ''
      if (localPath) {
        try {
          fs.accessSync(localPath)
          const systemInfo = Taro.getSystemInfoSync()
          // tip: ios真机情况无法使用本地路径(wxfile://...)
          // todo 此处是否考虑限制大小
          resolve({
            cacheId: data.cacheId,
            webPath: imgStore[cacheIndex].webPath,
            localPath
          })
        } catch (e) {
          // 错误情况 localStorage有 但是本地没有保存该文件(用户手动清理了手机)
          isFull(data).then(res => {
            imgStore.splice(cacheIndex, 1)
            imgStore.push({
              ...res
            });
            Taro.setStorageSync("imgStor", imgStore);
            checkImgStore(data)
          })
        }
      } else {
        // 错误情况 localStorage里本地路径映射未知原因删除 
        imgStore.splice(cacheIndex, 1)
        Taro.setStorageSync("imgStor", imgStore);
        checkImgStore(data)
      }
    }
  })
}
// 检查缓存是否快满了
function isFull(data) {
  return new Promise((resolve, reject) => {
    fs.getSavedFileList({
      success(res) {
        let list = res.fileList
        let size = 0
        for (let i = 0; i < list.length; i++) {
          size += list[i].size
        }
        // 字节转mb
        size = size / 1048576
        // https://developers.weixin.qq.com/miniprogram/dev/framework/ability/file-system.html
        // 本地缓存文件大小限制200m 这里留50m buffer
        if (size > 150) {
          deleteImgStore(data).then(resp => {
            resolve(resp)
          })
        } else {
          downloadImg(data).then(resp => {
            resolve(resp)
          })
        }
      }
    })
  })
}
// 暴力点直接删除前1/4
function deleteImgStore(data) {
  let imgStore = Taro.getStorageSync('imgStor') || []
  let num = Math.floor(imgStore.length / 4)
  let promiseList = []
  return new Promise((resolve, reject) => {
    for (let i = 0; i < num; i++) {
      promiseList.push(new Promise((resolve, reject) => {
        fs.removeSavedFile({
          filePath: imgStore[i].localPath,
          success(res) {
            resolve(i)
          },
          fail(res) {
            resolve(i)
          }
        })
      }))
    }
    Promise.all(promiseList).then(res => {
      let cimgStor = Taro.getStorageSync('imgStor') || []
      for (let i = res.length -1; i >= 0; i--) {
        cimgStor.splice(i, 1)
      }
      Taro.setStorageSync("imgStor", cimgStor);
      downloadImg(data).then(data => {
        resolve(data)
      })
    })
  })
}

function downloadImg(data) {
  return new Promise((resolve, reject) => {
    Taro.downloadFile({
      url: data.webPath,
      success: function(res) {
        if (res.statusCode === 200) {
          fs.saveFile({
            tempFilePath: res.tempFilePath,
            success: function(rs) {
              resolve({
                cacheId: data.cacheId,
                webPath: data.webPath,
                localPath: rs.savedFilePath
              })
            }
          })
        }
      }
    })
  })

}

export default checkImgStore

以上为核心功能(我们用的taro框架,不同的改下前缀即可)

功能实现了,但是易用性差点,因为整体在promise下,所以只能在回调里去赋值,图片一多每个图都有个变量就显得很繁琐,所以这里有想到使用自定义指令自动获取网图地址然后替换为本地地址

想到要发个插件所以整体用插件的形式写

js 复制代码
import checkImgStore from "./src"

export default {
  install(app) {
    app.directive('cache', {
      created(el, { value }) {
        const data = {
          cacheId: getImageNameFromPath(value),
          webPath: value
        }
        const tag = el.tagName.toLowerCase()
        const fnMap = {
          view: (res) => {
            el.style.backgroundImage = `url(${res.localPath || res.webPath})`
          },
          image: (res) => {
            el.props.src = res.localPath || res.webPath
          }
        }
        checkImgStore(data).then(res => {
          fnMap[tag](res)
        }).catch(err => {
          fnMap[tag]({localPath: value})
        })
      }
    })
  }
}
function getImageNameFromPath(imagePath) {
  const lastSlashIndex = imagePath.lastIndexOf('/');
  const pathName = imagePath.substring(imagePath.lastIndexOf('/', lastSlashIndex - 1) + 1);
  return pathName;
}

这里怕不同路径有相同的名字,所以cacheId是最后一级路径+名称

使用

html 复制代码
<view v-cache="url"></view>
<image v-cache="url"></image>

这样view会替换背景图,image替换src

以上,但是就当我以为大功告成的时候,真机调试给了我当头一棒

ios只要走缓存图片直接消失,翻遍了微信社区问答没找到特别好的答案,但是还是大致了解到ios不支持这个本地文件wxfile://,所以没办法ios老老实实转base64吧

修改下返回的逻辑

js 复制代码
 if(systemInfo.platform === 'ios') {
    fs.readFile({
      filePath: localPath,
      encoding: 'base64',
      success: res => {
        // 获取图片后缀格式
        const type = imgStore[cacheIndex].webPath.split('.').pop()
        resolve({
          cacheId: data.cacheId,
          webPath: imgStore[cacheIndex].webPath,
          localPath: `data:image/${type};base64,` + res.data
        })
      },
      fail: () => reject()
    })
  } else {
    resolve({
      cacheId: data.cacheId,
      webPath: imgStore[cacheIndex].webPath,
      localPath
    })
  }

终于大功告成 顺便发个npm包 www.npmjs.com/package/plu...

sh 复制代码
    npm install plugin-cache-taro

源码地址github.com/wuvital/plu...

还有些后续的想法,目前缓存删除思路是FIFO后续可能会增加LRU

这也是为什么上边说的使用对象的形式把id拿到外边当key会好一点,我们可以用 id_时间戳 这样去做key 这样删除时只用读key不用读value对性能可能友好一点

查阅资料:

blog.csdn.net/qq_36683019... blog.csdn.net/Wall_E10577... developers.weixin.qq.com/miniprogram...

相关推荐
xisai888 分钟前
2025年开考科目有哪些?
java·开发语言·javascript·算法·kotlin
DN金猿37 分钟前
vue项目PC端和移动端实现在线预览pptx文件
前端·javascript·vue.js·ppt
hahaqi95271 小时前
layui 表格点击编辑感觉很好用,实现方法如下
前端·javascript·layui
我爱学习_zwj1 小时前
ArkTS的进阶语法-4(函数补充,正则表达式)
前端·华为·正则表达式·harmonyos
北【辰】、1 小时前
uview Collapse折叠面板无法动态设置展开问题(微信小程序)
javascript·vue.js·微信小程序·小程序·前端框架
cdcdhj2 小时前
利用服务工作线程serviceWorker缓存静态文件css,html,js,图片等的方法,以及更新和删除及版本控制
javascript·css·缓存·html
咔咔库奇2 小时前
【CSS问题】margin塌陷
前端·javascript·css
无敌最俊朗@2 小时前
c#————委托Action使用例子
java·前端·c#
见过夏天2 小时前
CSS 中渐变色的使用
前端·css
见过夏天2 小时前
JavaScript 中正则表达式的使用
前端·javascript