This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.

自定义导航器

编辑页面

了解如何在 Expo Router 中构建自己的导航器,以及库作者如何将现有导航器与路由器集成。


Expo Router 内置了适用于最常见模式的导航器 — Stack、Tabs、Native tabs 和 Drawer。如果这些导航器都不符合你的需求,你可以构建自己的导航器并将其用作布局,同时继续使用基于文件的路由、深度链接和类型化路由,体验与内置导航器完全一致。

请选择与你的目标相匹配的入口点:

对于将路由渲染为 Web 模态覆盖层的栈导航器,请参阅构建自定义 Web 模态框。

在应用中创建导航器

使用 createStandardRouterNavigator 将内容组件转换为可作为布局渲染的导航器。它需要两个参数:

  • NavigatorContent:一个渲染导航器 UI 的组件。它会接收当前的导航 state、每个屏幕的 descriptors、用于导航的 actions,以及用于发送事件的 emitter。
  • router:要使用的路由行为。从 expo-router 中导入 StackRouter 以实现类似栈的导航,或导入 TabRouter 以实现类似标签页的导航。

下面的示例构建了一个最小的标签页导航器:

components/Tabs.tsx
import { createStandardRouterNavigator, TabRouter, type NavigatorContentProps } from 'expo-router'; import { Pressable, Text, View } from 'react-native'; // 第一个类型参数是你可以为每个屏幕设置的选项 type TabsContentProps = NavigatorContentProps<{ title?: string }>; function TabsContent({ state, descriptors, actions }: TabsContentProps) { const focusedRoute = state.routes[state.index]; return ( <View style={{ flex: 1 }}> {/* 渲染当前聚焦路由对应的屏幕。 */} <View style={{ flex: 1 }}>{descriptors[focusedRoute.key].render()}</View> {/* 一个简单的标签栏。 */} <View style={{ flexDirection: 'row' }}> {state.routes.map(route => ( <Pressable key={route.key} style={{ flex: 1, padding: 16 }} onPress={() => actions.navigate(route.name)}> <Text>{descriptors[route.key].options.title ?? route.name}</Text> </Pressable> ))} </View> </View> ); } export const Tabs = createStandardRouterNavigator(TabsContent, TabRouter);

返回的导航器有一个用于声明屏幕的 .Screen 子组件,因此你可以像其他布局一样在 _layout 文件中使用它:

app/_layout.tsx
import { Tabs } from '../components/Tabs'; export default function Layout() { return ( <Tabs> <Tabs.Screen name="index" options={{ title: '主页' }} /> <Tabs.Screen name="settings" options={{ title: '设置' }} /> </Tabs> ); }
属性说明
state当前导航状态:{ index, routes },其中每个路由都有 key、name、params 和 href。
descriptors以 route.key 为键的映射。每个 descriptor 都暴露该屏幕解析后的 options 以及一个用于渲染该屏幕的 render() 函数。
actions用于更改导航状态的函数:navigate(name, params?) 和 back()。
emitter一个带有 emit() 方法的对象,用于向屏幕发送事件。

类型化事件

如果你的导航器会发出事件,请在 NavigatorContentProps 的第二个类型参数中声明它们。每个键都是一个事件名,其值描述了该事件的 data 以及它是否 canPreventDefault。随后 emitter.emit 就会基于该映射进行类型检查——未知事件名和不匹配的负载都会被拒绝:

