开发 React Native APP —— 从改造官方Demo开始

栏目: 服务器 · 发布时间: 5年前

本文步骤参考L小庸的文章,https://juejin.im/post/5a9602745188257a5c609b2f, 感谢L小庸

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 目录介绍

开发 React Native APP —— 从改造官方Demo开始

上面的目录结构说明如下,重要的有:

android/
ios/
index.js
 App.js

上面是最重要的四个目录/文件,其他说明如下:  

app.json
package.json
node_modules
yarn.lock

3 配置路由

这里使用 react navigation 管理路由,大而全的介绍或者原理说明不是这部分的重点,这里主要讲怎么用。

react navigation 常用 API 有三个:

createStackNavigator
createTabNavigator
DrawerNavigator

最为常用的是前两个,demo中也只用到了前两个。

PS:需要注意的, react navigation 不同版本 的方法名可能不同,本人在敲L小庸的代码时,安装了依赖后各种跑不起来,如下的图困扰了很久,由于是新手,完全不知道错在哪里,仔细查看api,尝试使用createStackNavigator后终于运行成功,有点坑~官方好像也没有地方说明版本升级的变化~~还是要仔细看api文档~~不过这火爆的生态圈,版本升级,连方法名都换了,如果用于生产环境,个人感觉坑很大…………

开发 React Native APP —— 从改造官方Demo开始

3.1 createStackNavigator实现页面间跳转

首先我们要调整下目录结构 ,调整后的结构如下:

开发 React Native APP —— 从改造官方Demo开始

  • src/    放置所有原始的 react native 代码
  • config/    配置文件,比如路由配置
  • route.js   路由配置文件
  • screens/   所有页面文件
  • ScreenHome/ 这个目录是放具体页面文件的,为了进一步进行代码分离,里面又分为三个文件: index.js 中包含逻辑部分, style.js 中包含样式部分; view.js 中包含视图或者说页面元素部分。其他页面文案结构与此相同。

注意页面文件的命名方式:大驼峰命名法,react native 推荐组件命名用大驼峰命名法,每个页面相当于一个组件。

1)首先配置路由:路由文件 route.js 此时内容如下,这也是 createStackNavigator 最简单的使用方式,我的demo里使用的是简写的方式配置的路由,关于是否可以使用简写以及区别,还没有看的特别明白(https://reactnavigation.org/docs/en/hello-react-navigation.html),后面看明白了再补上

/**
 * 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复制代码

// 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 />;
  }
}
复制代码
/**
 * 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);
  }
}

复制代码
// 引入依赖
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>
);
复制代码

开发 React Native APP —— 从改造官方Demo开始

3.2 createTabNavigator实现页面底部 tab 切换

首先在 screens 目录下新建 ScreenBottomTab 页面,用于配置 TabNavigator 。每个 tab 对应一个页面,按需新建页面,并且新建的页面需要在 route.js 中进行配置,更新后的目录结构如下:

开发 React Native APP —— 从改造官方Demo开始

/**
 * 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复制代码

效果如下:

开发 React Native APP —— 从改造官方Demo开始

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 APP —— 从改造官方Demo开始

四 自定义组件

五 网络请求

react native 使用上有个最大的好处是可以不用考虑新语法兼容性的问题,既然如此,自然使用设计更加优良的 API,在网络请求方面,本项目使用fetch API。

添加网络请求后目录结构调整如下:

开发 React Native APP —— 从改造官方Demo开始

5.1 配置 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 文件中,以 get 请求为例, xgRequest.js 代码如下:

/**
 * xgRequest.js
 */

import XgHttp from "./xgHttp";

export default {
  todayOnHistory: urlPar => XgHttp("GET", "/japi/toh", { urlParams: urlPar })
};
复制代码

再调用聚合数据历史上的今天 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);
  }
}
复制代码
/**
 * 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),
  },
}); 复制代码

效果如下:

开发 React Native APP —— 从改造官方Demo开始

六 集成 redux

在 App 中有一些全局状态是所有页面共享的,比如登录状态,或者账户余额(购买商品后所有展示余额的页面都要跟着更新)。在本项目中,使用 Redux 进行状态管理。

引入 redux 后后目录结构调整如下:

开发 React Native APP —— 从改造官方Demo开始

如果对 redux 毫无概念,可以看下这篇文章 Redux 入门教程

按照小庸的demo敲了之后,发现Redux 实际上是非常难用的,,,如果之前使用过vuex的话,在使用 Redux 的过程中,会发现需要自己配置的东西太多(不喜勿喷,只是表达个人想使用感受而已),为了简化 Redux 的操作, Redux 作者开发了 react-redux ,虽然使用的便捷性上还没法和 vuex 比,但总算是比直接使用 Redux 好用很多。

在集成 Redux 进行状态管理之前我们先思考一个问题:集成过程中难点在哪?

因为在一个 App 中 Redux 只有一个 Store,这个 Store 应该为所有(页面)组件共享,所以,集成的难点就是 如何使所有(页面)组件可以访问到这个唯一的 store,并且可以触发 action 。为此,redux-react 引入了 connect 函数和 Provide 组件,他们必须配合使用才能实现 redux 的集成。

通过这 connectProvide 实现 store 在组件间共享的思想是:

  1. Redux store 可以(注意是“可以”,并不是“一定”,需要配置,见第 2 条)对 connect 方法可见,所以在组件中可以通过调用 connect 方法实现对 store 数据的访问;
  2. 实现 Redux store 对 connect 的可见的前提条件是, 需要保证这个组件为 Provide 组件的子组件 ,这样通过将 store 作为 Provide 组件的 props,就可以层层往下传递给所有子组件;
  3. 但子组件必须通过 connect 方法实现对 store 的访问,而无法直接访问。

6.1 引入依赖

首先是安装依赖 redux,react-redux:

yarn add redux react-redux复制代码

6.2 配置 redux

这里指的是配置 actions , reducersstore

据说应用大了,最好将 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
  },
});

复制代码

最终效果图如下:

开发 React Native APP —— 从改造官方Demo开始

6.4 持久化存储

手机 App 一般都有这样的需求: 除非用户主动退出,不然即便 App 进程被杀死,App 重新打开后登录信息依旧会保存

在本项目中,为了便于各组件共享登录状态,我把登录状态写在了 redux store 中,但原生 redux 有个特性:页面刷新后 redux store 会回恢复初始状态。为了达到上述需求,就需要考虑 redux store 持久化存储方案。本项目中使用了 redux-persist ,下面介绍如何配置:

  • 引入依赖
    yarn add redux-persist复制代码
  • 修改 redux 配置
1)修改 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 移入。至于为什么要将初始化 statestore.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 的文件
1)修改根目录下的 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 (而非初始状态 小光 )。效果图如下:

开发 React Native APP —— 从改造官方Demo开始

七 小结

经过这部分介绍,App 框架基本构建完成,


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Looking For a Challenge

Looking For a Challenge

the University of Warsaw / the University of Warsaw / 2012-11 / 0

LOOKING FOR PROGRAMMING CHALLENGES? Then this book is for you! Whether it's the feeling of great satisfaction from solving a complex problem set, or the frustration of being unable to solve a task,......一起来看看 《Looking For a Challenge》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

在线进制转换器
在线进制转换器

各进制数互转换器

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器