容器与导航

本库内置 drawer、tabs、stack、screen 四种布局对象,前三种也被称为容器对象,因为它们可以容纳其它布局对象。

Navigator 是一个主管导航的类,它有一些静态(类)方法:

setRoot

setRoot(layout, sticky = false) 设置应用的 UI 层级。

hybrid-navigation 一共有内置四种布局: screen, stack, tabs 以及 drawer。最简单的布局只有一个页面,我们用 screen 来表示,它不可以包含其它布局对象。

Navigator.setRoot({
  screen: {
    moduleName: 'Navigation', // required
    props: {},
    options: {},
  },
})

screen 布局对象有三个属性,分别是 moduleName, props, options,其中 moduleName 是必须的,它就是我们上面注册的那些模块名,props 是我们要传递给该页面的初始属性,options 的类型是 NavigationItem,通常不会设置这个参数,而是通过静态配置页面来定制页面样式。

如果需要使用 push 或 pop 等导航功能在不同页面之间进行切换,那么我们需要 stack。

stack 布局对象有两个属性,分别是 children, options,其中 children 是必须的,它是一个表示布局对象的数组,options 用来配置 stack 的其它属性,暂时没有什么用。

Navigator.setRoot({
  stack: {
    children: [{ screen: { moduleName: 'Navigation' } }], // required
    options: {},
  },
})

注意:stack 中 不可嵌套 stack

如果我们需要像微信那样,底部有几个 tab 可以切换,那么我们需要用到 tabs。

tabs 布局对象有 children, options 两个属性,其中 children 是必须的,它是一个表示布局对象的数组,options 用来配置 tabs 的其它属性。

Navigator.setRoot({
  tabs: {
    children: [
      {
        stack: {
          children: [{ screen: { moduleName: 'Navigation' } }],
        },
      },
      {
        stack: {
          children: [{ screen: { moduleName: 'Options' } }],
        },
      },
    ],
    options: {
      selectedIndex: 1, // 默认选中的 tab
      tabBarModuleName: 'CustomTabBar',
      sizeIndeterminate: true, // 如果希望自定义 TabBar 实现中间凸起效果,必须指定这个字段。
    },
  },
})

tabs 有一个默认的 TabBar, 支持未读消息数,小红点提示等功能。你也可以通过在 options 中传递 tabBarModuleName 来实现自定义 TabBar,但此时未读消息数,小红点提示等功能均需要你自己实现。如何自定义 TabBar, 请参考自定义 TabBar

WARNING

tabBarModuleName 指定的组件也需要通过 ReactRegistry.registerComponent 进行注册

抽屉(drawer) 起源于 Android,不过我们在 iOS 也支持了它。

drawer 布局对象有 children, options 两个属性,其中 children 是必须的,它是一个表示布局对象的数组,长度必须为 2. options 用来配置 drawer 的其它属性。

Navigator.setRoot({
  drawer: {
    children: [
      {
        stack: {
          children: [{ screen: { moduleName: 'Navigation' } }],
        },
      },
      {
        { screen: { moduleName: 'Menu' } },
      },
    ],
    options: {
      maxDrawerWidth: 240, // Menu 的最大宽度
      minDrawerMargin: 60, // Menu 右侧需要预留的最小空间
      menuInteractive: true // 是否可以通过手势打开 menu
    },
  },
});

至此,四种内置布局就已经介绍完成了,我们也可以自定义布局(容器)和导航,deckopen in new window 就是一个自定义容器,不过这是比较高级的话题了。

TIP

可以先通过 Navigator.setRoot 设置一个入口页面,然后根据应用状态再次调用 Navigator.setRoot 决定要进入哪个页面。

TIP

Navigator.setRoot 还接受第二个参数,是个 boolean,用来决定 Android 按返回键退出 app 后,再次打开时,是否恢复到首次将该参数设置为 true 时的那个 layout。通常用来决定按返回键退出 app 后重新打开时,要不要走闪屏逻辑。请参考 iReading Forkopen in new window 这个项目对 Navigator.setRoot 的使用

setRootLayoutUpdateListener

setRootLayoutUpdateListener(willSetRoot = () => {}, didSetRoot = () => {}) 监听 setRoot 的调用,在前后插入相应逻辑。

dispatch

dispatch(sceneId, action, params): Promise<boolean> 派发导航操作,返回一个 Promise,表示是否成功派发。

