react-navigation 使用与原理

1. 整体简介

如果你要使用 react-native 开发一款完整的应用,那么肯定需要选择一款完善导航组件来串联各个页面。react-navigation 是 facebook 官方正式推荐的导航组件,也是目前最可靠的选择。

2. 基本用法

要讲 react-navigation 的使用方法,就需要先从了解 Navigator 导航器开始。其内建导航器有以下几种:

  1. StackNavigator 是普通的新老页面切换

  2. TabNavigator 是底部工具栏

  3. DrawerNavigator 是抽屉式的侧边栏

我们常见的应用页面之间的组织方式基本都是基于以上导航器之间的配合使用。

2.1 Navigator 使用方法

任何一个应用都离不开页面的切换,所以我们先以 StackNavigator() 为例介绍一下 Navigator 的使用方法。

2.1.1 StackNavigator() 的参数

StackRouter(routeConfigMap, stackRouterConfig) 需要输入两个参数,第一个参数是的路由对象:routeConfigMap,第二个参数可以设置的参数:stackRouterConfig。

如果我们把整个应用比喻成一本书,那么这个路由对象就像一个目录,而参数设置就像是可以选择的装订方式和排版方式。

参数一:routeConfigMap 的类型和来源

观察以下样例代码我们可以发现 routeConfigMap 的类型是一个 map 对象,value 值就是页面组件。

import MyHomeScreen from './MyHomeScreen';
import MyProfileScreen from './MyProfileScreen';

const routeConfigMap = {
    Home: {screen: MyHomeScreen},
    Profile: {screen: MyProfileScreen},
}

参数二:stackRouterConfig 的类型和来源

观察以下样例代码我们可以发现 stackRouterConfig 的类型是一个 map 对象,有诸多属性可以通过配置这个参数来改变 StackNavigator() 的特征。具体每一参数设置的效果可以通过查阅 react-navigation 的官方文档了解详细内容

const stackConfig = {
initialRouteName,
initialRouteParams,
paths,
headerMode,
mode,
cardStyle,
transitionConfig,
onTransitionStart,
onTransitionEnd,
navigationOptions,
}

2.1.2 在 react-native 当中的使用

从下图的代码中我们可以看出,开发者只要将页面组织好以后作为第一个参数传入 StackNavigator() ,然后设置好的它的配置项作为第二个参数传进去,最后通过 AppRegistry.registerComponent() 注册到 react-native 上就可以了。

...
import MyHomeScreen from './MyHomeScreen';

const SimpleApp = StackNavigator(
// 参数一
    {
    // routeName 是 Home ,对应的页面组件是 MyHomeScreen
    Home: {screen: MyHomeScreen},
    },
// 参数二
    {
        animationEnabled: false,
        swipeEnabled: false,
    }
);

AppRegistry.registerComponent('SimpleApp', () => SimpleApp);

2.2 Navigator 常见的构建应用方法

前面比较抽象的介绍了关于 Navigator 用法,接下来我们详细阐述一下如何在实际的项目中使用它来构建应用。首先,我们定一个明确而且有代表性的目标比如:使用内建的 Navigator 配合来创建一个和微信类似的应用,如下图所示。

demo

2.2.1 组织页面的方式

我们分解一下基本需求:

  • 需要有 Login 页面,如果用户行为需要登录应用可以自动跳出此页面。
  • 底部需要有 Tabbar,可承载诸如:Home、Search、Like、Account
  • 点击首页的 cell 可以进入详情页面 Detail,且顶部有当行条。

我们通过上述的需求可以简单整理成一个图,具体如下:

screen_pages

根据上述图文,我们可以用一下代码来实现效果

// 引入两个常用的导航器
import {
    StackNavigator,
    TabNavigator,
} from 'react-navigation';
// 引入各个页面
import HomeScreen from './screen/HomeScreen';
import Detail from './screen/Detail';
import Account from './screen/Account';
import Login from './screen/Login';
import Search from './screen/Search';
import Like from './screen/Like';

