Vue-Router路由

1,471 阅读6分钟

简单来说:Vue-Router本质是将url和组件建立映射关系

路由: <router-link :to="path">

动态路由

我们每个人都有自己的主页,显示主页内容用的都是同一个组件,这时只需要更改传进来显示的用户的id就能显示对应的用户内容,而并不需要重新写一个组件。

例如:juejin.cn/user/449027…;更改后面的id就是不同的用户

const router = new VueRouter({
  routes: [
    // 动态路径参数 以冒号开头
    { path: '/user/:id', component: User },
    // 多段路径
    { path: '/city/:cityname/house/:house_id', }
  ]
})

“路径参数”使用冒号 : 标记。当匹配到一个路由时,参数值会被设置到 this.$route.params,可以在每个组件内使用。

const User = {
  template: '<div>User {{ $route.params.id }}</div>'
}
const City = {
 template: '<div>City {{ $route.params.cityname }} -- House {{house_id}}</div>'
}

路由变化

当使用路由参数时,原来的组件实例会被复用,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会再被调用

但是会触发路由的导航守卫beforeRouteUpdate

两种检测路由对象的变化方法:

const User = {
  template: '...',
  watch: {
    $route(to, from) {
      // from 从何路由 跳向 to目的路由 
      // 对路由变化作出响应...
    }
  }
}
const User = {
  template: '...',
  beforeRouteUpdate(to, from, next) {
    // react to route changes...
    // don't forget to call next()
  }
}

捕获未指定路由

常规参数只会匹配被 / 分隔的 URL 片段中的字符。如果想匹配非指定的任意路径,我们可以使用通配符 (*):

{
  // 会匹配所有路径
  path: '*'
}
{
  // 会匹配以 `/user-` 开头的任意路径
  path: '/user-*'
}

当使用通配符路由时,请确保路由的顺序是正确的,也就是说含有通配符的路由应该放在最后。路由 { path: '*' } 通常用于客户端 404 错误。

$route.params 内会自动添加一个名为 pathMatch 参数。它包含了 URL 通过通配符被匹配的部分:

// 给出一个路由 { path: '/user-*' }
this.$router.push('/user-admin')
this.$route.params.pathMatch // 'admin'
// 给出一个路由 { path: '*' }
this.$router.push('/non-existing')
this.$route.params.pathMatch // '/non-existing'

嵌套路由

URL 中各段动态路径也按某种结构对应嵌套的各层组件,借助 vue-router,使用嵌套路由配置,就可以很简单地表达这种关系。

const router = new VueRouter({
  routes: [
    {
      path: '/user/:id',
      component: User,
      children: [
        {
          ///user/:id/profile 匹配成功,
          // UserProfile 会被渲染在 User<router-view> 中
          path: 'profile',
          component: UserProfile
        },
        {
          ///user/:id/posts 匹配成功
          // UserPosts 会被渲染在 User<router-view> 中
          path: 'posts',
          component: UserPosts
        }
      ]
    }
  ]
})

组件嵌套

 <div class="user">
      <h2>User {{ $route.params.id }}</h2>
      <router-view></router-view>
 </div>

要注意,以 / 开头的嵌套路径会被当作根路径。 这让你充分的使用嵌套组件而无须设置嵌套的路径。 Vue文件的根文件就是以 / 开头的嵌套路径

  <div id="app">
    <router-view/>
  </div>

命名路由

你可以在创建 Router 实例的时候,在 routes 配置中给某个路由设置名称。

const router = new VueRouter({
  routes: [
    {
      path: '/user/:userId',
      name: 'user',
      component: User
    }
  ]
})

要链接到一个命名路由,可以给 router-linkto 属性传一个对象:

<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>

命名视图

你可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。如果 router-view 没有设置名字,那么默认为 default

<router-view class="view one"></router-view>
<router-view class="view two" name="a"></router-view>
<router-view class="view three" name="helper"></router-view>

然后你可以用这个路由配置完成该布局:

{
  path: '/settings',
  // 你也可以在顶级路由就配置命名视图
  component: UserSettings,
  children: [{
    path: 'emails',
    component: UserEmailsSubscriptions
  }, {
    path: 'profile',
    components: {
      default: UserProfile,
      helper: UserProfilePreview,
      a: Loading
    }
  }]
}

路由重定向和别名

重定向

重定向也是通过 routes 配置来完成,下面例子:

