安装
vue-router是一个vue的插件,用来实现前端的路由, 推荐使用
pnpm add vue-router@4
进行安装。推荐配合vue3组合式api使用
基础
从一个例子开始
<!-- App.vue文件 -->
<div id="app">
<h1>Hello App!</h1>
<p>
<!--使用 router-link 组件进行导航 -->
<!--通过传递 `to` 来指定链接 -->
<!--`<router-link>` 将呈现一个带有正确 `href` 属性的 `<a>` 标签-->
<router-link to="/hello/112233">hello</router-link>
</p>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</div>
// router/index.js 文件
//createRouter 用来创建路由
//createWebHashHistory 指定路由是hash模式
import { createRouter,createWebHashHistory } from 'vue-router'
import Hello from '../components/Hello.vue'
import ErrorPage from '../components/ErrorPage.vue'
// 配置路由规则
const routers = [
// 匹配所有路由(正则匹配权重低),自定义404
{path: '/:all(.*)*', component: ErrorPage},
{path: '/hello/:id',component: Hello}
]
// 导出路由
export default createRouter({
history: createWebHashHistory(), // 这里我们使用简单的hash模式
routes: routers
})
// main.js 文件
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import routers from './routers'
createApp(App)
.use(routers) // 挂载路由
.mount('#app')
项目中的路由对象
tips: 别忘记挂载路由哦 如 use(router)
- $router: 通过this调用,或者通过
import
导出路由也一样,此路由对象,就是导出的路由对象,可以调用push等 ... <-- $route: 通过this调用,此路由对象是当前的路由对象,比如说 /Home 拿到的是当前 /Home 的路由对象,可以通过 $route.params 拿到路由参数等
动态路由
匹配规则
匹配模式 | 匹配路径 | 匹配说明 | $route.params | $route.query | $route.hash |
---|---|---|---|---|---|
/users/:username | /users/eduardo?sid=123#ok | ok | |||
/users/:username/posts/:postId | /users/eduardo/posts/123?sid=123#ok | ok | |||
/:pathMatch(.*)* | /a/b | 匹配所有 | |||
/user-:afterUser(.*) | /user-admin | 匹配user- 以开头的 | |||
/:orderId(\\d+) | /123 | 匹配数字 | |||
/:chapters+ | /a/a/a | 匹配重复的(一个或多个) |
从上面可以看出,路由匹配是支持自定义正则的,官方推荐的路由调试工具 https://paths.esm.dev/
Sensitive 与 strict 路由配置
const router = createRouter({
history: createWebHistory(),
routes: [
{
// /users ok
// /Users error
// /user/ error
path: '/users',
sensitive: true, // 大小写敏感
strict: true, // 是否严格按照path匹配
},
],
strict: true, // 或者这里设置全局
})
嵌套路由
比如我们有两个路由 /home/user,/home/posts。可以看出来他们都是处于 /home下的,属于是嵌套关系,我们可以使用以下代码实现
const routes = [
{
path: '/home',
component: Home,
children: [ // home的子级
{
path: 'user',
component: User,
},
{
path: 'posts',
component: Posts,
},
],
},
]
编程式导航
顶文的有个例子使用
<router-link to="/home">点我啊</router-link>
进行路由导航,其实点击<router-link :to="...">
相当于调用router.push(...)
,编程时导航就是通过js代码来跳转路由,通过以下例子介绍下
// 字符串路径
router.push('/users/admin')
// 带有路径的对象
router.push({ path: '/users/admin' })
// 命名的路由,让路由建立 url, 可以传递params, query,hash, 传递的参数vue-router会自动编码,如果放在path中需要先编码一下
router.push({ name: 'user', params: { username: 'admin' }, query: { plan: '花儿为什么这么红' }, hash: '#h1' } })
-
push
: 这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,会回到之前的 URL -
replace
: 会替代当前位置,无法回退,其余的同上 -
forward
: 向前移动一条记录 -
back
: 向后移动一条记录 -
go
: go(-1)后退,反之亦然
命名路由
除了
path
之外,你还可以为任何路由提供name
。这有以下优点:
- 没有硬编码的 URL
params
的自动编码/解码。- 防止你在 url 中出现打字错误。
const routes = [
{
path: '/user/:username',
name: 'user',
component: User,
},
]
router.push({ name: 'user', params: { username: 'any' } }
命名视图
有时候我们想要在同一个页面里展示多个视图,又不想嵌套展示,例如创建一个布局,sidebar(侧边栏),main(主要区域),示例代码如下。当然也可以嵌套命名视图,
<router-view class="view left-sidebar" name="LeftSidebar"></router-view>
<router-view class="view main-content"></router-view>
<router-view class="view right-sidebar" name="RightSidebar"></router-view>
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
components: {
default: Home,
// LeftSidebar: LeftSidebar 的缩写
LeftSidebar,
// 它们与 `<router-view>` 上的 `name` 属性匹配
RightSidebar,
},
},
],
})
// 或使用嵌套命名视图
{
path: '/settings',
// 你也可以在顶级路由就配置命名视图
component: UserSettings,
children: [{
path: 'emails',
component: UserEmailsSubscriptions
}, {
path: 'profile',
components: {
default: UserProfile,
helper: UserProfilePreview
}
}]
}
重定向和别名
// redirect可以是一个字符串,一个对象,一个函数。示例如下
const routes = [
{
// /search/screens -> /search?q=screens
path: '/search/:searchText',
redirect: to => {
// 方法接收目标路由作为参数
// return 重定向的字符串路径/路径对象
return { path: '/search', query: { q: to.params.searchText } }
},
},
{
path: '/search',
// ...
},
]
// 别名,就是说当访问这个别名的时候,跳转到此路由,不受路由限制。推荐少用少用少用!!!
const routes = [
{
path: '/home',
alias: ['/',‘’] // 也可以设置两个别名
}
]
给组件传递props
我们可以通过$route拿到参数,但是这样写的话组件过于依赖路由,我们有更好的一种方式,通过设置
props: true
开启props传参, 示例如下
const routes = [
{
path: '/user/:id',
components: { default: User, sidebar: Sidebar },
// 有以下几种写法
// 1:props: true
// 2:props: { default: true, sidebar: false }
// 3:{ id: '传入的参数' }
// 4: route=> ({ id: route.params.id })
}
]
路由模式
Hash 模式
在浏览器url使用了哈希字符 # 这部分url没有被发送服务器。 在SEO优化 有不好的影响,如果担心这个问题可以使用HTML5模式
import { createRouter, createWebHashHistory } from 'vue-router'
const router = createRouter({
history: createWebHashHistory(),
routes: [
//...
],
})
HTML5 模式
这种模式看起来像一个比较正常的url 比如
https://example.com/home/user
。 需要在服务器配置
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
//...
],
})
服务器配置
-
nginx
location / { try_files $uri $uri/ /index.html; }
-
node
const http = require('http') const fs = require('fs') const httpPort = 80 http .createServer((req, res) => { fs.readFile('index.html', 'utf-8', (err, content) => { if (err) { console.log('We cannot open "index.html" file.') } res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', }) res.end(content) }) }) .listen(httpPort, () => { console.log('Server listening on: http://localhost:%s', httpPort) })
-
iis
<?xml version="1.0" encoding="UTF-8"?> <configuration> <system.webServer> <rewrite> <rules> <rule name="Handle History Mode and custom 404/500" stopProcessing="true"> <match url="(.*)" /> <conditions logicalGrouping="MatchAll"> <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" /> <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" /> </conditions> <action type="Rewrite" url="/" /> </rule> </rules> </rewrite> </system.webServer> </configuration>
-
进阶
好啦,看到这已经理解基础了,想了解更深的话继续往下看,我们有时候需要做点别的 事情
导航守卫
导航守卫就是AOP的思想,在路由进入前、进入后 、更新,做点事情。导航守卫可以使用多个会依次执行,类似于管道,路由匹配的组件会复用。 它可以挂载到全局、单个路由、单个组件。各有妙用,望君细品。代码示例如下
全局守卫
// 进入前
router.beforeEach((to, from)=>{
//返回 false 以取消导航,也可以返回一个对象,比如 {name:'home'},相当于重定向
})
// 或者加入第三个参数 next,如果声明了第三个参数,则必须调用一个,否则会一直等待
router.beforeEach((to, from, next)=>{
// next(false) 或者 next({path: '/home'})
})
// 进入后
router.afterEach((to, from)=>{
})
// 解析守卫,发生在beforeEach后
router.beforeResolve((to, from)=>{
})
路由专用守卫
// 进入路由触发(但是还没有进入,在beforeResolve前触发)
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], // 可以是数组,按顺序调用
},
]
组件内的守卫
// 选项式: beforeRouteEnter,beforeRouteUpdate,beforeRouteLeave
// 组合式:onBeforeRouteUpdate,onBeforeRouteLeave
const UserDetails = {
template: `...`,
beforeRouteEnter(to, from) {
// 在渲染该组件的对应路由被验证前调用
// 不能获取组件实例 `this` !
// 因为当守卫执行时,组件实例还没被创建!
},
beforeRouteUpdate(to, from) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
// 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
},
beforeRouteLeave(to, from) {
// 在导航离开渲染该组件的对应路由时调用
// 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
},
}
完整的导航解析流程
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave
守卫。 - 调用全局的
beforeEach
守卫。 - 在重用的组件里调用
beforeRouteUpdate
守卫(2.2+)。 - 在路由配置里调用
beforeEnter
。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter
。 - 调用全局的
beforeResolve
守卫(2.5+)。 - 导航被确认。
- 调用全局的
afterEach
钩子。 - 触发 DOM 更新。
- 调用
beforeRouteEnter
守卫中传给next
的回调函数,创建好的组件实例会作为回调函数的参数传入。
路由元信息
有时候想要将信息附加到路由上,我们看下面这个例子
const routes = [
{
path: '/posts',
component: PostsLayout,
children: [
{
path: 'new',
component: PostsNew,
// 只有经过身份验证的用户才能创建帖子
meta: { requiresAuth: true }
},
{
path: ':id',
component: PostsDetail
// 任何人都可以阅读文章
meta: { requiresAuth: false }
}
]
}
]
// 我们可以通过 $route.meta 拿到meta数据
router.beforeEach((to, from) => {
// 而不是去检查每条路由记录
// to.matched.some(record => record.meta.requiresAuth)
if (to.meta.requiresAuth && !auth.isLoggedIn()) {
// 此路由需要授权,请检查是否已登录
// 如果没有,则重定向到登录页面
return {
path: '/login',
// 保存我们所在的位置,以便以后再来
query: { redirect: to.fullPath },
}
}
})
在组合式API的应用
在组合式API公开下面两个导航守卫API
import { onBeforeRouteUpdate,onBeforeRouteLeave } from 'vue-router'
onBeforeRouteLeave: 即将离开路由
onBeforeRouteUpdate: 路由的hash或者query发生了变化
自定义router-link
<template> <div @click="handle"> <slot></slot> </div> </template> <script> import { RouterLink, useLink } from 'vue-router' import { computed } from 'vue' export default { name: 'AppLink', props: { // 如果使用 TypeScript,请添加 @ts-ignore ...RouterLink.props, inactiveClass: String, }, methods:{ handle(){ this.$router.push(this.to) } }, setup(props) { const { // 解析出来的路由对象 route, // 用在链接里的 href href, // 布尔类型的 ref 标识链接是否匹配当前路由 isActive, // 布尔类型的 ref 标识链接是否严格匹配当前路由 isExactActive, // 导航至该链接的函数 navigate } = useLink(props) const isExternalLink = computed( () => typeof props.to === 'string' && props.to.startsWith('http') ) return { isExternalLink, href, navigate, isActive } }, } </script>
过度动效
顾名思义就是给路由的切换添加动画效果
例子如下
<router-view v-slot="{ Component, route }"> <transition name="fade"> <component :is="Component" :key="route.path" /> </transition> </router-view>
我们用v-slot拿到了 component和route,然后通过动态组件的方式来显示,在transition指定动画效果
由于vue会自动复用看起来类似的组件,所有我们需要加上唯一 key
滚动行为
这个比较简单就是一个 api
例子如下
const router = createRouter({ history: createWebHashHistory(), routes: [...], scrollBehavior (to, from, savedPosition) { // return 期望滚动到哪个的位置,示例如下 } }) /* 返回值支持以下,也可以返回promise,支持延迟滚动 return { // 也可以这么写 // el: document.getElementById('main'), // el: to.hash, 定位锚点 // behavior: 'smooth', 滚动流畅 el: '#main', top: -10, } */
如果返回一个 falsy(非真值) 的值,或者是一个空对象,那么不会发生滚动。
返回 savedPosition,在按下 后退/前进 按钮时,就会像浏览器的原生表现那样:
路由懒加载
vue-router支持动态导入我们这样写,示例如下
const router = createRouter({ routes: [ { path: '/users/:id', component: () => import('./views/UserDetails.vue') } ], })
为了避免请求过多,我们有时候需要组件按组分块,示例如下
// 在路由下配置webpackChunkName,vite支持webpack的这种写法。同名的设置为一组 const UserDetails = () => import(/* webpackChunkName: "group-user" */ './UserDetails.vue') const UserDashboard = () => import(/* webpackChunkName: "group-user" */ './UserDashboard.vue') const UserProfileEdit = () => import(/* webpackChunkName: "group-user" */ './UserProfileEdit.vue') // 我们在vite.config.js 配置 export default defineConfig({ build: { rollupOptions: { // https://rollupjs.org/guide/en/#outputmanualchunks output: { manualChunks: { 'group-user': [ // group-user组 './src/UserDetails.vue', './src/UserDashboard.vue', './src/UserProfileEdit.vue', ], }, }, }, }, }) // 配置后将会在build后 才能生效!!!!
扩展router-link
在router-link组件上封装一层(自定义 RouterLink 组件),示例如下
<template> <a v-if="isExternalLink" v-bind="$attrs" :href="to" target="_blank"> <slot /> </a> <router-link v-else v-bind="$props" custom v-slot="{ isActive, href, navigate }"> <a v-bind="$attrs" :href="href" @click="navigate" :class="isActive ? activeClass : inactiveClass"> <slot /> </a> </router-link> </template> <script> import { RouterLink } from 'vue-router' export default { name: 'AppLink', inheritAttrs: false, props: { // 如果使用 TypeScript,请添加 @ts-ignore ...RouterLink.props, inactiveClass: String, }, computed: { isExternalLink() { return typeof this.to === 'string' && this.to.startsWith('http') }, }, } </script>
使用组件
<AppLink v-bind="$attrs" class="inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out" active-class="border-indigo-500 text-gray-900 focus:border-indigo-700" inactive-class="text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:text-gray-700 focus:border-gray-300" > 点我点我 </AppLink>
路由导航检测
有时候我们需要在push后做点什么,可以在 push 前面加上 await 示例如下
通过 push等方法进行路由导航,有时候未必会成功跳转,比如已经在目标路由,或者路由返回了false,示例如下
// push返回的是一个promise,因此我们可以这样写 await $router.push('/user/list') this.isMenuOpen = false; // 判断是否导航成功 var navigationResult = await $router.push('/user/list'); if (navigationResult) { // 导航被阻止 } else { // 导航成功 (包括重新导航的情况) this.isMenuOpen = false } // 其实通过navigationResult对象可以拿到详细的错误信息,这个用的比较少,这里就不写了
动态路由
对于已经运行的程序,我们想添加路由,就看这里,动态路由主要通过两个函数实现
router.addRoute()
和router.removeRoute()
var removeRoute = router.addRoute({path: '/user', name: 'user', component: xxx })
// router.replace(router.currentRoute.value.fullPath) // 然后手动调用replace覆盖当前路由
// 可以覆盖替换,当名字一样的情况下
removeRoute('填入路由的名字') // 调用它的返回值可以删掉路由
// 添加嵌套路由
router.addRoute('父路由的名字', {path: '/user', name: 'user', component: xxx })
// 查看现有路由
router.hasRoute() // 检查路由是否存在
router.getRoutes() // 获取一个包含所有路由记录的数组
扩展
unplugin-vue-router
基于文件的自动路由,可以使用vite提供的功能(vite-plugin-pages)
也可以通过官方提供的一个插件 unplugin-vue-router
emmmm... 这个内容有点多,还是看官网吧...
GitHub unplugin-vue-router
持续学习
阅读 vue-router@4 API
详细见官网