likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 6

业务背景

小程序某个页面采用的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正合适。 做的比较匆忙当时设计的数据结构是数组

imgStore: [{cacheId:'', webPath: '', localPath: ''}]

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

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下,所以只能在回调里去赋值,图片一多每个图都有个变量就显得很繁琐,所以这里有想到使用自定义指令自动获取网图地址然后替换为本地地址

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

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是最后一级路径+名称

使用

<view v-cache="url"></view>
<image v-cache="url"></image>

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

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

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

修改下返回的逻辑

 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…

    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…