什么是 VueRouter ?
Vue Router 是 Vue.js 的官方路由 (SPA / MPA(SSR))。 Vue Router 与 Vue.js 核心深度集成,让用 Vue.js 构建单页应用变得轻而易举。 Vue Router 的功能包括:
- 嵌套路由映射
- 动态路由选择
- 模块化、基于组件的路由配置
- 路由参数、查询、通配符
- 展示由 Vue.js 的过渡系统提供的过渡效果
- 细致的导航控制
- 自动激活 CSS 类的链接
- HTML5 history 模式或 hash 模式
- 可定制的滚动行为
- URL 的正确编码
使用:
CDN 链接引入:
Unpkg 提供了基于 npm 的 CDN 链接,在 html 中引入以下代码即可:
perl
# vue
https://unpkg.com/vue@3
# vue-router
https://unpkg.com/vue-router@4
html
<script type="text/javascript" src="https://unpkg.com/vue-router@4" defer></script>
依赖安装:
shell
# 使用 npm 安装
npm install -S vue-router@4
# 使用 yarn 安装
yarn add vue-router@4
# 使用 pnpm 安装
pnpm install -S vue-router@4
基本使用:
CDN 链接直接使用:
使用步骤:
- 引入
vue
和vue-router
的 CDN 链接 - 使用
<router-view>
组件渲染对应的路由视图 - 使用
<router-link>
作为可以点击的跳转页面标签(类似于 HTML 里面的<a>
标签)
代码示例:
html
<script src="https://unpkg.com/vue@3"></script>
<script src="https://unpkg.com/vue-router@4"></script>
<div id="app">
<h1>Hello App!</h1>
<p>
<!--使用 router-link 组件进行导航 -->
<!--通过传递 `to` 来指定链接 -->
<!--`<router-link>` 将呈现一个带有正确 `href` 属性的 `<a>` 标签-->
<router-link to="/">Go to Home</router-link>
<router-link to="/about">Go to About</router-link>
</p>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</div>
<script>
const { createApp } = Vue;
const Home = { template: '<div>Home</div>' };
const About = { template: '<div>About</div>' };
const routes = [
{
path: '/',
component: Home
},
{
path: '/about',
component: About
}
];
const router = VueRouter.createRouter({
history: VueRouter.createWebHashHistory(), // 先使用 createWebHashHistory 创建 hash 路由
routes,
});
createApp({}).use(router).mount('#app');
</script>
NPM 安装依赖后使用:
使用步骤:
- 声明两个页面:
views/Home.vue
、views/About.vue
- 根据已有的组件需要创建路由对象
router
- 在 app 实例中使用
router
- 在
App.vue
中使用路由
目录结构:
html
router-demo
|- ...
|- src
|- App.vue
|- main.js
|- pages
|- Home.vue
|- About.vue
|- router
|- index.js
代码示例:
html
<template>
<h1>Home</h1>
</template>
<script setup></script>
html
<template>
<h1>About</h1>
</template>
<script setup></script>
javascript
import { createRouter } from 'vue-router';
import Home from '../pages/Home.vue';
const routes = [
{
path: '/',
component: Home
},
{
path: '/about',
component: () => import(/* webpackChunkName: Aboout */ '../pages/About.vue');
}
];
const router = createRouter({
history: VueRouter.createWebHashHistory(),
routes,
});
export default router;
javascript
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
const app = createApp(App);
app.use(router);
app.mount('#app');
html
<template>
<<h1>Hello App!</h1>
<p class="switcher">
<router-link to="/">Go to Home</router-link>
<router-link to="/about">Go to About</router-link>
</p>
<router-view></router-view>
</template>
<script setup></script>
说明
<router-view>
是vue-router
提供的一个内置组件,它的职能是把当前对应的路由视图渲染到设置的区域位置上<router-view>
中的页面如果需要被缓存,请配合<keep-alive>
组件一起使用<router-link>
是一个跳转页面的标签,提供设置to="xxx"
跳转到 xxx 页面。使用<router-link>
组件不会引起页面的全局刷新造成闪屏 (基于 history API 进行封装)- Vue@3 需要配合 VueRouter@4 使用,因为 Vue3 的插件几乎都是以 provide + inject 的方式进行注入使用的 (如果是 Vue@2 就需要配合 VueRouter@3 使用)
路由的参数传递:
路由的参数传递有两种方式:
- 通过路径参数传递
- 通过 query 传递
通过路径字符串中的动态参数传递
步骤:
- 定义路由,在路径中使用
:动态参数名
表明需要使用的动态参数 - 在动态组件中通过 params 获取
代码示例:
javascript
import User from '../views/User.vue';
// 这些都会传递给 `createRouter`
const routes = [
// 动态字段以冒号开始
{ path: '/users/:id', component: User },
// 在路径 /users/tom、/users/jerry 都能匹配到,参数就是后面被替换的字符串
];
html
<template>
<div>
ID: {{ id }}
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
const id = computed(() => {
const { id } = route.params;
return id;
});
</script>
匹配模式 | 匹配路径 | route.params |
---|---|---|
/users/:username | /users/eduardo | { username: 'eduardo' } |
/users/:username/posts/:postId | /users/eduardo/posts/123 | { username: 'eduardo', postId: '123' } |
通过 query 传递
步骤:
- 定义路由
- 跳转时设置 query
- 从 route 中获取当前的 query
代码示例:
- 定义路由:
javascript
import Detail from '../views/Detail.vue';
const routes = [
// ...
{
path: '/detail',
name: 'Detail',
component: Detail
}
];
- 跳转时设置 query:
- 使用标签式跳转
html
<router-link to="/detail?id=1&name=zhangsan">去张三详情页</router-link>
- 使用函数式跳转
javascript
import { useRouter } from 'vue-router';
const router = useRouter();
const handleBtnClick = () => {
router.push('/detail', {
query: {
id: 1,
name: 'zhangsan'
}
});
}
- 从 route 中获取当前的 query:
javascript
import { computed, onMounted } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
const query = computed(() => route.query);
onMounted(() => {
console.log(query);
});
匹配 404 路由:
常规参数只匹配 url 片段之间的字符,用 /
分隔。 如果我们想匹配任意路径,我们可以使用自定义的 路径参数 正则表达式,在 路径参数 后面的括号中加入 正则表达式 :
javascript
const routes = [
// 将匹配所有内容并将其放在 `route.params.pathMatch` 下
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
// 将匹配以 `/user-` 开头的所有内容,并将其放在 `route.params.afterUser` 下
{ path: '/user-:afterUser(.*)', component: UserGeneric },
// 通配路由,找不到就返回 404 页面
{ path: '/(.*)' }
]
嵌套路由:
描述:
一些应用程序的 UI 由多层嵌套的组件组成。在这种情况下,URL 的片段通常对应于特定的嵌套组件结构,例如:
javascript
/user/johnny/profile /user/johnny/posts
+------------------+ +-----------------+
| User | | User |
| +--------------+ | | +-------------+ |
| | Profile | | +------------> | | Posts | |
| | | | | | | |
| +--------------+ | | +-------------+ |
+------------------+ +-----------------+
在上述例子中:
- Vue Router 里面有一个一级路由需要渲染:
User
User
里面有两个二级路由:Profile
、Posts
对于这种需要嵌套显示的路由视图,我们需要怎么渲染呢?
步骤:
- 定义
User
中需要渲染的子路由数组userRoutes
- 将
childRoutes
放到 User 的children 里面 - 将
router
安装到 app 中 User
页面对应位置渲染路由
代码实现:
markdown
|- router
|- index.js
|- routes
|- userRoutes.js
|- views
|- User
|- index.vue
|- pages
|- Profile.vue
|- Posts.vue
- 定义
User
中需要渲染的子路由数组userRoutes
:
javascript
const userRoutes = [
{
path: 'profile',
name: 'Profile',
component: () => import('@/views/User/pages/Profile.vue')
},
{
path: 'posts',
name: 'Posts',
component: () => import('@/views/User/pages/Posts.vue')
}
];
export default userRoutes;
- 将
childRoutes
放到 User 的children 里面:
javascript
const { createRouter, createWebHashHistory } = VueRouter;
import userRoutes from './userRoutes';
const routes = [
{
path: '/user',
// name: 'User', // 如果有子路由,就不需要定义父级路由的 name
component: () => import('@/views/User/index.vue'),
children: [
...userRoutes
]
}
];
const router = createRouter({
routes,
mode: createWebHashHistory()
});
export default router;
- 将
router
安装到 app 中:
javascript
import App from './App.vue';
import router from './router';
const { createApp } = Vue;
const app = createApp(App);
app.use(router);
app.mount('#app');
User
页面对应位置渲染路由:
html
<template>
<!-- User.vue -->
<!-- ... -->
<router-view />
</template>
编程式导航:
概述:
在 Vue 的实例中,可以使用 useRouter
获取到路由的实例 router
,然后调用 router
上面的方法 push 或者 replace 进行跳转
对比:
声明式 | 编程式 |
---|---|
<router-link :to="..."> | router.push(...) |
router.replace(...) | |
router.back(...) | |
router.go(...) |
代码示例:
javascript
const { useRouter } = VueRouter;
const router = useRouter();
const handleSwitch = () => {
// 直接往历史栈中推入一个记录,具体参考:https://router.vuejs.org/zh/guide/essentials/navigation.html#%E5%AF%BC%E8%88%AA%E5%88%B0%E4%B8%8D%E5%90%8C%E7%9A%84%E4%BD%8D%E7%BD%AE
router.push('/...', {});
// 替换当前的历史记录,具体参考:https://router.vuejs.org/zh/guide/essentials/navigation.html#%E6%9B%BF%E6%8D%A2%E5%BD%93%E5%89%8D%E4%BD%8D%E7%BD%AE
router.replace('/...', {});
// 返回上一条历史记录
router.back();
// 横跨历史,具体参考:https://router.vuejs.org/zh/guide/essentials/navigation.html#%E6%A8%AA%E8%B7%A8%E5%8E%86%E5%8F%B2
router.go(-1);
}
重定向和别名
重定向 redirect
重定向也是通过 routes 配置来完成,下面例子是从 /home
重定向到 /
:
javascript
const routes = [{ path: '/home', redirect: '/' }]
重定向的目标也可以是一个命名的路由:
javascript
const routes = [{ path: '/home', redirect: { name: 'homepage' } }]
甚至是一个方法,动态返回重定向目标:
javascript
const routes = [
{
// /search/screens -> /search?q=screens
path: '/search/:searchText',
redirect: to => {
// 方法接收目标路由作为参数
// return 重定向的字符串路径/路径对象
return { path: '/search', query: { q: to.params.searchText } }
},
},
{
path: '/search',
// ...
},
]
注意:
- 导航守卫对于重定向配置的路由对象不生效
- 在写
redirect
的时候,可以省略component
配置,因为它从来没有被直接访问过
别名 alias
重定向是指当用户访问 /home
时,URL 会被 /
替换,然后匹配成 /
。那么什么是别名呢? 将 /
别名为 /home
,意味着当用户访问 /home
时,URL 仍然是 /home
,但会被匹配为用户正在访问 /
。 上面对应的路由配置为:
javascript
const routes = [{ path: '/', component: Homepage, alias: '/home' }]
通过别名,开发者可以:
- 自由地将 UI 结构映射到一个任意的 URL,而不受配置的嵌套结构的限制使别名以 / 开头
- 以使嵌套路径中的路径成为绝对路径。
多个别名:
javascript
const routes = [
{
path: '/users',
component: UsersLayout,
children: [
// 为这 3 个 URL 呈现 UserList
// - /users
// - /users/list
// - /people
{ path: '', component: UserList, alias: ['/people', 'list'] },
],
},
]
如果你的路由有参数,请确保在任何绝对别名中包含它们:
javascript
const routes = [
{
path: '/users/:id',
component: UsersByIdLayout,
children: [
// 为这 3 个 URL 呈现 UserDetails
// - /users/24
// - /users/24/profile
// - /24
{ path: 'profile', component: UserDetails, alias: ['/:id', ''] },
],
},
]
路由守卫:
Vue Router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。
全局前置守卫:
概述:
当一个导航触发时,全局前置守卫按照创建顺序调用。守 卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于等待中。 每个守卫方法接收两个参数:
可以返回的值如下:
false
: 取消当前的导航。如果浏览器的 URL 改变了(可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到from
路由对应的地址。
javascript
const router = createRouter({ ... });
router.beforeEach((to, from) => {
// ...
// 返回 false 以取消导航
return false;
});
- 一个路由地址: 通过一个路由地址跳转到一个不同的地址,就像你调用 router.push() 一样,你可以设置诸如
replace: true
或name: 'home'
之类的配置。当前的导航被中断,然后进行一个新的导航,就和from
一样。
javascript
router.beforeEach(async (to, from) => {
if (
// 检查用户是否已登录
!isAuthenticated &&
// ❗️ 避免无限重定向
to.name !== 'Login'
) {
// 将用户重定向到登录页面
return { name: 'Login' };
}
});
javascript
router.beforeEach(async (to, from) => {
// canUserAccess() 返回 `true` 或 `false`
const canAccess = await canUserAccess(to)
if (!canAccess) return '/login';
});
第三个参数 - next()
在之前的 Vue Router 版本中,也是可以使用第三个参数 next
方法。 但是 next 方法是一个常见的错误来源,可以通过 RFC 来消除错误。 next
方法 仍然是被支持的,这意味着你可以向任何导航守卫传递第三个参数。 在使用 next
方法时, 确保 next
在任何给定的导航守卫中都被严格调用一次。它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下。
全局后置守卫:
你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:
javascript
router.afterEach((to, from) => {
sendToAnalytics(to.fullPath);
});
它们对于分析、更改页面标题、声明页面等辅助功能以及许多其他事情都很有用。 它们也反映了 navigation failures 作为第三个参数:
javascript
router.afterEach((to, from, failure) => {
if (!failure) sendToAnalytics(to.fullPath);
});
单个守卫:
路由独享:
你可以直接在路由配置上定义 beforeEnter 守卫:
javascript
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: (to, from) => {
// reject the navigation
return false
},
},
];
beforeEnter
守卫 只在进入路由时触发,不会在 params
、query
或 hash
改变时触发。例如,从 /users/2
进入到 /users/3
或者从 /users/2#info
进入到 /users/2#projects
。它们只有在 从一个不同的 路由导航时,才会被触发。 你也可以将一个函数数组传递给 beforeEnter
,这在为不同的路由重用守卫时很有用:
javascript
function removeQueryParams(to) {
if (Object.keys(to.query).length)
return { path: to.path, query: {}, hash: to.hash }
}
function removeHash(to) {
if (to.hash) return { path: to.path, query: to.query, hash: '' }
}
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: [removeQueryParams, removeHash],
},
{
path: '/about',
component: UserDetails,
beforeEnter: [removeQueryParams],
},
]
请注意,你也可以通过使用路径 meta 字段和全局导航守卫来实现类似的行为。
组件独享:
你可以为路由组件添加以下配置:
onBeforeRouteUpdate
onBeforeRouteLeave
javascript
const { onBeforeRouteUpdate, onBeforeRouteLeave } = VueRouter;
onBeforeRouteUpdate(() => {});
onBeforeRouteLeave(() => {});
路由元信息:
概述:
有时,你可能希望将任意信息附加到路由上,(如过渡名称、谁可以访问路由等。)这些事情可以通过接收属性对象的meta
属性来实现,并且它可以在路由地址和导航守卫上都被访问到。定义路由的时候你可以这样配置 meta
字段:
javascript
const routes = [
{
path: '/posts',
component: PostsLayout,
children: [
{
path: 'new',
component: PostsNew,
// 只有经过身份验证的用户才能创建帖子
meta: { requiresAuth: true }
},
{
path: ':id',
component: PostsDetail
// 任何人都可以阅读文章
meta: { requiresAuth: false }
}
]
}
];
配合路由守卫使用:
一个路由匹配到的所有路由记录会暴露为 route
对象(还有在导航守卫中的路由对象)的 route.matched
数组。我们需要遍历这个数组来检查路由记录中的 meta
字段。 但是 Vue Router 还为你提供了一个 route.meta
方法,它是一个非递归合并所有 meta
字段的(从父字段到子字段)的方法:
javascript
router.beforeEach((to, from) => {
// 而不是去检查每条路由记录
// to.matched.some(record => record.meta.requiresAuth)
if (to.meta.requiresAuth && !auth.isLoggedIn()) {
// 此路由需要授权,请检查是否已登录
// 如果没有,则重定向到登录页面
return {
path: '/login',
// 保存我们所在的位置,以便以后再来
query: { redirect: to.fullPath },
}
}
})
路由懒加载
概述:
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。 如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。
使用:
Vue Router 支持开箱即用的动态导入,这意味着你可以用动态导入代替静态导入:
javascript
// 将
// import UserDetails from './views/UserDetails.vue'
// 替换成
const UserDetails = () => import('./views/UserDetails.vue')
const router = createRouter({
// ...
routes: [{ path: '/users/:id', component: UserDetails }],
})
component
(和 components
) 配置接收一个返回 Promise
组件的函数,Vue Router 只会在第一次进入页面时才会获取这个函数,然后使用缓存数据。这意味着你也可以使用更复杂的函数,只要它们返回一个 Promise
:
javascript
const UserDetails = () =>
Promise.resolve({
/* 组件定义 */
})
一般来说,对所有的路由都使用动态导入是个好主意。
注意
- 不要在路由中使用异步组件。异步组件仍然可以在路由组件中使用,但路由组件本身就是动态导入的。
- 如果你使用的是 webpack 之类的打包器,它将自动从代码分割中受益。
- 如果你使用的是 Babel,你将需要添加 syntax-dynamic-import 插件,才能使 Babel 正确地解析语法。
把组件按组分块:
使用 webpack
有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。只需要使用命名 chunk,一个特殊的注释语法来提供 chunk name
(需要 Webpack > 2.4):
javascript
const UserDetails = () =>
import(/* webpackChunkName: "group-user" */ './UserDetails.vue');
const UserDashboard = () =>
import(/* webpackChunkName: "group-user" */ './UserDashboard.vue');
const UserProfileEdit = () =>
import(/* webpackChunkName: "group-user" */ './UserProfileEdit.vue');
webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。
使用 Vite
在Vite中,你可以在 rollupOptions 下定义分块:
javascript
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
// https://rollupjs.org/guide/en/#outputmanualchunks
output: {
manualChunks: {
'group-user': [
'./src/UserDetails',
'./src/UserDashboard',
'./src/UserProfileEdit',
],
},
},
},
},
});
动态路由:
概述:
对路由的添加通常是通过 routes选项来完成的,但是在某些情况下,你可能想在应用程序已经运行的时候添加或删除路由。具有可扩展接口(如 Vue CLI UI )这样的应用程序可以使用它来扩展应用程序。
使用:
动态路由主要通过两个函数实现。router.addRoute()
和 router.removeRoute()
。它们只注册一个新的路由,也就是说,如果新增加的路由与当前位置相匹配,就需要你用 router.push() 或 router.replace() 来手动导航,才能显示该新路由。
替换路由:
看一下官网上面的例子: 想象一下,只有一个路由的以下路由:
javascript
const router = createRouter({
history: createWebHistory(),
routes: [{ path: '/:articleName', component: Article }],
})
进入任何页面,/about
,/store
,或者 /xxx
最终都会呈现 <Article>
组件。 如果我们在 /about
上添加一个新的路由:
javascript
router.addRoute({ path: '/about', component: About });
页面仍然会显示 Article 组件,我们需要**手动调用 router.replace() **来覆盖原来的位置(而不是添加一个新的路由,最后在我们的历史中两次出现在同一个位置):
javascript
router.addRoute({ path: '/about', component: About });
// 我们也可以使用 this.$route 或 route = useRoute() (在 setup 中)
router.replace(router.currentRoute.value.fullPath);
如果你需要等待新的路由显示,可以使用 await router.replace()
。
在导航守卫中添加路由:
如果你决定在导航守卫内部添加或删除路由,你不应该调用 router.replace()
,而是通过返回新的位置来触发重定向:
javascript
router.beforeEach(to => {
if (!hasNecessaryRoute(to)) {
router.addRoute(generateRoute(to));
// 触发重定向
return to.fullPath;
}
});
上面的例子有两个假设:第一,新添加的路由记录将与 to 位置相匹配,实际上导致与我们试图访问的位置不同。第二,hasNecessaryRoute()
在添加新的路由后返回 false
,以避免无限重定向。 因为是在重定向中,所以我们是在替换将要跳转的导航,实际上行为就像之前的例子一样。而在实际场景中,添加路由的行为更有可能发生在导航守卫之外,例如,当一个视图组件挂载时,它会注册新的路由。
添加嵌套路由
- 方式一:
javascript
router.addRoute({
name: 'admin',
path: '/admin',
component: Admin,
children: [{ path: 'settings', component: AdminSettings }],
});
- 方式二:
javascript
router.addRoute({ name: 'admin', path: '/admin', component: Admin });
router.addRoute('admin', { path: 'settings', component: AdminSettings });
删除路由
有几个不同的方法来删除现有的路由:
- 通过添加一个名称冲突的路由。如果添加与现有途径名称相同的途径,会先删除路由,再添加路由:js
javascript
router.addRoute({ path: '/about', name: 'about', component: About });
// 这将会删除之前已经添加的路由,因为他们具有相同的名字且名字必须是唯一的
router.addRoute({ path: '/other', name: 'about', component: Other });
- 通过调用
router.addRoute()
返回的回调:
javascript
const removeRoute = router.addRoute(routeRecord);
removeRoute(); // 删除路由如果存在的话
当路由没有名称时,这很有用。
- 通过使用
router.removeRoute()
按名称删除路由:
javascript
router.addRoute({ path: '/about', name: 'about', component: About });
// 删除路由
router.removeRoute('about');
需要注意的是,如果你想使用这个功能,但又想避免名字的冲突,可以在路由中使用 Symbol
作为名字。 当路由被删除时,所有的别名和子路由也会被同时删除