大多数导航操作,譬如 pushpop 都是转发给该方法完成,也可以直接使用,尤其是自定义了容器和导航之后。

// 以下两行代码的效果是等同的
Navigator.dispatch(this.props.sceneId, 'push', { moduleName: 'Profile' })
this.props.navigator.push('Profile')

setInterceptor

setInterceptor((action, from, to, extras) => {}) 拦截 dispatch 的操作,可以插入一些横切面逻辑。

Navigator.setInterceptor((action, from, to, extras) => {
  console.info(`action:${action} from:${from} to:${to}`)
  // 当返回 true 时,表示你要拦截该操作
  // 譬如用户想要跳到的页面需要登录,你可以在这里验证用户是否已经登录,否则就重定向到登录页面
  return false
})

extras 中有我们需要的额外信息。譬如 sceneId,它表示动作发出的页面, 通过 Navigator.of(sceneId) 可以获取该页面的 navigator。如果 action 是 switchTab,我们还可以从 extras 中获取 index 这个属性,它表示将要切换到的 tab 的位置,从 0 开始。

of

of(sceneId): Navigator 接受 sceneId 作为参数,返回一个已经存在的 Navigator 实例

this.props.navigator === Navigator.of(this.props.sceneId)
// true

find

find(moduleName): Promise<Navigator> 接受 moduleName 作为参数,返回一个已经存在的 Navigator 实例

有时候,你想知道某个页面存不存在,这个方法就很方便了。

current

current(): Promise<Navigator> 返回当前有效的 navigator,通常是用户当前可见的那个页面的 navigator

const navigator = await Navigator.current()
this.props.navigator === navigator
// true

currentRoute

currentRoute(): Promise<Route> 获取当前路由信息

import { Navigator } from 'hybrid-navigation'

const route = await Navigator.currentRoute()

// {
//   sceneId: 'xxxxxxxx',
//   moduleName: 'Name'
//   mode: 'modal'
// }

const navigator = Navigator.of(route.sceneId)

以上操作等同于

const navigator = await Navigator.current()

routeGraph

routeGraph(): Promise<RouteGraph[]> 有时,我们不光需要知道当前正处于哪个页面,还需要知道当前整个 UI 层级或者说路由图

import { Navigator } from 'hybrid-navigation';

const graph = await Navigator.routeGraph();
console.info(graph);

const sceneId = // 通过 graph 抽取出我们想要的 sceneId

const navigator = Navigator.of(sceneId);

graph 是一个数组,它长下面这个样子

;[
  {
    layout: 'drawer',
    sceneId: '',
    children: [], // 又是一个 graph 数组
    mode: '', // modal, nornal, present,表示该页面是通过 prensent、showModal 或者其它方式显示
  },

  {
    layout: 'tabs',
    sceneId: '',
    children: [],
    mode: '',
    state: { selectedIndex: 1 },
  },

  {
    layout: 'stack',
    sceneId: '',
    children: [],
    mode: '',
  },

  {
    layout: 'screen',
    sceneId: '36d60707-354e-4f87-a790-20590261500b',
    moduleName: 'Navigation',
    mode: '', // modal, present, normal
  },
]

Navigator.routeGraph 帮助我们获得整张路由图,它是实现 DeepLink 的基础。本库已经提供了 DeepLink 的默认实现。

下面,我们开始介绍实例方法:

Screen

screen 是最基本的页面,它用来表示通过 ReactRegistry.registerComponent 注册的组件。它有一些基本的导航能力,所有容器均继承了这些能力。

present

present(moduleName, prop, options): Promise<[number, Result]> 是一种模态交互方式,类似于 Android 的 startActivityForResult,要求被 present 的页面返回结果给发起 present 的页面。在 iOS 中,present 表现为从底往上弹出界面。

比如 A 页面 present 出 B 页面

// A.js
navigator.present('B')

B 页面通过 setResult返回结果给 A 页面

// B.js
navigator.setResult(RESULT_OK, { text: 'greeting' })
navigator.dismiss()

注意:仅支持返回可以序列化为 json 的对象,不支持函数

A 页面通过实现 async-await 的方式来接收结果

// A.js
const [resultCode, data] = await navigator.present('B')
if (resultCode === RESULT_OK) {
  this.setState({ text: data.text || '', error: undefined })
}

A 在 present B 时,可以通过第二个参数传值给 B

// A.js
navigator.present('B', {})

