React-Native從零搭建App(長文)

1、須知

全文技術棧 html

  • 核心庫:React-Native@0.54.0 
  • 路由導航:React-Native-Navigation 
  • 狀態管理:Redux、Redux-Thunk、Redux-Saga、Redux-persist 
  • 靜態測試:Flow  

本文適合有對React家族有必定使用經驗,但對從零配置一個App不是很熟悉,又想要從零體驗一把搭建App的同窗。前端

我本身就是這種狀況,中途參與到項目中,一直沒有掌控全局的感受,因此此次趁着項目重構的機會,本身也跟着從零配置了一遍,並記錄了下來,但願能跟同窗們一塊兒學習,若是有說錯的地方,也但願你們指出來,或者有更好的改進方式,歡迎交流。 node

若是有時間的同窗,跟着親手作一遍是最好的,對於如何搭建一個真實項目比較有幫助。
react

整個項目已經上傳到github,懶的動手的同窗能夠直接clone下來跟着看,歡迎一塊兒完善,目前的初步想法是對一部分的同窗有所幫助,後面有時間的話,可能會完善成一個比較健壯的RN基礎框架,能夠直接clone就開發項目那種android

該項目github倉庫傳送門ios


這裏對每一個庫或者內容只作配置和基礎用法介紹  git

物理環境:mac,xcode github

window系統的同窗也能夠看,不過須要本身搞好模擬器開發環境
json

2、快速創建一個RN App

React-native官網redux

若是RN的基礎配置環境沒有配置好,請點擊上方連接到官網進行配置

react-native init ReactNativeNavigationDemo
cd ReactNativeNavigationDemo
react-native run-ios
複製代碼

由於一開始就計劃好了用React-Native-Navigation做爲導航庫,因此名字起得長了點,你們起個本身喜歡的吧

成功後會看到這個界面


這時候能夠看下目錄結構,RN自動集成了babel、git、flow的配置文件,仍是很方便的

3、路由導航:React-Native-Navigation

React Native Navigation


爲何用React Native Navigation而不用React Navigation ?

它是目前惟一一款使用原生代碼來實現navigator的插件,使用後navigator的push/pop的動畫將脫離js線程而改由原生的UI線程處理, 切屏效果會和原生態同樣流暢, 不再會出現因爲js線程渲染致使的navigator切屏動畫的卡頓效果了, 而且該插件還同時內置實現了原生態版本的tabbar


英文好的同窗看着官方文檔配就能夠了,實在看不懂的能夠對照着我下面的圖看。 

iOS的須要用到xcode,沒作過的可能會以爲有點複雜,因此我跑了一遍流程並截圖出來了 至於android的配置,文檔寫的很清晰,就不跑了。

一、安裝

yarn add react-native-navigation@latest複製代碼

二、添加xcode工程文件

圖中的路徑文件是指./node_modules/react-native-navigation/ios/ReactNativeNavigation.xcodeproj


三、把上面添加的工程文件添加到庫中


四、添加路徑

$(SRCROOT)/../node_modules/react-native-navigation/ios記得圖中第5點設置爲recursive


五、修改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
複製代碼

六、基礎使用

一、先新建幾個頁面,結構如圖

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數據






4、狀態管理:Redux

redux中文文檔

一、初始化

一、安裝

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

如今來體驗一下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雖然有點繞,但若是跟着作下來應該也有了必定的輪廓了


5、狀態跟蹤:Redux-logger

這個時候,咱們會發現,雖然狀態共享了,但目前尚未辦法跟蹤狀態,以及每一步操做帶來的狀態變化。

 但總不能每次都手動打印狀態到控制檯裏面吧?

redux-logger該上場了

它大概長下面這樣,把每次派發action的先後狀態都自動輸出到控制檯上


具體使用看下官方文檔,很簡單,直接上代碼吧

redux-logger

安裝

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,控制檯都會自動打印出來,是否是省心省事?


6、異步管理: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 => {
    // 處理錯誤
  })
}
複製代碼



7、異步管理:Redux-Saga

基本概念請移步

自述 | 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篇(番外)



10、後話


到目前爲止,咱們已經引入了redux-logger、redux-thunk、redux-saga、redux-persist

核心開發代碼庫已經配置完畢了

項目已經傳上github,歡迎star

該項目github倉庫傳送門


接下來還有一些能夠做爲開發時的輔助性配置,好比Flow 、Babel(RN初始化時已經配好了)、Eslint等等

另外,既然是App,那最終目的固然就是要上架App Store和各大安卓市場,後面可能還會分享一下關於極光推送jPush、熱更新CodePush、打包上傳審覈等方面的內容。


感謝您耐心看到這裏,但願有所收穫! 

若是不是很忙的話,麻煩點個star⭐【Github博客傳送門】,舉手之勞,倒是對做者莫大的鼓勵。 

我在學習過程當中喜歡作記錄,分享的是本身在前端之路上的一些積累和思考,但願能跟你們一塊兒交流與進步,更多文章請看【amandakelake的Github博客】

相關文章
相關標籤/搜索