Vue Router 学习笔记-核心

1/29/2023 Vue

[TOC]

# Vue Router 学习笔记-核心

Vue Router是Vue.js的官方路由,用于构建SPA(单页面应用) (opens new window)

理解Vue Router从核心和扩展两部分入手,核心部分是Vue Router的最基础最核心功能,扩展部分是基于核心而扩展用于不同场景和需求的写法。

核心部分,是路由最小核心,是路由不可或缺的部分,分为三部分:

  • 路由配置
  • 导航函数
  • 导航守卫

扩展部分,在基础的基础上进行了更多的扩展写法,适用于更复杂的场景。例如:

  • 对路由配置进行扩展,加入了动态路由匹配、路由正则匹配等
  • 引入嵌套路由、命名路由、命名视图、重定向和别名、路由组件传参等
  • 历史记录模式分为hash模式和H5模式
  • 路由元信息,可将任意附加信息添加到路由上
  • 引入组合式API、过渡动效、滚动行为、路由懒加载、自定义RouterLink、导航故障、动态路由等

注,这里核心是指把路由拆解后的最小核心功能,扩展是基于最小核心添加适合不同场景和需求的一些额外写法(没有严格按照官方文档的基础与进阶来写)。

# 最小核心

# 路由配置

使用路由前需要对路由进行配置,即定义path、componet等,一般配置文件放在 /src/router/目录下。

路由配置的最小单元需包含path和component,配置示例如下:

import Home from "Home.vue"

const routes = [
  {
    path: "/",
    component: Home,
  },
  {
    path: "/about",
    component: () => import("@/page/about.vue"),		// 路由懒加载写法,一般直接使用这种写法
  },
];

下面是一个较为完整的路由配置文件示例,包含了路由配置、创建Router等

// 1. 创建路由配置
const routes = [
  { path: "/home", redirect: "/" },
  {
    path: "/",
    component: () => import("@/page/Home.vue"),
  },
  {
    path: "/a/:test",
    name: "PageA",
    props: true,
    component: () => import("@/page/PageA.vue"),
    beforeEnter: (to, from) => {
      // ...
      console.log("配置守卫 beforeEnter", to, from);
      return true;
    },
    children: [
      {
        path: "a/sub/:userId",
        name: "pageSub",
        component: () => import("@/page/PageASub.vue"),
        props: true,
      },
    ],
  },
];

// 2. 创建Router
const router = createRouter({
  history: createWebHashHistory(),
  routes,
});

// 3. 导出Router,让Vue使用
export default router;

路由配置好后,就可以使用<router-view>组件进行显示,下面是一个示例:

  <router-view v-slot="{ Component, route }">
    <component :is="Component" :key="route.meta.name"></component>
  </router-view>

# 导航函数

在HTML文件内,可以使用<router-link>标签进行跳转,代码示例:

<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>

但是更多的时候是使用导航函数来实现跳转(编程式导航),常用的导航函数有:

  • push,这个方法会向 history 栈添加一个新的记录,点击返回会返回到之前的URL
  • replace,这个方法会向history 栈添加新记录,其他基本与push相同
  • go,这个方法表示横跨历史,正数表示前进,负数表示后退

注意点:

  1. 导航方法都是异步的,返回的结果是一个promise对象,该promise会在所有的导航守卫走完之后触发,表示导航的结果。
  2. **导航函数如果传入了path,则会忽略params。**因为path本身就是路径,params是用于动态路由中组成path的一部分(例如path: "/a/:userId"中的userId这个动态参数),所以path和params不能互相传。(params可通过this.$route.params获取)
  3. <router-link> to的参数类型为 RouteLocationRaw,与push函数的入参相同

# push

向 history 栈添加一个新的记录,点击返回会返回到之前的URL,需要注意方法是异步函数,返回对象时Promise对象,表示导航是否成功。

这里列几个官方的例子参考:

// 字符串路径
router.push('/users/eduardo')

