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 中构建自己的导航器,以及库作者如何将现有导航器与路由集成。


重要 本页所述的自定义导航器 API 处于 alpha 阶段,并在 SDK 56 及更高版本中可用。该 API 可能会发生破坏性更改。

Expo Router 配备了用于最常见模式的导航器 — 堆栈标签页原生标签页抽屉。当这些都不适用时,你可以自己构建导航器并将其作为布局使用,基于文件的路由、深度链接和类型化路由的功能与内置导航器完全相同。

🌐 Expo Router ships with navigators for the most common patterns — Stack, Tabs, Native tabs, and Drawer. When none of them fit, you can build your own navigator and use it as a layout, with file-based routing, deep linking, and typed routes working exactly as they do for the built-in navigators.

选择与你的目标相符的入口点:

🌐 Choose the entry point that matches your goal:

在你的应用中创建一个导航器

🌐 Create a navigator in your app

使用 unstable_createStandardRouterNavigator 将内容组件转换为可作为布局呈现的导航器。它需要两个必需的参数:

🌐 Use unstable_createStandardRouterNavigator to turn a content component into a navigator you can render as a layout. It takes two required arguments:

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

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

🌐 The following example builds a minimal tab navigator:

components/Tabs.tsx
import { unstable_createStandardRouterNavigator, TabRouter, type NavigatorContentProps, } from 'expo-router'; import { Pressable, Text, View } from 'react-native'; // The first type argument is the options you can set per screen type TabsContentProps = NavigatorContentProps<{ title?: string }>; function TabsContent({ state, descriptors, actions }: TabsContentProps) { const focusedRoute = state.routes[state.index]; return ( <View style={{ flex: 1 }}> {/* Render the screen for the focused route. */} <View style={{ flex: 1 }}>{descriptors[focusedRoute.key].render()}</View> {/* A simple tab bar. */} <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 = unstable_createStandardRouterNavigator(TabsContent, TabRouter);

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

🌐 The returned navigator has a .Screen child for declaring screens, so you can use it in a _layout file like any other layout:

app/_layout.tsx
import { Tabs } from '../components/Tabs'; export default function Layout() { return ( <Tabs> <Tabs.Screen name="index" options={{ title: 'Home' }} /> <Tabs.Screen name="settings" options={{ title: 'Settings' }} /> </Tabs> ); }

🌐 What NavigatorContent receives

属性描述
state当前导航状态:{ index, routes },每个路由都有一个 keynameparamshref
descriptors一个以 route.key 为键的映射。每个描述符公开屏幕的解析 options 和一个渲染屏幕的 render() 函数。
actions改变导航状态的函数:navigate(name, params?)back()
emitter一个带有 emit() 方法的对象,用于向屏幕发送事件。

键入事件

🌐 Typed events

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

🌐 If your navigator emits events, declare them in the second type argument to NavigatorContentProps. Each key is an event name, and its value describes the event's data and whether it canPreventDefault. emitter.emit is then typed against that map — unknown event names and mismatched payloads are rejected:

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

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

选项

🌐 Options

unstable_createStandardRouterNavigatorunstable_integrateWithRouter 的第三个参数都可以是可选的 options 对象:

🌐 Both unstable_createStandardRouterNavigator and unstable_integrateWithRouter accept an optional options object as their third argument:

  • useOnlyUserDefinedScreens:当 true 时,只有你使用 <Navigator.Screen> 声明的屏幕会被渲染。Expo Router 会忽略那些未声明、仅从文件系统中发现的路由。默认为 false,此时 Expo Router 会在渲染你声明的屏幕的同时,也渲染从文件系统中发现的路由。
  • createProps:从底层路由状态为 NavigatorContent 派生额外属性。将其用于路由特定的信息,这些信息不属于标准的 stateactions
export const Tabs = unstable_createStandardRouterNavigator(TabsContent, TabRouter, { useOnlyUserDefinedScreens: true, createProps: ({ state, dispatch }) => ({ activeRouteKey: state.routes[state.index].key, preload: (name: string) => dispatch({ type: 'PRELOAD', payload: { name } }), }), });

NavigatorContentProps 的第三个类型参数中声明 createProps 返回的 props,这样 NavigatorContent 就可以以类型化的方式接收它们:

🌐 Declare the props returned by createProps in the third type argument to NavigatorContentProps so NavigatorContent receives them in a typed way:

type TabsContentProps = NavigatorContentProps< { title?: string }, // No custom events in this example. Record<string, never>, // Props injected by `createProps`. { activeRouteKey: string; preload: (name: string) => void } >; function TabsContent({ activeRouteKey, preload }: TabsContentProps) { // ... }

信息 createProps 接收原始的 Expo Router statedispatch。这些是内部的,可能在版本之间有重大变化,因此当 NavigatorContent 传入的 stateactions 足够使用时,优先使用它们。如果标准的 stateactionsemitter 中缺少构建导航器所需的内容,请在 GitHub 上提交问题

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

🌐 Integrate an existing navigator (library authors)

标准导航器 API

🌐 The standard navigator API

上面显示的 NavigatorContent 组件是一个 标准导航器。它实现了由 standard-navigation 包定义的最小、与框架无关的契约。你的内容所接收的 statedescriptorsactionsemitter 与上面的应用内导航器具有完全相同的 API。唯一的区别是是谁创建了导航器。

🌐 The NavigatorContent component shown above is a standard navigator. It implements a minimal, framework-agnostic contract defined by the standard-navigation package. The state, descriptors, actions, and emitter your content receives are exactly the same API as the in-app navigator above. The only difference is who creates the navigator.

unstable_createStandardRouterNavigator 是一个快捷方式,它为你调用 createStandardNavigator(来自 standard-navigation)并将结果与 Expo Router 一步整合。作为库的作者,请自行调用 createStandardNavigator 并保留对导航器的引用:

src/index.ts
import { createStandardNavigator } from 'standard-navigation'; import { TabsContent } from './TabsContent'; // Framework-agnostic: this navigator targets the standard contract, not any one host. // The first type argument is the per-screen options; the second is the event map. export const navigator = createStandardNavigator< { title?: string }, { tabPress: { data: undefined; canPreventDefault: true } } >(TabsContent);

因为 TabsContentnavigator 仅依赖于标准合同,相同的代码可以在 Expo Router、React Navigation 或任何实现它的其他宿主上运行。你只需编写一次导航器,并为每个框架提供一个轻量级的集成入口点。

🌐 Because TabsContent and navigator depend only on the standard contract, the same code runs on Expo Router, React Navigation, or any other host that implements it. You write the navigator once and ship a thin integration entry point per framework.

与 Expo 路由集成

🌐 Integrate with Expo Router

使用 unstable_integrateWithRouter 将你的导航器连接到 Expo Router:

🌐 Wire your navigator into Expo Router with unstable_integrateWithRouter:

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

返回的组件的工作方式与来自 unstable_createStandardRouterNavigator 的组件完全相同,包括 .Screen 子组件和相同的 选项

🌐 The returned component works exactly like the one from unstable_createStandardRouterNavigator, including the .Screen child and the same options.

库入口点

🌐 Library entry points

保持导航器内容和标准导航器与框架无关,然后为每个框架提供一个入口点,以便使用者导入与其应用匹配的集成:

🌐 Keep the navigator content and the standard navigator framework-agnostic, then expose one entry point per framework so consumers import the integration that matches their app:

.
src
  TabsContent.tsxNavigator UI implementing the standard navigator API
  index.tsRoot entry — exports the framework-agnostic navigator
  react-navigation.tsReact Navigation entry — integrates the same navigator
  expo-router.tsExpo Router entry — unstable_integrateWithRouter(navigator, ...)
package.jsonMaps subpath exports to each framework entry

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

🌐 Map each entry point to a subpath export in your library's package.json, pointing at your build output:

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'),同时你在一个地方维护导航器逻辑。

🌐 Consumers then import the integration for their framework (for example, import { Tabs } from 'my-tabs/expo-router') while you maintain the navigator logic in one place.

React Navigation 集成

了解如何将相同的标准导航器与 React Navigation 集成,并阅读定义 NavigatorContent 接收的状态、描述符、操作和触发器的契约。