// 把部分页面用装入 TabNavigator 导航器,并且设置好配置项,TabNavigator() 方法会返回一个
// 带有各种路由状态和参数的 Component。这种 Component 是一个 NavigationView
const HomeNavigator = TabNavigator({
    Home: {screen: HomeScreen},
    Search: {screen: Search},
    Like: {screen: Like},
    Account: {screen: Account},
}, {
    headerMode: 'none',        // NavBar 去掉顶部导航,因为别的 stack 已经有了
    tabBarPosition: 'bottom',  // 让 Android tabs 也放在底下
    tabBarOptions: {
        showIcon: 'true', // 底部工具栏显示 icon
        showLabel: false, // 底部工具栏不显示文字
        style: {
            backgroundColor: 'white'
        }
    },

});

// 把 TabNavigator 构建完的 HomeNavigator 对象作为一个子路由放到 StackNavigator 里面,
// 再构建一个 NavigationView , 这样当前 route 是 Home 的时候是有 TableBar 的,因为子路由是由 TabNavigator 构建的
const MainNavigator = StackNavigator({
        Home: {screen: HomeNavigator},
        Detail: {screen: Detail}
    },
);

// 这边把 Login 页面放在最外面,在未登录状态下可以弹出登录页面
export const AppNavigator = StackNavigator({
        Main: {screen: MainNavigator},
        Login: {screen: Login},
    }, {
        headerMode: 'none',
        mode: 'modal',
        navigationOptions: {
            gesturesEnabled: false,
        }
    }
);

AppRegistry.registerComponent('AppNavigator', () => AppNavigator);

2.3 如何使用 Navigator 让页面有序流转起来

每一个被 Navigator 组织起来的页面都会接收到一个 navigation 的属性,这个属性主要包含一下这些方法,以下这些方法组合使用就可以有序的组织页面的流转。

  1. navigate(routeName, params, action) 用于跳转到其他也页面,可以带有参数以及子路由(路由嵌套的内容,后面会详细说明)的 action

     class HomeScreen extends React.Component {
       render() {
         //从 navigation 中获取 navigate 方法
         const {navigate} = this.props.navigation;
         return (
           <View>
             <Text>This is the home screen of the app</Text>
             <Button
             // routeName 是 Profile 参数是 name
               onPress={() => navigate('Home')}
             />
           </View>
          )
        }
     }
    
  2. state 帮助开发者获取页面当前状态和路径

     {
       // the name of the route config in the router
       routeName: 'profile',
       //a unique identifier used to sort routes
       key: 'main0',
       //an optional object of string options for this screen
       params: { hello: 'world' }
     }
    
     class ProfileScreen extends React.Component {
       render() {
         const {state} = this.props.navigation;
         // state.routeName === 'Profile'
         return (
           <Text>Name: {state.params.name}</Text>
         );
       }
     }
    
  3. setParams 改变路由当中的参数

     class ProfileScreen extends React.Component {
       render() {
       // 改变参数名字
         const {setParams} = this.props.navigation;
         return (
           <Button
             onPress={() => setParams({name: 'Lucy'})}
             title="Set title name to 'Lucy'"
           />
          )
        }
     }
    
  4. goBack 关掉当前页面返回上一个

     class HomeScreen extends React.Component {
       render() {
         const {goBack} = this.props.navigation;
         return (
           <View>
             <Button
               onPress={() => goBack()}
               title="Go back from this HomeScreen"
             />
             <Button
               onPress={() => goBack(null)}
               title="Go back anywhere"
             />
             <Button
               onPress={() => goBack('screen-123')}
               title="Go back from screen-123"
             />
           </View>
          )
        }
     }
    
  5. dispatch 发送一个 action 到 router,这个方法的实现在后面会详细介绍。

     import { NavigationActions } from 'react-navigation'
    
     const navigateAction = NavigationActions.navigate({
       routeName: 'Profile',
       params: {},
    
       // navigate can have a nested navigate action that will be run inside the child router
       action: NavigationActions.navigate({ routeName: 'SubProfileRoute'})
     })
     this.props.navigation.dispatch(navigateAction)
    