// 带有路径的对象
router.push({ path: '/users/eduardo' })

// 命名的路由,并加上参数,让路由建立 url
router.push({ name: 'user', params: { username: 'eduardo' } })

// 带查询参数,结果是 /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } })

// 带 hash,结果是 /about#team
router.push({ path: '/about', hash: '#team' })

const username = 'eduardo'
// 我们可以手动建立 url,但我们必须自己处理编码
router.push(`/user/${username}`) // -> /user/eduardo
// 同样
router.push({ path: `/user/${username}` }) // -> /user/eduardo
// 如果可能的话,使用 `name` 和 `params` 从自动 URL 编码中获益
router.push({ name: 'user', params: { username } }) // -> /user/eduardo
// `params` 不能与 `path` 一起使用
router.push({ path: '/user', params: { username } }) // -> /user

push返回对象时Promise对象,如果导航报错会返回 NavigationFailureType 类型,无错会返回undefined

// 写法一,判断是否有错误
const navigationResult = await router.push('/my-profile')
if (navigationResult) {
  // 导航被阻止
} else {
  // 导航成功 (包括重新导航的情况)
  this.isMenuOpen = false
}


// 写法二,检察错误是否属于某种类型
import { NavigationFailureType, isNavigationFailure } from 'vue-router'

// 试图离开未保存的编辑文本界面
const failure = await router.push('/articles/2')
// 判断错误是否是 NavigationFailureType.aborted 类型
if (isNavigationFailure(failure, NavigationFailureType.aborted)) {
  // 给用户显示一个小通知
  showToast('You have unsaved changes, discard and leave anyway?')
}

# replace

使用基本与push相同,只是不会在history新增栈,而是取代当前栈。

此外,也可在push方法里新增 replace: true参数来使push达到replace的效果

router.replace({ path: '/home' })
// 相当于
router.push({ path: '/home', replace: true })

# go

表示在历史堆栈中前进或后退多少部,相当于 window.history.go(n)

// 向前移动一条记录,与 router.forward() 相同
router.go(1)

// 返回一条记录,与 router.back() 相同
router.go(-1)

// 前进 3 条记录
router.go(3)

// 如果没有那么多记录,静默失败
router.go(-100)
router.go(100)

# 导航传参

  • query,添加到url上的query部分

  • params,是path的一部分,需要实现在路由上定义,不定义的参数不能传递,即不能随意传参。例

    1. 路由定义:/users/:username/posts/:postId,params为:{ username: 'eduardo', postId: '123' }
    2. 路由定义:/users/:username,params为:{ username: 'eduardo' }
    3. 路由定义:/users/:username,如果 设置 params为:{ username: 'eduardo', postId: '123' },页面只能收到username参数,其他path未定义的参数都会忽略
    
  • 如果需要传递复杂对象,可以通过Storage存储和读取

# 导航守卫

导航守卫用于通过跳转或取消的方式守卫导航,用于鉴权、修改标题等等需要在路由发生变化时做的操作。

# 介绍

导航守卫从作用类型上分为两类:

  • 守卫函数,这类函数里可以修改导航行为,比如跳转到其他页面或取消等操作
  • 钩子函数,这类函数只是导航行为到某个阶段的通知,不可以修改导航行为(只有一个钩子函数:全局后置钩子 afterEach)

注意点:

  1. 导航守卫函数都接受两个参数:tofrom,表示要进入的目标和要离开的目标,类型为RouteLocationNormalized (opens new window)

  2. 守卫函数还接受第三个函数next函数,用于实现修改导航行为,但现在基本已经可以不用,直接在守卫函数内使用return语句返回,即可实现修改、取消等行文,守卫函数返回的值返回类型为:

    • false,取消当前的导航
    • 一个路由地址 (opens new window),即同push函数的传参一样,会中断当前的导航开始新的导航
    • 无返回、undefined、true,表示当前导航有效,继续导航
  3. 钩子函数(只有一个钩子函数 afterEach)接受第三个参数failure (opens new window),表示导航结果

