一、須知
全文技術棧
- 核心庫:React-Native@0.54.0
- 路由導航:React-Native-Navigation
- 狀態管理:Redux、Redux-Thunk、Redux-Saga、Redux-persist
- 靜態測試:Flow
本文適合有對React家族有一定使用經驗,但對從零配置一個App不是很熟悉,又想要從零體驗一把搭建App的同學。
我自己就是這種情況,中途參與到專案中,一直沒有掌控全域性的感覺,所以這次趁著專案重構的機會,自己也跟著從零配置了一遍,並記錄了下來,希望能跟同學們一起學習,如果有說錯的地方,也希望大家指出來,或者有更好的改進方式,歡迎交流。
如果有時間的同學,跟著親手做一遍是最好的,對於如何搭建一個真實專案比較有幫助。
整個專案已經上傳到github,懶的動手的同學可以直接clone下來跟著看,歡迎一起完善,目前的初步想法是對一部分的同學有所幫助,後面有時間的話,可能會完善成一個比較健壯的RN基礎框架,可以直接clone就開發專案那種
這裡對每個庫或者內容只做配置和基礎用法介紹
物理環境:mac,xcode
window系統的同學也可以看,不過需要自己搞好模擬器開發環境
二、快速建立一個RN App
如果RN的基礎配置環境沒有配置好,請點選上方連結到官網進行配置
react-native init ReactNativeNavigationDemo
cd ReactNativeNavigationDemo
react-native run-ios
複製程式碼
因為一開始就計劃好了用React-Native-Navigation作為導航庫,所以名字起得長了點,大家起個自己喜歡的吧
成功後會看到這個介面
這時候可以看下目錄結構,RN自動整合了babel、git、flow的配置檔案,還是很方便的
三、路由導航:React-Native-Navigation
為什麼用React Native Navigation而不用React Navigation ?
它是目前唯一一款使用原生程式碼來實現navigator的外掛,使用後navigator的push/pop的動畫將脫離js執行緒而改由原生的UI執行緒處理, 切屏效果會和原生態一樣流暢, 再也不會出現由於js執行緒渲染導致的navigator切屏動畫的卡頓效果了, 並且該外掛還同時內建實現了原生態版本的tabbar
英文好的同學看著官方文件配就可以了,實在看不懂的可以對照著我下面的圖看。
iOS的需要用到xcode,沒做過的可能會覺得有點複雜,所以我跑了一遍流程並截圖出來了
至於android的配置,文件寫的很清晰,就不跑了。
1、安裝
yarn add react-native-navigation@latest複製程式碼
2、新增xcode工程檔案
圖中的路徑檔案是指./node_modules/react-native-navigation/ios/ReactNativeNavigation.xcodeproj
3、把上面新增的工程檔案新增到庫中
4、新增路徑
$(SRCROOT)/../node_modules/react-native-navigation/ios
記得圖中第5點設定為recursive
5、修改ios/[app name]/AppDelegate.m檔案
把整個檔案內容替換成下面程式碼
#import "AppDelegate.h"
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import "RCCManager.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSURL *jsCodeLocation;
#ifdef DEBUG
jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
#else
jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.window.backgroundColor = [UIColor whiteColor];
[[RCCManager sharedInstance] initBridgeWithBundleURL:jsCodeLocation launchOptions:launchOptions];
return YES;
}
@end
複製程式碼
6、基礎使用
1、先新建幾個頁面,結構如圖
cd src
mkdir home mine popularize
touch home/index.js mine/index.js popularize/index.js
複製程式碼
每個index.js
檔案裡面都是一樣的結構,非常簡單
import React, { Component } from `react`;
import { Text, View } from `react-native`;
type Props = {};
export default class MineHome extends Component<Props> {
render() {
return (
<View>
<Text>MineHome</Text>
</View>
);
}
}
複製程式碼
2、src/index.js
註冊所有的頁面,統一管理
import { Navigation } from `react-native-navigation`;
import Home from `./home/index`;
import PopularizeHome from `./popularize/index`;
import MineHome from `./mine/index`;
// 註冊所有的頁面
export function registerScreens() {
Navigation.registerComponent(`home`,() => home);
Navigation.registerComponent(`popularize`,() => popularize);
Navigation.registerComponent(`mine`,() => mine);
}
複製程式碼
在這裡先插一句,如果要引入Redux的話,就在這裡直接傳入store和Provider
export function registerScreens(store,Provider) {
Navigation.registerComponent(`home`,() => PageOne,store,Provider)
}
複製程式碼
3、App.js
檔案修改app的啟動方式,並稍微修改一下頁面樣式
import { Navigation } from `react-native-navigation`;
import { registerScreens } from `./src/screen/index`;
// 執行註冊頁面方法
registerScreens();
// 啟動app
Navigation.startTabBasedApp({
tabs: [
{
label: `home`,
screen: `home`,
title: `首頁`,
icon: require(`./src/assets/home.png`),
},
{
screen: `popularize`,
title: `推廣`,
icon: require(`./src/assets/add.png`),
iconInsets: {
top: 5,
left: 0,
bottom: -5,
right: 0
},
},
{
label: `mine`,
screen: `mine`,
title: `我`,
icon: require(`./src/assets/mine.png`),
}
],
appStyle: {
navBarBackgroundColor: `#263136`,//頂部導航欄背景顏色
navBarTextColor: `white`//頂部導航欄字型顏色
},
tabsStyle: {
tabBarButtonColor: `#ccc`,//底部按鈕顏色
tabBarSelectedButtonColor: `#08cb6a`,//底部按鈕選擇狀態顏色
tabBarBackgroundColor: `#E6E6E6`//頂部條背景顏色
}
});
複製程式碼
啟動App,目前模擬器能看到的介面
7、頁面跳轉和傳遞引數
在screen/home資料夾下面新建一個NextPage.js
檔案,記得到src/screen/index.js
裡面註冊該頁面
Navigation.registerComponent(`nextPage`, () => NextPage, store, Provider);複製程式碼
然後在src/screen/home/index.js
檔案裡面加一個跳轉按鈕,並傳遞一個props資料
四、狀態管理:Redux
1、初始化
1、安裝
yarn add redux react-redux複製程式碼
2、目錄構建
目前有以下兩種常見的目錄構建方式
一是把同一個頁面的action和reducer寫在同一個資料夾下面(可以稱之為元件化),如下
二是把所有的action放在一個資料夾,所有的reducer放在一個資料夾,統一管理
這兩種方式各有好壞,不在此探究,這裡我用第二種
一通操作猛如虎,先建立各種資料夾和檔案
cd src
mkdir action reducer store
touch action/index.js reducer/index.js store/index.js
touch action/home.js action/mine.js action/popularize.js
touch reducer/home.js reducer/mine.js reducer/popularize.js
複製程式碼
以上命令敲完後,目錄結構應該長下面這樣,每個頁面都分別擁有自己的action和reducer檔案,但都由index.js
檔案集中管理輸出
關於建立這三塊內容的先後順序,理論上來說,應該是先有store,然後有reducer,再到action
但寫的多了之後,就比較隨心了,那個順手就先寫哪個。
按照我自己的習慣,我喜歡從無寫到有,比如說
store裡面要引入合併後的reducer,那我就會先去把reducer給寫了
import combinedReducer from `../reducer`複製程式碼
但寫reducer之前,好像又需要先引入action,所以我由可能跑去先寫action
這裡不討論正確的書寫順序,我就暫且按照自己的習慣來寫吧
3、action
我喜歡集中管理的模式,所以所有的antion我都會集中起來 index.js
檔案作為總的輸出口
這裡定義了所有的action-type常量
// home頁面
export const HOME_ADD = `HOME_ADD`;
export const HOME_CUT = `HOME_CUT`;
// mine頁面
export const MINE_ADD = `MINE_ADD`;
export const MINE_CUT = `MINE_CUT`;
// popularize頁面
export const POPULARIZE_ADD = `POPULARIZE_ADD`;
export const POPULARIZE_CUT = `POPULARIZE_CUT`;
複製程式碼
然後去寫其他各自頁面的action.js檔案,這裡只以home頁面作為例子,其他頁面就不寫了,開啟action/home.js
檔案
import * as actionTypes from `./index`;
export function homeAdd(num) {
return {
type: actionTypes.HOME_ADD,
num
}
}
export function homeCut(num) {
return {
type: actionTypes.HOME_CUT,
num
}
}
複製程式碼
最是返回了一個最簡單的action物件
4、reducer
先寫一個home頁面的reducer,開啟reducer/home.js
檔案
其他頁面也同理
import * as actionTypes from `../action/index`;
// 初始state,我先隨手定義了幾個,後面可能會用到
const initState = {
initCount: 0,
name: ``,
age: ``,
job: ``
}
export default function count(state = initState, action) {
switch (action.type) {
case actionTypes.HOME_ADD:
return {
...state,
...action.initCount:
}
case actionTypes.HOME_CUT:
return {
...state,
...action.initCount
}
default:
return state;
}
}
複製程式碼
然後把所有子reducer頁面合併到reducer/index.js
檔案進行集中輸出
import homeReducer from `./home`;
import popularizeReducer from `./popularize`;
import mineReducer from `./mine`;
const combineReducers = {
home: homeReducer,
popularize: popularizeReducer,
mine: mineReducer
}
export default combineReducers複製程式碼
5、建立store
建立好reducer之後,開啟store/index.js
檔案
import {createStore } from `redux`;
import combineReducers from `../reducer/index`;
const store = createStore(combineReducers)
export default store;
複製程式碼
就是這麼簡單
6、store注入
使用過redux的同學都知道,react-redux上場了,它提供了Provider和connect方法
前面有提到react-native-navigation注入redux的方式,其實差不多
但需要每個子頁面都注入store、Provider
src/index.js
修改如下
import { Navigation } from `react-native-navigation`;
import Home from `./home/index`;
import PopularizeHome from `./popularize/index`;
import MineHome from `./mine/index`;
// 註冊所有的頁面
export function registerScreens(store, Provider) {
Navigation.registerComponent(`home`, () => Home, store, Provider);
Navigation.registerComponent(`popularize`, () => PopularizeHome, store, Provider);
Navigation.registerComponent(`mine`, () => MineHome, store, Provider);
}
複製程式碼
App.js
修改執行頁面註冊的方法即可
import { Provider } from `react-redux`;
import store from `./src/store/index`;
// 執行註冊頁面方法
registerScreens(store, Provider);
複製程式碼
2、體驗Redux
現在來體驗一下redux,開啟src/screen/home/index.js
檔案
import兩個方法
import { connect } from `react-redux`;
import { bindActionCreators } from `redux`;
複製程式碼
匯入action
import * as homeActions from `../../action/home`;
複製程式碼
定義兩個方法,並connect起來
function mapStateToProps(state) {
return {
home: state.home
};
}
function mapDispatchToProps(dispatch) {
return {
homeActions: bindActionCreators(homeActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Home);
複製程式碼
現在在頁面上列印initCount
來看一下,只要被connect過的元件,以及從該元件通過路由push跳轉的子頁面都可以通過this.props
拿到資料
src/screen/home/index.js
完整程式碼如下
import React, { Component } from `react`;
import { Text, View, StyleSheet } from `react-native`;
import { connect } from `react-redux`;
import { bindActionCreators } from `redux`;
import * as homeActions from `../../action/home`;
type Props = {};
class Home extends Component<Props> {
render() {
return (
<View style={styles.container}>
<Text>Home</Text>
<Text>initCount: {this.props.home.initCount}</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
backgroundColor: `#ccc`,
flex: 1,
justifyContent: `center`,
alignItems: `center`
}
})
function mapStateToProps(state) {
return {
home: state.home
};
}
function mapDispatchToProps(dispatch) {
return {
homeActions: bindActionCreators(homeActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Home);
複製程式碼
從頁面可以看到,已經讀到狀態樹裡面的資料了,initCount為0
讓我們再來試一下action的加法和減法
src/screen/home/index.js
完整程式碼如下
import React, { Component } from `react`;
import { Text, View, StyleSheet, TouchableOpacity } from `react-native`;
import { connect } from `react-redux`;
import { bindActionCreators } from `redux`;
import * as homeActions from `../../action/home`;
type Props = {};
class Home extends Component<Props> {
render() {
return (
<View style={styles.container}>
<Text>Home</Text>
<Text>initCount: {this.props.home.initCount}</Text>
<TouchableOpacity
style={styles.addBtn}
onPress={() => {
this.props.homeActions.homeAdd({
initCount: this.props.home.initCount + 2
});
}}
>
<Text style={styles.btnText}>加2</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.cutBtn}
onPress={() => {
this.props.homeActions.homeCut({
initCount: this.props.home.initCount - 2
});
}}
>
<Text style={styles.btnText}>減2</Text>
</TouchableOpacity>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
backgroundColor: `#ccc`,
flex: 1,
justifyContent: `center`,
alignItems: `center`
},
addBtn: {
backgroundColor: `green`,
marginVertical: 20,
width: 200,
height: 59,
justifyContent: `center`,
alignItems: `center`,
borderRadius: 10
},
cutBtn: {
backgroundColor: `red`,
width: 200,
height: 59,
justifyContent: `center`,
alignItems: `center`,
borderRadius: 10
},
btnText: {
fontSize: 18,
color: `white`
}
});
function mapStateToProps(state) {
return {
home: state.home
};
}
function mapDispatchToProps(dispatch) {
return {
homeActions: bindActionCreators(homeActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Home);
複製程式碼
現在點選兩個按鈕都應該能得到反饋
現在再來驗證下一個東西,這個頁面改完store裡面的狀態後,另一個頁面mine會不會同步,也就是全域性資料有沒有共享了
src/mine/index.js
檔案修改如下
import React, { Component } from `react`;
import { Text, View } from `react-native`;
import { connect } from `react-redux`;
import { bindActionCreators } from `redux`;
import * as homeActions from `../../action/home`;
type Props = {};
class MineHome extends Component<Props> {
render() {
return (
<View>
<Text>initCount: {this.props.home.initCount}</Text>
</View>
);
}
}
function mapStateToProps(state) {
return {
home: state.home
};
}
function mapDispatchToProps(dispatch) {
return {
homeActions: bindActionCreators(homeActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(MineHome);
複製程式碼
在該頁面上讀取同一個資料this.props.home.initCount
,然後在第一個頁面home上加減資料,再看mine頁面,會發現initCount也同步變化
也就是說明:我們已經在進行狀態管理了
到這裡是不是很開心,redux雖然有點繞,但如果跟著做下來應該也有了一定的輪廓了
五、狀態跟蹤:Redux-logger
這個時候,我們會發現,雖然狀態共享了,但目前還沒有辦法跟蹤狀態,以及每一步操作帶來的狀態變化。
但總不能每次都手動列印狀態到控制檯裡面吧?
redux-logger該上場了
它大概長下面這樣,把每次派發action的前後狀態都自動輸出到控制檯上
具體使用看下官方文件,很簡單,直接上程式碼吧
安裝
yarn add redux-logger
複製程式碼
它作為一箇中介軟體,中介軟體的用法請回 redux中文文件 查閱
store/index.js
檔案修改如下
import { createStore, applyMiddleware } from `redux`;
import combineReducers from `../reducer/index`;
import logger from `redux-logger`;
const store = createStore(combineReducers, applyMiddleware(logger));
export default store;
複製程式碼
command+R重新整理一下模擬器,再點選一下+2,看看控制檯是不是長下面這樣?
接下來每次派發action,控制檯都會自動列印出來,是不是省心省事?
六、非同步管理:Redux-Thunk
基礎理解
redux-thunk是什麼請移步 redux-thunk github
出發點:需要元件對同步或非同步的 action 無感,呼叫非同步 action 時不需要顯式地傳入 dispatch
通過使用指定的 middleware,action 建立函式除了返回 action 物件外還可以返回函式。這時,這個 action 建立函式就成為了 thunk
當 action 建立函式返回函式時,這個函式會被 Redux Thunk middleware 執行。這個函式並不需要保持純淨;它還可以帶有副作用,包括執行非同步 API 請求。這個函式還可以 dispatch action,就像 dispatch 前面定義的同步 action 一樣
> thunk 的一個優點是它的結果可以再次被 dispatch
安裝
yarn add redux-thunk
複製程式碼
注入store
作為一箇中介軟體,它的使用方式和上面logger一樣,stroe/index.js
直接引入即可
import thunk from `redux-thunk`;
middleware.push(thunk);
複製程式碼
使用方式
action/home.js
檔案修改如下
import post from `../utils/fetch`;
export function getSomeData() {
return dispatch => {
post(`/get/data`,{}, res => {
const someData = res.data.someData;
dispatch({
type: actionTypes.HOME_GET_SOMEDATA,
someData
})
})
}
}複製程式碼
題外話:封裝請求函式post
此處稍微插入一句,關於封裝請求函式post(以下是精簡版,只保留了核心思想)
cd src
mkdir utils
touch utils/fetch.js
複製程式碼
公用的方法和函式都封裝在utils資料夾中
utils/fetch.js
檔案如下
export default function post(url, data, sucCB, errCB) {
// 域名、body、header等根據各自專案配置,還有部分安全,加密方面的設定,
const host = `www.host.com`;
const requestUrl = `${host}/${url}`;
const body = {};
const headers = {
`Content-Type`: `application/json`,
`User-Agent`: ``
};
// 用的是fetch函式
fetch(requestUrl, {
method: `POST`,
headers: headers,
body: body
}).then(res => {
if (res && res.status === 200) {
return res.json();
} else {
throw new Error(`server`);
}
}).then(res => {
// 精簡版判斷
if(res && res.code === 200 && res.enmsg === `ok`) {
// 成功後的回撥
sucCB(res);
}else {
// 失敗後的回撥
errCB(res);
}
}).catch(err => {
// 處理錯誤
})
}
複製程式碼
七、非同步管理:Redux-Saga
基本概念請移步
出發點:需要宣告式地來表述複雜非同步資料流(如長流程表單,請求失敗後重試等),命令式的 thunk 對於複雜非同步資料流的表現力有限
安裝
yarn add redux-saga複製程式碼
建立saga檔案
建立順序有點像reducer
我們先建立saga相關資料夾和檔案,最後再來注入store裡面
cd src
mkdir saga
touch saga/index.js saga/home.js saga/popularize.js saga/mine.js複製程式碼
先修改saga/home.js
檔案
import { put, call, takeLatest } from `redux-saga/effects`;
import * as actionTypes from `../action/index`;
import * as homeActions from `../action/home`;
import * as mineActions from `../action/mine`;
import post from `../utils/fetch`;
function getSomeThing() {
post(`/someData`, {}, res => {}, err => {});
}
// 這個函式中的請求方法都是隨手寫的,沒引入真實API,
function* getUserInfo({ sucCb, errCB }) {
try {
const res = yield call(getSomeThing());
const data = res.data;
yield put(homeActions.getSomeData())
yield put(homeActions.setSomeData(data))
yield call(sucCb);
} catch (err) {
yield call(errCB, err);
}
}
export const homeSagas = [
takeLatest(actionTypes.HOME_GET_SOMEDATA, getUserInfo)
]
複製程式碼
saga/mine.js
檔案
export const mineSagas = []
複製程式碼
saga/popularize.js
檔案
export const popularizeSagas = []
複製程式碼
saga/index.js
檔案作為總輸出口,修改如下
import { all } from `redux-saga/effects`;
import { homeSagas } from `./home`;
import { mineSagas } from `./mine`;
import { popularizeSagas } from `./popularize`;
export default function* rootSaga() {
yield all([...homeSagas, ...mineSagas, ...popularizeSagas]);
}
複製程式碼
把saga注入store
store/index.js
檔案修改
import createSagaMiddleware from `redux-saga`;
import rootSaga from `../saga/index`;
// 生成saga中介軟體
const sagaMiddleware = createSagaMiddleware(rootSaga);
middleware.push(sagaMiddleware);
複製程式碼
八:資料持久化:Redux-persist
GitHub – rt2zz/redux-persist: persist and rehydrate a redux store
顧名思義,資料持久化,一般用來儲存登入資訊等需要儲存在本地的資料
因為store中的資料,在每次重新開啟app後,都會回覆到reducer中的initState的初始狀態,所以像登入資訊這種資料就需要持久化的儲存了。
RN自帶的AsyncStorage可以實現這個功能,但使用起來比較繁瑣,而且沒有注入到store中去,沒辦法實現統一狀態管理,所以redux-persist就出場了
安裝
yarn add redux-persist複製程式碼
注入store
store/index.js
檔案完整程式碼如下
import { createStore, applyMiddleware } from `redux`;
import logger from `redux-logger`;
import thunk from `redux-thunk`;
import createSagaMiddleware from `redux-saga`;
import { persistStore, persistCombineReducers } from `redux-persist`;
import storage from `redux-persist/es/storage`;
import combineReducers from `../reducer/index`;
import rootSaga from `../saga/index`;
const persistConfig = {
key: `root`,
storage,
// 白名單:只有mine的資料會被persist
whitelist: [`mine`]
};
// 對reducer資料進行persist配置
const persistReducer = persistCombineReducers(persistConfig, combineReducers);
const sagaMiddleware = createSagaMiddleware();
// 中介軟體
const createStoreWithMiddleware = applyMiddleware(
thunk,
sagaMiddleware,
logger
)(createStore);
const configuerStore = onComplete => {
let store = createStoreWithMiddleware(persistReducer);
let persistor = persistStore(store, null, onComplete);
sagaMiddleware.run(rootSaga);
return { persistor, store };
};
export default configuerStore;
複製程式碼
這個地方,不再把middleware當做陣列,而是直接寫入applyMiddleware方法中
store也不再直接匯出,而是到處一個生成store的函式configuerStore 相應,
App.js
檔案的引入也要修改一點點
import configuerStore from `./src/store/index`;
const { store } = configuerStore(() => { });複製程式碼
九:靜態測試:Flow
待更新,這有一篇連結可以先看React Native填坑之旅–Flow篇(番外)
十、後話
到目前為止,我們已經引入了redux-logger、redux-thunk、redux-saga、redux-persist
核心開發程式碼庫已經配置完畢了
專案已經傳上github,歡迎star
接下來還有一些可以作為開發時的輔助性配置,比如Flow 、Babel(RN初始化時已經配好了)、Eslint等等
另外,既然是App,那最終目的當然就是要上架App Store和各大安卓市場,後面可能還會分享一下關於極光推送jPush、熱更新CodePush、打包上傳稽核等方面的內容。
感謝您耐心看到這裡,希望有所收穫!
如果不是很忙的話,麻煩點個star⭐【Github部落格傳送門】,舉手之勞,卻是對作者莫大的鼓勵。
我在學習過程中喜歡做記錄,分享的是自己在前端之路上的一些積累和思考,希望能跟大家一起交流與進步,更多文章請看【amandakelake的Github部落格】