開發 React Native APP —— 從改造官方Demo開始
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/">Android Studio,iOS 使用 Xcode。它們如何配合 React Native 使用在 官方文件有說明,遇到問題自行谷歌一般都有解決方案。
需要說明的是 Android Studio 很多依賴更新需要訪問谷歌服務,所以請自備梯子。
這段完全copy自L小庸的文章,個人沒有mac,所以很多細節並不瞭解,也先記錄著,方便後續採坑參考。
2.2 官方 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文件~~不過這火爆的生態圈,版本升級,連方法名都換了,如果用於生產環境,個人感覺坑很大…………
3.1 createStackNavigator實現頁面間跳轉
首先我們要調整下目錄結構 ,調整後的結構如下:
-
src/
放置所有原始的 react native 程式碼 -
config/
配置檔案,比如路由配置 -
route.js
路由配置檔案 -
screens/
所有頁面檔案 -
ScreenHome/
這個目錄是放具體頁面檔案的,為了進一步進行程式碼分離,裡面又分為三個檔案:index.js
中包含邏輯部分,style.js
中包含樣式部分;view.js
中包含檢視或者說頁面元素部分。其他頁面文案結構與此相同。
注意頁面檔案的命名方式:大駝峰命名法,react native 推薦元件命名用大駝峰命名法,每個頁面相當於一個元件。
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> ); 複製程式碼
3.2 createTabNavigator實現頁面底部 tab 切換
/** * 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> ); } } 複製程式碼
此時只需要配置 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;複製程式碼
// 引入依賴 import { createStackNavigator, createAppContainer } from 'react-navigation' // 引入頁面元件 import ScreenBottomTab from '../screens/ScreenBottomTab'; // 配置路由 const navigator = createStackNavigator({ ScreenBottomTab: ScreenBottomTab, }) const App = createAppContainer(navigator) export default App複製程式碼
/** * 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 元件。目錄結構調整如下:
檔案 config/pxToDp.js
用於尺寸自適應,在 XgToast.js
中有使用,這段元件從L小庸的程式碼中拷貝而來,具體功能可自行檢視
五 網路請求
react native 使用上有個最大的好處是可以不用考慮新語法相容性的問題,既然如此,自然使用設計更加優良的 API,在網路請求方面,本專案使用fetch API。
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(); } }複製程式碼
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 }) }; 複製程式碼
介面呼叫是在頁面檔案的 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);複製程式碼
首先是在 index.js
中把需要動態展示的資料先寫入 state
:
/** * ScreenTab1/index.js */ // 將需要動態更新的資料放入 state this.state = { todayOnHistoryInfo: {} };複製程式碼
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> );複製程式碼
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 進行狀態管理。
按照小庸的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 框架基本構建完成,