B 页面可以通过 props 来获取传递的值

WARNING

第二个参数仅支持可以序列化为 json 的对象,不支持函数

有些时候,比如选择一张照片,我们先要跳到相册列表页面,然后进入某个相册选择相片返回。这也是没有问题的。

A 页面 present 出相册列表页面

//A.js
navigator.present('AlbumList')

相册列表页面 push 到某个相册

// AlbumList.js
navigator.push('Album')

在相册页面选好相片后返回结果给 A 页面

// Album.js
navigator.setResult(RESULT_OK, { uri: 'file://...' })
navigator.dismiss()

A 页面通过实现 async-await 的方式来接收结果(略)。

presentLayout

presentLayout(layout: Layout): Promise<[number, Result]> present 的加强版,通过传递一个布局对象,用来 present UI 层级比较复杂的页面,同样使用 dismiss 来关闭。

// A.js
navigator.presentLayout(
  {
    stack: {
      children: { screen: { moduleName: 'B' } },
    },
  },
  REQUEST_CODE,
)

以上效果实际等同于:

// A.js
navigator.present('B')

也就是说,present 出来的组件,默认会嵌套在 stack 里面,因为当使用 present 时,把目标页面嵌套在 stack

setResult

setResult(resultCode, data) 设置返回结果。结果会延迟到页面关闭时传递,因此在页面关闭之前,可以多次调用,以最后一次调用为准。

在调用 dismisshideModalpop 等关闭页面的方法后,结果会被传递到目标页面。

dismiss

dismiss() 关闭 present 出来的页面,如果该页面是容器,可以在容器的任何子页面调用此方法。

WARNING

在调用 navigator.dismiss 后,该 navigator 将会失效,不要再使用该 navigator 执行任何导航操作。

showModal

showModal(moduleName, props, options): Promise<[number, Result]>

将 React.Component 作为 Modal 显示,用来取代官方的 Modal 组件,比较适合做透明弹窗。

showModal 的底层实现和 present 基本相同。类似于 iOS 上 UIModalPresentationOverFullScreenUIModalPresentationCurrentContext 的区别。

navigator.showModal('ReactModal')

可以通过第二个参数来给 modal 传递属性:

navigator.showModal('ReactModal', { x: '123' })

modal 通过 props 来获取传递过来的属性

modal 在关闭前通过以下方式设置返回值:

navigator.setResult(resultCode, data)
navigator.hideModal()

目标页面(即将 modal 显示出来的页面)可以通过 async-await 的方式来接收结果:

const [resultCode, data] = await navigator.showModal('ReactModal')

WARNING

如果遭遇到 Android 生命周期噩梦,请使用 React.ContextRedux 等方案来获取结果。

showModalLayout

showModalLayout<T>(layout: Layout): Promise<[number, T]> showModal 的加强版,可以将布局对象作为 Modal 显示,同样使用 hideModal 来关闭。

hideModal

hideModal() 隐藏作为 Modal 显示的页面,如果 Modal 是一个容器,可以在该容器的任何子页面调用此方法。

WARNING

在调用 navigator.hideModal 后,该 navigator 将会失效,不要再使用该 navigator 执行任何导航操作。

Stack

stack 以栈的方式管理它的子页面,它支持 pushpop 等操作。

push

push(moduleName, props, options): Promise<[number, Result]> 打开一个新的页面,等同于 iOS 的 push 操作。

由 A 页面跳转到 B 页面。

// A.js
navigator.push('B')

可以通过第二个参数来传值给 B 页面,B 页面通过 props 来访问传递过来的值。

// A.js
navigator.push('B', {...});

WARNING

第二个参数只支持可以序列化为 json 的对象,不支持函数

pushLayout

pushLayout(layout: Layout): Promise<[number, Result]> push 加强版,通过传递一个布局对象,展示 UI 层级比较复杂的页面。

pop

pop() 返回到前一个页面。比如你由 A 页面 push 到 B 页面,现在想返回到 A 页面。

// B.js
navigator.pop()

popTo

popTo(moduleName: string) 返回到之前的指定页面。比如你由 A 页面 push 到 B 页面,由 B 页面 push 到 C 页面,由 C 页面 push 到 D 页面,现在想返回 B 页面,只需要 popTo('B') 即可

从 B 页面跳到 C 页面

// B.js
navigator.push('C')

从 C 页面跳到 D 页面

