React 导航
本指南将介绍在基于 Ionic 和 React 构建的应用中,路由是如何工作的。
IonReactRouter 底层使用了流行的 React Router 库。借助 Ionic 和 React Router,你可以创建具有丰富页面转场效果的多页面应用。
你在 React Router 中学到的所有路由知识都可以直接应用于 Ionic React。让我们先来了解一下 Ionic React 应用的基础知识以及路由是如何与之协同工作的。
Ionic React 中的路由
下面是一个简单的 App 组件示例,它定义了一条指向 "/dashboard" URL 的路由。当访问 "/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 属性(或者如果 Route 上使用了 exact,则是 path 属性)精确匹配,该路由才会生效。如果没有 exact,由于所有路由都以 "/" 开头,这个重定向将会为每个路由渲染。
你也可以根据条件(例如检 查用户是否已认证)在 Route 的 render 方法中进行编程式重定向:
<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>
);
};
这里,我们定义了另外几个路由,用于指向 Dashboard 区域内的不同页面。注意,我们需要在路径中定义完整的路由,即使我们已经从该 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",因为正是这个 URL 被用来渲染 DashboardPage。
这些路由被分组在一个 IonRouterOutlet 中,接下来我们就来讨论它。
IonRouterOutlet
IonRouterOutlet 组件为渲染 Ionic "页面" 的路由提供了一个容器。当一个页面位于 IonRouterOutlet 内时,该容器不仅会控制页面之间的转场动画,还会管理页面的创建和销毁时机。这有助于在视图间来回切换时,保持之前页面的状态。
上面的 DashboardPage 展示了一个用户列表页和一个详情页。当在这两个页面之间导航时,IonRouterOutlet 会提供适合平台的页面转场效果,并保留前一个页面的状态。这样,当用户返回列表页时,它会保持与离开时相同的状态。
一个 IonRouterOutlet 应该只包含 Route 或 Redirect。任何其他组件都应该作为 Route 渲染的结果来呈现,或者放在 IonRouterOutlet 之外。
默认路由(404 处理)
一个常见的路由需求是提供一个“默认”或“后备”路由,以便在用户导航到未定义的路由时呈现。
我们可以通过将不带 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 组件。
IonPage 对于正确的样式也是必需的。它提供了一个 Flex 容器,确保页面内容(如 IonContent)能够正确调整大小,并且不会与其他 UI 元素(如 IonTabBar)重叠。
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
import React from 'react';
const Home: React.FC = () => {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>首页</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent className="ion-padding">你好,世界</IonContent>
</IonPage>
);
};
export default Home;
导航
在 Ionic React 应用中,有多种方式可以导航到不同的视图。这里,UsersListPage 使用 IonItem 的 routerLink 属性来指定点击项目时要跳转的路由:
UsersListPage.tsx
const UsersListPage: React.FC = () => {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>用户</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonList>
<IonItem routerLink="/dashboard/users/1">
<IonLabel>用户 1</IonLabel>
</IonItem>
<IonItem routerLink="/dashboard/users/2">
<IonLabel>用户 2</IonLabel>
</IonItem>
</IonList>
</IonContent>
</IonPage>
);
};
其他支持 routerLink 属性的组件还包括 IonButton、IonCard、IonRouterLink、IonFabButton 和 IonItemOption。
这些组件也都有一个 routerDirection 属性,用于明确设置页面转场的类型(可选值为 "forward"、"back" 或 "root")。
除了这些带有 routerLink 属性的组件,你也可以使用 React Router 的 Link 组件在视图间导航:
<Link to="/dashboard/users/1">用户 1</Link>
只要情况允许,我们推荐优先使用上述方法进行路由。这样做的好处是它们都会渲染成一个锚点(<a>)标签,这对于提升应用的整体可访问性非常有利。
编程式导航的选项是使用 React Router 通过路由传递给组件的 history 属性。
<IonButton
onClick={(e) => {
e.preventDefault();
history.push('/dashboard/users/1');
}}
>
前往用户 1
</IonButton>
history 是一个属性 (prop)。
使用 history.go 进行导航
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>用户详情</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>用户 {match.params.id}</IonContent>
</IonPage>
);
};
match 属性包含了匹配路由的相关信息,包括 URL 参数。我们从中获取 id 参数并显示在屏幕上。
注意我们是如何使用 TypeScript 接口来为 props 对象提供强类型的。这个接口在组件内部为我们提供了类型安全和代码补全功能。
线性路由 vs 非线性路由
线性路由
如果你开发过使用路由的 Web 应用,你可能之前使用过线性路由。线性路由意味着你可以通过推入(push)和弹出(pop)页面在应用历史记录中前进或后退。
以下是移动应用中线性路由的一个示例:
此示例中的应用历史记录具有以下路径:
辅助功能 --> 旁白 --> 语音
当我们按下返回按钮时,我们会沿着相同的路由路径反向返回。线性路由有助于实现简单且可预测的路由行为。
线性路由的缺点是无法支持复杂的用户交互,例如标签视图。这就引出了非线性路由的概念。
非线性路由
非线性路由对于许多学习使用 Ionic 构建移动应用的 Web 开发者来说可能是一个新概念。
非线性路由意味着用户应该返回的视图不一定是屏幕上先前显示的视图。
以下是非线性路由的示例:
在上面的例子中,我们从“原创”标签页开始。点击一个卡片会将我们带到该标签页内的《泰德·拉索》视图。
从这里,我们切换到“搜索”标签页。然后,我们再次点击“原创”标签页,又被带回到《泰德·拉索》视图。此时,我们就已经开始使用非线性路由了。
为什么这是非线性路由?我们之前 的视图是“搜索”视图。然而,在《泰德·拉索》视图按下返回按钮应该会带我们回到“原创”标签页的根视图。这是因为在移动应用中,每个标签页都被视为自己的独立堆栈。标签页的使用部分会更详细地讨论这一点。
如果在《泰德·拉索》视图按下返回按钮只是简单地调用 history.go(-1),我们将会被带回到“搜索”视图,这显然是不正确的。
非线性路由可以实现线性路由无法处理的复杂用户流程。然而,某些线性路由的 API(如 history.go())在这种非线性环境下无法使用。这意味着在使用标签页或嵌套的 IonRouterOutlet 时,不应该使用 history.go()。
我应该选择哪一种?
我们建议在确实需要添加非线性路由之前,尽量保持应用的简单性。非线性路由非常强大,但它也会给移动应用带来相当大的复杂性。
非线性路由最常见的两种用途是配合标签页和嵌套的 IonRouterOutlet 使用。我们建议只有当你的应用确实需要用到标签页或嵌套路由出口时,才考虑使用非线性路由。
更多关于标签页的信息,请参阅 标签页的使用。
更多关于嵌套路由出口的信息,请参阅 嵌套路由。
共享 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 标签页启动应用时,你会看到 IonTabBar 和 IonTabs 组件在第一个 IonRouterOutlet 中渲染。IonTabs 组件会渲染另一个 IonRouterOutlet,它负责渲染每个标签页的内容。
在移动应用中,很少有其他场景适合使用嵌套路由。如果拿不准,就使用共享 URL 的路由配置。我们强烈建议不要在标签页以外的上下文中使用嵌套路由,因为它可能会很快让你的应用导航变得混乱。