App缓存优化篇(react-native)

备注:react-native中,Image组件仅支持本地缓存的图片的base64形式显示图片(uri: http/base64/local asset url)非http,也非静态资源图片路径,除非有办法直接调用原生android或者Ios图片缓存模块进行显示,否则base64图片很大时,加载慢这个问题无法解决

关于离线缓存,我们需要区分一件事,APP资源在有网络的情况下加载完成了,然后突然没网络,但是又没有杀掉app时,这个时候用的是React-Native Image或者List相关组件自带的内存缓存数据

我们这边提的离线缓存,是指,在没有网络的情况下,直接进的APP时所需要展示的数据(图片、列表等),这种情况下,RN的内存缓存数据是会随着上一次App杀掉而释放掉的

有人可能会疑惑,既然没有网络,那么怎么绕过登录呢?

其实,APP一般具有快捷登录的,比如指纹,手势密码等。这个是不需要再次调用登录接口就可以绕过登录的

 

下面进入正文

APP中的缓存分为两种:

一、离线缓存,没有网络时,显示缓存在内存或者本地磁盘的数据

逻辑是在请求前,优先获取缓存,其步骤又分为(1)先获取内存中的缓存(2)获取不到内存中的缓存时,获取本地磁盘的缓存(3)获取不到内存和磁盘中的缓存时,发起请求获取接口的数据

这种缓存主要用来优化图片渲染速度,但也可以支持缓存其他类型的接口数据

_getCacheImage = (url) => {
    // 加载失败可重试3次
    if (this._loadErrorTimes > 3) return;

    // 传入md5直接用,没传入则先获取md5 (userId必传)
    getCacheImage(url)
      .then((r) => {
        this.setState({ picture: r });
        this._loadErrorTimes = 0;
      })
      .catch((e) => {
        console.warn('_getCacheImage err:', e, url);
        this._loadErrorTimes++;
        setTimeout(() => {
          this._getCacheImage(url);
        }, 300);
      });
  };
/**
 * @author huangzhixin
 */
import { STORAGE_KEY_AVATAR } from './commonConstants';
import RNFSHelper, { USER_FILES_PATH } from './RNFSHelper';
import { downloadImageBase64 } from './AvatarUtil';
import RNFS from 'react-native-fs';

//  RNFS持久化 + 内存缓存 + 接口缓存, key: portraitsMD5
let memoryCache = {};
// 内存缓存用户头像md5,避免频繁调用portraitsMD5获取接口 {oaCode: portraitsMD5}
let md5MemCache = {};
let keys = [];
let MAX_MC_NUM = 50; // 头像内存缓存最大数量

// 根据缓存策略获取URL图片
export function getCacheImage(url: tring) {
  return new Promise((resolve, reject) => {
    if (!url || !/^https?:\/\//.test(url)) {
      reject('invalid image url: ' + url);
      return;
    }
    const key = global.md5(url);
    // 优先读内存
    var val = memoryCache[key];
    if (val) {
      console.log('内存中的缓存', val, memoryCache)
      resolve(val);
      return;
    }

    // 存本地文件方式
    // const filePath = USER_FILES_PATH + STORAGE_KEY_AVATAR + '/' + key;
    // RNFSHelper.existsFile(filePath).then((dirExists) => {
    //   MOALog.log('getCacheImage existsFile:', dirExists, filePath);
    //   if (!dirExists) {
    //     // 发起请求
    //     downloadImageBase64(url)
    //       .then((webAvt) => {
    //         if (webAvt) {
    //           resolve(webAvt);
    //         } else {
    //           MOALog.warn('downImgBase64 获取图片异常 webAvt:', webAvt);
    //           reject({ error: '获取图片异常' });
    //         }
    //       })
    //       .catch((e) => {
    //         reject(e);
    //       });
    //   } else {
    //     resolve(filePath);
    //   }
    // });
    // return;

    // 读本地缓存
    getLocalStorage(key)
      .then((localAvt) => {
        console.log('本地中的缓存', localAvt)
        if (localAvt) {
          // 内存缓存
          saveMemory(key, localAvt);
          resolve(localAvt);
        } else {
          throw new Error('getLocalAvatar null');
        }
      })
      .catch(() => {
        // 发起请求
        downloadImageBase64(url)
          .then((webAvt) => {
            if (webAvt) {
              // 内存缓存+本地缓存
              saveMemory(key, webAvt);
              saveLocalStorage(key, webAvt);
              resolve(webAvt);
            } else {
              MOALog.warn('downImgBase64 获取图片异常 webAvt:', webAvt);
              reject({ error: '获取图片异常' });
            }
          })
          .catch((e) => {
            reject(e);
          });
      });
  });
}

