跳到主要内容
版本:v8

Vue 导航

本指南介绍在使用 Ionic 和 Vue 构建的应用中路由是如何工作的。

IonRouterOutlet 组件内部使用流行的 Vue Router 库。借助 Ionic 和 Vue Router,您可以创建具有丰富页面过渡效果的多页面应用。

您所了解的关于使用 Vue Router 进行路由的所有知识都可以延续到 Ionic Vue 中。让我们看看 Ionic Vue 应用的基础知识以及路由是如何与之配合工作的。

简要说明

在阅读本指南时,您可能会注意到这些概念与没有 Ionic 框架的 Vue Router 中的概念非常相似。您的观察是正确的!Ionic Vue 利用了 Vue Router 的最佳部分,使过渡到使用 Ionic 框架构建应用变得尽可能无缝。因此,我们建议尽可能依赖 Vue Router 的功能,而不是尝试构建自己的路由解决方案。

简单路由

这是一个示例路由配置,定义了一个指向 "/home" URL 的单一路由。当您访问 "/home" 时,路由会渲染 HomePage 组件。

router/index.ts

import { createRouter, createWebHistory } from '@ionic/vue-router';
import { RouteRecordRaw } from 'vue-router';
import HomePage from '@/views/Home.vue';

const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: HomePage,
},
];

const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
});

export default router;

在应用的初始加载时,应用将渲染 HomePage 组件,因为这是此处配置的内容。

处理重定向

如果我们想在初始加载时访问不同的路径怎么办?为此,我们可以使用路由重定向。重定向的工作方式与典型的路由对象相同,但包含一些不同的键:

const routes: Array<RouteRecordRaw> = [
{
path: '/',
redirect: '/home',
},
{
path: '/home',
name: 'Home',
component: HomePage,
},
];

在我们的重定向中,我们查找应用的索引路径。然后,如果加载该路径,我们会重定向到 home 路由。

导航到不同的路由

这些都很好,但实际如何导航到一个路由呢?为此,我们可以使用 router-link 属性。让我们创建一个新的路由设置:

const routes: Array<RouteRecordRaw> = [
{
path: '/',
redirect: '/home',
},
{
path: '/home',
name: 'Home',
component: HomePage,
},
{
path: '/detail',
name: 'Detail',
component: DetailPage,
},
];

假设我们从 home 路由开始,并且想要添加一个按钮,带我们到 detail 路由。我们可以使用以下 HTML 来导航到 detail 路由:

<ion-button router-link="/detail">Go to detail</ion-button>

我们也可以使用路由 API 在应用中以编程方式导航:

<template>
<ion-page>
<ion-content>
<ion-button @click="() => router.push('/detail')">Go to detail</ion-button>
</ion-content>
</ion-page>
</template>

<script setup lang="ts">
import { IonButton, IonContent, IonPage } from '@ionic/vue';
import { useRouter } from 'vue-router';

const router = useRouter();
</script>

两种选项都提供相同的导航机制,只是适用于不同的使用场景。

router-link 属性可以设置在任何 Ionic Vue 组件上,当点击该组件时,路由将导航到指定的路由。router-link 属性接受字符串值以及命名路由,就像 Vue Router 的 router.push 一样。为了获得更多控制,还可以设置 router-directionrouter-animation 属性。

router-direction 属性接受 "forward""back""root" 值,用于控制页面过渡的方向。

router-animation 属性接受一个 AnimationBuilder 函数,用于提供仅在点击设置了该属性的组件时使用的自定义页面过渡。AnimationBuilder 类型是一个返回 Ionic Animation 实例的函数。有关在 Ionic Vue 中使用动画的更多信息,请参阅动画文档

<ion-button router-link="/page2" router-direction="back" :router-animation="myAnimation">Click Me</ion-button>

使用 useIonRouter 导航

使用 router-link 的一个缺点是,您无法在导航之前运行自定义代码。这使得诸如在导航之前发起网络请求之类的任务变得困难。您可以直接使用 Vue Router,但这样会失去对页面过渡的控制。这时 useIonRouter 工具就派上用场了。

useIonRouter 工具是一个函数,提供了编程式导航的方法,同时完全控制页面过渡。这使得在导航之前运行自定义代码变得容易。

第一个示例让我们可以将新页面推送到堆栈上,并使用自定义页面过渡:

import { useIonRouter } from '@ionic/vue';
import { customAnimation } from '@/animations/customAnimation';

const ionRouter = useIonRouter();
ionRouter.push('/page2', customAnimation);

useIonRouter 提供了便捷的 pushreplacebackforward 方法,使执行常见导航操作变得容易。它还提供了一个 navigate 方法,可用于更复杂的导航场景:

import { useIonRouter } from '@ionic/vue';
import { customAnimation } from '@/animations/customAnimation';

const ionRouter = useIonRouter();
ionRouter.navigate('/page2', 'forward', 'replace', customAnimation);

上面的示例让应用导航到 /page2,使用向前方向的自定义动画。此外,replace 值确保应用在导航时替换当前历史记录条目。

备注

useIonRouter 使用 Vue 的 inject() 函数,因此只应在 setup() 函数内部使用。

更多详细信息和类型信息,请参阅 useIonRouter 文档

使用 router.go 导航

Vue Router 有一个 router.go 方法,允许开发者在应用历史中向前或向后移动。让我们看一个例子。

假设您有以下应用历史记录:

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

如果您在 /pageC 上调用 router.go(-2),您将回到 /pageA。如果您随后调用 router.go(2),您将进入 /pageC

router.go() 的一个关键特性是它期望您的应用历史是线性的。这意味着 router.go() 不应该在使用非线性路由的应用中使用。有关更多信息,请参阅线性路由与非线性路由

延迟加载路由

我们当前设置路由的方式使得它们在加载应用时包含在同一个初始代码块中,这并不总是理想的。相反,我们可以设置路由,使组件在需要时才加载:

const routes: Array<RouteRecordRaw> = [
{
path: '/',
redirect: '/home',
},
{
path: '/home',
name: 'Home',
component: HomePage,
},
{
path: '/detail',
name: 'Detail',
component: () => import('@/views/DetailPage.vue'),
},
];

这里的设置与之前相同,只是这次 DetailPage 被替换为一个 import 调用。这将导致 DetailPage 组件不再属于应用加载时请求的代码块的一部分。

线性路由与非线性路由

线性路由

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

以下是移动应用中线性路由的示例:

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

Accessibility --> VoiceOver --> Speech

当我们按下返回按钮时,我们按照相同的路由路径返回。线性路由有助于实现简单且可预测的路由行为。这也意味着我们可以使用 Vue Router API,例如 router.go()

线性路由的缺点是它不允许复杂的用户体验,例如标签视图。这就是非线性路由的用武之地。

非线性路由

非线性路由是一个许多学习使用 Ionic 构建移动应用的 Web 开发者可能不熟悉的概念。

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

以下是非线性路由的示例:

在上面的示例中,我们从 Originals 标签开始。点击一个卡片会将我们带到 Originals 标签内的 Ted Lasso 视图。

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

为什么这是非线性路由?我们之前所在的视图是 Search 视图。但是,在 Ted Lasso 视图上按下返回按钮应该会回到 Originals 标签的根视图。这是因为移动应用中的每个标签都被视为自己的堆栈。使用标签部分将更详细地讨论这一点。

如果在 Ted Lasso 视图中简单地调用 router.go(-1),我们会回到 Search 视图,这是不正确的。

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

应该选择哪种?

我们建议在您需要添加非线性路由之前,尽可能保持应用的简单性。非线性路由非常强大,但它也为移动应用增加了相当大的复杂性。

非线性路由最常见的两种用法是标签页和嵌套的 ion-router-outlet。我们建议仅在您的应用满足标签页或嵌套路由出口使用场景时才使用非线性路由。

有关标签页的更多信息,请参阅使用标签

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

共享 URL 与嵌套路由

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

共享 URL

共享 URL 是一种路由配置,其中路由具有共同的 URL 片段。以下是共享 URL 配置的示例:

const routes: Array<RouteRecordRaw> = [
{
path: '/dashboard',
component: DashboardMainPage,
},
{
path: '/dashboard/stats',
component: DashboardStatsPage,
},
];

上述路由被认为是"共享"的,因为它们重用了 URL 中的 dashboard 片段。

嵌套路由

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

const routes: Array<RouteRecordRaw> = [
{
path: '/dashboard/:id',
component: DashboardRouterOutlet,
children: [
{
path: '',
component: DashboardMainPage,
},
{
path: 'stats',
component: DashboardStatsPage,
},
],
},
];

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

应该选择哪种?

当您想从页面 A 过渡到页面 B,同时保持两个页面在 URL 中的关系时,共享 URL 非常有用。在我们之前的示例中,/dashboard 页面上的按钮可以过渡到 /dashboard/stats 页面。两个页面之间的关系通过 a) 页面过渡和 b) url 得以保持。

当您想在出口 A 中渲染内容,同时在嵌套出口 B 内渲染子内容时,应该使用嵌套路由。您会遇到的最常见用例是标签页。当您加载一个标签页 Ionic 启动应用时,您会看到 ion-tab-barion-tabs 组件在第一个 ion-router-outlet 中渲染。ion-tabs 组件渲染另一个 ion-router-outlet,负责渲染每个标签页的内容。

在移动应用中,很少有使用场景使嵌套路由有意义。如有疑问,请使用共享 URL 路由配置。我们强烈建议避免在标签页以外的上下文中使用嵌套路由,因为它可能会使应用导航变得混乱。

使用标签

当使用标签页时,Ionic Vue 需要一种方式知道哪个视图属于哪个标签。IonTabs 组件在这里很有用,但让我们先看看路由设置是什么样的:

const routes: Array<RouteRecordRaw> = [
{
path: '/',
redirect: '/tabs/tab1',
},
{
path: '/tabs/',
component: Tabs,
children: [
{
path: '',
redirect: 'tab1',
},
{
path: 'tab1',
component: () => import('@/views/Tab1.vue'),
},
{
path: 'tab2',
component: () => import('@/views/Tab2.vue'),
},
{
path: 'tab3',
component: () => import('@/views/Tab3.vue'),
},
],
},
];

在这里,我们的 tabs 路径加载了一个 Tabs 组件。我们将每个标签页作为 children 数组中的路由对象提供。在这个示例中,我们将路径命名为 tabs,但这是可以自定义的。

让我们先来看看我们的 Tabs 组件:

<template>
<ion-page>
<ion-tabs>
<ion-router-outlet></ion-router-outlet>
<ion-tab-bar slot="bottom">
<ion-tab-button tab="tab1" href="/tabs/tab1">
<ion-icon :icon="triangle" />
<ion-label>Tab 1</ion-label>
</ion-tab-button>

<ion-tab-button tab="tab2" href="/tabs/tab2">
<ion-icon :icon="ellipse" />
<ion-label>Tab 2</ion-label>
</ion-tab-button>

<ion-tab-button tab="tab3" href="/tabs/tab3">
<ion-icon :icon="square" />
<ion-label>Tab 3</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>
</ion-page>
</template>

<script setup lang="ts">
import { IonTabBar, IonTabButton, IonTabs, IonLabel, IonIcon, IonPage, IonRouterOutlet } from '@ionic/vue';
import { ellipse, square, triangle } from 'ionicons/icons';
</script>

如果您之前使用过 Ionic 框架,这应该很熟悉。我们创建一个 ion-tabs 组件并提供一个 ion-tab-barion-tab-bar 提供 ion-tab-button 组件,每个组件都有一个 tab 属性,与路由配置中对应的标签相关联。我们还提供了一个 ion-router-outlet,为 ion-tabs 提供一个出口来渲染不同的标签视图。

Ionic 中标签的工作方式

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

这种行为很重要,因为它不同于在其他基于 Web 的 UI 库中发现的大多数标签实现。其他库通常将标签作为单一历史堆栈来管理。

由于 Ionic 专注于帮助开发者构建移动应用,Ionic 中的标签被设计为尽可能接近原生移动标签。因此,Ionic 标签中的某些行为可能不同于您在其他 UI 库中看到的标签实现。请继续阅读以了解更多关于这些差异的信息。

标签内的子路由

向标签添加额外路由时,应将其编写为兄弟路由,以父标签作为路径前缀。下面的示例将 /tabs/tab1/view 路由定义为 /tabs/tab1 路由的兄弟路由。由于这个新路由具有 tab1 前缀,它将在 Tabs 组件内渲染,并且 Tab 1 在 ion-tab-bar 中仍然会被选中。

const routes: Array<RouteRecordRaw> = [
{
path: '/',
redirect: '/tabs/tab1',
},
{
path: '/tabs/',
component: Tabs,
children: [
{
path: '',
redirect: 'tab1',
},
{
path: 'tab1',
component: () => import('@/views/Tab1.vue'),
},
{
path: 'tab1/view',
component: () => import('@/views/Tab1View.vue'),
},
{
path: 'tab2',
component: () => import('@/views/Tab2.vue'),
},
{
path: 'tab3',
component: () => import('@/views/Tab3.vue'),
},
],
},
];

在标签之间切换

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

实际应用中一个很好的例子是 iOS App Store 和 Google Play Store 移动应用。这些应用都提供标签界面,但两者都不会将用户路由到其他标签。例如,iOS App Store 应用中的"Games"标签永远不会将用户引导到"Search"标签,反之亦然。

让我们看看一些使用标签时常犯的错误。

多个标签引用的设置标签

一种常见做法是将设置视图创建为单独的标签。如果开发者需要展示几个嵌套的设置菜单,这很好。但是,其他标签不应该尝试路由到设置标签。正如我们上面提到的,设置标签唯一应该被激活的方式是用户点击适当的标签按钮。

如果您发现您的标签需要引用设置标签,我们建议使用 ion-modal 将设置视图设为模态框。这是 iOS App Store 应用中的做法。通过这种方法,任何标签都可以呈现模态框,而不会破坏每个标签作为独立堆栈的移动标签模式。

下面的示例展示了 iOS App Store 应用如何处理从多个标签呈现"Account"视图。通过在模态框中呈现"Account"视图,应用可以在移动标签最佳实践范围内,在多个标签中显示相同的视图。

跨标签复用视图

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

相反,我们建议在每个标签中创建引用同一组件的路由。这是 Spotify 等流行应用中的做法。例如,您可以从"Home"、"Search"和"Your Library"标签访问专辑或播客。在访问专辑或播客时,用户停留在该标签内。应用通过在代码库中为每个标签创建路由并共享公共组件来实现这一点。

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

Home 标签Search 标签

组件

IonRouterOutlet 组件

IonRouterOutlet 组件提供了一个容器来渲染您的视图。它类似于其他 Vue 应用中的 RouterView 组件,不同之处在于 IonRouterOutlet 可以在同一个出口中以 DOM 方式渲染多个页面。当一个组件在 IonRouterOutlet 中渲染时,我们认为这是 Ionic 框架的"页面"。路由出口容器控制页面之间的过渡动画,以及控制页面何时被创建和销毁。这有助于在视图之间来回切换时保持状态。

在模板中设置 IonRouterOutlet 时,不应在其内部提供任何内容。虽然 IonRouterOutlet 可以嵌套在子组件中,但我们不建议这样做,因为它通常会使应用导航变得混乱。有关更多信息,请参阅共享 URL 与嵌套路由

IonPage 组件

IonPage 组件包裹 Ionic Vue 应用中的每个视图,使页面过渡和堆栈导航正常工作。使用路由导航到的每个视图都必须包含一个 IonPage 组件。

IonPage 也是正确样式所必需的。它提供了一个 flex 容器,确保页面内容(如 IonContent)具有适当的大小,并且不会与其他 UI 元素(如 IonTabBar)重叠。

<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Home</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">Hello World</ion-content>
</ion-page>
</template>

<script setup lang="ts">
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/vue';
</script>

通过 IonModalIonPopover 呈现的组件通常不需要 IonPage 组件,除非您需要一个包装元素。在这种情况下,我们建议使用 IonPage,以便组件尺寸仍然能够正确计算。

函数

useIonRouter 函数

useIonRouter(): UseIonRouterResult

返回 Ionic 路由实例,包含用于导航、自定义页面过渡和原生功能路由上下文的 API 方法。此函数可以与 Vue 的 useRouter 函数结合使用。

有关用法示例,请参考我们的工具函数

URL 参数

让我们扩展我们的原始路由示例,展示如何使用 URL 参数。我们建议将 URL 参数作为 props 传递,这样组件就不需要直接引用路由,从而更容易隔离地复用和测试。

const routes: Array<RouteRecordRaw> = [
{
path: '/',
redirect: '/home',
},
{
path: '/home',
name: 'Home',
component: HomePage,
},
{
path: '/detail/:id',
name: 'Detail',
component: DetailPage,
props: true,
},
];

请注意,我们现在在 detail 路径字符串的末尾添加了 :id。URL 参数是路由路径的动态部分。当用户导航到像 /details/1 这样的 URL 时,"1"被保存到名为 "id" 的参数中,当路由渲染时可以在组件中访问。在路由记录上设置 props: true 告诉 Vue Router 将匹配的 URL 参数作为 props 传递给组件。

让我们看看如何在组件中使用它:

<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Details</ion-title>
</ion-toolbar>
</ion-header>

<ion-content> Detail ID: {{ id }} </ion-content>
</ion-page>
</template>

<script setup lang="ts">
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/vue';

defineProps<{ id: string }>();
</script>

URL 中的 id 参数作为 prop 接收并呈现在屏幕上。该组件本身不依赖路由。

路由历史

Vue Router 带有可配置的历史模式。让我们看看不同的选项以及为什么您可能想要使用它们。

  • createWebHistory:此选项创建 HTML5 历史。它利用 History API 实现 URL 导航而无需重新加载页面。这是单页应用最常见的历史模式。如有疑问,请使用 createWebHistory

  • createWebHashHistory:此选项在您的 URL 中添加哈希(#)。这对于没有主机或无法完全控制服务器路由的 Web 应用很有用。搜索引擎有时会忽略哈希片段,因此如果 SEO 对您的应用很重要,您应该改用 createWebHistory

  • createMemoryHistory:此选项创建基于内存的历史。主要用于处理服务器端渲染(SSR)。

更多信息

有关使用 Vue Router 进行 Vue 路由的更多信息,请查阅他们的文档:http://router.vuejs.org/。