备注: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) => {
          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);
                    APP中的缓存分为两种:一、离线缓存,没有网络时,显示缓存到内存或者本地磁盘的数据逻辑是在请求前,优先获取缓存,其步骤又分为(1)先获取内存中的缓存(2)获取不到内存中的缓存时,获取本地磁盘的缓存(3)获取不到内存和磁盘中的缓存时,发起请求获取接口的数据这种缓存主要用来优化图片渲染速度,但也可以支持缓存其他类型的接口数据/** * @author huangzhixin */import { STORAGE_KEY_AVATAR } from './commonConstants';
npm install --save react-native-clear-app-cache
Then link with:
react-native link react-native-clear-app-cache
				
文章目录一、函数(1)图片绝对路径转文件流File1. 主函数2. 获取图片base643. base64转file二、页面结构(最终图片以file格式提交给后端)即formdata(1)html页面结构显示1. 主页面2. 子页面(2) 判断浏览器是否自带复原函数(3)图片旋转函数三、页面使用(1)不涉及图片回显(2)涉及图片回显(回显格式为路径)四、后端接口(1)后端上传接口(2)axios封装http (1)图片绝对路径转文件流File 路径就是类似下面这种 "/pic/userpic/4
配置的血泪史 一、0.49版本后没有了index.android.js和index.ios.js,统一为index.js。android目录下assets必须有index.android.bundle文件。 java.lang.RuntimeException: Unable to load script from assets 'index.android.bundle'...
本文均为RN开发过程中遇到的问题、坑点的分析及解决方案,各问题点之间无关联,希望能帮助读者少走弯路,持续更新中... (2019年3月29日更新) 原文链接:www.kovli.com/2018/06/25/… 作者:Kovli - 如何在原生端(iOS和android两个平台)使用ReactNative里的本地图片(路径类似require('./xxximage.png'))。 在ReactNa...
仅介绍需要动态修改数据的本地缓存 第一步:定义两个方法,方法1,设置本地缓存键值对,方法2,读取本地缓存键值对进行判断,未存值发出请求,否则读取本地缓存数据设置state setItem = async (key,item) => { try { await AsyncStorage.setItem(key, JSON.stringify(i...
1、npm install react-native-file-selector --save 2、react-native link react-native-file-selector 3、/app/build.gradle: 增加&lt;...
React Native 错误:EMFILE:打开的文件太多,请注意 React Native Error: EMFILE: too many open files, watch Error: EMFILE: too many open files, watch at FSEvent.FSWatcher._handle.onchange (internal/fs/watchers.js:127:28) Emitted 'error' event on NodeWatcher instance at:
很多使用RN里需要使用mobx来做全局状态管理,一般变量的更新比较好做,比如string,number类型,但是涉及到list,有时候更新后其他observer并不会实时刷新。 究其原因,可以参考这个https://github.com/mobxjs/mobx/issues/476#issuecomment-250969418 代码如下: 注意dataSource的部分,list使用的部分也要用dataSource,不要直接用privateList。 import { observable, co