跳至主要内容
版本: v8

React Navigation

本指南涵盖了使用 Ionic 和 React 构建的应用程序中的路由工作原理。

IonReactRouter 在幕后使用流行的 React Router 库。借助 Ionic 和 React Router,您可以创建具有丰富页面过渡效果的多页应用程序。

您对使用 React Router 进行路由的所有知识都将迁移到 Ionic React 中。让我们看看 Ionic React 应用程序的基础知识以及路由如何与它一起工作。

Ionic React 中的路由

这是一个定义单个路由到“/dashboard”URL 的示例 App 组件。当您访问“/dashboard”时,路由将渲染 DashboardPage 组件。

App.tsx

const App: React.FC = () => (
<IonApp>
<IonReactRouter>
<IonRouterOutlet>
<Route path="/dashboard" component={DashboardPage} />
<Redirect exact from="/" to="/dashboard" />
</IonRouterOutlet>
</IonReactRouter>
</IonApp>
);

紧接 Route 之后,我们定义了默认的 Redirect,当用户访问应用程序的根 URL(“/”)时,它将重定向到“/dashboard”URL。

重定向还设置了 exact 属性,这意味着 URL 必须与 from 属性(如果 exact 用在 Route 上,则为 path 属性)完全匹配,才能使该路由匹配。如果没有它,该重定向将在每个路由上渲染,因为每个路由都以“/”开头。

您还可以根据条件(例如检查用户是否已通过身份验证)从路由的渲染方法中以编程方式重定向。

<Route
exact
path="/dashboard"
render={(props) => {
return isAuthed ? <DashboardPage {...props} /> : <LoginPage />;
}}
/>

IonReactRouter

IonReactRouter 组件包装了来自 React Router 的传统 BrowserRouter 组件,并为路由设置了应用程序。因此,请使用 IonReactRouter 来代替 BrowserRouter。您可以将任何属性传递给 IonReactRouter,它们将被传递到底层的 BrowserRouter

嵌套路由

在 Dashboard 页面中,我们定义了更多与应用程序的此特定部分相关的路由。

DashboardPage.tsx

const DashboardPage: React.FC = () => {
return (
<IonPage>
<IonRouterOutlet>
<Route exact path="/dashboard" component={UsersListPage} />
<Route path="/dashboard/users/:id" component={UserDetailPage} />
</IonRouterOutlet>
</IonPage>
);
};

这里定义了几个路由,指向应用程序仪表板部分中的页面。请注意,我们需要在路径中定义整个路由,即使我们从该 URL 到达了此页面,我们也不能省略“/dashboard”。React Router 需要完整的路径,不支持相对路径。

但是,我们可以使用 match 对象的 url 属性来提供匹配的 URL 以渲染组件,这在使用嵌套路由时很有帮助。

const DashboardPage: React.FC<RouteComponentProps> = ({ match }) => {
return (
<IonPage>
<IonRouterOutlet>
<Route exact path={match.url} component={UsersListPage} />
<Route path={`${match.url}/users/:id`} component={UserDetailPage} />
</IonRouterOutlet>
</IonPage>
);
};

这里,match.url 包含“/dashboard”的值,因为这是用来渲染 DashboardPage 的 URL。

这些路由分组在 IonRouterOutlet 中,让我们接下来讨论一下。

IonRouterOutlet

IonRouterOutlet 组件为渲染 Ionic“页面”的路由提供了一个容器。当页面在 IonRouterOutlet 中时,该容器将控制页面之间的过渡动画,以及控制页面何时创建和销毁,这有助于在来回切换视图时保持视图之间的状态。

上面的 DashboardPage 显示了用户列表页面和详细信息页面。当在这两个页面之间导航时,IonRouterOutlet 将提供适当的平台页面过渡,并保持先前页面的状态完好无损,以便当用户导航回列表页面时,它将以与离开时相同的状态出现。

IonRouterOutlet 应该只包含 RouteRedirect。任何其他组件都应该作为 Route 的结果或在 IonRouterOutlet 之外渲染。

回退路由

一个常见的路由用例是提供一个“回退”路由,以便在导航到的位置与定义的任何路由都不匹配的情况下进行渲染。

我们可以通过将没有 path 属性的 Route 组件作为最后一个路由定义在 IonRouterOutlet 中来定义回退路由。

DashboardPage.tsx

const DashboardPage: React.FC<RouteComponentProps> = ({ match }) => {
return (
<IonRouterOutlet>
<Route exact path={match.url} component={UsersListPage} />
<Route path={`${match.url}/users/:id`} component={UserDetailPage} />
<Route render={() => <Redirect to={match.url} />} />
</IonRouterOutlet>
);
};

这里,我们看到,如果某个位置与前两个 Route 不匹配,IonRouterOutlet 将把 Ionic React 应用程序重定向到 match.url 路径。

您也可以选择提供一个要渲染的组件,而不是提供重定向。

const DashboardPage: React.FC<RouteComponentProps> = ({ match }) => {
return (
<IonRouterOutlet>
<Route exact path={match.url} component={UsersListPage} />
<Route path={`${match.url}/users/:id`} component={UserDetailPage} />
<Route component={NotFoundPage} />
</IonRouterOutlet>
);
};

IonPage

IonPage 组件将 Ionic React 应用程序中的每个视图包装起来,并允许页面过渡和堆栈导航正常工作。使用路由器导航到的每个视图都必须包含 IonPage 组件。

import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
import React from 'react';

const Home: React.FC = () => {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Home</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent className="ion-padding">Hello World</IonContent>
</IonPage>
);
};
export default Home;

在 Ionic React 应用程序中路由到不同视图时,有多种选项可用。这里,UsersListPage 使用 IonItemrouterLink 属性来指定在点击/单击该项目时要转到的路由。

UsersListPage.tsx

const UsersListPage: React.FC = () => {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Users</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonList>
<IonItem routerLink="/dashboard/users/1">
<IonLabel>User 1</IonLabel>
</IonItem>
<IonItem routerLink="/dashboard/users/2">
<IonLabel>User 2</IonLabel>
</IonItem>
</IonList>
</IonContent>
</IonPage>
);
};

其他具有 routerLink 属性的组件包括 IonButtonIonCardIonRouterLinkIonFabButtonIonItemOption

这些组件中的每一个还都有一个 routerDirection 属性,用于明确设置要使用的页面过渡类型(“back”、“forward”或“none”)。

除了具有 routerLink 属性的这些组件之外,您还可以使用 React Routers 的 Link 组件在视图之间导航。

<Link to="/dashboard/users/1">User 1</Link>

我们建议尽可能使用上述方法进行路由。这些方法的优点是它们都渲染了一个锚点(<a>)标签,这适合于整个应用程序的可访问性。

一种以编程方式进行导航的选项是使用 React Router 提供给它通过路由渲染的组件的 history 属性。

<IonButton
onClick={(e) => {
e.preventDefault();
history.push('/dashboard/users/1');
}}
>
Go to User 1
</IonButton>
注意

history 是一个属性。

React Router 使用 history 包,该包有一个 history.go 方法,允许开发人员在应用程序历史记录中向前或向后移动。让我们看一个例子。

假设您有以下应用程序历史记录。

/pageA --> /pageB --> /pageC

如果您在 /pageC 上调用 router.go(-2),您将被带回 /pageA。如果您然后调用 router.go(2),您将被带到 /pageC

目前尚不支持在 Ionic React 中使用 history.go()。有兴趣看到此功能添加到 Ionic React 中吗?在 GitHub 上告诉我们

URL 参数

在 Dashboard 页面中定义的第二个路由定义了一个 URL 参数(“:id(路径中的部分)。URL 参数是path 中的动态部分,当用户导航到像 "/dashboard/users/1" 这样的 URL 时,"1" 会被保存到名为 "id" 的参数中,这个参数可以在路由渲染的组件中访问。让我们看看它是如何实现的。

UserDetailPage.tsx

interface UserDetailPageProps
extends RouteComponentProps<{
id: string;
}> {}

const UserDetailPage: React.FC<UserDetailPageProps> = ({ match }) => {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>User Detail</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>User {match.params.id}</IonContent>
</IonPage>
);
};

match 属性包含有关匹配路由的信息,包括 URL 参数。我们在这里获取 id 参数并在屏幕上显示它。

请注意我们如何使用 TypeScript 接口来对 props 对象进行强类型化。该接口为我们在组件内部提供了类型安全性和代码补全。

线性路由与非线性路由

线性路由

如果您构建了使用路由的 Web 应用程序,那么您可能之前使用过线性路由。线性路由意味着您可以通过推入和弹出页面来向前或向后移动应用程序历史记录。

以下是在移动应用程序中使用线性路由的示例

此示例中的应用程序历史记录具有以下路径

Accessibility --> VoiceOver --> Speech

当我们按下后退按钮时,我们按照相同的路由路径进行操作,只是方向相反。线性路由在允许简单且可预测的路由行为方面很有帮助。

线性路由的缺点是它不允许复杂的 用户体验,例如选项卡视图。这就是非线性路由发挥作用的地方。

非线性路由

非线性路由是一个概念,对于许多学习使用 Ionic 构建移动应用程序的 Web 开发人员来说可能很陌生。

非线性路由意味着用户应该返回到的视图不一定是屏幕上显示的先前视图。

以下是非线性路由的示例

在上面的示例中,我们从 Originals 选项卡开始。点击卡片将我们带到 Originals 选项卡内的 Ted Lasso 视图。

从这里,我们切换到 Search 选项卡。然后,我们再次点击 Originals 选项卡,并被带回到 Ted Lasso 视图。此时,我们已经开始使用非线性路由。

为什么这是非线性路由?我们之前所在的视图是 Search 视图。但是,在 Ted Lasso 视图上按下后退按钮应该将我们带回到根 Originals 视图。之所以发生这种情况,是因为移动应用程序中的每个选项卡都被视为其自身的堆栈。在 使用选项卡 部分中将更详细地介绍这一点。

如果点击后退按钮只是从 Ted Lasso 视图调用 history.go(-1),那么我们将被带回到 Search 视图,这不是正确的。

非线性路由允许实现线性路由无法处理的复杂用户流程。但是,某些线性路由 API(例如 history.go())不能在非线性环境中使用。这意味着在使用选项卡或嵌套出口时,不应使用 history.go()

我应该选择哪一个?

我们建议尽可能地使您的应用程序保持简单,直到您需要添加非线性路由。非线性路由非常强大,但它也会为移动应用程序增加相当多的复杂性。

非线性路由最常见的两种用途是使用选项卡和嵌套的 IonRouterOutlets。我们建议只有在您的应用程序满足选项卡或嵌套路由出口用例时才使用非线性路由。

有关选项卡的更多信息,请参阅 使用选项卡

有关嵌套路由出口的更多信息,请参阅 嵌套路由

共享 URL 与嵌套路由

在设置路由时,一个常见的困惑点是决定是使用共享 URL 还是嵌套路由。本指南的这一部分将解释两者并帮助您决定使用哪一个。

共享 URL

共享 URL 是一种路由配置,其中路由具有共同的 URL 部分。以下是一个共享 URL 配置的示例

const App: React.FC = () => (
<IonApp>
<IonReactRouter>
<IonRouterOutlet>
<Route path="/dashboard" exact={true}>
<DashboardMainPage />
</Route>
<Route path="/dashboard/stats" exact={true}>
<DashboardStatsPage />
</Route>
</IonRouterOutlet>
</IonReactRouter>
</IonApp>
);

上面的路由被认为是“共享的”,因为它们重用了 URL 中的 dashboard 部分。

嵌套路由

嵌套路由是一种路由配置,其中路由被列为其他路由的子路由。以下是一个嵌套路由配置的示例

const App: React.FC = () => (
<IonApp>
<IonReactRouter>
<IonRouterOutlet>
<Route path="/dashboard/:id">
<DashboardRouterOutlet />
</Route>
</IonRouterOutlet>
</IonReactRouter>
</IonApp>
);

