全文技術棧 html
本文適合有對React家族有必定使用經驗,但對從零配置一個App不是很熟悉,又想要從零體驗一把搭建App的同窗。前端
我本身就是這種狀況,中途參與到項目中,一直沒有掌控全局的感受,因此此次趁着項目重構的機會,本身也跟着從零配置了一遍,並記錄了下來,但願能跟同窗們一塊兒學習,若是有說錯的地方,也但願你們指出來,或者有更好的改進方式,歡迎交流。 node
若是有時間的同窗,跟着親手作一遍是最好的,對於如何搭建一個真實項目比較有幫助。
react
整個項目已經上傳到github,懶的動手的同窗能夠直接clone下來跟着看,歡迎一塊兒完善,目前的初步想法是對一部分的同窗有所幫助,後面有時間的話,可能會完善成一個比較健壯的RN基礎框架,能夠直接clone就開發項目那種android
這裏對每一個庫或者內容只作配置和基礎用法介紹 git
物理環境:mac,xcode github
window系統的同窗也能夠看,不過須要本身搞好模擬器開發環境
json
React-native官網redux
若是RN的基礎配置環境沒有配置好,請點擊上方連接到官網進行配置
react-native init ReactNativeNavigationDemo
cd ReactNativeNavigationDemo
react-native run-ios
複製代碼
由於一開始就計劃好了用React-Native-Navigation做爲導航庫,因此名字起得長了點,你們起個本身喜歡的吧
成功後會看到這個界面
這時候能夠看下目錄結構,RN自動集成了babel、git、flow的配置文件,仍是很方便的
爲何用React Native Navigation而不用React Navigation ?
它是目前惟一一款使用原生代碼來實現navigator的插件,使用後navigator的push/pop的動畫將脫離js線程而改由原生的UI線程處理, 切屏效果會和原生態同樣流暢, 不再會出現因爲js線程渲染致使的navigator切屏動畫的卡頓效果了, 而且該插件還同時內置實現了原生態版本的tabbar
英文好的同窗看着官方文檔配就能夠了,實在看不懂的能夠對照着我下面的圖看。
iOS的須要用到xcode,沒作過的可能會以爲有點複雜,因此我跑了一遍流程並截圖出來了 至於android的配置,文檔寫的很清晰,就不跑了。
yarn add react-native-navigation@latest複製代碼
圖中的路徑文件是指./node_modules/react-native-navigation/ios/ReactNativeNavigation.xcodeproj
$(SRCROOT)/../node_modules/react-native-navigation/ios
記得圖中第5點設置爲recursive
把整個文件內容替換成下面代碼
#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
複製代碼
一、先新建幾個頁面,結構如圖
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>
);
}
}
複製代碼
二、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)
}
複製代碼
三、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,目前模擬器能看到的界面
在screen/home文件夾下面新建一個NextPage.js
文件,記獲得src/screen/index.js
裏面註冊該頁面
Navigation.registerComponent('nextPage', () => NextPage, store, Provider);複製代碼
而後在src/screen/home/index.js
文件裏面加一個跳轉按鈕,並傳遞一個props數據
一、安裝
yarn add redux react-redux複製代碼
二、目錄構建
目前有如下兩種常見的目錄構建方式
一是把同一個頁面的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
這裏不討論正確的書寫順序,我就暫且按照本身的習慣來寫吧
三、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對象
四、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複製代碼
五、建立store
建立好reducer以後,打開store/index.js
文件
import {createStore } from 'redux';
import combineReducers from '../reducer/index';
const store = createStore(combineReducers)
export default store;
複製代碼
就是這麼簡單
六、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);
複製代碼
如今來體驗一下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該上場了
它大概長下面這樣,把每次派發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 github
出發點:須要組件對同步或異步的 action 無感,調用異步 action 時不須要顯式地傳入 dispatch
經過使用指定的 middleware,action 建立函數除了返回 action 對象外還能夠返回函數。這時,這個 action 建立函數就成爲了 thunk
當 action 建立函數返回函數時,這個函數會被 Redux Thunk middleware 執行。這個函數並不須要保持純淨;它還能夠帶有反作用,包括執行異步 API 請求。這個函數還能夠 dispatch action,就像 dispatch 前面定義的同步 action 同樣 > thunk 的一個優勢是它的結果能夠再次被 dispatch
yarn add redux-thunk
複製代碼
做爲一箇中間件,它的使用方式和上面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(如下是精簡版,只保留了核心思想)
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 => {
// 處理錯誤
})
}
複製代碼
基本概念請移步
出發點:須要聲明式地來表述複雜異步數據流(如長流程表單,請求失敗後重試等),命令式的 thunk 對於複雜異步數據流的表現力有限
yarn add redux-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]);
}
複製代碼
store/index.js
文件修改
import createSagaMiddleware from 'redux-saga';
import rootSaga from '../saga/index';
// 生成saga中間件
const sagaMiddleware = createSagaMiddleware(rootSaga);
middleware.push(sagaMiddleware);
複製代碼
GitHub - rt2zz/redux-persist: persist and rehydrate a redux store
顧名思義,數據持久化,通常用來保存登陸信息等須要保存在本地的數據
由於store中的數據,在每次從新打開app後,都會回覆到reducer中的initState的初始狀態,因此像登陸信息這種數據就須要持久化的存儲了。
RN自帶的AsyncStorage能夠實現這個功能,但使用起來比較繁瑣,並且沒有注入到store中去,沒辦法實現統一狀態管理,因此redux-persist就出場了
yarn add redux-persist複製代碼
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(() => { });複製代碼
待更新,這有一篇連接能夠先看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博客】