開發 React Native APP —— 從改造官方Demo開始(1)

elainema發表於2018-12-04
   本文步驟參考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上。

    學習沒有捷徑,個人經驗就是,看、敲~  多看API,別人的demo,乍一看,很懵逼,再看似懂非懂,再看好像能明白了,慢慢就自己可以寫,融會貫通~~敲,一定要自己敲程式碼~

    鑑於以上原因,所以決定寫篇文章記錄下學習過程,再次感謝L小庸的文章和demo, 內容較多,這部分主要內容為:

  • react navigation 作為路由(導航)元件的初步使用 
  • 自定義元件 
  • 通過 fetch API 傳送網路請求 
  • 整合 redux,並實現 redux 狀態的持久化儲存
新手一個,如果有什麼寫的不對的地方,歡迎指正,我回及時更新,免得誤導別的新手~

github完整程式碼地址:https://github.com/elainema/ELAINE/tree/master/RN/AwesomeProject

一 準備工作

    使用自己喜歡的編輯器,安裝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開始(1)

上面的目錄結構說明如下,重要的有:

  • 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.json 
  • package.json 專案依賴包配置檔案 
  • node_modules 依賴包安裝目錄 
  • yarn.lock yarn 包管理檔案 
  • 其他配置檔案暫時無需改動,在此不做說明

三 配置路由

   這裡使用 react navigation 管理路由,大而全的介紹或者原理說明不是這部分的重點,這裡主要講怎麼用。 

   react navigation 常用 API 有三個:  

  • createStackNavigator:頁面間跳轉(每次跳轉後都會將前一個頁面推入返回棧,需要返回上個頁面特別好用)
  •  createTabNavigator:頂部或底部 tab 跳轉,一般在底部使用 
  • DrawerNavigator:側滑導航 
     最為常用的是前兩個,demo中也只用到了前兩個。 
 需要注意的,react navigation不同版本 的方法名可能不同,本人在敲L小庸的程式碼時,安裝了依賴後各種跑不起來,如下的圖困擾了很久,由於是新手,完全不知道錯在哪裡,仔細檢視api,嘗試使用createStackNavigator後終於執行成功,有點坑~官方好像也沒有地方說明版本升級的變化~~還是要仔細看api文件~~不過這火爆的生態圈,版本升級,連方法名都換了,如果用於生產環境,個人感覺坑很大…………

開發 React Native APP —— 從改造官方Demo開始(1)

3.1 createStackNavigator實現頁面間跳轉

首先我們要調整下目錄結構,調整後的結構如下:

開發 React Native APP —— 從改造官方Demo開始(1)

  • 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複製程式碼

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>
);
複製程式碼

經過上述配置,效果如下:

開發 React Native APP —— 從改造官方Demo開始(1)

3.2 createTabNavigator實現頁面底部 tab 切換

首先在 screens 目錄下新建 ScreenBottomTab 頁面,用於配置 TabNavigator。每個 tab 對應一個頁面,按需新建頁面,並且新建的頁面需要在 route.js 中進行配置,更新後的目錄結構如下:

開發 React Native APP —— 從改造官方Demo開始(1)

  • 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 相同。 

 效果如下:

開發 React Native APP —— 從改造官方Demo開始(1)

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開始(1)

四 自定義元件

     react native 已經封裝了很多常用元件,但有時我們仍然需要在次基礎上進行封裝,比如某些元件需要大量複用而原生元件樣式或者互動邏輯不符合需求。 

    這裡只介紹目錄結構的調整,具體程式碼可參考 Github 上專案程式碼,因為自定義元件的需求千差萬別,具體編寫過程也有很多教程,這裡不再具體介紹,只新增了自定義 Toast 元件。目錄結構調整如下:

開發 React Native APP —— 從改造官方Demo開始(1)

  • components/ 自定義元件都放這裡
  •  XgToast.js 自定義元件具體程式碼 
檔案 config/pxToDp.js 用於尺寸自適應,在 XgToast.js 中有使用,這段元件從L小庸的程式碼中拷貝而來,具體功能可自行檢視

五 網路請求

react native 使用上有個最大的好處是可以不用考慮新語法相容性的問題,既然如此,自然使用設計更加優良的 API,在網路請求方面,本專案使用fetch API。  

新增網路請求後目錄結構調整如下:

開發 React Native APP —— 從改造官方Demo開始(1)

  • 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 列表檔案
    具體 api 請求程式碼我放在了 xgRequest.js 檔案中,以 get 請求為例,xgRequest.js 程式碼如下:

/**
 * 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),
  },
}); 複製程式碼

效果如下:

開發 React Native APP —— 從改造官方Demo開始(1)

六 整合 redux

    在 App 中有一些全域性狀態是所有頁面共享的,比如登入狀態,或者賬戶餘額(購買商品後所有展示餘額的頁面都要跟著更新)。在本專案中,使用 Redux 進行狀態管理。 

   引入 redux 後後目錄結構調整如下:

開發 React Native APP —— 從改造官方Demo開始(1)

  • 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 的整合。

通過這 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開始(1)

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開始(1)

七 小結

經過這部分介紹,App 框架基本構建完成,




相關文章