Vue-vueRouter

465 阅读7分钟

vue-router

  • 版本: "4.0.12" Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,是使用vue生态必不可少的一个技能

安装使用

在vue-router中一共有两个组件 和

用法:

  • to
<!-- 字符串 -->
<router-link to="/home">Home</router-link>
<!-- 渲染结果 -->
<a href="/home">Home</a>

<!-- 使用 v-bind 的 JS 表达式 -->
<router-link :to="'/home'">Home</router-link>

<!-- 同上 -->
<router-link :to="{ path: '/home' }">Home</router-link>

<!-- 命名的路由 -->
<router-link :to="{ name: 'user', params: { userId: '123' }}">User</router-link>

<!-- 带查询参数,下面的结果为 `/register?plan=private` -->
<router-link :to="{ path: '/register', query: { plan: 'private' }}">
  Register
</router-link>
  • replace

设置 replace 属性的话,当点击时,会调用 router.replace(),而不是 router.push(),所以导航后不会留下历史记录。

<router-link to="/abc" replace></router-link>
  • custom 配合 v-slot使用

是否应该将其内容包裹在 元素中。在使用 v-slot 创建自定义 RouterLink 时很有用。默认情况下, 会将其内容包裹在 元素中,即使使用 v-slot 也是如此。传递自定义的 prop,可以去除这种行为。

<router-link to="/home" custom v-slot="{ navigate, href, route }">
  <a :href="href" @click="navigate">{{ route.fullPath }}</a>
</router-link>
渲染结果:
<a href="/home">/home</a>

<router-link to="/home" v-slot="{ route }">
  <span>{{ route.fullPath }}</span>
</router-link>
渲染结果:
<a href="/home"><span>/home</span></a>
<!--  这里能看到 custom 的真正作用 - 去掉a标签 但想要点击生效必须加上click事件 -->
<router-link to="/home" custom v-slot="{ route, navigate }">
  <span  @click="navigate">{{ route.fullPath }}</span>
</router-link>
渲染结果:
<span>/home</span>
  • active-class

链接激活时,应用于渲染的 的 class。 "router-link-active" (或者全局 linkActiveClass)

  • exact-active-class

链接精准激活时,应用于渲染的 的 class。

  • aria-current-value

当链接激活时,传递给属性 aria-current 的值。

<router-link
  to="/about"
  custom
  v-slot="{ href, route, navigate, isActive, isExactActive }"
>
  <NavLink :active="isActive" :href="href" @click="navigate">
    {{ route.fullPath }}
  </NavLink>
</router-link>
  • name

默认:"default"
如果 设置了 name,则会渲染对应的路由配置中 components 下的相应组件。

// 在命名视图中使用名称

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

// router.js
const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    {
      path: '/',
      components: {
        default: Home,
        // LeftSidebar: LeftSidebar(导入的对应的vue模块页面) 的缩写
        LeftSidebar,
        // 它们与 `<router-view>` 上的 `name` 属性匹配
        RightSidebar,
      },
    },
  ],
})
  • route

一个路由地址的所有组件都已被解析(如果所有组件都被懒加载),因此可以显示。

  • v-slot

主要使用 和 组件来包裹你的路由组件。

<Suspense>
  <template #default>
    <router-view v-slot="{ Component, route }">
      <transition :name="route.meta.transition || 'fade'" mode="out-in">
        <keep-alive>
          <component
            :is="Component"
            :key="route.meta.usePathKey ? route.path : undefined"
          />
        </keep-alive>
      </transition>
    </router-view>
  </template>
  <template #fallback> Loading... </template>
</Suspense>
createRouter、createWebHistory、createWebHashHistory、createMemoryHistory、NavigationFailureType

START_LOCATION

路由所在的初始路由地址。可用于导航守卫中,以区分初始导航。

import { START_LOCATION } from 'vue-router'

