在APP开发中,不管是Android还是IOS,都会有一些配置信息需要保存到设备中。保存数据大部分都是用数据库,但是若数据量不是很多的话,去用数据来保存的话就有点杀鸡焉用牛刀了。对于用户轻量级的数据持久化,在Android平台上是用的SharedPreference,而在IOS平台上用的是NSUserDefaults,它们都是以 "key-value(键值对)"形式保存数据的。
对于RN来说,它也提供了一个存储轻量级数据的结构,也就是我们今天要学习的AsyncStorage。它是一个简单的、具有异步特性的的键值对的存储系统。通过一个简单的购物车Demo来学习AsyncStorage的用法,效果如下:
AsyncStorage_IOS.gifAPI学习
对于数据的存储,一般会涉及到四个方面,增删改查。那我们就来看看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有完美的解决方案的话,烦请告诉我啊。
网友评论