const DashboardRouterOutlet: React.FC = () => (
<IonRouterOutlet>
<Route path="/dashboard" exact={true}>
<DashboardMainPage />
</Route>
<Route path="/dashboard/stats" exact={true}>
<DashboardStatsPage />
</Route>
</IonRouterOutlet>
);

上面的路由是嵌套的,因为它们位于父路由的 children 数组中。请注意,父路由渲染 DashboardRouterOutlet 组件。当您嵌套路由时,您需要渲染 IonRouterOutlet 的另一个实例。

我应该选择哪一个?

当您希望从页面 A 转到页面 B,同时保留 URL 中两个页面之间的关系时,共享 URL 很棒。在我们之前的示例中,/dashboard 页面上的按钮可以转到 /dashboard/stats 页面。这两个页面之间的关系得以保留,因为:a) 页面转换,b) URL。

当您希望在出口 A 中渲染内容,同时在嵌套出口 B 中渲染子内容时,应该使用嵌套路由。您将遇到的最常见用例是选项卡。当您加载选项卡 Ionic 启动应用程序时,您将在第一个 IonRouterOutlet 中看到 IonTabBarIonTabs 组件的渲染。IonTabs 组件渲染另一个 IonRouterOutlet,该 IonRouterOutlet 负责渲染每个选项卡的内容。

在移动应用程序中使用嵌套路由有很少的用例。如有疑问,请使用共享 URL 路由配置。我们强烈建议不要在除选项卡之外的上下文中使用嵌套路由,因为这会很快使您应用程序的导航变得混乱。

使用选项卡

在使用选项卡时,Ionic 需要一种方法来知道哪个视图属于哪个选项卡。IonTabs 组件在这里派上用场,但让我们看看为此设置的路由配置是什么样子

<IonApp>
<IonReactRouter>
<IonRouterOutlet>
<Route path="/tabs" render={() => <Tabs />} />
<Route exact path="/">
<Redirect to="/tabs" />
</Route>
</IonRouterOutlet>
</IonReactRouter>
</IonApp>

在这里,我们的 tabs 路径加载 Tabs 组件。我们在此组件内部提供每个选项卡作为路由对象。在此示例中,我们调用路径 tabs,但这可以自定义。

让我们首先看看我们的 Tabs 组件

import { Redirect, Route } from 'react-router-dom';
import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react';
import { IonReactRouter } from '@ionic/react-router';
import { ellipse, square, triangle } from 'ionicons/icons';
import Tab1 from './pages/Tab1';
import Tab2 from './pages/Tab2';
import Tab3 from './pages/Tab3';

const Tabs: React.FC = () => (
<IonTabs>
<IonRouterOutlet>
<Redirect exact path="/tabs" to="/tabs/tab1" />
<Route exact path="/tabs/tab1">
<Tab1 />
</Route>
<Route exact path="/tabs/tab2">
<Tab2 />
</Route>
<Route path="/tabs/tab3">
<Tab3 />
</Route>
<Route exact path="/tabs">
<Redirect to="/tabs/tab1" />
</Route>
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="tab1" href="/tabs/tab1">
<IonIcon icon={triangle} />
<IonLabel>Tab 1</IonLabel>
</IonTabButton>
<IonTabButton tab="tab2" href="/tabs/tab2">
<IonIcon icon={ellipse} />
<IonLabel>Tab 2</IonLabel>
</IonTabButton>
<IonTabButton tab="tab3" href="/tabs/tab3">
<IonIcon icon={square} />
<IonLabel>Tab 3</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
);

export default Tabs;

如果您之前使用过 Ionic Framework,这应该很熟悉。我们创建一个 IonTabs 组件并提供 IonTabBarIonTabBar 提供 IonTabButton 组件,每个组件都具有 tab 属性,该属性与其在路由配置中的对应选项卡相关联。我们还提供 IonRouterOutlet 以使 IonTabs 拥有一个出口来渲染不同的选项卡视图。

提示

IonTabs 为您渲染 IonPage,因此您不需要在此处手动添加 IonPage

Ionic 中的选项卡工作原理

Ionic 中的每个选项卡都被视为一个独立的导航堆栈。这意味着如果您的应用程序中有三个选项卡,则每个选项卡都有自己的导航堆栈。在每个堆栈中,您可以向前导航(推入视图)和向后导航(弹出视图)。

此行为很重要,因为它不同于其他基于 Web 的 UI 库中找到的大多数选项卡实现。其他库通常将选项卡管理为一个单个历史堆栈。

由于 Ionic 专注于帮助开发人员构建移动应用程序,因此 Ionic 中的选项卡旨在尽可能地与本机移动选项卡相匹配。因此,Ionic 中的选项卡可能存在某些与您在其他 UI 库中看到的选项卡实现不同的行为。继续阅读以详细了解其中的一些差异。

选项卡内的子路由

在向选项卡添加其他路由时,应将它们编写为兄弟路由,并将父选项卡作为路径前缀。下面的示例将 /tabs/tab1/view 路由定义为 /tabs/tab1 路由的兄弟路由。由于此新路由具有 tab1 前缀,因此它将在 Tabs 组件内部渲染,而 Tab 1 将在 IonTabBar 中仍然处于选中状态。

<IonTabs>
<IonRouterOutlet>
<Redirect exact path="/tabs" to="/tabs/tab1" />
<Route exact path="/tabs/tab1">
<Tab1 />
</Route>
<Route exact path="/tabs/tab1/view">
<Tab1View />
</Route>
<Route exact path="/tabs/tab2">
<Tab2 />
</Route>
<Route path="/tabs/tab3">
<Tab3 />
</Route>
<Route exact path="/tabs">
<Redirect to="/tabs/tab1" />
</Route>
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="tab1" href="/tabs/tab1">
<IonIcon icon={triangle} />
<IonLabel>Tab 1</IonLabel>
</IonTabButton>
<IonTabButton tab="tab2" href="/tabs/tab2">
<IonIcon icon={ellipse} />
<IonLabel>Tab 2</IonLabel>
</IonTabButton>
<IonTabButton tab="tab3" href="/tabs/tab3">
<IonIcon icon={square} />
<IonLabel>Tab 3</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>

在选项卡之间切换

由于每个选项卡都是其自己的导航堆栈,因此需要注意的是,这些导航堆栈永远不应该交互。这意味着 Tab 1 中永远不应该有一个按钮将用户路由到 Tab 2。换句话说,选项卡只能通过用户点击选项卡栏中的选项卡按钮来更改。

iOS App Store 和 Google Play Store 移动应用程序就是一个很好的实际例子。这些应用程序都提供了选项卡界面,但两者都不会在选项卡之间路由用户。例如,iOS App Store 应用程序中的“游戏”选项卡永远不会将用户定向到“搜索”选项卡,反之亦然。

让我们看一下使用选项卡时常见的几个错误。

多个选项卡引用的设置选项卡

一个常见的做法是将设置视图创建为其自己的选项卡。如果开发人员需要显示多个嵌套设置菜单,这很好。但是,其他选项卡不应该尝试路由到设置选项卡。如上所述,设置选项卡应该被激活的唯一方式是用户点击相应的选项卡按钮。

如果您发现您的选项卡需要引用设置选项卡,我们建议通过使用 ion-modal 将设置视图设置为模态视图。这是 iOS App Store 应用程序中的一种做法。使用这种方法,任何选项卡都可以显示模态视图,而不会破坏移动选项卡模式,该模式要求每个选项卡都是其自己的堆栈。

下面的示例显示了 iOS App Store 应用程序如何从多个选项卡显示“帐户”视图。通过在模态视图中显示“帐户”视图,该应用程序可以在移动选项卡最佳实践范围内工作,以便在多个选项卡中显示相同的视图。

在选项卡之间重复使用视图

另一种常见做法是在多个标签中呈现相同的视图。开发人员通常尝试通过将视图包含在单个标签中,而其他标签路由到该标签来实现。如上所述,这会破坏移动标签模式,应避免。

