在上一篇介绍了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
package.json
为项目依赖管理文件;index.ios.js
为ios平台入口文件,index.android.js
为android平台入口文件,一般用来注册React Native App根组件;.babelrc
文件,babel的配置文件,React Native默认使用babel编译JavaScript代码;__tests__
项目测试目录。咱们看到并无存放React Native原生JavaScript代码的目录,这须要咱们本身进行建立了,一般建立一个src
目录做为App应用Javascript部分全部代码和资源的根目录,一个src/constants
目录以保存全局共享常量数据,一个src/config
目录保存全局配置,一个src/helpers
存放全局辅助,工具类方法,一个src/app.js
做为RN部分入口文件,另外一般还须要建立保存各模块redux的目录,redux中间件的目录等。android
项目架构搭建很大部分依赖于项目的技术栈,因此先对整个技术栈进行分析,总结:ios
根据以上划分决定选用如下第三方库和工具构成项目的完整技术栈:git
针对以上分析,完善后的项目结构如图:github
如上图,在项目根目录下建立src
目录,而在src目录中依次建立12个目录与1个React Native部分入口js文件。
React Native App开发目前已经有诸多调试工具,经常使用的如atom和Nuclide,移动端模拟器自带的调试工具,Reactron等。
Nuclide是由Facebook提供的基于atom的集成开发环境,可用于编写、运行和 调试React Native应用。
在模拟器启动运行App后,浏览器会自动打开 http://localhost:8081/debugger-ui
页,能够在控制台进行js调试输出及远端js断点调试;在模拟器终端使用快捷键command
加D
键便可打开调试工具,包括从新加载应用,开启热加载,切换DOM审视器等:
Reactotron是一款跨平台调试React及React Native应用的桌面应用,能动态实时监测并输出React应用等redux,action,saga异步请求等信息,如图:
首先初始化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,一般遵循如下组件设计思路:
展现型组件 | 容器组件 | |
---|---|---|
目标 | 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提供的更好方式,后文介绍。
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会检测该文件是否存在.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, });
不一样于React应用的单页面路由,React Native一般都是多页面形式存在,以导航方式在不一样页面和组件间切换,而不是路由方式控制不一样组件展现,最常使用的是react-navigation导航库。
在React web应用中,页面UI组件展现和切换彻底由路由控制,每个路由都有对应的URL及路由信息,在React Native应用则不是由路由驱动组件展现,而是由导航控制切换屏展现,每一屏有各自的路由信息。
或许你已经依赖react-router的单页面应用路由配置方式,但愿建立一个Url驱动的跨平台App应用,托福于活跃的的开源社区,你可使用react-router-native,可是并不推荐,由于对于App而言,从交互和体验考虑,仍是更适合使用多页面(屏)形式。
使用react-navigation能够定义跨平台的应用导航结构,也支持配置渲染跨平台的导航栏,tab栏等组件。
react-navigation提供如下几个方法支持建立不一样的导航类型:
StackNavigator支持跨平台以变换方式切换不一样屏,而且将当前屏放置在栈顶,调用方式以下:
StackNavigator(RouteConfigs, StackNavigatorConfig)
导航栈路由(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 };
路由配置对象,能够选择性配置可选属性,如:
initialRouteName
,初始导航栈默认屏,必须是路由配置对象中的某一键名;initialRouteParams
,初始路由的默认参数;navigationOptions
,设置默认的导航屏配置;
headerMode
,是否显示顶部导航栏:
mode
,导航切换屏时的样式和变换效果:
card
:默认方式,标准的屏变换;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能够建立一屏,拥有TabRouter能够切换不一样Tab,调用方式如:
TabNavigator(RouteConfigs, TabNavigatorConfig)
Tab路由配置对象,格式相似StackNavigator。
Tab导航相关配置对象,如:
TabBarBottom
组件,android平台默认使用TabBarTop
组件;top
或bottom
;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(RouteConfigs, DrawerNavigatorConfig)
const MyDrawer = DrawerNavigator({ Home: { screen: MyHomeDrawerScreen, }, Notifications: { screen: MyNotificationsDrawerScreen, }, });
抽屉式导航路由配置对象,格式相似StackNavigator。
抽屉式导航屏配置对象,如:
left
或right
;DrawerItems
;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' })
RN应用的每一屏将接受一个navigation属性包含如下方法和属性:
使用navigate方法导航至其余屏:
navigate(routeName, params, action)
改变当前导航路由信息,如设置修改导航标题等信息:
class ProfileScreen extends React.Component { render() { const { setParams } = this.props.navigation; return ( <Button onPress={() => setParams({name: 'Jh'})} title="Set title" /> ) } }
从当前屏(参数为空)或者指定屏(参数为屏路由键名)导航回退至该屏的上一屏,而且关闭该屏;若传递null
参数,则未指定来源屏,即不会关闭屏。
每一屏都有本身的路由信息,能够经过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' } }
该方法用来分发导航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);
在使用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。
现代的任何大型web应用若是少了状态管理容器,那这个应用就缺乏了时代特征,可选的库诸如mobx,redux等,实际上大同小异,各取所需,以redux为例,redux是最经常使用的react应用状态容器库,对于React Native应用也适用。
和React应用同样,须要将Redux和应用链接起来,才能统一使用redux管理应用状态,使用官方提供的react-redux库。
class App extends Component { render () { return ( <Provider store={store}> <AppContainer /> </Provider> ) } }
使用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默认提供了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()
,保证状态统一可预测。
咱们知道浏览器默认有资源的缓存功能而且提供本地持久化存储方式如localStorage,indexDb,webSQL等,一般能够将某些数据存储在本地,在必定周期内,当用户再次访问时,直接从本地恢复数据,能够极大提升应用启动速度,用户体验更有优点,对于App应用而言,本地持久化一些启动数据甚至离线应用更是常见的需求,咱们可使用AsyncStorage(相似于web的localStorage)存储一些数据,若是是较大量数据存储可使用SQLite。
另外不一样于以往的直接存储数据,启动应用时本地读取而后恢复数据,对于redux应用而言,若是只是存储数据,那么咱们就得为每个reducer拓展,当再次启动应用时去读取持久化的数据,这是比较繁琐并且低效的方式,是否能够尝试存储reducer key,而后根据key恢复对应的持久化数据,首先注册Rehydrate reducer,当触发action时根据其reducer key恢复数据,而后只须要在应用启动时分发action,这也很容易抽象成可配置的拓展服务,实际上三方库redux-persist已经为咱们作好了这一切。
要实现redux的持久化,包括redux store的本地持久化存储及恢复启动两个过程,若是彻底本身编写实现,代码量比较复杂,可使用开源库redux-persist
,它提供persistStore
和autoRehydrate
方法分别持久化本地存储store及恢复启动store,另外还支持自定义传入持久化及恢复store时对store state的转换拓展。
以下在建立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 :
接收参数主要以下:
和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为准,新版本有一些更新,详细请点击查看。
前面已经提到Redux与Immutable的整合,上文使用的redux-persist默认也只能处理原生JavaScript对象的redux store state,因此须要拓展以兼容Immutable。
使用redux-persist-immutable库能够很容易实现兼容,所作的仅仅是使用其提供的persistStore
方法替换redux-persist所提供的方法:
import { persistStore } from 'redux-persist-immutable';
咱们知道持久化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之后,须要尽可能保证如下几点:
前面两点已经在前面两节阐述过,第三点过于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做为HTTP请求库,axios是一个Promise格式的HTTP客户端,选择此库的缘由主要有如下几点:
redux-saga是一个致力于使应用中如数据获取,本地缓存访问等异步任务易于管理,高效运行,便于测试,能更好的处理异常的三方库。
Redux-saga是一个redux中间件,它就像应用中一个单独的进程,只负责管理异步任务,它能够接受应用主进程的redux action以决定启动,暂停或者是取消进程任务,它也能够访问redux应用store state,而后分发action。
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流也应该是并列的,须要以多分支形式并列,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。
以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.LOGIN
action,当接收到该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.LOGIN
action传递的负载取得,yield
语句取回请求响应,赋值给res,随后经过响应内容判断登陆是否成功:
LoginActions.loginSuccess
action,随后将执行监听此action的reducer及loginSuccessSaga
saga;LoginActions.loginFail
action;put是redux-saga提供的可分发action方法。
前面已经配置好可使用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应用和项目工程实践都有了更深的理解及思考,在大前端成长之路继续砥砺前行。