RN的生态圈很火爆,但是很难找到一个开箱即用的 React Native APP Demo。目前存在的 Demo 要么过于简单,比如 React Native 官网提供的 Demo AwesomeProject ,这个 Demo 只提供了最简功能,对于路由(导航组件)、状态管理等并没有涉及。虽然 React Native 教程中对于复杂应用应如何选择组件及第三方库都有提及,但并没有给出完整示例。还有一些demo可能版本比较旧,对于新手来说,语法和代码组织方式都有变化,结合官方api看的话,会比较懵逼,哪哪对不上的赶脚。而另一方面,又有很多 React Native APP 虽已开源,但都是用于特定场合的完整 APP,有些 APP 的目录结构本身就不友好,并且也没有完整的说明文档。 其次,相对于vue,React 本身的学习曲线就相对陡峭,尤其涉及状态管理部分,很难找到可以直接 copy-paste 的代码,除此之外原生 App 本身还有很多区别于 web 的需求。
找了很多demo,L小庸的demo真的很棒,个人没有直接download小庸的github代码,基本上市按照步骤自己敲或者copy一部分代码,先让整个demo可以跑起来,再慢慢研究相关的功能和语法,API等,虽然敲的过程也遇到了很多麻烦,运行不起来等问题,但全程撸完一遍代码,有一个比较完整的demo实现,也算有一点点成就感。
个人的代码还未更新到github上,按照本文的步骤,step by step就运行起来。后续会掺杂一些对某部分内容的额外的理解或更多使用场景的demo,在代码里会写比较详细的注释,后续都更新到github上。
鉴于以上原因,所以决定写篇文章记录下学习过程,再次感谢L小庸的文章和demo, 内容较多,这部分主要内容为:
- react navigation 作为路由(导航)组件的初步使用
- 自定义组件
- 通过 fetch API 发送网络请求
- 集成 redux,并实现 redux 状态的持久化存储
一 准备工作
使用自己喜欢的编辑器,安装RN相关插件,个人使用的sublime text3,配置了插件后,使用起来也还是不比较顺手的。
二 官方 Demo 下载及介绍
官方 demo 虽然不完整,但却是一个很好的开始。介绍完官方 Demo(包括环境配置),后文会一步步介绍如何从这个不完整的官方 Demo 改造成可用于生产的 APP。
2.1 环境配置
下载官方 Demo:AwesomeProject,然后运行。
所需的环境配置官方文档讲的很清楚,这里不在赘述。需要指出的是 React Native 对于运行 Demo 提供了两种方法:一种是在 Expo 客户端中运行,另一种是编译成原生代码(安卓编译成 Java,iOS 编译成 objective-C)后在模拟器或者在真机上运行。推荐直接使用第二种,如果想发布 APP 这也是绕不过去的。
如果之前没有开发过原生 APP,还需要熟悉下原生 APP 的开发工具:安卓使用 Android Studio,iOS 使用 Xcode。它们如何配合 React Native 使用在 官方文档有说明,遇到问题自行谷歌一般都有解决方案。
需要说明的是 Android Studio 很多依赖更新需要访问谷歌服务,所以请自备梯子。
这段完全copy自L小庸的文章,个人没有mac,所以很多细节并不了解,也先记录着,方便后续采坑参考。
2.2 官方 Demo 目录介绍
上面的目录结构说明如下,重要的有:
android/
android 原生代码ios/
ios 原生代码-
index.js
打包 app 时进入 react native(js 部分) 的入口文件(0.49 以后安卓、ios 共用一个入口文件),旧版本应该是ios.index.js和android.index.js两个入口 App.js
可以理解为 react native(js 部分) 代码部分的入口文件,比如整个项目的路由在这里导入
app.json
项目说明,主要给原生 app 打包用,包括项目名称和手机桌面展示名称 React Native : 0.41 app.jsonpackage.json
项目依赖包配置文件node_modules
依赖包安装目录yarn.lock
yarn 包管理文件- 其他配置文件暂时无需改动,在此不做说明
三 配置路由
这里使用 react navigation 管理路由,大而全的介绍或者原理说明不是这部分的重点,这里主要讲怎么用。
react navigation 常用 API 有三个:
- createStackNavigator:页面间跳转(每次跳转后都会将前一个页面推入返回栈,需要返回上个页面特别好用)
- createTabNavigator:顶部或底部 tab 跳转,一般在底部使用
- DrawerNavigator:侧滑导航
需要注意的,react navigation不同版本 的方法名可能不同,本人在敲L小庸的代码时,安装了依赖后各种跑不起来,如下的图困扰了很久,由于是新手,完全不知道错在哪里,仔细查看api,尝试使用createStackNavigator后终于运行成功,有点坑~官方好像也没有地方说明版本升级的变化~~还是要仔细看api文档~~不过这火爆的生态圈,版本升级,连方法名都换了,如果用于生产环境,个人感觉坑很大…………
3.1 createStackNavigator实现页面间跳转
首先我们要调整下目录结构,调整后的结构如下:
- src/ 放置所有原始的 react native 代码
- config/ 配置文件,比如路由配置 route.js 路由配置文件
- screens/ 所有页面文件 ScreenHome/ 这个目录是放具体页面文件的,为了进一步进行代码分离,里面又分为三个文件:index.js 中包含逻辑部分,style.js 中包含样式部分;view.js 中包含视图或者说页面元素部分。其他页面文案结构与此相同。
注意页面文件的命名方式:大驼峰命名法,react native 推荐组件命名用大驼峰命名法,每个页面相当于一个组件。
/**
* route.js
*/
// 引入依赖
import React, { Component } from 'react'
import { createStackNavigator, createAppContainer } from 'react-navigation'
//引入页面组件
import ScreenHome from "../screens/ScreenHome";
import ScreenSome1 from '../screens/ScreenSome1'
// 配置路由
const navigator = createStackNavigator({
ScreenHome: { screen: ScreenHome },
/*或者
Home: {
screen: HomeScreen
}
*/
ScreenSome1: { screen: ScreenSome1 }
})
const App = createAppContainer(navigator)
export default App复制代码
2)更新 App.js,对接路由文件:
// App.js
import React, { Component } from 'react';
import App from './src/config/route'
export default class RootApp extends Component {
constructor(props) {
super(props);
}
render() {
// 渲染页面
return <App />;
}
}
复制代码
3)具体页面设置,以 ScreenHome 为例
在 index.js 中自定义当前页面路由逻辑和样式,比如 title 及其样式、在导航栏自定义按钮等,到目前为止,我们只需要简单设置 title 就好,先让代码可以跑起来:
/**
* ScreenHome/index.js
*/
import React, {Component} from 'react';
import view from './view'
export default class ScreenHome extends Component {
// 自定义当前页面路由配置,后面用到的createBottomTabNavigator也使用这个对象中的属性 static navigationOptions = {
title: '首页',
};
constructor(props) {
super(props);
this.navigation = props.navigation;
}
render() {
return view(this);
}
}
复制代码
在 view.js 中在具体元素上定义具体跳转页面
// 引入依赖
import React, {Component} from 'react';
import {Text, View, Button} from 'react-native'
export default self => (
<View>
<Text style={{ fontSize: 36 }}>home</Text>
<Button
title="ScreenSome1"
// 路由跳转
onPress={() => self.navigation.navigate("ScreenSome1")}
/>
</View>
);
复制代码
经过上述配置,效果如下:
3.2 createTabNavigator实现页面底部 tab 切换
首先在 screens 目录下新建 ScreenBottomTab 页面,用于配置 TabNavigator。每个 tab 对应一个页面,按需新建页面,并且新建的页面需要在 route.js 中进行配置,更新后的目录结构如下:
- ScreenBottomTab 配置底部 tab 导航
- ScreenTab1/2/3 新建页面,配合底部 tab 导航
- 三个tab页可简单的参考tab1写即可
/**
* ScreenTab1/index.js
*/
import React, {Component} from 'react';
import {Text, View, Button} from 'react-native'
export default class ScreenSome1 extends Component {
// 自定义当前页面路由配置,后面用到的createBottomTabNavigator也使用这个对象中的属性 static navigationOptions = {
// 设置 title
title: "TAB1"
};
constructor(props) {
super(props);
this.navigation = props.navigation;
}
render() {
return(
<View>
<Text style={{ fontSize: 36 }}>TAB1</Text>
</View>
);
}
}
复制代码
1)没有 tab 图标的最简配置
此时只需要配置 ScreenBottomTab 里面的 index.js 文件就好,如下:
/**
* ScreenBottomTab/index.js
*/
import { createBottomTabNavigator } from 'react-navigation'
import ScreenHome from '../../screens/ScreenHome';
import ScreenTab1 from '../../screens/ScreenTab1';
import ScreenTab2 from '../../screens/ScreenTab2';
import ScreenTab3 from '../../screens/ScreenTab3';
const ScreenTab = createBottomTabNavigator(
// 配置 tab 路由
{
ScreenHome: ScreenHome,
ScreenTab1: ScreenTab1,
ScreenTab2: ScreenTab2,
ScreenTab3: ScreenTab3,
},
// 其他配置选项
{
tabBarPosition: "bottom"
}
);
export default ScreenTab;复制代码
route.js
// 引入依赖
import { createStackNavigator, createAppContainer } from 'react-navigation'
// 引入页面组件
import ScreenBottomTab from '../screens/ScreenBottomTab';
// 配置路由
const navigator = createStackNavigator({
ScreenBottomTab: ScreenBottomTab,
})
const App = createAppContainer(navigator)
export default App复制代码
页面文件现在无需配置,需要注意的是 tab 下面的文字默认和在 StackNavigator 中定义的头部导航 title 相同。
效果如下:
2)自定义 tab 图标
tab 图标除了自定义外,还需要根据是否选中显示不同颜色,这可以通过配置 createBottomTabNavigator的 tabBarIcon 实现,修改的具体文件是 tab 对应页面的 index.js 文件。demo里只是为了展示功能,icon使用的是一个。
/**
* ScreenHome/index.js
*/
import React, {Component} from 'react';
import { Image } from 'react-native'
import view from './view'
export default class ScreenHome extends Component {
static navigationOptions = {
title: '首页',
tabBarIcon: ({ focused }) => {
const icon = focused
? require('../../assets/images/tab_home_active.png')
: require('../../assets/images/tab_home.png');
return <Image source={icon} style={{ height: 22, width: 22 }} />;
},
};
constructor(props) {
super(props);
this.navigation = props.navigation;
}
render() {
return view(this);
}
}
复制代码
效果如下:
四 自定义组件
react native 已经封装了很多常用组件,但有时我们仍然需要在次基础上进行封装,比如某些组件需要大量复用而原生组件样式或者交互逻辑不符合需求。
这里只介绍目录结构的调整,具体代码可参考 Github 上项目代码,因为自定义组件的需求千差万别,具体编写过程也有很多教程,这里不再具体介绍,只添加了自定义 Toast 组件。目录结构调整如下:
- components/ 自定义组件都放这里
- XgToast.js 自定义组件具体代码
五 网络请求
react native 使用上有个最大的好处是可以不用考虑新语法兼容性的问题,既然如此,自然使用设计更加优良的 API,在网络请求方面,本项目使用fetch API。
添加网络请求后目录结构调整如下:
- xgHttp.js 配置 fetch api
- xgRequest.js api 请求列表
5.1 配置 fetch api
xgHttp.js全部代码如下,里面有简单注释,这里不再详解,fetch api 的使用可以参考 fetch API 简介
/**
* xgHttp.js
*/
// 请求服务器host
const host = "http://api.juheapi.com";
export default async function(
method,
url,
{ bodyParams = {}, urlParams = {} }
) {
const headers = new Headers();
headers.append("Content-Type", "application/json");
// 将url参数写入URL
let urlParStr = "";
const urlParArr = Object.keys(urlParams);
if (urlParArr.length) {
Object.keys(urlParams).forEach(element => {
urlParStr += `${element}=${urlParams[element]}&`;
});
urlParStr = `?${urlParStr}`.slice(0, -1);
}
const res = await fetch(
new Request(`${host}${url}${urlParStr}`, {
method,
headers,
// 如果是 get 或者 head 方法,不添加请求头部
body: method === ("GET" || "HEAD") ? null : JSON.stringify(bodyParams)
})
);
if (res.status < 200 || res.status > 299) {
console.log(`出错啦:${res.status}`);
} else {
return res.json();
}
}复制代码
上面的配置还不完善,比如,生产环境中很多接口都有验证功能,一般是 token + 用户 id,上面的配置并没有这个功能。但现在实现这个功能还会涉及到在哪存放 token,一展开又有很多内容,缺少验证功能暂时并不影响 APP 的完整度,所以这个坑后续填。
5.2 请求 api 编写及使用
- api 列表文件
/**
* xgRequest.js
*/
import XgHttp from "./xgHttp";
export default {
todayOnHistory: urlPar => XgHttp("GET", "/japi/toh", { urlParams: urlPar })
};
复制代码
其中 "/japi/toh" 为接口地址,这里使用了聚合数据历史上的今天 API。这里的key还用的是小庸的,后续改成自己申请的,或者从其他地方获取数据展示
再调用聚合数据历史上的今天 API 的时候使用了我自己的 APPKEY,每天免费调用 100 次,超出后回报错request exceeds the limit!,如果你想进行更多的测试,注册后替换成自己的 APPKEY 就可以。
- 使用
接口调用是在页面文件的 index.js 中进行的,以 ScreenTab1/index.js 为例:
/**
* ScreenTab1/index.js
*/
const urlPar = {
// 大佬们,这个是我申请的聚合数据应用的key,每天只有100免费请求次数
key: '7606e878163d494b376802115f30dd4e',
v: '1.0',
month: Number(this.state.inputMonthText),
day: Number(this.state.inputDayText),
};
// 拿到返回数据后就可以进一步操作了
const todayOnHistoryInfo = await XgRequest.todayOnHistory(urlPar);复制代码
然后,展示数据。
拿到数据以后就可以在做进一步操作了,一般就是在页面中展示了。react 是数据驱动的框架,对于动态变化的展示数据一般是放在 react native 的 state 对象中,state 一经改变,便会触发 render() 函数重新渲染 DOM 中变化了的那部分。
首先是在 index.js 中把需要动态展示的数据先写入 state:
/**
* ScreenTab1/index.js
*/
// 将需要动态更新的数据放入 state
this.state = {
todayOnHistoryInfo: {}
};复制代码
index.js完整代码
import React, { Component } from 'react';
import { Image,Alert } from 'react-native';
import view from './view';
import XgRequest from '../../config/xgRequest';
export default class ScreenTab1 extends Component {
static navigationOptions = {
title: '网络请求(TAB1)',
tabBarIcon: ({ focused }) => {
const icon = focused
? require('../../assets/images/tab_home_active.png')
: require('../../assets/images/tab_home.png');
return <Image source={icon} style={{ height: 22, width: 22 }} />;
},
};
constructor(props) {
super(props);
this.navigation = props.navigation;
// 将需要动态更新的数据放入 state
this.state = {
todayOnHistoryInfo: {},
inputMonthText: '',
inputDayText: '',
};
}
async getTodayOnHistoryInfo() {
if (!this.state.inputMonthText || !this.state.inputDayText) {
this.xgToast.show('请输入有效数据', 2000, 'error');
return;
}
try {
const urlPar = {
// 大佬们,这个是我申请的聚合数据应用的key,每天只有100免费请求次数
key: '7606e878163d494b376802115f30dd4e',
v: '1.0',
month: Number(this.state.inputMonthText),
day: Number(this.state.inputDayText),
};
const todayOnHistoryInfo = await XgRequest.todayOnHistory(urlPar);
// 捕获错误,具体捕获过程需与写api的同学商量确定
if (todayOnHistoryInfo.error_code) {
this.xgToast.show(todayOnHistoryInfo.reason, 2000, 'error');
} else {
// 更新state,render函数自动重新渲染DOM中变化了的那部分
this.setState({ todayOnHistoryInfo });
}
} catch (e) {
console.log(e);
}
}
render() {
return view(this);
}
}
复制代码
然后在 view.js 中读取 state 中的数据:
/**
* ScreenTab1/view.js
*/
{
/* 查询 */
}
<Button title="查询" onPress={() => self.getTodayOnHistoryInfo()} />;
{
/* 展示查询数据 */
}
<Text>
发生了啥事:{self.state.todayOnHistoryInfo.result
? self.state.todayOnHistoryInfo.result[0].des
: "暂无数据"}
</Text>;复制代码
view.js完整代码,其中style.js可直接copy先看效果
import React from 'react';
import { View, Button, Text, TextInput } from 'react-native';
import styles from './style';
// 引入 toast 组件
import XgToast from '../../components/XgToast';
export default self => (
<View style={{ alignItems: 'center' }}>
<Text style={{ fontSize: 24 }}>历史上的今天</Text>
<TextInput
style={[styles.input]}
placeholder="month"
onChangeText={text => self.setState({ inputMonthText: text })}
/>
<TextInput
style={[styles.input]}
placeholder="day"
onChangeText={text => self.setState({ inputDayText: text })}
/>
<Button title="查询" onPress={() => self.getTodayOnHistoryInfo()} />
<Text>
发生了啥事:{self.state.todayOnHistoryInfo.result
? self.state.todayOnHistoryInfo.result[0].des
: '暂无数据'}
</Text>
<XgToast
ref={(element) => {
self.xgToast = element;
}}
/>
</View>
);复制代码
style.js
import { StyleSheet } from 'react-native';
import pxToDp from '../../config/pxToDp';
export default StyleSheet.create({
inputContainer: {
height: pxToDp(100),
paddingTop: pxToDp(20),
borderBottomWidth: pxToDp(1),
borderBottomColor: '#ddd',
},
input: {
textAlign: 'center',
height: pxToDp(80),
width: pxToDp(600),
marginTop: pxToDp(30),
marginBottom: pxToDp(30),
color: '#000',
fontSize: pxToDp(30),
borderBottomColor: '#000',
borderBottomWidth: pxToDp(0.5),
},
}); 复制代码
效果如下:
六 集成 redux
在 App 中有一些全局状态是所有页面共享的,比如登录状态,或者账户余额(购买商品后所有展示余额的页面都要跟着更新)。在本项目中,使用 Redux 进行状态管理。
引入 redux 后后目录结构调整如下:
- redux 存放 redux 相关配置文件
- actions.js redux action
- reducers.js redux reducer
- store.js redux store
如果对 redux 毫无概念,可以看下这篇文章 Redux 入门教程
按照小庸的demo敲了之后,发现Redux 实际上是非常难用的,,,如果之前使用过 vuex的话,在使用 Redux 的过程中,会发现需要自己配置的东西太多(不喜勿喷,只是表达个人想使用感受而已),为了简化 Redux 的操作, Redux 作者开发了 react-redux,虽然使用的便捷性上还没法和 vuex 比,但总算是比直接使用 Redux 好用很多。
在集成 Redux 进行状态管理之前我们先思考一个问题:集成过程中难点在哪?
因为在一个 App 中 Redux 只有一个 Store,这个 Store 应该为所有(页面)组件共享,所以,集成的难点就是如何使所有(页面)组件可以访问到这个唯一的 store,并且可以触发 action。为此,redux-react 引入了 connect
函数和 Provide
组件,他们必须配合使用才能实现 redux 的集成。
通过这 connect
和 Provide
实现 store 在组件间共享的思想是:
- Redux store 可以(注意是“可以”,并不是“一定”,需要配置,见第 2 条)对
connect
方法可见,所以在组件中可以通过调用connect
方法实现对 store 数据的访问; - 实现 Redux store 对
connect
的可见的前提条件是,需要保证这个组件为Provide
组件的子组件,这样通过将 store 作为Provide
组件的 props,就可以层层往下传递给所有子组件; - 但子组件必须通过
connect
方法实现对 store 的访问,而无法直接访问。
6.1 引入依赖
首先是安装依赖 redux,react-redux:
yarn add redux react-redux复制代码
6.2 配置 redux
这里指的是配置 actions
, reducers
和 store
。
据说应用大了,最好将 redux 分拆,但现在项目还小,暂时没有做拆分。
- 配置
actions
/**
* actions.js
*/
export function setUserInfo(userInfo) {
return {
// action 类型
type: "SET_USER_INFO",
// userinfo 是传进来的参数
userInfo
};
}
export function clearReduxStore() {
return {
type: "CLEAR_REDUX_STORE"
};
}
复制代码
- 配置
reducers
/**
* reducers.js
*/
import { initialState } from "./store";
function reducer(state = initialState, action) {
switch (action.type) {
case "SET_USER_INFO":
// 合并 userInfo 对象
action.userInfo = Object.assign({}, state.userInfo, action.userInfo);
// 更新状态
return Object.assign({}, state, { userInfo: action.userInfo });
case "CLEAR_REDUX_STORE":
// 清空 store 中的 userInfo 信息
return { userInfo: {} };
default:
return state;
}
}
export default reducer;
复制代码
注意 SET_USER_INFO
这条路径下的代码,使用了 Object.assign()
。这是因为 reducer
函数每次都会返回全新的 state
对象,这意味着如果 state
对象含有多个属性而在 reducer
函数返回时没有合并之前的 state
,可能会导致 state
对象属性丢失。
这是一个很常见的错误,因为通常我们在触发 actions
时只需要传入更改的那部分 state
属性,而不是将整个 state
再传一遍。
redux 经典计数器教程在触发
state
变化时通常这样写return { defaultNum: state.defaultNum - 1 };
,因为计数器例子中只有一个属性,即defaultNum
,所以合并之前的state
就没有意义了,但生产环境中的应用state
对象中往往不止一个属性,此时上述的写法就会出错。
- 配置
store
/**
* store.js
*/
import { createStore } from "redux";
import reducers from "./reducers";
// 定义初始值
const initialState = {
userInfo: {
name: "小光",
gender: "男"
}
};
const store = createStore(reducers, initialState);
export default store;复制代码
6.3 组件中使用
配置完 redux,接下来就是使用了。
- 配置
index.js
在配置 index.js
中 主要是配置 Provide
作为根组件,并传入 store
作为其属性,为接下来组件使用 redux 创造条件。
/**
* index.js
*/
import React from "react";
import { AppRegistry } from "react-native";
import { Provider } from "react-redux";
import App from "./App";
import store from "./src/redux/store";
const ReduxApp = () => (
// 配置 Provider 为根组件,同时传入 store 作为其属性
<Provider store={store}>
<App />
</Provider>
);
AppRegistry.registerComponent("AwesomeProject", () => ReduxApp);
复制代码
- 配置组件
这里以 ScreenTab2
为例,注意,引入的style.js可直接copy使用
首先,在 index.js
中关联 redux
/**
* ScreenTab2/index.js
*/
// redux 依赖
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as actionCreators from '../../redux/actions';
import React, { Component } from 'react';
import { Image } from 'react-native';
import view from './view';
class ScreenTab2 extends Component {
static navigationOptions = {
title: 'Redux(TAB2)',
tabBarIcon: ({ focused }) => {
const icon = focused
? require('../../assets/images/tab_home_active.png')
: require('../../assets/images/tab_home.png');
return <Image source={icon} style={{ height: 22, width: 22 }} />;
},
};
constructor(props) {
super(props);
this.navigation = props.navigation;
}
changeReduxStore(userInfo) {
// 设置 redux store
this.props.setUserInfo(userInfo);
}
render() {
return view(this);
}
}
// 将 store 中的状态映射(map)到当前组件的 props 中
function mapStateToProps(state) {
return { userInfo: state.userInfo };
}
// 将 actions 中定义的方法映射到当前组件的 props 中
function mapDispatchToProps(dispatch) {
return bindActionCreators(actionCreators, dispatch);
}
// 将 store 和 当前组件连接(connect)起来
export default connect(mapStateToProps, mapDispatchToProps)(ScreenTab2);
复制代码
然后,就是在 view 中控制具体改变的数据
import React from 'react';
import { View, Text, Button } from 'react-native';
import pxToDp from '../../config/pxToDp';
import styles from './style';
export default self => (
<View>
<View>
<Text style={{ fontSize: pxToDp(36) }}>名字:{self.props.userInfo.name}</Text>
<Text style={{ fontSize: pxToDp(36) }}>性别:{self.props.userInfo.gender}</Text>
</View>
<View style={{ alignItems: 'center' }}>
<View style={styles.buttonContainer}>
<Button title="改变名字" onPress={() => self.changeReduxStore({ name: 'vince' })} />
</View>
<View style={styles.buttonContainer}>
<Button style={styles.buttonContainer} title="改变性别" onPress={() => self.changeReduxStore({ gender: '女' })} />
</View>
<View style={styles.buttonContainer}>
<Button style={styles.buttonContainer} title="还原" onPress={() => self.changeReduxStore({ name: '小光', gender: '男' })} />
</View>
</View>
</View>
);
复制代码
style.js
import { StyleSheet } from 'react-native';
export default StyleSheet.create({
buttonContainer: {
margin:20
},
});
复制代码
最终效果图如下:
6.4 持久化存储
手机 App 一般都有这样的需求:除非用户主动退出,不然即便 App 进程被杀死,App 重新打开后登录信息依旧会保存。
在本项目中,为了便于各组件共享登录状态,我把登录状态写在了 redux store 中,但原生 redux 有个特性:页面刷新后 redux store 会回恢复初始状态。为了达到上述需求,就需要考虑 redux store 持久化存储方案。本项目中使用了 redux-persist,下面介绍如何配置:
- 引入依赖
yarn add redux-persist复制代码
- 修改 redux 配置
store.js
。
除了引入 redux-persist
外,这里使用了 react native 提供的 AsyncStorage 作为持久化存储的容器。另外,初始化 state
移到了 reducers.js
中。
/**
* store.js
* 更改为持久化存储
*/
import { createStore } from "redux";
// 引入 AsyncStorage 作为存储容器
import { AsyncStorage } from "react-native";
// 引入 redux-persist
import { persistStore, persistCombineReducers } from "redux-persist";
import reducers from "./reducers";
// 持久化存储配置
const config = {
key: "root",
storage: AsyncStorage
};
const persistReducers = persistCombineReducers(config, {
reducers
});
const configureStore = () => {
const store = createStore(persistReducers);
const persistor = persistStore(store);
return { persistor, store };
};
export default configureStore;
复制代码
2)修改 reducers.js
只是将初始化 state
移入。至于为什么要将初始化 state
从 store.js
移入 reducers.js
实在是无奈之举:不然在 store.js
中创建 store
报错,后续再填坑,暂时先放在 reducers.js
中。
/**
* reducers.js
* 更改为持久化存储
*/
//import { initialState } from "./store";
// 初始化 state 放在这里
const initialState = {
userInfo: {
name: "小光",
gender: "男"
}
};
function reducer(state = initialState, action) {
switch (action.type) {
case "SET_USER_INFO":
// 合并 userInfo 对象
action.userInfo = Object.assign({}, state.userInfo, action.userInfo);
// 更新状态
return Object.assign({}, state, { userInfo: action.userInfo });
case "CLEAR_REDUX_STORE":
// 清空 store 中的 userInfo 信息
return { userInfo: {} };
default:
return state;
}
}
export default reducer;
复制代码
- 修改使用 redux 的文件
index.js
:
/**
* index.js
* 更改为持久化存储
*/
import React from "react";
import { PersistGate } from "redux-persist/es/integration/react";
import configureStore from "./src/redux/store";
import { AppRegistry } from "react-native";
import { Provider } from "react-redux";
import App from "./App";
const { persistor, store } = configureStore();
const ReduxApp = () => (
// 配置 Provider 为根组件,同时传入 store 作为其属性
<Provider store={store}>
<PersistGate persistor={persistor}>
<App />
</PersistGate>
</Provider>
);
AppRegistry.registerComponent("AwesomeProject", () => ReduxApp);复制代码
2)因为修改为持久化存储的过程过程中把初始化的 state
存在了 reducers.js
中,所以在页面组件映射 state
到当前页面时需要还需要修改对应属性的引入地址,依然以 ScreenTab2
为例:
//修改前
// 将 store 中的状态映射(map)到当前组件的 props 中
/*function mapStateToProps(state) {
return { userInfo: state.userInfo };
}*/
// 修改后
function mapStateToProps(state) {
// 引用 state.reducers.userInfo
return { userInfo: state.reducers.userInfo };
}复制代码
经过上述修改,便可以实现 redux 的持久化存储:初始化姓名是 小光
,更改为 vince
后重新加载页面,姓名还是 vince
(而非初始状态 小光
)。效果图如下:
七 小结
经过这部分介绍,App 框架基本构建完成,