router.beforeEach((to, from) => {
  if (from === START_LOCATION) {
    // 初始导航
  }
})

Composition API

  • onRoute - this.$route
  • onRouter - this.$router
  • onBeforeRouteLeave - 在当前位置的组件将要离开时触发
  • onBeforeRouteUpdate - 在当前位置即将更新时触发
  • useLink -
import { useRouter, useRoute } from 'vue-router'
export default {
  setup() {
    const router = useRouter()
    const route = useRoute()

    function pushWithQuery(query) {
      router.push({
        name: 'search',
        query: {
          ...route.query,
        },
      })
    }
  },
}

// useLink 实例
import { RouterLink, useLink } from 'vue-router'

export default {
  name: 'AppLink',

  props: {
    // 如果使用 TypeScript,请添加 @ts-ignore
    ...RouterLink.props,
    inactiveClass: String,
  },

  setup(props) {
    const { route, href, isActive, isExactActive, navigate } = useLink(props)

    const isExternalLink = computed(
      () => typeof props.to === 'string' && props.to.startsWith('http')
    )

    return { isExternalLink, href, navigate, isActive }
  },
}
params 参数
const routes = [
  // 匹配 /o/3549
  { path: '/o/:orderId' },
  // 匹配 /p/books
  { path: '/p/:productName' },
]


const routes = [
  // /:orderId -> 仅匹配数字
  { path: '/:orderId(\\d+)' },
  // /:productName -> 匹配其他任何内容
  { path: '/:productName' },
]

const routes = [
  // /:chapters ->  匹配 /one, /one/two, /one/two/three, 等
  { path: '/:chapters+' },
  // /:chapters -> 匹配 /, /one, /one/two, /one/two/three, 等
  { path: '/:chapters*' },
]

如果你需要匹配具有多个部分的路由,如 /first/second/third,你应该用 *(0 个或多个)和 +(1 个或多个)将参数标记为可重复:

// 给定 { path: '/:chapters*', name: 'chapters' },
router.resolve({ name: 'chapters', params: { chapters: [] } }).href
// 产生 /
router.resolve({ name: 'chapters', params: { chapters: ['a', 'b'] } }).href
// 产生 /a/b

// 给定 { path: '/:chapters+', name: 'chapters' },
router.resolve({ name: 'chapters', params: { chapters: [] } }).href
// 抛出错误,因为 `chapters` 为空 

这些也可以通过在右括号后添加它们与自定义正则结合使用:

const routes = [
  // 仅匹配数字
  // 匹配 /1, /1/2, 等
  { path: '/:chapters(\\d+)+' },
  // 匹配 /, /1, /1/2, 等
  { path: '/:chapters(\\d+)*' },
]

你也可以通过使用 ? 修饰符(0 个或 1 个)将一个参数标记为可选:

const routes = [
  // 匹配 /users 和 /users/posva
  { path: '/users/:userId?' },
  // 匹配 /users 和 /users/42
  { path: '/users/:userId(\\d+)?' },
]

动态路由和动态路由匹配

动态路由 和动态路由匹配是两个概念 动态路由匹配 - 指的是通过:id 和params 接参实现的相同url带不同参数来展示不同内容 动态路由 - 在vue4.0版本中通过 addRoute来添加新的路由 removeRoute() 来删除路由

它们只注册一个新的路由,也就是说,如果新增加的路由与当前位置相匹配,就需要你用 router.push() 或 router.replace() 来手动导航,才能显示该新路由

const router = createRouter({
  history: createWebHistory(),
  routes: [{ path: '/:articleName', component: Article }],
})
情况一: 在路由(导航)守卫之外使用
router.addRoute({ path: '/about', component: About })
router.addRoute({ path: '/about', component: About })
// 我们也可以使用 this.$route 或 route = useRoute() (在 setup 中)
router.replace(router.currentRoute.value.fullPath)
情况二: 在路由(导航)守卫中添加路由
router.beforeEach(to => {
  if (!hasNecessaryRoute(to)) {
    router.addRoute(generateRoute(to))
    // 触发重定向
    return to.fullPath
  }
})
<!--
上面的例子有两个假设:
第一,新添加的路由记录将与 to 位置相匹配,实际上导致与我们试图访问的位置不同。
第二,hasNecessaryRoute() 在添加新的路由后返回 false,以避免无限重定向。
-->