const router = new VueRouter({
  routes: [
    // 是从 `/a` 重定向到 `/b`
    { path: '/a', redirect: '/b' },
    // 重定向的目标也可以是一个命名的路由
    { path: '/c', redirect: { name: 'foo' }},
    // 甚至是一个方法,动态返回重定向目标
    { path: '/a', redirect: to => {
      // 方法接收 目标路由 作为参数
      // return 重定向的 字符串路径/路径对象
    }}
  ]
})

注意:重定向不会触发导航守卫

别名

正如人的小名一样,虽然叫的名字不同,但是你还是你;路由也是一样,路径名不同但是还是会跳到一样的组件

const router = new VueRouter({
  routes: [
    { path: '/a', component: A, alias: '/b' }
  ]
})

编程式路由

除了使用 <router-link> 创建 a 标签来定义导航链接,我们还可以借助 router 的实例方法,通过编写代码来实现。

注意:在 Vue 实例内部,你可以通过 $router 访问路由实例。

router对象方法

$router.push()

想要导航到不同的 URL,则使用 router.push 方法。这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的 URL。

点击 <router-link :to="..."> 等同于调用 router.push(...)

router.push(location, onComplete?, onAbort?)

该方法的参数可以是一个字符串路径,或者一个描述地址的对象。例如:

// 字符串
router.push('home')
​
// 对象
router.push({ path: 'home' })
​
// 命名的路由
router.push({ name: 'user', params: { userId: '123' }})
​
// 带查询参数,变成 /register?plan=private
router.push({ path: 'register', query: { plan: 'private' }})
​
const userId = '123'
router.push({ name: 'user', params: { userId }}) // -> /user/123
router.push({ path: `/user/${userId}` }) // -> /user/123
// 这里的 params 不生效
router.push({ path: '/user', params: { userId }}) // -> /user
$router.replace()

router.push 很像,但是它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录。

用法是一样的

$router.go()

这个方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步,类似 window.history.go(n)

// 在浏览器记录中前进一步,等同于 history.forward()
router.go(1)
​
// 后退一步记录,等同于 history.back()
router.go(-1)

History原生方法

如果你已经熟悉 Browser History APIs (opens new window),那么在 Vue Router 中操作 history 就是超级简单的。

还有值得提及的,Vue Router 的导航方法 (pushreplacego) 在各类路由模式 (historyhashabstract) 下表现一致。

路由传参

const User = {
  template: '<div>User {{ $route.params.id }}</div>'
}
const router = new VueRouter({
  routes: [{ path: '/user/:id', component: User }]
})

在组件中使用 $route 会使之与其对应路由形成高度耦合,从而使组件只能在某些特定的 URL 上使用,限制了其灵活性。

通过 props 解耦

const User = {
  props: ['id'],
  template: '<div>User {{ id }}</div>'
}
const router = new VueRouter({
  routes: [
    { path: '/user/:id', component: User, props: true },

    // 对于包含命名视图的路由,你必须分别为每个命名视图添加 `props` 选项:
    {
      path: '/user/:id',
      components: { default: User, sidebar: Sidebar },
      props: { default: true, sidebar: false }
    }
  ]
})

布尔值

如果 props 被设置为 trueroute.params 将会被设置为组件属性。

对象值

如果 props 是一个对象,它会被按原样设置为组件属性。当 props 是静态的时候有用。

函数模式

你可以创建一个函数返回 props。这样你便可以将参数转换成另一种类型,将静态值与基于路由的值结合等等。

const router = new VueRouter({
  routes: [
    {
      path: '/search',
      component: SearchUser,
      props: route => ({ query: route.query.q })
    }
  ]
})

URL /search?q=vue 会将 {query: 'vue'} 作为属性传递给 SearchUser 组件。

路由元信息

定义路由的时候可以配置 meta 对象:

routes 配置中的每个路由对象为 路由记录。路由记录可以是嵌套的,因此,当一个路由匹配成功后,他可能匹配多个路由记录。

const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      children: [
        {
          path: 'bar',
          component: Bar,
          // a meta field
          meta: { requiresAuth: true }
        }
      ]
    }
  ]
})

一个路由匹配到的所有路由记录会暴露为 $route 对象;

示例:遍历路由匹配字段数组,查看记录中meta字段确定是否要登录

router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    if (!auth.loggedIn()) {
      next({
        path: '/login',
        query: { redirect: to.fullPath }
      })
    } else {
      next()
    }
  } else {
    next() // 确保一定要调用 next()
  }
})

