Vue 路由基础 - Vue Router Essentials

  • 学一下 Vue Router 的一些 API
  • Ref:https://router.vuejs.org/

创建路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { createMemoryHistory, createRouter } from "vue-router";

import HomeView from "./HomeView.vue";
import AboutView from "./AboutView.vue";

const routes = [
{ path: "/", component: HomeView },
{ path: "/about", component: AboutView },
];

const router = createRouter({
history: createMemoryHistory(),
routes,
});

export default router;

这里用的是 createWebHistory,也可以使用 createWebHashHistory,或 createMemoryHistory,具体可以参考官方文档。

随后还需要将 router 挂载到 Vue 应用上:

1
2
3
4
5
6
7
8
9
10
import { createApp } from "vue";

import App from "./App.vue";
import router from "./router";

const app = createApp(App);

app.use(router);

app.mount("#app");

引入 RouterView 组件来渲染路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<h1>Hello App!</h1>
<p><strong>Current route path:</strong> {{ $route.fullPath }}</p>
<nav>
<RouterLink to="/">Go to Home</RouterLink>
<br />
<RouterLink to="/about">Go to About</RouterLink>
</nav>
<main>
<RouterView />
</main>
</template>

<style scoped></style>

动态路由匹配

带参数的动态路由匹配

将给定匹配模式的路由映射到同一个组件,例如 RESTful 风格的路由:

1
2
3
4
5
6
7
import User from "./User.vue";

// 这些都会传递给 `createRouter`
const routes = [
// 动态字段以冒号开始
{ path: "/users/:id", component: User },
];

路径参数 用冒号 : 表示。当一个路由被匹配时,它的 params 的值将在每个组件中以 route.params 的形式暴露出来

1
2
3
4
5
6
<template>
<div>
<!-- 当前路由可以通过 $route 在模板中访问 -->
User {{ $route.params.id }}
</div>
</template>

响应路由参数的变化

要对同一个组件中参数的变化做出响应的话,你可以简单地 watch $route 对象上的任意属性

1
2
3
4
5
6
7
8
9
10
11
import { watch } from "vue";
import { useRoute } from "vue-router";

const route = useRoute();

watch(
() => route.params.id,
(newId, oldId) => {
// 对路由变化做出响应...
},
);

捕获所有路由或 404 Not found 路由

1
2
3
4
5
6
const routes = [
// 将匹配所有内容并将其放在 `route.params.pathMatch` 下
{ path: "/:pathMatch(.*)*", name: "NotFound", component: NotFound },
// 将匹配以 `/user-` 开头的所有内容,并将其放在 `route.params.afterUser` 下
{ path: "/user-:afterUser(.*)", component: UserGeneric },
];

路由的匹配语法

在参数中自定义正则

1
2
3
4
5
6
const routes = [
// /:orderId -> 仅匹配数字
{ path: "/:orderId(\\d+)" },
// /:productName -> 匹配其他任何内容
{ path: "/:productName" },
];

确保转义反斜杠 (\),就像我们对 \d (变成\\d)所做的那样,在 JavaScript 中实际传递字符串中的反斜杠字符。

可重复的参数

通过 *(0 个或多个)和 +(1 个或多个)将参数标记为可重复:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const routes = [
// /:chapters -> 匹配 /one, /one/two, /one/two/three, 等
{ path: "/:chapters+" },
// /:chapters -> 匹配 /, /one, /one/two, /one/two/three, 等
{ path: "/:chapters*" },
];

// 给定 { path: '/:chapters*', name: 'chapters' },
router.resolve({ name: "chapters", params: { chapters: [] } }).href;
// 产生 /
router.resolve({ name: "chapters", params: { chapters: ["a", "b"] } }).href;
// 产生 /a/b

// 给定 { path: '/:chapters+', name: 'chapters' },
router.resolve({ name: "chapters", params: { chapters: [] } }).href;
// 抛出错误,因为 `chapters` 为空

也可以通过在右括号后添加它们与自定义正则结合使用

1
2
3
4
5
6
7
const routes = [
// 仅匹配数字
// 匹配 /1, /1/2, 等
{ path: "/:chapters(\\d+)+" },
// 匹配 /, /1, /1/2, 等
{ path: "/:chapters(\\d+)*" },
];

Sensitive 与 strict 路由配置

默认情况下,所有路由是不区分大小写的,并且能匹配带有或不带有尾部斜线的路由。例如,路由 /users 将匹配 /users/users/、甚至 /Users/。这种行为可以通过 strictsensitive 选项来修改,它们既可以应用在整个全局路由上,又可以应用于当前路由上

1
2
3
4
5
6
7
8
9
10
11
12
const router = createRouter({
history: createWebHistory(),
routes: [
// 将匹配 /users/posva 而非:
// - /users/posva/ 当 strict: true
// - /Users/posva 当 sensitive: true
{ path: "/users/:id", sensitive: true },
// 将匹配 /users, /Users, 以及 /users/42 而非 /users/ 或 /users/42/
{ path: "/users/:id?" },
],
strict: true, // applies to all routes
});

可选参数

可以通过使用 ? 修饰符(0 个或 1 个)将一个参数标记为可选

1
2
3
4
5
6
const routes = [
// 匹配 /users 和 /users/posva
{ path: "/users/:userId?" },
// 匹配 /users 和 /users/42
{ path: "/users/:userId(\\d+)?" },
];

嵌套路由

一个被渲染的组件也可以包含自己嵌套的 <router-view>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const routes = [
{
path: "/user/:id",
component: User,
children: [
{
// 当 /user/:id/profile 匹配成功
// UserProfile 将被渲染到 User 的 <router-view> 内部
path: "profile",
component: UserProfile,
},
{
// 当 /user/:id/posts 匹配成功
// UserPosts 将被渲染到 User 的 <router-view> 内部
path: "posts",
component: UserPosts,
},
],
},
];

注意,以 / 开头的嵌套路径将被视为根路径(如果加了的话,子路由 path 就不包含父路由的 path)。这允许你利用组件嵌套,而不必使用嵌套的 URL。

此时,按照上面的配置,当你访问 /user/eduardo 时,在 Userrouter-view 里面什么都不会呈现,因为没有匹配到嵌套路由。也许你确实想在那里渲染一些东西。在这种情况下,你可以提供一个空的嵌套路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
const routes = [
{
path: "/user/:id",
component: User,
children: [
// 当 /user/:id 匹配成功
// UserHome 将被渲染到 User 的 <router-view> 内部
{ path: "", component: UserHome },

// ...其他子路由
],
},
];

嵌套的命名路由

1
2
3
4
5
6
7
8
const routes = [
{
path: "/user/:id",
component: User,
// 请注意,只有子路由具有名称
children: [{ path: "", name: "user", component: UserHome }],
},
];
1
2
3
4
5
6
7
8
const routes = [
{
path: "/user/:id",
name: "user-parent",
component: User,
children: [{ path: "", name: "user", component: UserHome }],
},
];

这两种配置的区别在于,当我们采用基于 name 的路由跳转时,可以选择是否渲染子路由,如果使用父路由的 name 那么不会渲染子路由,反之则同时渲染父子路由,但需要注意的是,如果通过 path 或是跳转后刷新页面,则父子路由都将会被渲染。

忽略父组件

仅利用路由的父子关系,但不嵌套路由组件。这对于将具有公共路径前缀的路由分组在一起或使用更高级的功能时很有用,例如:路由独享的守卫或路由元信息。

1
2
3
4
5
6
7
8
9
10
const routes = [
{
path: "/admin",
children: [
{ path: "", component: AdminOverview },
{ path: "users", component: AdminUserList },
{ path: "users/:id", component: AdminUserDetails },
],
},
];

由于父级没有指定路由组件,顶级 <router-view> 将跳过父级并仅使用子路由组件。

命名路由

我们可以使用 name 而不是 path 来传递 to 属性给 <router-link>

1
2
3
4
5
6
7
const routes = [
{
path: "/user/:username",
name: "profile",
component: User,
},
];
1
2
3
<router-link :to="{ name: 'profile', params: { username: 'erina' } }">
User profile
</router-link>

编程式导航

导航到不同的位置

声明式 编程式
<router-link :to="..."> router.push(...)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { useRouter } from "vue-router";
const router = useRouter();
// 字符串路径
router.push("/users/eduardo");

