React Native App應用架構設計

在上一篇介紹了React Native開發環境搭建,咱們已經能夠在本地成功運行一個helloword應用了,本節將開始詳細分析如何搭建一個React Native App應用架構,並支持完整本地運行預覽。javascript

完整代碼見githubhtml

歡迎訪問個人我的博客前端

前言

如今已經有不少腳手架工具,如ignite,支持一鍵建立一個React Native App項目結構,很方便,可是享受方便的同時,也失去了對項目架構及技術棧完整學習的機會,並且一般腳手架建立的應用技術架構並不能徹底知足咱們的業務需求,須要咱們本身修改,完善,因此若是但願對項目架構有更深掌控,最好仍是從0到1理解一個項目。java

項目結構與技術棧

首先使用react-native-cli工具建立一個React Native應用:node

react-native init fuc

生成項目結構以下圖:react

RN項目初始結構

  1. andorid和ios目錄分別存放對應原平生臺代碼;
  2. package.json爲項目依賴管理文件;
  3. index.ios.js爲ios平臺入口文件,index.android.js爲android平臺入口文件,一般用來註冊React Native App根組件;
  4. .babelrc文件,babel的配置文件,React Native默認使用babel編譯JavaScript代碼;
  5. __tests__項目測試目錄。

咱們看到並無存放React Native原生JavaScript代碼的目錄,這須要咱們本身進行建立了,一般建立一個src目錄做爲App應用Javascript部分全部代碼和資源的根目錄,一個src/constants目錄以保存全局共享常量數據,一個src/config目錄保存全局配置,一個src/helpers存放全局輔助,工具類方法,一個src/app.js做爲RN部分入口文件,另外一般還須要建立保存各模塊redux的目錄,redux中間件的目錄等。android

技術棧

項目架構搭建很大部分依賴於項目的技術棧,因此先對整個技術棧進行分析,總結:ios

  1. react native + react庫是項目前提
  2. App應用導航(不一樣於React應用的路由概念)
  3. 應用狀態管理容器
  4. 是否須要Immutable數據
  5. 應用狀態的持久化
  6. 異步任務管理
  7. 測試及輔助工具或函數
  8. 開發調試工具

根據以上劃分決定選用如下第三方庫和工具構成項目的完整技術棧:git

  1. react-native + react類庫;
  2. react-navigation管理應用導航;
  3. redux做爲JavaScript狀態容器,react-redux將React Native應用與redux鏈接;
  4. Immutable.js支持Immutable化狀態,redux-immutable使整個redux store狀態樹Immutable化;
  5. 使用redux-persist支持redux狀態樹的持久化,並添加redux-persist-immutable拓展以支持Immutable化狀態樹的持久化;
  6. 使用redux-saga管理應用內的異步任務,如網絡請求,異步讀取本地數據等;
  7. 使用jest集成應用測試,使用lodash,ramda等可選輔助類,工具類庫;
  8. 使用reactotron調試工具

針對以上分析,完善後的項目結構如圖:github

RN項目結構

如上圖,在項目根目錄下建立src目錄,而在src目錄中依次建立12個目錄與1個React Native部分入口js文件。

開發調試工具

React Native App開發目前已經有諸多調試工具,經常使用的如atom和Nuclide,移動端模擬器自帶的調試工具,Reactron等。

Nuclide

Nuclide是由Facebook提供的基於atom的集成開發環境,可用於編寫、運行調試React Native應用。

模擬器調試工具

在模擬器啓動運行App後,瀏覽器會自動打開 http://localhost:8081/debugger-ui頁,能夠在控制檯進行js調試輸出及遠端js斷點調試;在模擬器終端使用快捷鍵commandD鍵便可打開調試工具,包括從新加載應用,開啓熱加載,切換DOM審視器等:

RN應用調試工具

Reactotron

Reactotron是一款跨平臺調試React及React Native應用的桌面應用,能動態實時監測並輸出React應用等redux,action,saga異步請求等信息,如圖:

Reactotron

首先初始化Reactotron相關配置:

import Config from './DebugConfig';
import Immutable from 'immutable';
import Reactotron from 'reactotron-react-native';
import { reactotronRedux as reduxPlugin } from 'reactotron-redux';
import sagaPlugin from 'reactotron-redux-saga';

if (Config.useReactotron) {
  // refer to https://github.com/infinitered/reactotron for more options!
  Reactotron
    .configure({ name: 'Os App' })
    .useReactNative()
    .use(reduxPlugin({ onRestore: Immutable }))
    .use(sagaPlugin())
    .connect();

  // Let's clear Reactotron on every time we load the app
  Reactotron.clear();

  // Totally hacky, but this allows you to not both importing reactotron-react-native
  // on every file.  This is just DEV mode, so no big deal.
  console.tron = Reactotron;
}

而後啓使用console.tron.overlay方法拓展入口組件:

import './config/ReactotronConfig';
import DebugConfig from './config/DebugConfig';

class App extends Component {
  render () {
    return (
      <Provider store={store}>
        <AppContainer />
      </Provider>
    )
  }
}

// allow reactotron overlay for fast design in dev mode
export default DebugConfig.useReactotron
  ? console.tron.overlay(App)
  : App

至此就可使用Reactotron客戶端捕獲應用中發起的全部的redux和action了。

組件劃分

React Native應用依然遵循React組件化開發原則,組件負責渲染UI,組件不一樣狀態對應不一樣UI,一般遵循如下組件設計思路:

  1. 佈局組件:僅僅涉及應用UI界面結構的組件,不涉及任何業務邏輯,數據請求及操做;
  2. 容器組件:負責獲取數據,處理業務邏輯,一般在render()函數內返回展現型組件;
  3. 展現型組件:負責應用的界面UI展現;
  4. UI組件:指抽象出的可重用的UI獨立組件,一般是無狀態組件;
展現型組件 容器組件
目標 UI展現 (HTML結構和樣式) 業務邏輯(獲取數據,更新狀態)
感知Redux
數據來源 props 訂閱Redux store
變動數據 調用props傳遞的回調函數 Dispatch Redux actions
可重用 獨立性強 業務耦合度高

跨平臺適應

建立跨平臺應用時,雖然React Native作了大量跨平臺兼容的工做,可是依然存在一些須要爲不一樣平臺開發不一樣代碼的狀況,這時候須要額外處理。

跨平臺目錄

咱們能夠將不一樣平臺代碼文件以不一樣目錄區分開來,如:

/common/components/
/android/components/
/ios/components/

common目錄下存放公用文件,android目錄存放android文件代碼,ios存放ios文件代碼,可是一般都選擇React Native提供的更好方式,後文介紹。

Platform模塊

React Native內置了一個Platform模塊,用以區分應用當前運行平臺,當運行在ios平臺下時,Platform.OS值爲ios,運行在android平臺下則爲android,能夠利用此模塊加載對應平臺文件:

var StatusBar = Platform.select({
  ios: () => require('ios/components/StatusBar'),
  android: () => require('android/components/StatusBar'),
})();

而後正常使用該StatusBar組件便可。

React Native平臺檢測

當引用某組件時,React Native會檢測該文件是否存在.android.ios後綴,若是存在則根據當前平臺加載對應文件組件,如:

StatusBar.ios.js
StatusBar.indroid.js

同一目錄下存在以上兩個文件,則可使用如下方式引用:

import StatusBar from './components/StatusBar';

React將會根據當前平臺加載對應後綴文件,推薦使用此方式作平臺組件級代碼適配,而對於局部小部分須要適配平臺的代碼可使用Platform.OS值,以下,若僅僅須要在ios平臺下添加一個更高的margin-top值且不是公用樣式時:

var styles = StyleSheet.create({
  marginTop: (Platform.OS === 'ios') ? 20 : 10,
});

App應用導航與路由

不一樣於React應用的單頁面路由,React Native一般都是多頁面形式存在,以導航方式在不一樣頁面和組件間切換,而不是路由方式控制不一樣組件展現,最常使用的是react-navigation導航庫。

導航和路由

在React web應用中,頁面UI組件展現和切換徹底由路由控制,每個路由都有對應的URL及路由信息,在React Native應用則不是由路由驅動組件展現,而是由導航控制切換屏展現,每一屏有各自的路由信息。

或許你已經依賴react-router的單頁面應用路由配置方式,但願建立一個Url驅動的跨平臺App應用,託福於活躍的的開源社區,你可使用react-router-native,可是並不推薦,由於對於App而言,從交互和體驗考慮,仍是更適合使用多頁面(屏)形式。

react-navigation

使用react-navigation能夠定義跨平臺的應用導航結構,也支持配置渲染跨平臺的導航欄,tab欄等組件。

內置導航模塊

react-navigation提供如下幾個方法支持建立不一樣的導航類型:

  1. StackNavigator:建立導航屏棧(stack),全部屏(screen)以棧的方式存在,一次渲染一屏,在切換屏時提升變換動畫,當打開某一屏時,將該屏放置在棧頂;
  2. TabNavigator:建立一個Tab式導航,渲染一個Tab菜單欄,使用戶能夠切換不一樣屏;
  3. DrawerNavigator:建立抽屜式導航,從屏的左邊滑出一屏;

StackNavigator

StackNavigator支持跨平臺以變換方式切換不一樣屏,而且將當前屏放置在棧頂,調用方式以下:

StackNavigator(RouteConfigs, StackNavigatorConfig)
RouteConfigs

導航棧路由(route)配置對象,定義route名和route對象,該route對象定義當前路由對應的展現組件,如:

// routes爲路由信息對象
StackNavigator({
  [routes.Main.name]: Main,
  [routes.Login.name]: {
    path: routes.Login.path,
    screen: LoginContainer,
    title: routes.Login.title
  }
}

如上,代表當應用導航至路由routes.Login.name時,渲染LoginContainer組件,由對象screen屬性指定;而導航至路由routes.Main.name值時,對應渲染MainStack,代碼中Main對象爲:

{
  path: routes.Main.path,
  screen: MainStack,
  navigationOptions: {
    gesturesEnabled: false,
  },
}

而MainStack是一個Stacknavigator:

const MainStack = StackNavigator({
  Home: HomeTabs
})

HomeTabs是一個TabNavigator:

{
  name: 'Home Tabs',
  description: 'Tabs following Home Tabs',
  headerMode: 'none',
  screen: HomeTabs
};
StackNavigatorConfig

路由配置對象,能夠選擇性配置可選屬性,如:

  1. initialRouteName,初始導航棧默認屏,必須是路由配置對象中的某一鍵名;
  2. initialRouteParams,初始路由的默認參數;
  3. navigationOptions,設置默認的導航屏配置;
    1. title:導航屏頂部標題;
  4. headerMode,是否顯示頂部導航欄:
    1. none:不顯示導航欄;
    2. float:在頂部渲染一個獨立的導航欄,而且在切換屏時伴有動畫,一般是ios的展現模式;
    3. screen:爲每一屏綁定一個導航欄,而且伴隨着屏切換淡入淡出,一般是android的展現模式;
  5. mode,導航切換屏時的樣式和變換效果:
    1. card:默認方式,標準的屏變換;
    2. modal:僅在ios平臺有效,使屏幕底部滑出新屏;
{
  initialRouteName: routes.Login.name,
  headerMode: 'none', // 去除頂部導航欄
  /**
   * Use modal on iOS because the card mode comes from the right,
   * which conflicts with the drawer example gesture
   */
  mode: Platform.OS === 'ios' ? 'modal' : 'card'
}

TabNavigator

使用TabNavigator能夠建立一屏,擁有TabRouter能夠切換不一樣Tab,調用方式如:

TabNavigator(RouteConfigs, TabNavigatorConfig)
RouteConfigs

Tab路由配置對象,格式相似StackNavigator。

TabNavigatorConfig

Tab導航相關配置對象,如:

  1. tabBarComponent: tab菜單欄使用的組件,ios平臺默認使用TabBarBottom組件,android平臺默認使用TabBarTop組件;
  2. tabBarPosition:tab菜單欄位置,topbottom;
  3. tabBarOptions: tab菜單欄配置:
    1. activeTintColor:激活tab的菜單欄項的字體和圖標的顏色
  4. initialRouteName: 初始加載時的默認tabRoute路由的routeName,對應路由配置對象的鍵名
  5. order:tab排序,routeName組成的數組;
const HomeTabs = TabNavigator(
  {
    Notification: {
      screen: NotificationTabContainer,
      path: 'notification',
      navigationOptions: {
        title: '消息通知'
      }
    },
    Myself: {
      screen: MyselfTabContainer,
      path: 'myself',
      navigationOptions: {
        title: '個人'
      }
    }
  },
  {
    tabBarOptions: {
      activeTintColor: Platform.OS === 'ios' ? '#e91e63' : '#fff',
    },
    swipeEnabled: true
  }
);

DrawerNavigator

使用DrawerNavigator能夠建立抽屜式導航屏,調用方式以下:

DrawerNavigator(RouteConfigs, DrawerNavigatorConfig)
const MyDrawer = DrawerNavigator({
  Home: {
    screen: MyHomeDrawerScreen,
  },
  Notifications: {
    screen: MyNotificationsDrawerScreen,
  },
});
RouteConfigs

抽屜式導航路由配置對象,格式相似StackNavigator。

DrawerNavigatorConfig

抽屜式導航屏配置對象,如:

  1. drawerWidth:抽屜屏的寬度;
  2. drawerPosition:抽屜屏位置,leftright
  3. contentComponent:抽屜屏內容組件,如內置提供的DrawerItems
  4. initialRouteName:初始路由的路由名;
import { DrawerItems } from 'react-navigation';

const CustomDrawerContentComponent = (props) => (
  <View style={styles.container}>
    <DrawerItems {...props} />
  </View>
);

const DrawerNavi = DrawerNavigator({}, {
  drawerWidth: 200,
  drawerPosition: 'right',
  contentComponent: props => <CustomDrawerContentComponent  {...props}/>,
  drawerBackgroundColor: 'transparent'
})

Navigation prop

RN應用的每一屏將接受一個navigation屬性包含如下方法和屬性:

  1. navigate:導航至其餘屏的輔助方法;
  2. setParams:變動路由參數方法;
  3. goBack:關閉當前屏並後退;
  4. state:當前屏的狀態或路由信息;
  5. dispatch:發佈action;
navigate

使用navigate方法導航至其餘屏:

navigate(routeName, params, action)
  1. routeName:目標路由名,在App導航路由註冊過的路由鍵名;
  2. params:目標路由攜帶的參數;
  3. action:若是目標路由存在子路由,則在子路由內執行此action;
setParams

改變當前導航路由信息,如設置修改導航標題等信息:

class ProfileScreen extends React.Component {
  render() {
    const { setParams } = this.props.navigation;
    return (
      <Button
        onPress={() => setParams({name: 'Jh'})}
        title="Set title"
      />
     )
   }
}
goBack

從當前屏(參數爲空)或者指定屏(參數爲屏路由鍵名)導航回退至該屏的上一屏,而且關閉該屏;若傳遞null參數,則未指定來源屏,即不會關閉屏。

state

每一屏都有本身的路由信息,能夠經過this.props.navigation.state訪問,其返回數據格式如:

{
  // the name of the route config in the router
  routeName: 'Login',
  //a unique identifier used to sort routes
  key: 'login',
  //an optional object of string options for this screen
  params: { user: 'jh' }
}
dispatch

該方法用來分發導航action至路由,實現導航,可使用react-navigation默認提供的action建立函數NavigationActions,以下爲分發一個navigate導航切換屏action:

import { NavigationActions } from 'react-navigation'

const navigateAction = NavigationActions.navigate({
  routeName: routeName || routes.Login.name,
  params: {},
  // navigate can have a nested navigate action that will be run inside the child router
  action: NavigationActions.navigate({ routeName: 'Notification'})
});

// dispatch the action
this.props.navigation.dispatch(navigateAction);

Navigation與Redux

在使用Redux之後,須要遵循redux的原則:單一可信數據來源,即全部數據來源都只能是reudx store,Navigation路由狀態也不該例外,因此須要將Navigation state與store state鏈接,能夠建立一個Navigation reducer以合併Navigation state至store:

import AppNavigation from '../routes';

const NavigationReducer = (state = initialState, action) => {
  const newState = Object.assign({}, state, AppNavigation.router.getStateForAction(action, state));
  return newState || state;
};

export const NavigationReducers = {
  nav: NavigationReducer
};

這個reducer所作的只是將App導航路由狀態合併入store。

Redux

現代的任何大型web應用若是少了狀態管理容器,那這個應用就缺乏了時代特徵,可選的庫諸如mobx,redux等,實際上大同小異,各取所需,以redux爲例,redux是最經常使用的react應用狀態容器庫,對於React Native應用也適用。

react-redux

和React應用同樣,須要將Redux和應用鏈接起來,才能統一使用redux管理應用狀態,使用官方提供的react-redux庫。

class App extends Component {
  render () {
    return (
      <Provider store={store}>
        <AppContainer />
      </Provider>
    )
  }
}

createStore

使用redux提供的createStore方法建立redux store,可是在實際項目中咱們經常須要拓展redux添加某些自定義功能或服務,如添加redux中間件,添加異步任務管理saga,加強redux等:

// creates the store
export default (rootReducer, rootSaga, initialState) => {
  /* ------------- Redux Configuration ------------- */
  const middleware = [];
  const enhancers = [];

  /* ------------- Analytics Middleware ------------- */
  middleware.push(ScreenTracking);

  /* ------------- Saga Middleware ------------- */
  const sagaMonitor = Config.useReactotron ? console.tron.createSagaMonitor() : null;
  const sagaMiddleware = createSagaMiddleware({ sagaMonitor });
  middleware.push(sagaMiddleware);

  /* ------------- Assemble Middleware ------------- */
  enhancers.push(applyMiddleware(...middleware));

  /* ------------- AutoRehydrate Enhancer ------------- */
  // add the autoRehydrate enhancer
  if (ReduxPersist.active) {
    enhancers.push(autoRehydrate());
  }

  // if Reactotron is enabled (default for __DEV__), 
  // we'll create the store through Reactotron
  const createAppropriateStore = Config.useReactotron ? console.tron.createStore : createStore;
  const store = createAppropriateStore(rootReducer, initialState, compose(...enhancers));

  // configure persistStore and check reducer version number
  if (ReduxPersist.active) {
    RehydrationServices.updateReducers(store);
  }

  // kick off root saga
  sagaMiddleware.run(rootSaga);

  return store;
}

redux與Immutable

redux默認提供了combineReducers方法整合reduers至redux,然而該默認方法指望接受原生JavaScript對象而且它把state做爲原生對象處理,因此當咱們使用createStore方法而且接受一個Immutable對象做應用初始狀態時,reducer將會返回一個錯誤,源代碼以下:

if   (!isPlainObject(inputState)) {
	return   (                              
    	`The   ${argumentName} has unexpected type of "` +                                    ({}).toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +
      ".Expected argument to be an object with the following + 
      `keys:"${reducerKeys.join('", "')}"`   
	)  
}

如上代表,原始類型reducer接受的state參數應該是一個原生JavaScript對象,咱們須要對combineReducers其進行加強,以使其能處理Immutable對象,redux-immutable 即提供建立一個能夠和Immutable.js協做的Redux combineReducers

import { combineReducers } from 'redux-immutable';
import Immutable from 'immutable';
import configureStore from './CreateStore';

// use Immutable.Map to create the store state tree
const initialState = Immutable.Map();

export default () => {
  // Assemble The Reducers
  const rootReducer = combineReducers({
    ...NavigationReducers,
    ...LoginReducers
  });

  return configureStore(rootReducer, rootSaga, initialState);
}

如上代碼,能夠看見咱們傳入的initialState是一個Immutable.Map類型數據,咱們將redux整個state樹叢根源開始Immutable化,另外傳入了能夠處理Immutable state的reducers和sagas。

另外每個state樹節點數據都是Immutable結構,如NavigationReducer

const initialState = Immutable.fromJS({
  index: 0,
  routes: [{
    routeName: routes.Login.name,
    key: routes.Login.name
  }]
});

const NavigationReducer = (state = initialState, action) => {
  const newState = state.merge(AppNavigation.router.getStateForAction(action, state.toJS()));
  return newState || state;
};

reducer默認state節點使用Immutable.fromJS()方法將其轉化爲Immutable結構,而且更新state時使用Immutable方法state.merge(),保證狀態統一可預測。

redux持久化

咱們知道瀏覽器默認有資源的緩存功能而且提供本地持久化存儲方式如localStorage,indexDb,webSQL等,一般能夠將某些數據存儲在本地,在必定週期內,當用戶再次訪問時,直接從本地恢復數據,能夠極大提升應用啓動速度,用戶體驗更有優點,對於App應用而言,本地持久化一些啓動數據甚至離線應用更是常見的需求,咱們可使用AsyncStorage(相似於web的localStorage)存儲一些數據,若是是較大量數據存儲可使用SQLite。

另外不一樣於以往的直接存儲數據,啓動應用時本地讀取而後恢復數據,對於redux應用而言,若是隻是存儲數據,那麼咱們就得爲每個reducer拓展,當再次啓動應用時去讀取持久化的數據,這是比較繁瑣並且低效的方式,是否能夠嘗試存儲reducer key,而後根據key恢復對應的持久化數據,首先註冊Rehydrate reducer,當觸發action時根據其reducer key恢復數據,而後只須要在應用啓動時分發action,這也很容易抽象成可配置的拓展服務,實際上三方庫redux-persist已經爲咱們作好了這一切。

redux-persist

要實現redux的持久化,包括redux store的本地持久化存儲及恢復啓動兩個過程,若是徹底本身編寫實現,代碼量比較複雜,可使用開源庫redux-persist,它提供persistStoreautoRehydrate方法分別持久化本地存儲store及恢復啓動store,另外還支持自定義傳入持久化及恢復store時對store state的轉換拓展。

持久化store

以下在建立store時會調用persistStore相關服務-RehydrationServices.updateReducers()

// configure persistStore and check reducer version number
if (ReduxPersist.active) {
  RehydrationServices.updateReducers(store);
}

該方法內實現了store的持久化存儲:

// Check to ensure latest reducer version
AsyncStorage.getItem('reducerVersion').then((localVersion) => {
  if (localVersion !== reducerVersion) {
    if (DebugConfig.useReactotron) {
      console.tron.display({
        name: 'PURGE',
        value: {
          'Old Version:': localVersion,
          'New Version:': reducerVersion
        },
        preview: 'Reducer Version Change Detected',
        important: true
      });
    }
    // Purge store
    persistStore(store, config, startApp).purge();
    AsyncStorage.setItem('reducerVersion', reducerVersion);
  } else {
    persistStore(store, config, startApp);
  }
}).catch(() => {
  persistStore(store, config, startApp);
  AsyncStorage.setItem('reducerVersion', reducerVersion);
})

會在AsyncStorage存儲一個reducer版本號,這個是在應用配置文件中能夠配置,首次執行持久化時存儲該版本號及store,若reducer版本號變動則清空原來存儲的store,不然傳入store給持久化方法persistStore便可。

persistStore(store, [config, callback])

該方法主要實現store的持久化以及分發rehydration action :

  1. 訂閱 redux store,當其發生變化時觸發store存儲操做;
  2. 從指定的StorageEngine(如AsyncStorage)中獲取數據,進行轉換,而後經過分發 REHYDRATE action,觸發 REHYDRATE 過程;

接收參數主要以下:

  1. store: 持久化的store;
  2. config:配置對象
    1. storage:一個 持久化引擎,例如 LocalStorage 和 AsyncStorage;
    2. transforms: 在 rehydration 和 storage 階段被調用的轉換器;
    3. blacklist: 黑名單數組,指定持久化忽略的 reducers 的 key;
  3. callback:ehydration 操做結束後的回調;

恢復啓動

和persisStore同樣,依然是在建立redux store時初始化註冊rehydrate拓展:

// add the autoRehydrate enhancer
if (ReduxPersist.active) {
  enhancers.push(autoRehydrate());
}

該方法實現的功能很簡單,即便用 持久化的數據恢復(rehydrate) store 中數據,它實際上是註冊了一個autoRehydarte reducer,會接收前文persistStore方法分發的rehydrate action,而後合併state。

固然,autoRehydrate不是必須的,咱們能夠自定義恢復store方式:

import {REHYDRATE} from 'redux-persist/constants';

//...
case REHYDRATE:
  const incoming = action.payload.reducer
  if (incoming) {
    return {
      ...state,
      ...incoming
    }
  }
  return state;

版本更新

須要注意的是redux-persist庫已經發布到v5.x,而本文介紹的以v4.x爲準,新版本有一些更新,詳細請點擊查看

持久化與Immutable

前面已經提到Redux與Immutable的整合,上文使用的redux-persist默認也只能處理原生JavaScript對象的redux store state,因此須要拓展以兼容Immutable。

redux-persist-immutable

使用redux-persist-immutable庫能夠很容易實現兼容,所作的僅僅是使用其提供的persistStore方法替換redux-persist所提供的方法:

import { persistStore } from 'redux-persist-immutable';

transform

咱們知道持久化store時,針對的最好是原生JavaScript對象,由於一般Immutable結構數據有不少輔助信息,不易於存儲,因此須要定義持久化及恢復數據時的轉換操做:

import R from 'ramda';
import Immutable, { Iterable } from 'immutable';

// change this Immutable object into a JS object
const convertToJs = (state) => state.toJS();

// optionally convert this object into a JS object if it is Immutable
const fromImmutable = R.when(Iterable.isIterable, convertToJs);

// convert this JS object into an Immutable object
const toImmutable = (raw) => Immutable.fromJS(raw);

// the transform interface that redux-persist is expecting
export default {
  out: (state) => {
    return toImmutable(state);
  },
  in: (raw) => {
    return fromImmutable(raw);
  }
};

如上,輸出對象中的in和out分別對應持久化及恢復數據時的轉換操做,實現的只是使用fromJS()toJS()轉換Js和Immutable數據結構,使用方式以下:

import immutablePersistenceTransform from '../services/ImmutablePersistenceTransform'
persistStore(store, {
  transforms: [immutablePersistenceTransform]
}, startApp);

Immutable

在項目中引入Immutable之後,須要儘可能保證如下幾點:

  1. redux store整個state樹的統一Immutable化;
  2. redux持久化對Immutable數據的兼容;
  3. App Navigation兼容Immutable;

Immutable與App Navigation

前面兩點已經在前面兩節闡述過,第三點過於Navigation兼容Immutable,其實就是使Navigation路由狀態兼容Immutable,在App應用導航與路由一節已經介紹如何將Navigation路由狀態鏈接至Redux store,若是應用使用了Immutable庫,則須要另外處理,將Navigation router state轉換爲Immutable,修改前面提到的NavigationReducer:

const initialState = Immutable.fromJS({
  index: 0,
  routes: [{
    routeName: routes.Login.name,
    key: routes.Login.name
  }]
});

const NavigationReducer = (state = initialState, action) => {
  const newState = state.merge(AppNavigation.router.getStateForAction(action, state.toJS()));
  return newState || state;
};

將默認初始狀態轉換爲Immutable,而且合併state時使用merge()方法。

異步任務流管理

最後要介紹的模塊是異步任務管理,在應用開發過程當中,最主要的異步任務就是數據HTTP請求,因此咱們講異步任務管理,主要關注在數據HTTP請求的流程管理。

axios

本項目中使用axios做爲HTTP請求庫,axios是一個Promise格式的HTTP客戶端,選擇此庫的緣由主要有如下幾點:

  1. 能在瀏覽器發起XMLHttpRequest,也能在node.js端發起HTTP請求;
  2. 支持Promise;
  3. 能攔截請求和響應;
  4. 能取消請求;
  5. 自動轉換JSON數據;

redux-saga

redux-saga是一個致力於使應用中如數據獲取,本地緩存訪問等異步任務易於管理,高效運行,便於測試,能更好的處理異常的三方庫。

Redux-saga是一個redux中間件,它就像應用中一個單獨的進程,只負責管理異步任務,它能夠接受應用主進程的redux action以決定啓動,暫停或者是取消進程任務,它也能夠訪問redux應用store state,而後分發action。

初始化saga

redux-saga是一箇中間件,因此首先調用createSagaMiddleware方法建立中間件,而後使用redux的applyMiddleware方法啓用中間件,以後使用compose輔助方法傳給createStore建立store,最後調用run方法啓動根saga:

import { createStore, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootSaga from '../sagas/'

const sagaMiddleware = createSagaMiddleware({ sagaMonitor });
middleware.push(sagaMiddleware);
enhancers.push(applyMiddleware(...middleware));

const store = createStore(rootReducer, initialState, compose(...enhancers));

// kick off root saga
sagaMiddleware.run(rootSaga);

saga分流

在項目中一般會有不少並列模塊,每一個模塊的saga流也應該是並列的,須要以多分支形式並列,redux-saga提供的fork方法就是以新開分支的形式啓動當前saga流:

import { fork, takeEvery } from 'redux-saga/effects';
import LoginSagas from './LoginSagas';

const sagas = [
  ...LoginSagas,
  ...StartAppSagas
];

export default function * root() {
  yield sagas.map(saga => fork(saga)); 
};

如上,首先收集全部模塊根saga,而後遍歷數組,啓動每個saga流根saga。

saga實例

以LoginSagas爲例,對於登陸這一操做,可能在用戶開始登陸,登陸成功後須要進行一些異步請求,因此列出loginSaga, loginSuccessSaga,另外用戶退出帳戶時也可能須要進行HTTP請求,因此將logoutSaga放在此處:

...

// process login actions
export function * loginSaga () {
  yield takeLatest(LoginTypes.LOGIN, login);
}

export function * loginSuccessSaga () {
  yield takeLatest(LoginTypes.LOGIN_SUCCESS, loginSuccess);
}

export function * logoutSaga () {
  yield takeLatest(LogoutTypes.LOGOUT, logout);
}

const sagas = [
  loginSaga,
  loginSuccessSaga,
  logoutSaga
];

export default sagas;

在loginSaga內使用takeLatest方法監聽LoginTypes.LOGINaction,當接收到該action時,調用login,login本質上仍是一個saga,在裏面處理異步任務:

function * login (action) {
  const { username, password } = action.payload || {};

  if (username && password) {
    const res = yield requestLogin({
      username,
      password
    });

    const { data } = res || {};

    if (data && data.success) {
      yield put(LoginActions.loginSuccess({
        username,
        password,
        isLogin: true
      }));
    } else {
      yield put(LoginActions.loginFail({
        username,
        password,
        isLogin: false
      }));
    }
  } else {
    yield put(LoginActions.loginFail({
      username,
      password,
      isLogin: false
    }));
  }
}

requestLogin方法就是一個登陸HTTP請求,用戶名和密碼參數從LoginTypes.LOGINaction傳遞的負載取得,yield語句取回請求響應,賦值給res,隨後經過響應內容判斷登陸是否成功:

  1. 登陸成功,分發LoginActions.loginSuccessaction,隨後將執行監聽此action的reducer及loginSuccessSagasaga;
  2. 登陸失敗,分發LoginActions.loginFailaction;

put是redux-saga提供的可分發action方法。

saga與Reactotron

前面已經配置好可使用Reactotron捕獲應用全部redux和action,而redux-saga是一類redux中間件,因此捕獲sagas須要額外配置,建立store時,在saga中間件內添加sagaMonitor服務,監聽saga:

const sagaMonitor = Config.useReactotron ? console.tron.createSagaMonitor() : null;
const sagaMiddleware = createSagaMiddleware({ sagaMonitor });
middleware.push(sagaMiddleware);
...

總結

本文較詳細的總結了我的從0到1搭建一個項目架構的過程,對React,React Native, Redux應用和項目工程實踐都有了更深的理解及思考,在大前端成長之路繼續砥礪前行。

完整代碼見github

參考

  1. react native
  2. react native中文網
  3. react navigation
相關文章
相關標籤/搜索