2.4 Navigator 的使用小结

我们通过部分的样例代码简单说明了 Navigator 组织页面的方式、在工程中的构建、以及其属性方法的使用方式。 但是这部分说明不能代替官方文档的作用,在实际的开发项目的过程中请详细阅读官方文档

3. 实现原理

通过上面的使用方式介绍,我们得知使用 react-navigation 这个导航组件,开发者只要输入 routeConfigMap 和 stackConfig 两个参数,Navigator 就可以将这些页面组织到一个路由体系当中。这些页面都有 navigation 属性,通过 navigation 对象下的各个方法,可以做到页面之间的跳转以及传参等常规行为。

那么如此强大的 Navigator 是怎么做到的呢,下面我们来详细的了解一下它的实现。

3.1 整体原理说明

react-navigation 这个组件在设计的时候借鉴了 redux 的设计思路。简单来说就是,当用户有一个操作行为比如点击一个按钮,这时候会发出一个 action ,这个 action 又会去改变 nav 的 state。nav 的 state 又可以改变 component 的 props ,当 react component 检测出 props 的变化,就会自动触发一次新的 render 行为,那么这时候新的页面就被渲染出来了。

如果一次页面的跳转是按照如上述所说,那么首先要想两个问题:

  1. Navigator 的构成
  2. 导航栏的状态 state 是如何产生,如何变化的。
  3. state 的变化是如何驱动页面发送跳转的。

我们带着以上两个问题去深入探究一下 react-navigation 的原理

3.2 Navigator 的构成

要 以 StackNavigator 为例介绍

Navigto

仔细观察观察我们会发现 StackNavigator 的实现中有三个重要的方法,他们分别是:

  1. StackRouter()
  2. createNavigator()
  3. createNavigationContainer()

先说说这几个方法的参数、返回结果以及作用。

3.2.1 StackRouter 简要说明

StackRouter() 传入两个参数,第一个是诸多页面的 map 对象,第二个是相关的设置选项。那么返回的是一个类型为 NavigationRouter 的对象:router。这个对象是里面有六个核心方法。

  1. getStateForAction(action, state)更加操作改变状态
  2. getComponentForRouteName(routeName) 根据 routeName 获得对应要渲染的组件
  3. getComponentForState(state) 根据状态变化获得要渲染的组件
  4. getActionForPathAndParams(path, params) 根据路径和参数的变化来驱动新的 action
  5. getPathAndParamsForState(state) 根据状态来获得路径和参数
  6. getScreenOptions(navigation, screenProps) 获取 screen 的一些选项

可以说 StackRouter 返回的 router 赋予了 screen 更改 nav 状态的能力,而且也为实现路由嵌套(后续说明)提供的可能性。

3.2.2 createNavigator 简要说明

createNavigator() 方法需要出传入四个参数:

  1. router
  2. routeConfigMap
  3. stackConfig
  4. NavigatorTypes.STACK

createNavigator() 会先返回一个函数,这个函数需要一个 NavigationView 作为参数传入。最后返回一个叫做 Navigator 的 react component。

传入的四个参数变成了 Navigator 的四个属性,最重要的是 router 被作为 props 传给了 NavigationView。

3.2.3 createNavigationContainer() 简介

createNavigationContainer() 我们可以从方法名字看出他的功能:创建一个导航栏 Navigator 的容器,它的主要的作用是:如果外部传入了 navigation 属性,NavigationContainer 就不做任何事情,就要直接渲染出 Component 并把属性往下传递。如果没有 navigation 属性,则自己充当 container,派发 Action,管理 State,刷新 Navigator。

3.3 导航栏的状态管理

how_to_control_state

以上这张图是 navigation 状态管理以及流转的示意图,下面简要介绍一下这个过程。

