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

🌐 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:

对于将路由呈现为网页模态叠加的堆栈导航器,请参见 构建自定义网页模态。

🌐 For a stack navigator that renders routes as web modal overlays, see Build custom web modals.

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

🌐 Create a navigator in your app

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

🌐 Use 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 { 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 = 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 },每个路由都有一个 key、name、params 和 href。
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 }); // ... }

createStandardRouterNavigator 会根据组件推断事件映射,所以你在调用的时候不用再传一遍。对于不触发事件的导航器,可以省略第二个类型参数。

选项

🌐 Options

createStandardRouterNavigator 和 integrateWithRouter 都可以接受一个可选的 options 对象作为它们的第三个参数。使用 createProps 来派生导航器特定的属性,这些属性不属于标准的 state 和 actions:

🌐 Both createStandardRouterNavigator and integrateWithRouter accept an optional options object as their third argument. Use createProps to derive navigator-specific props that are not part of the standard state and 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 返回的 props,这样 NavigatorContent 就能以类型化的方式接收它们:

🌐 Declare the props returned by createProps in the fourth 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>, // No custom navigator props in this example. object, // Props injected by `createProps`. { activeRouteKey: string; preload: (name: string) => void } >; function TabsContent({ activeRouteKey, preload }: TabsContentProps) { // ... }

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

🌐 Integrate an existing navigator (library authors)

标准导航器 API

🌐 The standard navigator API

上面显示的 NavigatorContent 组件是一个 标准导航器。它实现了由 standard-navigation 包定义的最小、与框架无关的契约。你的内容所接收的 state、descriptors、actions 和 emitter 与上面的应用内导航器具有完全相同的 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.

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);

因为 TabsContent 和 navigator 仅依赖于标准合同,相同的代码可以在 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

用 integrateWithRouter 把你的导航器接入 Expo Router:

🌐 Wire your navigator into Expo Router with integrateWithRouter:

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

返回的组件的工作方式和 createStandardRouterNavigator 的组件完全一样,包括 .Screen 子组件和相同的 选项。

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

为内置导航器添加属性

🌐 Add props for a built-in navigator

当你的库封装了一个 Expo Router 导航器时,把它的 createProps 辅助工具传给 integrateWithRouter。这个辅助工具会添加 Expo Router 所期望的导航器特定行为。

🌐 When your library wraps an Expo Router navigator, pass its createProps helper to integrateWithRouter. The helper adds the navigator-specific behavior that Expo Router expects.

导航器辅助工具从...导入路由
JavaScript 堆栈createJSStackPropsexpo-router/js-stackStackRouter
JavaScript 标签页createJSTabsPropsexpo-router/js-tabsTabRouter
原生堆栈createNativeStackPropsexpo-routerStackRouter

例如,整合一个像这样的 JavaScript 技术栈:

🌐 For example, integrate a JavaScript stack like this:

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,然后添加你的导航器需要的行为。

🌐 For a lower-level implementation, use createBaseStackProps or createBaseTabProps from expo-router and add the behavior your navigator needs.

库入口点

🌐 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 — 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 集成。

自定义路由行为

🌐 Customize router behavior

当你只需要处理或拒绝导航操作时,使用 extendRouterActions。返回一个结果来处理操作,使用 null 拒绝操作,或者用 undefined 让基础路由来处理它。

🌐 Use extendRouterActions when you only need to handle or reject navigation actions. Return a result to handle the action, null to reject it, or undefined to let the base router handle it.

当你需要自定义其他路由成员时,比如 actionCreators、getStateForRouteFocus 或 normalizeState,可以使用 extendRouter。你没有返回的成员会从基础路由继承。

🌐 Use extendRouter when you need to customize other router members, such as actionCreators, getStateForRouteFocus, or normalizeState. Members you do not return are inherited from the base router.

下面的例子添加了一个 CLEAR 动作和一个为它创建动作的函数:

🌐 The following example adds a CLEAR action and an action creator for it:

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 在向状态添加路由时使用。

🌐 Both helpers provide baseRouter, options, and nextKey. Use baseRouter to delegate existing behavior, options to read values passed to the router factory, and nextKey when adding a route to the state.