从级别角度分为三类:

  • 全局守卫,包括:
    • 全局前置守卫(beforeEach),可以对导航 做取消、重定向、判断是否登录等前置拦截操作
    • 全局解析守卫(beforeResolve),最后一次可以取消导航的机会,一般用于权限申请等操作,该步骤过后,导航将被确认,是用于获取数据或执行任何其他操作(如果用户无法进入页面时你希望避免执行的操作)的理想位置。
    • 全局后置钩子(afterEach),对于分析、更改页面标题、声明页面等辅助功能以及许多其他事情都很有用
  • 路由守卫,包括:
    • 路由守卫(beforeEnter),路由守卫只会在进入路由时触发,不会在paramsqueryhash 改变时触发,例如从 /users/2 进入到 /users/3 或者从 /users/2#info 进入到 /users/2#projects时不会触发路由守卫,即只有进入一个不同的路由导航时,才会触发路由守卫。路由守卫与其他守卫不同的地方在于,可以给路由守卫传入一组函数,实现特定行为。
  • 组件守卫,包括:
    • beforeRouteEnter,在渲染该组件的对应路由被验证前调用,不能获取组件实例 this ,因为当守卫执行时,组件实例还没被创建。需要注意的是,可给该守卫的next函数传入一个回调,当导航确认后会执行该回调,并传入该组件实例
    • beforeRouteUpdate,组件被复用时触发
    • beforeRouteLeave,导航在离开该组件时触发

一些代码示例:

// 全局前置守卫 beforeEach
const router = createRouter({ ... })
router.beforeEach((to, from) => {
  // ...
  // 返回 false 以取消导航
  return false
})

// 全局解析守卫 beforeResolve
router.beforeResolve(async to => {
  if (to.meta.requiresCamera) {
    try {
      await askForCameraPermission()
    } catch (error) {
      if (error instanceof NotAllowedError) {
        // ... 处理错误,然后取消导航
        return false
      } else {
        // 意料之外的错误,取消导航并把错误传给全局处理器
        throw error
      }
    }
  }
})

// 全局钩子函数 afterEach
router.afterEach((to, from) => {
  sendToAnalytics(to.fullPath)
})


// 路由守卫 beforeEnter
const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    beforeEnter: (to, from) => {
      // reject the navigation
      return false
    },
  },
]

// 路由守卫可以接受一组函数,"这在为不同的路由重用守卫时很有用"
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],
  },
]


// 组件守卫
const UserDetails = {
  template: `...`,
  beforeRouteEnter(to, from) {
    // 在渲染该组件的对应路由被验证前调用
    // 不能获取组件实例 `this` !
    // 因为当守卫执行时,组件实例还没被创建!
  },
  beforeRouteUpdate(to, from) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
    // 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
  },
  beforeRouteLeave(to, from) {
    // 在导航离开渲染该组件的对应路由时调用
    // 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
  },
}

// 可给 beforeRouteEnter 的next函数传入一个回调函数,当导航确认后会执行该回调,并传入该组件实例
beforeRouteEnter (to, from, next) {
  next(vm => {
    // 通过 `vm` 访问组件实例
  })
}


# 组件守卫组合式API