3.3.1 state 是如何生成的

从上面的描述我们可以知道 react-navigation 只有 root stack 顶层一个状态,也就是说无论 stack 怎么嵌套,导航栏的更新都是依据顶部那个状态的变化。

所以如果这个 Navigator 是 root stack 也就是那个顶层的根路由,那么需要初始化构造一个 state 来描述导航的状态,然后通过 setState 方法来更新 state 来改变导航栏状态。

getStateForAction() 是在 router 的属性,在创建 Navigator 的时候被传进来,上图通过这个方法初始化了 state

3.3.2 state 是如何被改变的

由于在导航中的任何一个页面都可以改变 react-navigation 的状态,所以改变 state 的方法需要传给每一个 component。所以在 createNavigationContainer()中,通过 dispatch 方法把 getStateForAction 和 setState() 包装起来了,然后和 state 一起通过 addNavigationHelpers() 方法生成 navigation 作为一个 props 传给每一个 component。这样,任意一个页面通过 this.props.navigation 的产生对应的 action 就会引发 react-navigation 状态的变化。

3.4 react-navigation 如何驱动页面流转

我们现在已经知道页面的状态可以很好的被管理在根节点的 NavigationContainer 里面,但是新的疑问是这些状态的变化是怎么导致页面跳转的。

state_how_to_render_screen

观察上面的图我们会发现 state 作为 navigation 的属性传入的 NavigationView 的 props 里面。在 这个组件中通过 NavigationSencesReducer 方法获得 scenes。这个 scenes 其实是这个 stack 中 component 的的序列。

scenes 作为 props 传入子组件中,然后通过 getComponentForRouteName(scene.route.routeName) 获取 scene 对应的 component ,然后渲染出来。

当然这里要说明的是每一次 scene 对应的 component 的变化都是出发 react 的 diff 算法,导致 component 重新渲染。所以这个流程是,state 驱动了 scenes 的变化,scenes 驱动了页面的刷新。

3.5 路由嵌套是怎么处理的

react-navigation 的强大之处在于各个导航器的 stack 可以随意嵌套组合,以适应各式各样的使用场景,这也是 react-navigation 最难以理解的地方。

要了解这个问题我们需要先整理清楚思路:

  1. 路由的结构是怎么样的。
  2. 路由选择的策略是什么样的。

3.5.1 路由的解析

stack

上面的结果告诉了我们路由的结构特征是怎么样的,而且我们还发现:

  1. stack 被解析成了数组
  2. 页面是 Obj
  3. index 用来表明 active 的页面

以上这些特征的在 router 的实现里面有深刻的体现,开发者自己深入解读源码的时候需要注意。

3.5.2 routor 是处理嵌套的原则是什么

  1. 在处理 action 时会先判断该 action 不是 root stack 的 reset 操作时,找到指定的router(找到 action.key 对应的 router,当 action.key 无效时找到 activited router )去处理该 action,如果指定的 router 处理了这个 action(返回 null 或者 route!==childRoute )则在 state 中替换对应的 route。
  2. 如果指定(通过 key 或者 activited router)的 router 不处理 action ,则判断 action.routeName 有没有在 routeConfig 中注册过,对于这种直接注册的,就直接 push 就好。
  3. 如果 action.routeName 没有被注册过,则遍历所有子路由去尝试处理 action,一旦有子路由去处理了,则直接 push 这个处理结果。

(补充一个流程图)

3.6 动画

3.6.1 动画的实现

3.6.2 动画的配置

3.6.3 自定义动画

3.7 原理小结

说到这里我们基本解释清楚了 StackNavigator 方法是怎么通过 StackRouter() createNavigator() createNavigationContainer() 三个方法实现一个导航器的功能,Navigator 是怎么管理导航栏的状态的,状态又是怎么驱动页面变化的,流转动画是怎么实现的需要怎么自定义。

这样子整体的介绍 react-navigation 的原理对后续的使用会有很大的帮助。

results matching ""

    No results matching ""