export function getMD5Cache(oaCode: string) {
  return md5MemCache[oaCode];
}

export function clearAvatarMemory() {
  memoryCache = {};
  keys = [];
  md5MemCache = {};
}

export function clearAvatarsCache() {
  return RNFSHelper.deleteDir(STORAGE_KEY_AVATAR);
}

function saveMemory(key: string, val: string) {
  if (!memoryCache[key]) {
    memoryCache[key] = val;
    keys.push(key);

    // 控制内存存储量
    if (keys.length > MAX_MC_NUM) {
      var _del = keys.shift();
      delete memoryCache[_del];
    }
  }
}

function saveLocalStorage(key: string, val: string) {
  // key 文件名, STORAGE_KEY_AVATAR: 目录名
  RNFSHelper.writeFile(STORAGE_KEY_AVATAR + '/' + key, val)
    .then((r) => {
      MOALog.log('writeFile success:', r, STORAGE_KEY_AVATAR + '/' + key);
    })
    .catch((e) => {
      MOALog.log('writeFile error:', e, STORAGE_KEY_AVATAR + '/' + key);
    });
}

function getLocalStorage(key: string) {
  return RNFSHelper.readFile(STORAGE_KEY_AVATAR + '/' + key);
}

export default {
  clearMemory: clearAvatarMemory,
  clearCache: clearAvatarsCache,
};

二、在线数据缓存

这种缓存存在的目的是由于接口请求的数据再到渲染显示,会有一段空白期,这段时间用户体验很不好,除了用户登录后,APP第一次加载(没有缓存)时,才会出现这种空数据状态

为了提高用户体验,我们需要缓存上一次的接口缓存,在主要的场景页面请求列表或者轮播图等数据时,优先获取缓存的数据进行显示,之后用请求到的数据去覆盖掉缓存的数据,并且更新缓存数据

例如:

// 从本地缓存获取数据占位展示
  _getCachedData = () => {
    const { storageKey, storageId } = this.props;
    if (!storageKey || !storageId) {
      return;
    }
    let tasks = [
      new Promise((resolve, reject) => {
        mb.storage
          .load({
            key: storageKey + '@' + mb.getUserId(),
            id: storageId,
          })
          .then((list) => {
            resolve(list);
          })
          .catch((e) => {
            MOALog.warn('storage refreshPageList catch:', e);
            reject(e);
          });
      }),
    ];

    Promise.all(tasks.map(function (promiseItem) {
      return promiseItem.catch((err) => {
        MOALog.log('Promise.all catch', err);
        return err;
      });
    })).then((resArr) => {
      this.setState({
        items: resArr[0] && Array.isArray(resArr[0]) ? resArr[0] : [],
      });
    }).catch((err) => {
      console.warn('Promise.all catch:', err);
    });
  };

PS:除此之外,还有一种情况,就是图片没有无论是缓存还是磁盘还是接口都没有获取到,需要一张默认图片进行占位显示,例如轮播图(否则一片空白),这就需要判断图片加载成功与否了onLoad

 

补充所需文件:

PS:在这里贴一下接口返回的文件响应体数据:

 

 

downloadImageBase64:

export function downloadImageBase64(url, option = {}) {
  return new Promise((resolve, reject) => {
    // TODO: 添加请求池过滤重复头像请求
    // 首页是先取缓存数据再取接口请求数据,而缓存数据获取是慢于token的,由于没有携带token会报错取无法下载图片到本地供离线时使用,同时还会占据请求池相同的apiKey,而后的真实api就会被请求池返回true,这边针对首页进行开放限制
    if (RequestPool.checkCancel(url, resolve) && !isHome && !global.isAlreadyAppLogout) return;
    // 覆盖resolve来处理请求池结果回调
    resolve = (arg) => {
      RequestPool.handleResultInPool(url, arg);
    };

    const { auth = true } = option;
    let headers = {
      'Content-Type': 'image/*',
    };
    if (auth) {
      headers = {
        ...headers,
        'msg.callback': '',
        'auth.sysid': Config.BUSINESS_SYSID,
        'auth.permit': Config.BUSINESS_PERMIT,
        'auth.token': Zqmb.businessToken,
      };
    }
    if (auth && !Zqmb.businessToken) {
      reject('请先登录再进行操作');
      return;
    }

    // // 保存为本地路径
    // const key = global.md5(url);
    // const filePath = STORAGE_KEY_AVATAR + '/' + key;
    // download(url, filePath, { headers }).then((res) => {
    //   MOALog.log('download res:', res);
    //   resolve(filePath);
    // }).catch(err => {
    //   MOALog.warn('download catch err:', err);
    //   reject(err);
    // });
    // return;

    MOALog.log('downImgBase64 url:', url);
    // 注:Debug下头像base64编码会出现问题,打包应用未现异常
    fetch(url, {
      method: 'GET',
      headers: headers,
    })
      .then((r) => {
        if (
          r.headers.map &&
          r.headers.map['content-type'] &&
          !/image/.test(r.headers.map['content-type']) &&
          !/jpg|jpeg|gif|png/.test(r.headers.map['content-type'])
        ) {
          throw new Error(r._bodyText);
        }
        // 如果是RN 不能在React Navtive Debugger的Enable Network模式下测试 该模式下无法得到文件
        return r.blob();
        // return r._bodyBlob;
      })
      .then((blob) => {
        const reader = new FileReader();
        reader.onload = (e) => {
          let data = e.target.result;
          MOALog.log('downImgBase64', url, data && data.length);
          // android下前缀问题
          // data = data.replace('application/octet-stream', 'image/png')
          var rst = data.split('base64,')[1];
          resolve(rst);
        };
        reader.readAsDataURL(blob);
        MOALog.log('downImgBase64 readAsDataURL', url, blob);
      })
      .catch((e) => {
        MOALog.warn('downImgBase64 catch err:', e);
        reject(e);
      });
  });
}
// 请求池:避免同时刻重复请求
export const RequestPool = {
  _pool: {}, // {key: [result1, result2 ...]}
  checkCancel: function(key, rstCb) {
    // 检查已有进行中请求则需取消后面相同请求
    if (this._pool[key]) {
      this._pool[key].push(rstCb);
      return true;
    } else {
      this._pool[key] = [];
      this._pool[key].push(rstCb);
      return false;
    }
  },
  handleResultInPool: function(key, ...args) {
    // 结束请求同时对请求池中请求统一回调
    if (this._pool[key]) {
      this._pool[key].forEach(cb => {
        cb && cb(...args);
      });
      delete this._pool[key];
    }
  }
};
global.RequestPool = RequestPool;

 

 

附:js实现图片资源、blob、base64的各种场景转换

 

文件转babase64

function getImgToBase64(url,callback){//将图片转换为Base64
  var canvas = document.createElement('canvas'),
    ctx = canvas.getContext('2d'),
    img = new Image;
  img.crossOrigin = 'Anonymous';
  img.onload = function(){
    canvas.height = img.height;
    canvas.width = img.width;
    ctx.drawImage(img,0,0);
    var dataURL = canvas.toDataURL('image/png');
    callback(dataURL);
    canvas = null;
  };
  img.src = url;
}
/**
 * 获取指定文件的base64编码
 * @param  object   File       Blob 或 File 对象       这里是file对象
 * @param  Function callback   返回数据的回调函数
 * @return string              返回base64编码
 */
function getBase64(File,callback){
    var reader = new FileReader();                  //IE10+
    var AllowImgFileSize = 2100000;                 //上传图片最大值(单位字节)( 2 M = 2097152 B )超过2M上传失败
    var File = File||$("#file").get(0).files[0];    //获取上传的文件对象
    /*
        FileList {0: File, 1: File, length: 2} 多个文件
        File:{name: "fan.jpg", lastModified: 1559019043288, lastModifiedDate: Tue May 28 2019 12:50:43 GMT+0800 (中国标准时间), webkitRelativePath: "", size: 3346145, type: "image/jpeg"}
        FileList {0: File, 1: File, length: 2}  单个文件
     */
    if (File) {

        //读取指定的 Blob 或 File 对象  触发loadend 事件 并将图片的base64编码赋值给result
        reader.readAsDataURL(File);
        //reader.readAsText(File)
        //异步通信 回调函数返回
        reader.onload = function (e) {
           //var ImgFileSize = reader.result.substring(reader.result.indexOf(",") + 1).length;//截取base64码部分(可选可不选,需要与后台沟通)
           if (AllowImgFileSize != 0 && AllowImgFileSize < reader.result.length) {
                alert( '上传失败,请上传不大于2M的图片!');
                return;
            }else{
                var base64Data=reader.result;
                //返回base64编码
                callback(base64Data);
            }
        }
    }  
  
}

 

base64转换为file对象 

function Base64toFile(dataurl, filename) {//将base64转换为文件
        var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
            bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
        while(n--){
            u8arr[n] = bstr.charCodeAt(n);
        }
        return new File([u8arr], filename, {type:mime});
}

 

base64转blob

function blobToBase64(blob, callback) {
    let a = new FileReader();
    a.onload = function (e) { callback(e.target.result); }
    a.readAsDataURL(blob);
}

 

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hzxOnlineOk

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值