React Native 学习之AsyncStorage

在APP开发中,不管是Android还是IOS,都会有一些配置信息需要保存到设备中。保存数据大部分都是用数据库,但是若数据量不是很多的话,去用数据来保存的话就有点杀鸡焉用牛刀了。对于用户轻量级的数据持久化,在Android平台上是用的SharedPreference,而在IOS平台上用的是NSUserDefaults,它们都是以 "key-value(键值对)"形式保存数据的。

对于RN来说,它也提供了一个存储轻量级数据的结构,也就是我们今天要学习的AsyncStorage。它是一个简单的、具有异步特性的的键值对的存储系统。通过一个简单的购物车Demo来学习AsyncStorage的用法,效果如下:

AsyncStorage_IOS.gif

API学习

对于数据的存储,一般会涉及到四个方面,增删改查。那我们就来看看RN给我们提供哪些API来进行这四个操作了。

// 根据键来获取值,获取到的结果会在回调函数中
getItem(key : string, callback:(error,result)) 

//设置键值对
setItem(key : string, value : string, callback:(error))

//根据键移除一项
removeItem(key : string, callback:(error))

//合并现有值和输入值(其实就是更新某个key对应的旧value值)
mergeItem(key : string, value : string, callback:(error))

//清除所有项目
clear(callback:(error))

//获取所有的键
getAllKeys(callback:(error))

//获取多项,其中keys是字符串数组
multiGet(keys, callback:(error,result)) 

//设置多项,其中keyValuePairs是字符串的二维数组
multiSet(keyValuePairs, callback:(errors))

//删除多项,其中keys是字符串数组
multiRemove(keys, callback:(error)) 

//多个键值对合并,其中keyValuePairs是字符串的二维数组
multiMerge(keyValuePairs, callback:(errors))

我们可以看到,每个方法都有一个回调方法,而回调方法的第一个参数都是错误对象。如果发生错误,该对象就会展示错误信息,否则为null。所有的方法执行后,都会返回一个Promise对象。了解更多Promise信息 所以我们在使用AsyncStorage时,自己可以做一层封装,通过返回Promise对象来进行其他的一些异步操作等

Demo主要实现

  • 界面UI渲染

    水果列表界面肯定有很多数据,所以我们这里肯定是用ListView来显示

      render() {
          let count = this.state.count;
          let str = '';
          if (count) {
              str = ', 共' + count + '件商品';
          }
          return (
              <View style={{backgroundColor: 'white'}}>
                  <View style={styles.headViewContainer}>
                      <Text style={styles.headTextStyle}>水果列表</Text>
                  </View>
                  <ListView
                      dataSource={this.state.dataSource}
                      renderRow={this._renderRow.bind(this)}
                      contentContainerStyle={styles.listViewContentStyle}
                  />
    
                  <TouchableOpacity style={styles.btnStyle}
                                    activeOpacity={0.5}
                                    onPress={()=>this._onPress()}
                  >
                      <Text style={styles.btnTextStyle}>去结算{str}</Text>
                  </TouchableOpacity>
              </View>
          );
      }
    
  • 数据查询

    用户每次打开APP时,我们都需要给用户显示购物车是否有商品,所以要去AsyncStorage中查询商品数量。而这一步是属于耗时操作,所有我们将它放在componentDidMount()方法里面。在所有的生命周期方法,它一般用来处理一些复杂的逻辑以及耗时任务。

      _getAsyncStorageStatus() {
          AsyncStorage.getAllKeys((err, keys)=> {
    
              if (err) {
                  //TODO 存储数据出错,给用户提示错误信息
              }
    
              this.setState({
                  count: keys.length
              });
          });
      }
    
  • 添加商品到购物车

    在本Demo中,我们在点击商品时就会把它添加到购物车中,也就是用AsyncStorage将数据保存起来

      _addGoodsToShoppingCar(rowData) {
    
          console.log(rowData);
          let count = this.state.count;
          count++;
          this.setState({
              count: count
          });
    
          //AsyncStorage存储
          AsyncStorage.setItem('SP-' + this._getId() + '-SP', JSON.stringify(rowData), (err)=> {
    
              if (err) {
                  //TODO 存储出错
              }
          });
      }
    
      /**
       * 生成随机ID:GUID
       * GUID生成的代码来源于Stoyan Stefanov
       * @private
       */
      _getId() {
          return 'xxxxxxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c)=> {
              let r = Math.random() * 16 | 0;
              let v = c == 'x' ? r : (r & 0x3 | 0x8);
              return v.toString(16);
          }).toUpperCase();
      }
    

    这里之所以使用SP-为前缀、-SP为后缀,采用GUID为存储的键名的一部分,是为了区分其他数据,并且还有两个好处:

    • 可以区分用户数据,例如userName等信息;
    • 可以防止key值重复,保证同名商品都能被添加进购特车
  • 清除购物车

    在购物车界面,我们有一个button是用来清除购物车的,也就是AsyncStorage里面的数据

      _clearStorage() {
          AsyncStorage.clear((err)=> {
    
              //TODO err处理
    
              this.setState({
                  data: [],
                  price: 0
              }, ()=> {
                  //发送消息
                  DeviceEventEmitter.emit('clearStorage', {isClearSuccess: true});
              });
    
              alert('购物车已经清空');
    
          });
      }
    

Demo遇到的问题

好了,购物车Demo基本上就完了,但是在运行时发现一个小bug,如下图:

bug.gif

我们在购物车界面清除掉了购物车后,再回到水果列表界面,但是我们的 "去结算"button仍然显示还有4件商品,很显然是我们的AsyncStorage没有更新,怎样去更新数据是非常简单的,但问题是在哪里去更新?

  • 方式一:

    作为有经验的开发人员,我们马上会想到RN的生命周期。是的,不错,确实是这样。当我们启动APP时,会执行的生命周期有:constructor,componentWillMount,render,componentDidMount,componentDidUpdate,除了componentDidUpdate会多次执行外,在一个route未卸载时,其它方法都只会执行一次,所以,从水果列表界面跳转到购物车界面,再按返回键回到水果列表界面时,除了componentDidUpdate外,其它不会执行。因为我们跳转是用push,从水果列表界面push到购物车界面时,push方法并不会把水果列表界面卸载掉,所以当pop掉购物车界面时,水果列表界面的生命周期不会执行。

    那难道就没有其他方法了?当然不。既然push不能把一个route从routeStack里卸载掉的会,我们可以找一个可以卸载掉的方法嘛,那就是replace,这样的话,回到水果列表界面时就可以重新走生命周期了,也就是可以重新获取AsyncStorage里面的信息了。那按返回键时就不能直接pop掉了,可以用push或者replace方法。但这种做法有一点不好的是界面需要重新渲染,个人认为体验效果不是很好。

  • 方式二

    我想到的第二种方式是监听购物车的清空。在水果列表界面注册一个监听器:

      componentWillMount() {
          
          DeviceEventEmitter.addListener('clearStorage', (result)=> {
              if (result.isClearSuccess) {
                  this._getAsyncStorageStatus();
              }
          });
      }
    

    第一个参数是接收事件名,第二个参数是接收事件结果的回调。listner既然注册好了,那就在点击清空购物车button时发送一个息:

      DeviceEventEmitter.emit('clearStorage', {isClearSuccess: true});
    

我们来看看效果:

bug_resolve.gif

从效果图来看,确实可以实现,但是在Android平台上会出现一个warning,IOS没有。如下图:

warning_android.png

警告说setState只能在一个route被mounting或者mounted时才能被调用。很显然,我们在点击清空按钮时,水果列表界面压根就没有被mounting或者mounted,但我们却setState了。

这种报警告的问题,忽略它的话也没有什么问题,但我们公司在Code Review时是不被允许的,虽然看起来没有问题,但仍然存在一些潜在的风险。

好了,AsyncStorage的学习就到这里了。若各位对上面提的那个小bug有完美的解决方案的话,烦请告诉我啊。

完整代码下载

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,186评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,858评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,620评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,888评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,009评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,149评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,204评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,956评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,385评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,698评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,863评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,544评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,185评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,899评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,141评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,684评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,750评论 2 351

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,900评论 25 707
  • 前言 本文有配套视频,可以酌情观看。 文中内容因各人理解不同,可能会有所偏差,欢迎朋友们联系我。 文中所有内容仅供...
    珍此良辰阅读 1,503评论 13 7
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,644评论 18 139
  • 1.忙碌的日常 本周开始,我连着参加了四天培训,搞到最后,身心俱疲;然后接连着上课,写报告,周末做了的被告居然被告...
    木ding西阅读 284评论 0 0
  • 家人是世界上最为亲近的人,而恰恰是我们最容易忽略的人。因为我们有足够的安全感,知道无论怎样,他们不会抛弃我们。其实...
    idoixiu阅读 332评论 4 5