添加嵌套路由

可以将路由的 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 }],
})

路由删除

有几个不同的方法来删除现有的路由:当路由被删除时,所有的别名和子路由也会被同时删除 当路由被删除时,所有的别名和子路由也会被同时删除

  • 通过添加一个名称冲突的路由。如果添加与现有途径名称相同的途径,会先删除路由,再添加路由:
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')

查看功能方法

  • router.hasRoute():检查路由是否存在。
  • router.getRoutes():获取一个包含所有路由记录的数组。 相当于直接返回了 this.$router.options.routes

路由守卫

  • beforeEach()
const router = createRouter({ ... })

router.beforeEach((to, from, next) => {
  // ...
  // 返回 false 以取消导航
  return false
})
  • beforeResolve()

和 router.beforeEach 类似,因为它在 每次导航时都会触发,但是确保在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被正确调用。这里有一个例子,确保用户可以访问自定义 meta 属性

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

和前置守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身: 它们对于分析、更改页面标题、声明页面等辅助功能以及许多其他事情都很有用。

router.afterEach((to, from, failure) => {
  if (!failure) sendToAnalytics(to.fullPath)
})
  • 路由独享
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`
  },
}
  • 可用的配置 API
const UserDetails = {
  template: `...`,
  beforeRouteEnter(to, from, next) {
    // 在渲染该组件的对应路由被验证前调用
    // 不能获取组件实例 `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 的回调函数,创建好的组件实例会作为回调函数的参数传入。

核心实现原理及简单实现

在配置使用的时候主要分以下几步:

  • 一、使用router插件
import Router from 'vue-router'
Vue.use(Router)
  • 二、创建Router实例, router.js
export default new Router({...})
  • 三、在跟组件上添加实例 main.js
import router from './router'
new Vue({
    router,
}).$mount("#app")
  • 四: 添加路由视图
<router-view></router-view>

在vue的插件实现中会调用vue.use(Router) use的调用会 router插件中的install方法;

以下代码仅为简单的说明其核心实现的原理

  • 1,通过对url的监听 并记录当前要前往的路由current
  • 2,通过current和用户配置的router里的pathd对比活的其对应的component 并渲染到router-view中
// 将Vue单独保存出来方便使用
let _Vue; 

// 定义了VueRouter类 new 
class VueRouter{
  construcor(options){
      this.options = options;
  }  
}

// 实现install 会在vue.use的时候调用 在源码中有独立的文件定义install

VueRouter.install = function(Vue){
      _Vue = Vue
      // 由于在use的时候 没有实例化所以是没有options 所以需要借用混入的生命周期来延迟以获options
      Vue.mixin({
          beforeCreate(){
              if (this.$options.router){
                  Vue.prototype.$router = this.$options.router
                  
                 //使用vue提供的 defineReactive 将保存下来的路由参 设置为响应式的数据
                 window.addEventListener('hashchange', this.onHashChange.bind(this))
              }
          }
      })
      
// 实现vue-router的组件
Vue.component('router-link', Link)
vue.component('router-view', view)
}

//创建 router-link
export default {
    props:{
        to:String,
        required: true
    }
    render(h){
        return h('a', {
            attrs:{
                href: '#' + this.to
            }
        })
    }
}

//创建 router-view

export default {
    render(h){
        const { $options, current } = this.$router
        let component;
        $options.routes.map(n => {
            if (n.path == current){
                component = n.component
            }
        })
        return h(component)
    }
}


参考: