Vue-Router


文章目录

  • 前言
  • 一、简介
  • 二、安装与使用
  • 三、路由模式
    • 1.hash
    • 2.history
    • 3.abstact
  • 四、命名路由-编程式导航-历史记录
    • 1.命名路由
    • 2.编程式导航
    • 3.历史记录
  • 五、嵌套路由
  • 六、命名视图
  • 七、重定向-别名
    • 1.重定向
    • 2.别名
  • 八、进阶
    • 导航守卫
      • 全局前置守卫
      • 全局解析守卫
      • 全局后置守卫
      • 组件内的守卫
    • 路由元信息
    • 过渡动效
    • 滚动行为
    • 路由懒加载
    • 动态路由
      • 删除路由
      • 添加嵌套路由
  • 总结


前言

本文主要记录Vue-Router的使用以及其原理剖析。


一、简介

在这里不得不说Vue Router 的作用,因为构建出的vue应用是一个SPA(单页面),他只有一个html,Vue Router的作用就在于,如何在视口显示正确页面以及不同组件的跳转等问题。他能在不刷新页面的同时,更改URL进行页面的更改。

在官方文档中,是这样形容Vue Router的

  • 富有表现力的路由语法
    用直观且强大的语法来定义静态或动态路由。
  • 细致的导航控制
    可拦截任何导航并更精确地控制其结果。
  • 基于组件配置方法
    将每条路由映射到应该显示的组件上。
  • 支持历史模式
    有 HTML5、hash 或记忆历史模式可供选择。
  • 支持滚动控制
    可精确控制每个页面的滚动位置。
  • 支持自动编码
    可直接在代码中使用 unicode 字符(你好)

二、安装与使用

  • 使用命令安装 Vue Router 路由插件

注:如果是vue2项目需要安装 vue-router@3 版本的路由插件

//vue3
pnpm i vue-router@4

//vue2
pnpm i vue-router@3
  • 新建一个Router文件用于存放路由配置和组件映射等信息
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)
<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 的原因是,我们不想在每个需要操作路由的组件中都导入路由。

Vue-Router_第1张图片

Vue-Router_第2张图片

组合式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>

Vue-Router_第3张图片
Vue-Router_第4张图片

扩展:在这里使用 getCurrentInstance() 方法可以获取当前实例对象

Vue-Router_第5张图片

三、路由模式

1.hash

