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
相关文章
相关标签/搜索