- A+
安装
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 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,会回到之前的 URLreplace
: 会替代当前位置,无法回退,其余的同上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... 这个内容有点多,还是看官网吧...
持续学习
阅读 vue-router@4 API
详细见官网