过渡效果

<router-view> 是基本的动态组件,跳转不同路由类似使用不同组件

<!-- 使用动态的 transition name -->
<transition :name="transitionName">
  <router-view></router-view>
</transition>
// 接着在父组件内
// watch $route 决定使用哪种过渡
watch: {
  '$route' (to, from) {
    const toDepth = to.path.split('/').length
    const fromDepth = from.path.split('/').length
    this.transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left'
  }
}

导航守卫

vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。

记住参数或查询的改变并不会触发进入/离开的导航守卫

可以通过观察 $route 对象来应对这些变化,或使用 beforeRouteUpdate 的组件内守卫。

全局守卫

全局前置守卫

当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中

const router = new VueRouter({ ... })
​
router.beforeEach((to, from, next) => {
  // ...
})

示例:常用作于用户身份验证

router.beforeEach((to, from, next) => {
  if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
  else next()
})

每个守卫方法接收三个参数:

  • to: 即将要进入的目标 路由对象

  • from: 当前导航正要离开的路由对象

  • next: 执行下一步的方法,一定要调用该方法来resolve这个钩子;这个函数可以传参数:

    • next()/next(true) : 进入管道的下一个钩子;执行完毕导航状态就是 comfirmed
    • next(false) :中断当前路由;用户可以手动点浏览器后退按钮回到from的地址
    • next('/') / next({path: '/'}) :跳到一个指定的地址;next方法可以和$router.push() 传一样的参数
    • next(error) :终止导航,错误传给 router.onError()

全局解析守卫

router.beforeResolve 注册一个全局守卫。这和 router.beforeEach 类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。

全局后置钩子

不会接受 next 函数也不会改变导航本身

router.afterEach((to, from) => {
  // ...
})

路由独享守卫

在路由配置上直接定义 beforeEnter 守卫:

const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      beforeEnter: (to, from, next) => {
        // ...
      }
    }
  ]
})

组件内的守卫

在路由组件内直接定义以下路由导航守卫:

●beforeRouteEnter:组件进入之前,组件实例还没被创建(next(vm => {})) ●beforeRouteUpdate:组件更新 ●beforeRouteLeave:组件离开之前

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

数据请求

进入某个路由后,需要从服务器获取数据。例如,在渲染用户信息时,你需要从服务器获取用户的数据。我们可以通过两种方式来实现:

  • 导航完成之后获取:先完成导航,然后在接下来的组件生命周期钩子中获取数据。在数据获取期间显示“加载中”之类的指示。
  • 导航完成之前获取:导航完成前,在路由进入的守卫中获取数据,在数据获取成功后执行导航。

导航完成后获取数据

在数据获取期间展示一个 loading 状态,还可以在不同视图间展示不同的 loading 状态。

  created () {
    // 组件创建完后获取数据,
    // 此时 data 已经被 observed 了
    this.fetchData()
  },
  watch: {
    // 如果路由有变化,会再次执行该方法
    '$route': 'fetchData'
  },

在导航完成前获取数据

  beforeRouteEnter (to, from, next) {
    getPost(to.params.id, () => {
      next(vm => vm.fetchData())
    })
  },
  // 路由改变前,组件就已经渲染完了
  // 逻辑稍稍不同
  beforeRouteUpdate (to, from, next) {
    this.post = null
    getPost(to.params.id, () => {
      this.fetchData()
      next()
    })
  },

滚动行为

当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。 vue-router 能做到,而且更好,它让你可以自定义路由切换时页面如何滚动。

注意: 这个功能只在支持 history.pushState 的浏览器中可用。

创建一个 Router 实例,你可以提供一个 scrollBehavior 方法:

const router = new VueRouter({
  routes: [...],
  scrollBehavior (to, from, savedPosition) {
    // return 期望滚动到哪个的位置
  if (savedPosition) {
    return savedPosition
  } else {
    return { x: 0, y: 0 }
  }       
  }
})

scrollBehavior 方法接收 tofrom 路由对象。第三个参数 savedPosition 当且仅当 popstate 导航 (通过浏览器的 前进/后退 按钮触发) 时才可用。

这个方法返回滚动位置的对象信息,长这样:

  • { x: number, y: number }
  • { selector: string, offset? : { x: number, y: number }}

路由懒加载

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。

const Foo = () => import( './Foo.vue')
const Bar = () => import( './Bar.vue')
const Baz = () => import('./Baz.vue')