该方式通过window.location.hash`的方式进行URL匹配,他会在URL地址拼接一个‘#’,hash是 ‘#’ 之后的部分,用作锚点在页面内进行导航。

改变hash部分不会引起页面刷新

能让URL变化的方式有以下几种:

  • 通过浏览器回退和前进改变
    通过hashchang事件监听URL的变化
window.addEventListener(
    "hashchange",
    function (e) {
      console.log("The hash has changed!",e);
    },
    false,
);

Vue-Router_第6张图片

2.history

history模式在URL路径中是没有’#‘的,它是利用了H5 中history.pushStateAPI来完成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. 让我们来解释下这三个参数详细内容:

    • 状态对象 — 状态对象 state 是一个 JavaScript 对象,通过 pushState () 创建新的历史记录条目。无论什么时候用户导航到新的状态,popstate 事件就会被触发,且该事件的 state 属性包含该历史记录条目状态对象的副本。 状态对象可以是能被序列化的任何东西。原因在于 Firefox 将状态对象保存在用户的磁盘上,以便在用户重启浏览器时使用,我们规定了状态对象在序列化表示后有 640k 的大小限制。如果你给 pushState() 方法传了一个序列化后大于 640k 的状态对象,该方法会抛出异常。如果你需要更大的空间,建议使用 sessionStorage 以及 localStorage.
    • 标题 — Firefox 目前忽略这个参数,但未来可能会用到。在此处传一个空字符串应该可以安全的防范未来这个方法的更改。或者,你可以为跳转的 state 传递一个短标题。
    • URL — 该参数定义了新的历史 URL 记录。注意,调用 pushState() 后浏览器并不会立即加载这个 URL,但可能会在稍后某些情况下加载这个 URL,比如在用户重新打开浏览器时。新 URL 不必须为绝对路径。如果新 URL 是相对路径,那么它将被作为相对于当前 URL 处理。新 URL 必须与当前 URL 同源,否则 pushState() 会抛出一个异常。该参数是可选的,缺省为当前 URL。

以上是MDN给的解释和用法

history.pushState,是不会检查URL地址是否存在,浏览器不会加载该页面,甚至不会检查是否存在。只能通过手动刷新或者利用vue内部路由push方法进行浏览器URL导航,此时会发送请求,如果不存在则会404,所以在这里需要后端配合处理

利用popstate事件监听回退与前进。

window.addEventListener("popstate", () => {
  let currentState = history.state;
  console.log("currentState", currentState);
});

3.abstact

该路由方式用在服务端渲染,之前的两种路由方式与浏览器URL地址进行操作,与浏览器API不可分割,而这种路由方式就可以脱离浏览器API的环境,他的功能就是将在已存在的路由页面中嵌入新的路由页面,而地址不发生改变

location.reload() 方法可以让页面重载


四、命名路由-编程式导航-历史记录

1.命名路由

在路由配置项中可以配置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>

2.编程式导航

编程式导航相对于形式来说,增加了灵活性,不再是通过点击事件触发,也可以通过其他方式进行触发路由跳转。

利用useRouter().push()方法可以进行路由跳转。

import {useRouter} from 'vue-router'
const router = useRouter()
// 字符串
const toPage=(url:string)=>{
  // 字符串
  router.push(url)
  // 对象形式
  router.push({
    path:url,
    name:'A'
  })
}

以对象形式,可以很方便的进行路由传参。

  • 以query的方式传参 传递一个对象,这种传参方式会拼接在URL地址上
router.push({
    path:url,
    query:{
      name:'smz',
      age:18
    }
  })

组件内用useRoute()方式接收参数

const route = useRoute()
<div>这是路由传参{{route.query.name}}{{route.query.age}}</div>

Vue-Router_第7张图片

  • 以params方式传参,这种传参方式必须使用name形式路由跳转,这种不会拼接在URL上,存于内存中,刷新页面后数据消失。
    在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>

3.历史记录

在进行路由跳转的时候,浏览器会有一个历史记录,用户可以通过左右箭头来到达记录的页面,组织产生历史记录(登录后不再记录登录页)有如下方法:

标签中加入 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')
        }},
  },
]

七、重定向-别名

1.重定向

在访问某个路由时可以通过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')},
        ]},

2.别名

可以为某个路由起多个名称,利用属性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()

组件内的守卫

顾名思义就是在组件内定义的路由守卫,然后传递给路由配置,总共有三个组件内守卫。

  • beforeRouteEnter
    该守卫是不能访问this的,因为该守卫执行的时候,组件还未被创建,但是可以通过next()的回调拿到组件实例。
beforeRouteEnter (to, from, next) {
  next(vm => {
    // 通过 `vm` 访问组件实例
  })
}
  • beforeRouteUpdate
  • beforeRouteLeave
    离开守卫的应用场景是:用户在未保存的情况下突然跳转。通过返回false取消跳转
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`
  },
}

完整导航解析流程:

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
  5. 在路由配置里调用 beforeEnter。
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter。
  8. 调用全局的 beforeResolve 守卫(2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

路由元信息

路由元信息,希望路由上附带其他的一些信息,如权限校验标识,路由组件过度名称,持久化缓存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:当前路由信息,
  • Component:当前VNode
       <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']
        }
      }
    }

Vue-Router_第8张图片

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 })
  • 通过调用 router.addRoute() 返回的回调:没有名称时
const removeRoute = router.addRoute(routeRecord)
removeRoute() // 删除路由如果存在的话
  • 通过使用 router.removeRoute() 按名称删除路由:
router.addRoute({ path: '/about', name: 'about', component: About })
// 删除路由
router.removeRoute('about')

需要注意的是,如果你想使用这个功能,但又想避免名字的冲突,可以在路由中使用 Symbol 作为名字。
当路由被删除时,所有的别名和子路由也会被同时删除

添加嵌套路由

大多数情况下,后端返回的路由信息都是有嵌套的,这就需要进行添加嵌套路由的操作,其中有两种方式:

  • 可以将路由的 name 作为第一个参数传递给 router.addRoute(),这将有效地添加路由,就像通过 children 添加的一样:
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进行升级,可以参考官网对改动说明。

你可能感兴趣的:(vue学习,vue.js,前端,javascript)