// 带有路径的对象
router.push({ path: "/users/eduardo" });

// 命名的路由,并加上参数,让路由建立 url
router.push({ name: "user", params: { username: "eduardo" } });

// 带查询参数,结果是 /register?plan=private
router.push({ path: "/register", query: { plan: "private" } });

// 带 hash,结果是 /about#team
router.push({ path: "/about", hash: "#team" });

如果提供了 path,params 会被忽略,构建字符串路径或带有 path 的对象时,请提供已编码的路径。使用 encodeURIComponent 编码每个动态片段

router.push 和所有其他导航方法都会返回一个 Promise,让我们可以等到导航完成后才知道是成功还是失败。

替换当前位置

声明式 编程式
<router-link :to="..." replace> router.replace(...)

类似于 router.push,唯一不同的是,它在导航时不会向 history 添加新记录,正如它的名字所暗示的那样——它取代了当前的条目。

也可以直接在传递给 router.pushto 参数中增加一个属性 replace: true

1
2
3
router.push({ path: "/home", replace: true });
// 相当于
router.replace({ path: "/home" });

横跨历史

该方法采用一个整数作为参数,表示在历史堆栈中前进或后退多少步,类似于 window.history.go(n)

1
2
3
4
5
6
7
8
9
10
11
12
// 向前移动一条记录,与 router.forward() 相同
router.go(1);

// 返回一条记录,与 router.back() 相同
router.go(-1);

// 前进 3 条记录
router.go(3);

// 如果没有那么多记录,静默失败
router.go(-100);
router.go(100);

命名视图

有时候想同时 (同级) 展示多个视图,而不是嵌套展示,例如创建一个布局,有 sidebar (侧导航) 和 main (主内容) 两个视图,这个时候命名视图就派上用场了。你可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。如果 router-view 没有设置名字,那么默认为 default

1
2
3
<router-view class="view left-sidebar" name="LeftSidebar" />
<router-view class="view main-content" />
<router-view class="view right-sidebar" name="RightSidebar" />
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: "/",
components: {
default: Home,
// LeftSidebar: LeftSidebar 的缩写
LeftSidebar,
// 它们与 `<router-view>` 上的 `name` 属性匹配
RightSidebar,
},
},
],
});

注意这里是 components 而不是 component,因为我们有多个视图需要渲染。

嵌套命名视图

UserSettings.vue
1
2
3
4
5
6
<div>
<h1>User Settings</h1>
<NavBar />
<router-view />
<router-view name="helper" />
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
path: '/settings',
// 你也可以在顶级路由就配置命名视图
component: UserSettings,
children: [{
path: 'emails',
component: UserEmailsSubscriptions
}, {
path: 'profile',
components: {
default: UserProfile,
helper: UserProfilePreview
}
}]
}

其实就是在子路由中配置 components

重定向和别名

重定向

通过 routes 配置来完成

1
const routes = [{ path: "/home", redirect: "/" }];

也可以是命名路由

1
const routes = [{ path: "/home", redirect: { name: "homepage" } }];

甚至是一个方法,动态返回重定向目标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const routes = [
{
// /search/screens -> /search?q=screens
path: "/search/:searchText",
redirect: (to) => {
// 方法接收目标路由作为参数
// return 重定向的字符串路径/路径对象
return { path: "/search", query: { q: to.params.searchText } };
},
},
{
path: "/search",
// ...
},
];

请注意,导航守卫并没有应用在跳转路由上,而仅仅应用在其目标上。在上面的例子中,在 /home 路由中添加 beforeEnter 守卫不会有任何效果。

在写 redirect 的时候,可以省略 component 配置,因为它从来没有被直接访问过,所以没有组件要渲染。唯一的例外是嵌套路由:如果一个路由记录有 childrenredirect 属性,它也应该有 component 属性(这是因为子路由需要父路由的 router-view 组件承担。

相对重定向

1
2
3
4
5
6
7
8
9
10
const routes = [
{
// 将总是把/users/123/posts重定向到/users/123/profile。
path: "/users/:id/posts",
redirect: (to) => {
// 该函数接收目标路由作为参数
return to.path.replace(/posts$/, "profile");
},
},
];

别名

alias 与 redirect 的区别在与是否改变 URL

1
const routes = [{ path: "/", component: Homepage, alias: "/home" }];

这里访问 /home 也将渲染 Homepage 组件,同时 URL 不改变

也可以采用数组提供多个 alias

1
2
3
4
5
6
7
8
9
10
11
12
13
const routes = [
{
path: "/users",
component: UsersLayout,
children: [
// 为这 3 个 URL 呈现 UserList
// - /users
// - /users/list
// - /people
{ path: "", component: UserList, alias: ["/people", "list"] },
],
},
];

路由组件传参

布尔模式

在你的组件中使用 $route 或 useRoute() 会与路由紧密耦合,这限制了组件的灵活性,我们也可以通过 props 配置来传递路由参数:

1
2
3
4
5
6
7
8
9
10
<!-- User.vue -->
<script setup>
defineProps({
id: String,
});
</script>

<template>
<div>User {{ id }}</div>
</template>

同时需要配置 props: true,使 route.params 将被设置为组件的 props

1
const routes = [{ path: "/user/:id", component: User, props: true }];

命名视图

对于具有命名视图的路由需要指定每一个视图的 props 配置

1
2
3
4
5
6
7
const routes = [
{
path: "/user/:id",
components: { default: User, sidebar: Sidebar },
props: { default: true, sidebar: false },
},
];

对象模式

props 是一个对象时,它将被静态地传递给组件

1
2
3
4
5
6
7
const routes = [
{
path: "/promotion/from-newsletter",
component: Promotion,
props: { newsletterPopup: false },
},
];

函数模式

可以创建一个返回 props 的函数

1
2
3
4
5
6
7
const routes = [
{
path: "/search",
component: SearchUser,
props: (route) => ({ query: route.query.q }),
},
];

通过 RouterView

以通过 <RouterView> 插槽 传递任意参数

1
2
3
4
5
6
<RouterView v-slot="{ Component }">
<component
:is="Component"
view-prop="value"
/>
</RouterView>

本质上 RouterView 向 slot 暴露了需要渲染的 Component,因此我们可以使用 <component> 来渲染它,并传递任意参数。

活动链接

应用程序通常都会有一个渲染 RouterLink 列表的导航组件。我们也许想对这个列表中匹配当前路由的链接进行视觉区分。RouterLink 组件会为匹配当前路由的链接添加两个 CSS 类,router-link-activerouter-link-exact-active

链接在什么时候匹配当前路由

当满足以下条件时,RouterLink 被认为是匹配当前路由的:

  1. 它与当前路径匹配相同的路由记录(即配置的路由)。
  2. 它的 params 与当前路径的 params 相同。

如果你使用了嵌套路由,任何指向祖先路由的链接也会被认为是匹配当前路由的,只要相关的 params 匹配。

其他路由属性,例如 query,不会被考虑在内。

路径不一定需要完全匹配。例如,使用 alias 仍然会被认为是匹配的,只要它解析到相同的路由记录和 params

如果一个路由有 redirect,在检查链接是否匹配当前路由时不会跟随重定向。

精确匹配当前路由的链接

精确匹配不包括祖先路由。

1
2
3
4
5
6
<RouterLink to="/user/erina">
User
</RouterLink>
<RouterLink to="/user/erina/role/admin">
Role
</RouterLink>

考虑这两个链接,如果当前路径是 /user/erina/role/admin,那么这两个链接都会被认为是匹配当前路由的,因此 router-link-active 类会应用于这两个链接。但只有第二个链接会被认为是精确的,因此只有第二个链接会有 router-link-exact-active 类。

配置类名

RouterLink 组件有两个属性,activeClassexactActiveClass,可以用来更改应用的类名:

1
2
3
4
5
<RouterLink
activeClass="border-indigo-500"
exactActiveClass="border-indigo-700"
...
>

默认的类名也可以通过传递 linkActiveClasslinkExactActiveClass 选项给 createRouter() 来全局更改

1
2
3
4
5
const router = createRouter({
linkActiveClass: 'border-indigo-500',
linkExactActiveClass: 'border-indigo-700',
// ...
})