组件守卫在组合式API的写法需要特殊注意下(全局守卫和路由守卫与组合式API相同)。

  1. Vue Router提供onBeforeRouteLeave, onBeforeRouteUpdate两个组合式API函数

    import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
    import { ref } from 'vue'
    
    export default {
      setup() {
        // 与 beforeRouteLeave 相同,无法访问 `this`
        onBeforeRouteLeave((to, from) => {
          const answer = window.confirm(
            'Do you really want to leave? you have unsaved changes!'
          )
          // 取消导航并停留在同一页面上
          if (!answer) return false
        })
    
        const userData = ref()
    
        // 与 beforeRouteUpdate 相同,无法访问 `this`
        onBeforeRouteUpdate(async (to, from) => {
          //仅当 id 更改时才获取用户,例如仅 query 或 hash 值已更改
          if (to.params.id !== from.params.id) {
            userData.value = await fetchUser(to.params.id)
          }
        })
      },
    }
    
  2. beforeRouteEnter的写法有点特殊,因为在执行beforeRouteEnter时,组件还没被创立,即该函数的执行在setup函数之前,所以不能写在setup函数之内

    
    // 写法一,使用组合式API
    export default defineComponent({
        components: {},
        props: {},
    
        // This needs to be defined outside the setup block
        beforeRouteEnter(to, from, next) {
          next(vm => {
            // vm表示组件的实例,可以在这里对vm内属性进行赋值等操作
            console.log(vm);
          })
        },
        setup() {
            onBeforeRouteUpdate(to => {
    
            });
        }
    
    });
    
    // 写法二,setup标签写法,在增加一个script,不过需要注意lang要相同
    <script>
    export default {
      beforeRouteEnter: (to, from, next) => {
        next((vm) => {
          // vm表示组件的实例,可以在这里对vm内属性进行赋值等操作
          console.log(vm);
        });
        return true;
      },
    };
    </script>
    
    <script setup>
    import { useRouter, useRoute } from "vue-router";
    </script>
    
    
    //写法三,setup标签TS的写法
    <script lang="ts">
    import { defineComponent, ComponentPublicInstance } from 'vue'
    
    interface IInstance extends ComponentPublicInstance {
      setPathFrom(from: string): void
    }
    
    export default defineComponent({
      beforeRouteEnter(to, from, next) {
        next((vm) => {
          const instance = vm as IInstance
          instance.setPathFrom(from.path)
        })
      },
    })
    </script>
    
    <script lang="ts" setup>
    let pathFrom: string
    const setPathFrom = (path: string) => {
      pathFrom = path
      console.log('vue-route::from::', pathFrom)
    }
    
    defineExpose({ setPathFrom })
    </script>
    
    
    // 注意点,在 beforeRouteEnter 内也可以利用 路由的props特性,给组件的props赋值
    <script>
    import { useFetch } from '@vueuse/core'
    
    export default {
      beforeRouteEnter: function (to) {
        const { data } = useFetch(`/your/url/${to.params.id}`)
        // 赋值给 meta,最终利用路由的props配置给组件的props赋值
        to.meta.data = data
      }
    }
    </script>
    
    <script setup>
    defineProps({
      data: {
        type: [null, Object],
        required: true
      }
    })
    // ...
    </script>
    
    // 组件内写法
    {
      path: '/your/url/:id',
      // 利用 meta,赋值给props
      props: to => ({ data: to.meta.data })
    }
      
      
    

# 导航解析流程

完整的导航解析流程:

  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 的回调函数,创建好的组件实例会作为回调函数的参数传入。

以上是Vue Router文档路由导航解析的流程。

简单来说,执行顺序上 全局守卫 >(早于) 路由守卫 >(早于) 组件守卫

需要注意的是,导航函数(push、replace、go等)的回调会在最后执行,即在第12步以后执行。

        router.push({ name: "PageC" }).then((res) => {
          // 导航函数的回调,在最后执行,如果导航报错会返回 `NavigationFailureType` 类型,无错会返回`undefined`,错误类型可以用 isNavigationFailure 函数判断
          console.log("res", res);
        });

VueRouter完整导航解析流程图示:

注:

  1. 当组件重用时,不会触发路由配置守卫,且组件守卫beforeRouteUpdate会在全局前置守卫之后就触发,守卫触发流程如下:

    1. 全局前置守卫 router.beforeEach
    2. 组件守卫 beforeRouteUpdate
    3. 全局解析守卫 router.beforeResolve
    4. 全局后置钩子 router.afterEach
    
Last Updated: 3/1/2023, 5:27:01 PM