type TabsContentProps = NavigatorContentProps< { title?: string }, { tabPress: { data: undefined; canPreventDefault: true } } >; function TabsContent({ emitter }: TabsContentProps) { emitter.emit({ type: 'tabPress', canPreventDefault: true }); // ... }

createStandardRouterNavigator 会从组件推断事件映射,因此你无需在调用处再次传入。对于不发出任何事件的导航器,请省略第二个类型参数。

选项

createStandardRouterNavigator 和 integrateWithRouter 都接受一个可选的 options 对象作为第三个参数。使用 createProps 派生标准 state 和 actions 之外的导航器专属属性:

export const Tabs = createStandardRouterNavigator(TabsContent, TabRouter, { createProps: ({ state, dispatch }) => ({ activeRouteKey: state.routes[state.index].key, preload: (name: string) => dispatch({ type: 'PRELOAD', payload: { name } }), }), });

在 NavigatorContentProps 的第四个类型参数中声明 createProps 返回的属性,以便 NavigatorContent 以类型化方式接收它们:

type TabsContentProps = NavigatorContentProps< { title?: string }, // 本示例中没有自定义事件。 Record<string, never>, // 本示例中没有自定义导航器属性。 object, // 由 `createProps` 注入的属性。 { activeRouteKey: string; preload: (name: string) => void } >; function TabsContent({ activeRouteKey, preload }: TabsContentProps) { // ... }

集成现有导航器(库作者)

标准导航器 API

上面展示的 NavigatorContent 组件是一个标准导航器。它实现了由 standard-navigation 包定义的最小化、与框架无关的契约。你的内容接收到的 state、descriptors、actions 和 emitter,与上面的应用内导航器所使用的同一 API 完全一致。唯一的区别在于是谁创建了这个导航器。

createStandardRouterNavigator 是一个快捷方式:它会为你调用 standard-navigation 中的 createStandardNavigator,并一步将结果集成到 Expo Router 中。作为库作者,请自行调用 createStandardNavigator 并保留对导航器的引用:

src/index.ts
import { createStandardNavigator } from 'standard-navigation'; import { TabsContent } from './TabsContent'; // 与框架无关:此导航器面向标准契约,而不是任何单一宿主。 // 第一个类型参数是每个屏幕的选项;第二个是事件映射。 export const navigator = createStandardNavigator< { title?: string }, { tabPress: { data: undefined; canPreventDefault: true } } >(TabsContent);

由于 TabsContent 和 navigator 只依赖标准契约,同样的代码可以运行在 Expo Router、React Navigation 或任何其他实现了该契约的宿主上。你只需编写一次导航器,然后为每个框架提供一个轻量的集成入口点。

与 Expo Router 集成

使用 integrateWithRouter 将导航器接入 Expo Router:

src/expo-router.ts
import { integrateWithRouter, TabRouter } from 'expo-router'; import { navigator } from './index'; export const Tabs = integrateWithRouter(navigator, TabRouter);

返回的组件与 createStandardRouterNavigator 返回的组件完全相同,包括 .Screen 子组件和相同的选项。

为内置导航器添加属性

当你的库封装了 Expo Router 导航器时,请将其 createProps 辅助函数传递给 integrateWithRouter。该辅助函数会添加 Expo Router 所需的导航器专属行为。

导航器辅助函数导入位置路由器
JavaScript 栈createJSStackPropsexpo-router/js-stackStackRouter
JavaScript 标签页createJSTabsPropsexpo-router/js-tabsTabRouter
原生栈createNativeStackPropsexpo-routerStackRouter

例如,按以下方式集成 JavaScript 栈:

src/expo-router.ts
import { StackRouter, integrateWithRouter } from 'expo-router'; import { createJSStackProps } from 'expo-router/js-stack'; import { navigator } from './navigator'; export const Stack = integrateWithRouter(navigator, StackRouter, { createProps: createJSStackProps, });

如需更底层的实现,请使用 expo-router 中的 createBaseStackProps 或 createBaseTabProps,并添加导航器所需的行为。

库入口点

保持导航器内容和标准导航器与框架无关,然后为每个框架暴露一个入口点,这样使用者就可以导入与其应用匹配的集成方式:

.
 src
  TabsContent.tsx实现标准导航器 API 的导航器 UI
  index.ts根入口 — 导出与框架无关的导航器
  react-navigation.tsReact Navigation 入口 — 集成同一个导航器
  expo-router.tsExpo Router 入口 — integrateWithRouter(navigator, ...)
 package.json将子路径导出映射到各框架入口

在库的 package.json 中,将每个入口点映射为一个子路径导出,并指向你的构建输出:

package.json
{ "exports": { ".": { "types": "./lib/typescript/index.d.ts", "default": "./lib/module/index.js" }, "./react-navigation": { "types": "./lib/typescript/react-navigation.d.ts", "default": "./lib/module/react-navigation.js" }, "./expo-router": { "types": "./lib/typescript/expo-router.d.ts", "default": "./lib/module/expo-router.js" } } }

然后,使用者就可以根据自己的框架导入相应的集成方式(例如,import { Tabs } from 'my-tabs/expo-router'),而你则只需在一个地方维护导航器逻辑。

React Navigation 集成

了解如何将同一个标准导航器与 React Navigation 集成,并阅读定义 NavigatorContent 接收到的 state、descriptors、actions 和 emitter 的契约。

自定义路由器行为

如果你只需要处理或拒绝导航操作,请使用 extendRouterActions。返回一个结果以处理该操作,返回 null 以拒绝该操作,或返回 undefined 以交由基础路由器处理。

如果你需要自定义其他路由器成员,例如 actionCreators、getStateForRouteFocus 或 normalizeState,请使用 extendRouter。你未返回的成员会继承自基础路由器。

下面的示例添加了一个 CLEAR 操作及其对应的操作创建器:

src/router.ts
import { extendRouter, extendRouterActions, StackRouter, type CommonNavigationAction, type StackActionType, } from 'expo-router'; type ClearAction = { type: 'CLEAR' }; const RouterWithClearAction = extendRouterActions( StackRouter, (state, action: CommonNavigationAction | StackActionType | ClearAction, { nextKey }) => { if (action.type !== 'CLEAR') { return undefined; } const route = { key: nextKey('index'), name: 'index' }; return { state: { ...state, index: 0, routes: [route] }, affectedRouteKey: route.key, }; } ); export const Router = extendRouter(RouterWithClearAction, ({ baseRouter }) => ({ actionCreators: { ...baseRouter.actionCreators, clear: (): ClearAction => ({ type: 'CLEAR' }), }, }));

这两个辅助函数都会提供 baseRouter、options 和 nextKey。使用 baseRouter 委托现有行为,使用 options 读取传递给路由器工厂的值,并在向状态添加路由时使用 nextKey。