相反,我们建议在每个标签中使用指向相同组件的路由。这是 Spotify 等热门应用程序中采用的做法。例如,您可以从“主页”、“搜索”和“您的资料库”标签访问专辑或播客。访问专辑或播客时,用户会停留在该标签中。应用程序通过为每个标签创建路由并在代码库中共享一个公共组件来实现此功能。

以下示例展示了 Spotify 应用程序如何重复使用相同的专辑组件在多个标签中显示内容。请注意,每个屏幕截图显示的是相同的专辑,但来自不同的标签。

主页标签搜索标签

实时示例

如果您想动手实践上面描述的概念和代码,请查看我们在 StackBlitz 上的实时示例

标签视图中的 IonRouterOutlet

在标签视图中工作时,Ionic React 需要一种方法来确定哪些视图属于哪个标签。我们利用提供给 Route 的路径是正则表达式这一事实来实现这一点。

虽然语法看起来有点奇怪,但一旦你理解了它,它就相当简单。

例如,具有两个标签(会议和演讲者)的视图的路由可以这样设置

<IonRouterOutlet>
<Route path="/:tab(sessions)" component={SessionsPage} exact={true} />
<Route path="/:tab(sessions)/:id" component={SessionDetail} />
<Route path="/:tab(speakers)" component={SpeakerList} exact={true} />
</IonRouterOutlet>

如果导航的 URL 为“/sessions”,它将匹配第一个路由,并在传递给 SessionsPage 的结果 match 对象中添加一个名为“tab”的 URL 参数,其值为“sessions”。

当用户导航到会议详细信息页面(例如“/sessions/1”)时,第二个路由将添加一个名为“tab”的 URL 参数,其值为“sessions”。当 IonRouterOutlet 看到这两个页面都在同一个“sessions”标签中时,它将向新视图提供一个动画页面过渡。如果用户导航到一个新标签(在本例中为“speakers”),IonRouterOutlet 将知道不要提供动画。

IonRouterOutlet 中的切换

由于 IonRouterOutlet 接管了确定哪些路由将被渲染的任务,因此在 IonRouterOutlet 内部使用 React Router 的 Switch 将不会产生任何影响。在 IonRouterOutlet 外部使用时,切换仍然按预期工作。

实用程序

useIonRouter

useIonRouter 钩子可用于更直接地控制 Ionic React 中的路由。它允许您在调用 React Router 之前向 Ionic 传递其他元数据,例如自定义动画。

useIonRouter 钩子返回一个 UseIonRouterResult,其中包含用于路由的几个便利方法

type UseIonRouterResult = {
/**
* Navigates to a new pathname
* @param pathname - The path to navigate to
* @param routerDirection - Optional - The RouterDirection to use for transition purposes, defaults to 'forward'
* @param routeAction - Optional - The RouteAction to use for history purposes, defaults to 'push'
* @param routerOptions - Optional - Any additional parameters to pass to the router
* @param animationBuilder - Optional - A custom transition animation to use
*/
push(
pathname: string,
routerDirection?: RouterDirection,
routeAction?: RouteAction,
routerOptions?: RouterOptions,
animationBuilder?: AnimationBuilder
): void;
/**
* Navigates backwards in history, using the IonRouter to determine history
* @param animationBuilder - Optional - A custom transition animation to use
*/
goBack(animationBuilder?: AnimationBuilder): void;
/**
* Determines if there are any additional routes in the the Router's history. However, routing is not prevented if the browser's history has more entries. Returns true if more entries exist, false if not.
*/
canGoBack(): boolean;
/**
* Information about the current route.
*/
routeInfo: RouteInfo;
};

以下示例展示了如何使用 useIonRouter

import { useIonRouter } from '@ionic/react';

const MyComponent: React.FC = () => {
const router = useIonRouter();
const goToPage = () => {
router.push('/my-page', 'root', 'replace');
};

...
}

更多信息

有关使用 Ionic 内部使用的 React Router 实现的 React 路由的更多信息,请查看其文档:https://v5.reactrouter.com/web.

来自社区

Ionic 4 和 React:导航 - Paul Halliday