本文主要记录Vue-Router的使用以及其原理剖析。
在这里不得不说Vue Router 的作用,因为构建出的vue应用是一个SPA(单页面),他只有一个html,Vue Router的作用就在于,如何在视口显示正确页面以及不同组件的跳转等问题。他能在不刷新页面的同时,更改URL进行页面的更改。
在官方文档中,是这样形容Vue Router的
注:如果是vue2项目需要安装 vue-router@3 版本的路由插件
//vue3
pnpm i vue-router@4
//vue2
pnpm i vue-router@3
import {createRouter,createWebHashHistory,RouteRecordRaw} from 'vue-router'
/**
* 定义路由映射,每个组件对应一个路由映射
* 其中路由类型为 RouteRecordRaw
*/
const routes:Array<RouteRecordRaw> = [
{path: '/A',component:()=> 'coms/路由/Router-A.vue'},
{path: '/B',component:()=> 'coms/路由/Router-B.vue'},
]
/**
* 创建路由实例传递一些配置
* 配置:
* 1.history
* 路由模式存在三种:
* vue2--->mode:history === vue3--->history:createWebHistory()
* vue2--->mode:hash === vue3--->history:createWebHashHistory()
* vue2--->mode:abstact === vue3--->history:createMemoryHashHistory()
* 2.routes
* 路由映射关系
* 3.name
* 名称
* 4.scrollBehavior(可选)
* 5.parseQuery(可选)
* 6.stringifyQuery(可选)
* 7.linkActiveClass(可选)
* 8.linkExactActiveClass(可选)
*/
export const router = createRouter({
history:createWebHashHistory(),
routes, // `routes: routes` 的缩写
})
不同路由的介绍与原理在之后剖析
//vue-router
import {router} from "../Router"
app.use(router)
页面使用
页面使用分为两个部分
请注意,我们没有使用常规的 a 标签,而是使用一个自定义组件
router-link
来创建链接。这使得Vue Router
可以在不重新加载页面的情况下更改 URL
,处理 URL 的生成以及编码。我们将在后面看到如何从这些功能中获益。
路由匹配到的组件将渲染在这里<p>
<router-link to="/">Go to Homerouter-link>
<router-link to="/about">Go to Aboutrouter-link>
p>
<router-view>router-view>
全局挂载完后,在以选择式API
的组件中,我们可以使用this.$router
的形式访问路由信息,this.$route
的形式访问当前路由信息,官方也给出了这样做的目的:
在整个文档中,我们会经常使用 router 实例,请记住,
this.$router
与直接使用通过createRouter
创建的 router 实例完全相同。我们使用this.$router
的原因是,我们不想在每个需要操作路由的组件中都导入路由。
在组合式API
中,用useRouter
, useRoute
方法使用,在模板中可以使用$route
和$router
来获取路由信息,不用将useRouter
, useRoute
方法获取的路由返回。
<template>
<div style="background-color: red;width: 100vw;height: 50vh">这是A</div>
<div>这是路由{{$route}}</div>
</template>
<script setup lang="ts">
import {useRoute,useRouter} from 'vue-router'
const route = useRoute()
const router = useRouter()
console.log(route)
console.log(router)
</script>
扩展:在这里使用 getCurrentInstance() 方法可以获取当前实例对象
该方式通过window.location.hash`的方式进行URL匹配,他会在URL地址拼接一个‘#’,hash是 ‘#’ 之后的部分,用作锚点在页面内进行导航。
改变hash部分不会引起页面刷新
能让URL变化的方式有以下几种:
hashchang
事件监听URL的变化window.addEventListener(
"hashchange",
function (e) {
console.log("The hash has changed!",e);
},
false,
);
history模式在URL路径中是没有’#‘的,它是利用了H5 中history.pushState
API来完成URL的path改变而不重载页面,history提供了类似hashchange
事件的popstate
事件,但是通过history.pushState
改变URL不会被该事件监听。
MDN中是这样介绍的:
按指定的名称和 URL(如果提供该参数)将数据 push 进会话历史栈,数据被 DOM 进行不透明处理;你可以指定任何可以被序列化的 javascript 对象。请注意,除了 Safari 所有浏览器现在都忽略了 title 参数。更多的信息,请看使用 History API。
history 也提供了一些其他方法比如
window.history.back();
回退
window.history.forward();
前进
window.history.go(number);
去以当前页面为基数的第几个页面
history.pushState()
pushState() 需要三个参数:一个状态对象,一个标题 (目前被忽略), 和 (可选的) 一个 URL. 让我们来解释下这三个参数详细内容:
以上是MDN给的解释和用法
history.pushState,是不会检查URL地址是否存在,浏览器不会加载该页面,甚至不会检查是否存在。只能通过手动刷新或者利用vue内部路由push方法进行浏览器URL导航,此时会发送请求,如果不存在则会404,所以在这里需要后端配合处理
利用popstate
事件监听回退与前进。
window.addEventListener("popstate", () => {
let currentState = history.state;
console.log("currentState", currentState);
});
该路由方式用在服务端渲染,之前的两种路由方式与浏览器URL地址进行操作,与浏览器API不可分割,而这种路由方式就可以脱离浏览器API的环境,他的功能就是将在已存在的路由页面中嵌入新的路由页面,而地址不发生改变
location.reload() 方法可以让页面重载
在路由配置项中可以配置name属性,可以让跳转不用再写路由地址,也会避开路径排序。
const routes:Array<RouteRecordRaw> = [
{path: '/A',component:()=> import('coms/路由/Router-A.vue')},
{path: '/B',name:'B',component:()=> import('coms/路由/Router-B.vue')},
{path: '/C',name:'C',component:()=> import('coms/路由/Router-C.vue')},
]
<router-link to="/A">Go to Router-Arouter-link>
<br/>
<router-link :to="{name:'B'}">Go to Router-Brouter-link>
<br/>
<router-link :to="{name:'C'}">Go to Router-Crouter-link>
编程式导航相对于
形式来说,增加了灵活性,不再是通过点击事件触发,也可以通过其他方式进行触发路由跳转。
利用useRouter().push()
方法可以进行路由跳转。
import {useRouter} from 'vue-router'
const router = useRouter()
// 字符串
const toPage=(url:string)=>{
// 字符串
router.push(url)
// 对象形式
router.push({
path:url,
name:'A'
})
}
以对象形式,可以很方便的进行路由传参。
router.push({
path:url,
query:{
name:'smz',
age:18
}
})
组件内用useRoute()方式接收参数
const route = useRoute()
<div>这是路由传参{{route.query.name}}{{route.query.age}}</div>
在4.1.4版本后移除了params方式
// 这些都会传递给 `createRouter`
const routes = [
// 动态字段以冒号开始
{ path: '/users/:id', component: User },
]
router.push({
name:'A',
params:{
id:1
}
})
<div>这是路由传参params:{{route.params.id}}div>
在进行路由跳转的时候,浏览器会有一个历史记录,用户可以通过左右箭头来到达记录的页面,组织产生历史记录(登录后不再记录登录页)有如下方法:
在
标签中加入 replace
属性
<router-link replace to="/A">Go to Router-A</router-link>
在编程式导航时,使用 replace()方法
const router = useRouter()
router.replace(url)
一些应用程序的 UI 由多层嵌套的组件组成。在这种情况下,URL 的片段通常对应于特定的嵌套组件结构。
/user/johnny/profile /user/johnny/posts
+------------------+ +-----------------+
| User | | User |
| +--------------+ | | +-------------+ |
| | Profile | | +------------> | | Posts | |
| | | | | | | |
| +--------------+ | | +-------------+ |
+------------------+ +-----------------+
嵌套路由就是在路由配置的时候添加children
属性,将子路由放在里面,在父路由页面放
来放子路由页面
在路由地址匹配的时候需要拼接上父路由地址。
const routes = [
{
path: '/user/:id',
component: User,
// 请注意,只有子路由具有名称
children: [{ path: '', name: 'user', component: UserHome }],
},
]
希望组件在同级展示,在一个地址有多个组件时,可以设置多个
并匹配name属性的值来渲染,当没有设置name 属性的时候默认找default属性的组件。
<router-view class="view left-sidebar" name="aaa">router-view>
<router-view class="view main-content">router-view>
<router-view class="view right-sidebar" name="bbb">router-view>
const routes = [
{
{path: '/D',name:'D',component:{
defaults:()=> import('coms/路由/Router-C.vue')
}},
{path: '/E',name:'E',component:{
aaa:()=> import('coms/路由/Router-C.vue'),
bbb:()=> import('coms/路由/Router-C.vue')
}},
},
]
在访问某个路由时可以通过redirect
属性来配置该路由访问的路由。其中 redirect
可以以字符串、对象、函数的形式。
{path: '/E',name:'E',redirect:'/B',component:{
aaa:()=> import('coms/路由/Router-C.vue'),
bbb:()=> import('coms/路由/Router-C.vue')
},children:[
{path: '/B',name:'B',component:()=> import('coms/路由/Router-B.vue')},
]},
{path: '/E',name:'E',redirect: {name:'B'},component:{
aaa:()=> import('coms/路由/Router-C.vue'),
bbb:()=> import('coms/路由/Router-C.vue')
},children:[
{path: '/B',name:'B',component:()=> import('coms/路由/Router-B.vue')},
]},
{path: '/E',name:'E',redirect: to=>{
return '/B'
},component:{
aaa:()=> import('coms/路由/Router-C.vue'),
bbb:()=> import('coms/路由/Router-C.vue')
},children:[
{path: '/B',name:'B',component:()=> import('coms/路由/Router-B.vue')},
]},
可以为某个路由起多个名称,利用属性alias
属性
alias:['/E1','/E2']
路由守卫都接收三个参数to,from,next,之后将不再赘述。
全局前置守卫:router.beforeEach()
用来拦截路由跳转,在跳转前做判断有三个参数
to: 跳转到哪个路由
from: 从哪个路由跳转
next(): 执行跳转
next(false):终端导航
next(‘/’):跳转到不同的地址,中断当前导航,进行新的导航
使用场景:权限判断
在代码中,whileList为路由白名单,也就是在导航到白名单的路由时没有限制,首先判断是否是白名单路由或者本地存有token就放行,否则就跳转到指定路由地址。
// 白名单
const whileList = ['/']
router.beforeEach((to,from,next)=>{
if (whileList.includes(to.path) || localStorage.getItem('token')){
next()
}else {
next('/')
}
})
router.beforeResolve()
他与全局前置守卫差不多,但是调用时机不太一样,他在导航被确认前调用,在组件内路由和一部路由组件被解析,比前置守卫要晚一点。此时是获取数据或者进入页面发生错误时进行操作的最佳位置。
router.beforeResolve(async to => {
if (to.meta.requiresCamera) {
try {
await askForCameraPermission()
} catch (error) {
if (error instanceof NotAllowedError) {
// ... 处理错误,然后取消导航
return false
} else {
// 意料之外的错误,取消导航并把错误传给全局处理器
throw error
}
}
}
})
router.afterEach()
用来在路由跳转后,做的一些操作,比如加载条,与前置守卫有相同的参数,但是next对路由没影响
全局后置守卫,感觉没多大用,但是可以和前置守卫配合做一个顶部加载条loadingBar,在前置守卫利用JS添加长度逐渐变长的加载条,然后在90%后,在后置守卫设置为100%,这样就完成了。
后置守卫可以直接在单个路由配置中使用:
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: (to, from) => {
// reject the navigation
return false
},
},
]
场景:做loadingBar
loadingBar组件:
<template>
<div class="wraps">
<div ref="bar" class="bar"></div>
</div>
</template>
<script setup lang="ts">
/**
* requestAnimationFrame:
* 这里使用该定时器的原因在于采用系统时间,和setTimeout和setInterval不精确,
* 会导致动画卡顿或者过度绘制,该定时器会将回流与重绘收集起来一起执行。以60帧来渲染
*/
import {ref} from 'vue'
// 进度
let speed =ref<number>(1)
let bar = ref<HTMLElement>()
let timer = ref<number>(0)
// 开始加载
const startLoading = () =>{
let dom = bar.value as HTMLElement
timer.value = window.requestAnimationFrame(function fn() {
if (speed.value < 90){
speed.value +=1
dom.style.width = speed.value + '%'
timer.value = window.requestAnimationFrame(fn)
}else {
// 清除定时器
speed.value = 1
window.cancelAnimationFrame(timer.value)
}
})
}
// 结束加载
const endLoading= () =>{
let dom = bar.value as HTMLElement;
setTimeout(()=>{
window.requestAnimationFrame(()=>{
speed.value = 100
dom.style.width = speed.value + '%'
})
},500)
}
defineExpose({
startLoading,
endLoading
})
</script>
<style lang="less" scoped>
.wraps{
position: fixed;
top: 0;
width: 100%;
height: 3px;
.bar{
height: inherit;
width: 0%;
background: blue;
}
}
</style>
挂载到全局实例:
import loadingBar from "../src/components/路由/后置守卫/loadingBar.vue";
const Vnode = createVNode(loadingBar)
render(Vnode,document.body)
前置守卫:
运行组件startLoading()
方法
Vnode.component?.extends?.startLoading()
后置守卫:
运行组件endLoading()
方法
Vnode.component?.extends?.endLoading()
顾名思义就是在组件内定义的路由守卫,然后传递给路由配置,总共有三个组件内守卫。
this
的,因为该守卫执行的时候,组件还未被创建,但是可以通过next()
的回调拿到组件实例。beforeRouteEnter (to, from, next) {
next(vm => {
// 通过 `vm` 访问组件实例
})
}
beforeRouteLeave (to, from) {
const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
if (!answer) return false
}
const UserDetails = {
template: `...`,
beforeRouteEnter(to, from) {
// 在渲染该组件的对应路由被验证前调用
// 不能获取组件实例 `this` !
// 因为当守卫执行时,组件实例还没被创建!
},
beforeRouteUpdate(to, from) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
// 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
},
beforeRouteLeave(to, from) {
// 在导航离开渲染该组件的对应路由时调用
// 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
},
}
完整导航解析流程:
路由元信息,希望路由上附带其他的一些信息,如权限校验标识,路由组件过度名称,持久化缓存keep-alive配置,标题名称,在路由中添加 meta
对象,在里面可以定义一些值让路由更具表达性,可以在导航守卫或者路由对象中读取路由元信息。
routes:[
{
path:'/',
component:()=>import('@/views/路由/前置守卫/Login.vue'),
meta:{
title: '登录',
transition:"animate__fadeIn"
}
},
{
path:'/home',
component:()=>import('@/views/路由/前置守卫/Home.vue'),
meta:{
title: '主页',
transition:"animate__bounceIn"
}
}
]
获取路由元信息:
router.beforeEach((to,from,next)=>{
document.title = to.meta.title
})
利用meta定义 transition 属性来指定进入该路由的过渡css样式,这里采用了animate.css 来做动效
改造
标签,利用插槽和过度组件:
<router-view v-slot="{ router,Component }">
<transition :name="route.meta.transition">
<component :is="Component" />
</transition>
</router-view>
routes:[
{
path:'/',
component:()=>import('@/views/路由/前置守卫/Login.vue'),
meta:{
title: '登录',
transition:"animate__fadeIn"
}
},
{
path:'/home',
component:()=>import('@/views/路由/前置守卫/Home.vue'),
meta:{
title: '主页',
transition:"animate__bounceIn"
}
}
]
在跳转路由时,想要页面滚动到顶部,或者保持原先位置,vue-router提供了方法scrollBehavior
方法接收三个参数:
to: 路由到哪
from: 当前路由
savePosition: 通过浏览器前进和后退能使用该参数,记录上一次位置
vue-router3中是x和y, vue-router4中是top和left
return 期望滚动到哪个的位置
const router = createRouter({
scrollBehavior(to, from, savedPosition) {
// 始终滚动到顶部
return { top: 0 }
},
})
在官网中也列举了几种特殊的滚动条件,如滚动到锚点、滚动到某个元素、延迟滚动等
滚动行为
在vue-router中支持动态导入
// 将
// import UserDetails from './views/UserDetails.vue'
// 替换成
const UserDetails = () => import('./views/UserDetails.vue')
但是这里要记录的是vite和webpack分包的配置,虽然vue-router官网只是说vite和webpack可以将组件分包打包,但是这个配置对其他文件也适用,如第三方库等,都可以进行分包。
vite:
build: {
rollupOptions:{
manualChunks: {
vue:['vue'],
elementPlus:['element-plus'],
pinia:['pinia'],
vueRouter:['vue-router']
}
}
}
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')
一般动态路由是用来做权限控制的,路由由后端返回,在这里就需要将后端返回的路由添加到路由中,其中使用router.addRoute()
和router.removeRoute()
方法来添加路由和删除路由,这样就要使用 router.push()
或者router.replace()
手动导航,才能显示新路由。
有几个不同的方法来删除现有的路由:
router.addRoute({ path: '/about', name: 'about', component: About })
// 这将会删除之前已经添加的路由,因为他们具有相同的名字且名字必须是唯一的
router.addRoute({ path: '/other', name: 'about', component: Other })
const removeRoute = router.addRoute(routeRecord)
removeRoute() // 删除路由如果存在的话
router.addRoute({ path: '/about', name: 'about', component: About })
// 删除路由
router.removeRoute('about')
需要注意的是,如果你想使用这个功能,但又想避免名字的冲突,可以在路由中使用 Symbol 作为名字。
当路由被删除时,所有的别名和子路由也会被同时删除
大多数情况下,后端返回的路由信息都是有嵌套的,这就需要进行添加嵌套路由的操作,其中有两种方式:
router.addRoute({ name: 'admin', path: '/admin', component: Admin })
router.addRoute('admin', { path: 'settings', component: AdminSettings })
router.addRoute({
name: 'admin',
path: '/admin',
component: Admin,
children: [{ path: 'settings', component: AdminSettings }],
})
本文主要记录vue-router的使用与原理剖析,如果想要对项目vue-router进行升级,可以参考官网对改动说明。