// C.js
navigator.push('D')

现在想从 D 页面返回到 B 页面

// D.js
navigator.popTo('B')

popToRoot

popToRoot() 返回到 Stack 根页面。比如 A 页面是根页面,由 A 页面 push 到 B 页面,由 B 页面 push 到 C 页面,由 C 页面 push 到 D 页面,现在想返回到根部,也就是 A 页面:

// D.js
navigator.popToRoot()

pop, popTo, popToRoot 也可以通过 navigator.setResult(RESULT_OK, {...})返回结果给目标页面,目标页面通过 async-await 来接收结果。

redirectTo

redirectTo(moduleNamstring, props, options): void 用指定页面取代当前页面,比如当前页面是 A,想要替换成 B

// A.js
navigator.redirectTo('B')

现在 stack 里没有 A 页面了,被替换成了 B。

又比如 stack 里有 A, B, C 三个页面,现在想要让 B, C 出栈,并且让 D 入栈

找到 B 页面的 navigator,调用 navigator.redirectTo('D') 方法即可

// C.js
const navigator = await Navigator.find('B')
navigator.redirectTo('D')

现在 stack 里有 A, D 两个页面。

isStackRoot

isStackRoot(): Promise<boolean> 判断一个页面是否所在 stack 的根页面,返回值是一个 Promise.

componentDidMount() {
  navigator.isStackRoot().then((isRoot) => {
    if(isRoot) {
      garden.setLeftBarButtonItem({title: '取消', action: 'cancel'});
      this.setState({isRoot});
    }
  })
}

setParams

setParams(params) 存放和该 navigator 相关的属性或状态,可以通过 navigator.state.params 取出,具体应用参考 example/TopBarTitleViewopen in new window 这个例子。

Tabs

tabs 支持以下导航操作

switchTab

switchTab(index, popToRoot = false) 切换到指定 Tab

navigator.switchTab(1)

该方法还接受第二个参数,是个布尔值,用来控制在切换到其它 Tab 时,当前 Tab (该 Tab 是个 Stack) 要不要重置到根页面,默认是 false.

// 当前 Tab 会调用 popToRoot
navigator.switchTab(1, true)

Drawer

drawer 支持以下导航操作

toggleMenu

toggleMenu() 切换抽屉的开关状态

navigator.toggleMenu()

openMenu

openMenu() 打开抽屉

navigator.openMenu()

closeMenu

closeMenu() 关闭抽屉

navigator.closeMenu()

注意事项

  • 永远不可能 pesent 一个页面在 modal 之上

    譬如 A 是个 modal,那么不可能在它上面执行 navigator.present 操作。

    譬如 A 是个普通页面(非 modal),它通过 navigator.showModal 显示 B,那么在 B 被关闭前,A 不能通过 navigator.present 显示 C。

  • 如果一个页面已经 present 出一个页面,那么在该页面未关闭之前,它不能再 present 出另一个页面。

    譬如 A 是个普通页面(非 modal),它通过 navigator.present 显示 B,那么在 B 被关闭前,A 不能通过 navigator.present 显示 C。

  • 如果一个页面已经 show 出一个 modal,那么在该 modal 未关闭之前,它不能再 show 出另一个 modal。

    譬如 A (可以是 modal),它通过 navigator.showModal 显示 B,那么在 B 被关闭前,A 不能通过 navigator.showModal 显示 C。

  • 在调用 dismisshideModalpoppopTopopToRoot 或者 redirectTo 后,该 navigator 将会失效,不要再使用该 navigator 执行任何导航操作。

    navigator.hideModal()
    // 下面这行代码不会生效
    navigator.present('XXX')
    

    一个变通的办法是使用 Navigator.current

    navigator.hideModal()
    // 使用 modal 隐藏后出现的页面的 navigator
    const currrent = await Navigator.currrent()
    currrent.present('XXX')
    
  • 如果由于某些原因,需要在页面之外执行导航操作,可以使用 router,它会自动获取合适的 navigator 来执行操作,必要时关闭一些页面。

    router.open('/path/to/Foo')
    

    使用 router,在通过 ReactRegistry.registerComponent 注册模块时,需要传递第三个参数,详情请查看路由注册

    如果需要从第三方应用(譬如浏览器)打开 App 指定页面,则需要使用 DeepLink

    在应用内使用 router.open 并不需要激活 DeepLink,也不需要配置 schema

上次更新: