Vue全家桶之Vue-Router

这是 Vue 全家桶组件系列的文章,梳理一下比较难懂的几个点,此篇文章要说的就是 Vue-Router

什么是路由

在 web 领域,我们可以将 路由 简单的理解为 控制器, 这个控制器主要做什么呢?它能将输入到浏览器中的 url 导向至对应页面的组件,然后浏览器就将这个组件渲染出来。这个过程就是 路由

什么是 Vue-Router

以下是官方说明:

Vue Router 是 Vue.js 官方的 路由管理器
其实跟 Vuex 的思想是很像的,它通过一个全局的 router,来对浏览器中的 url 进行管理,最后导向不同的页面。

核心概念

主要有几大核心概念

  • 动态路由匹配
  • 重定向和别名
  • 路由组件传参
  • History 模式
  • 导航守卫
  • 路由懒加载

动态路由匹配

什么是动态路由匹配?比如我输入的 url 中有 /user/foo/user/bar,我们希望在输入这两个 url 后导向的是同一个页面的同一个组件,那么我们可以这么写(类似于用个变量代替 /user/ 后面的参数,然后就往这个变量丢参数就完事儿了,因为不写死,所以就是动态的)

// User 组件
const User = {
template: '<div>User</div>'
}

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

同时在 path 中也可以设置多段 路径的参数,比如我想要输入 /user/foo/post/100,你可以这样写

/user/:id/post/:post_id

当然了,动态路径的参数名字是可以随便取的,我们可以在 $route.params 中看到这个

console.log($route.params); // {id: 'foo', post_id: '100'}

注意一下这里的匹配优先级,若存在多个路径匹配同一个路由的情况,如

const router = new VueRouter({
routes: [
// 动态路径参数 以冒号开头
{ path: '/xxx', component: User1 },
{ path: '/xxx', component: User2 }
]
})

那么会优先匹配先定义的,也就是输入 /xxx ,会匹配到 User1

重定向和别名

Vue-Router 的这个功能可以实现一个逻辑,这个逻辑是什么呢?拿知乎来举例,若我是未登录的状态,那么当访问个人信息页面时,应该重定向到登录页面,如下

const router = new VueRouter({
routes: [
{ path: '/login', component: Login }
{ path: '/userInfo', redirect: '/login' }
]
})

也可以是一个已经命名过的路由

const router = new VueRouter({
routes: [
{ path: '/login', name: 'login', component: Login },
{ path: '/userInfo', redirect: { name: 'login' }}
]
})

也可以是一个方法

const router = new VueRouter({
routes: [
{ path: '/userInfo', redirect: to => {
// 方法接收 目标路由 作为参数
// return 重定向的 字符串路径/路径对象
return {
path: '/login',
name: 'login',
component: Login
}
}}
]
})

同样,你可以用 别名 的方式,什么是别名?比如在浏览器中输入的是 /bar,但实际上访问的是 /foo

const router = new VueRouter({
routes: [
{ path: '/foo', component: Foo, alias: '/bar' }
]
})

路由组件传参

什么是路由组件传参?我们可以这么理解,其实就是在浏览器输入一串 url ,然后渲染的对应组件能够接受到这串 url 中的某些参数,这就是

      参数
路由 ------> 组件

路由组件传参有 3 种方式: 布尔模式、对象模式、函数模式

  • 布尔模式

其实就是通过 props 传参,比如我输入 /user/foo 这个 url,我想把 url 中 foo 这个参数传给组件,可以这么写

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

前提条件是要设置对应匹配路由的 propstrue
如果你在使用 router-view 时使用到了 name 这个属性,比如

// 渲染 user
<router-view></router-view>

// 渲染 sidebar
<router-view name="sidebar"></router-view>

你就需要为每个对应的 命名视图 添加 props

const router = new VueRouter({
routes: [
{
path: '/user/:id',
components: { default: User, sidebar: Sidebar },
props: { default: true, sidebar: false }
}
]
})

  • 对象模式

比如组件中 props 是个对象

// User 组件
const User = {
props: {
name: ''
},
template: '<div>User {{ name }}</div>'
}
// 路由
const router = new VueRouter({
routes: [
{ path: '/user/new-name', component: User, props: { name: 'strugglebak' } }
]
})

  • 函数模式

其实本质上这个函数返回的还是一个对象,比如在浏览器中输入 /search?name=strugglebak 表示查询,可以这么写

// User 组件
const User = {
props: {
name: ''
},
template: '<div>User {{ name }}</div>'
}
// 路由
const router = new VueRouter({
routes: [
{ path: '/search', component: User, props: (route) => ({ query: route.query.name }) }
]
})

router.query.name 返回的就是 strugglebak 这个字符串

History 模式

什么是 History 模式?我们知道,网易云的 url 一般来说是这种形式

http://music.163.com/#/my/m/music/playlist?id=121597667

在播放歌曲时,每个 url 下都有一个 # 符号,我们知道这个符号是 url 的 hash 部分,这个符号后面那一大串不会传递到服务端,那么它有什么用?它的作用就是 通过这个 hash 来模拟传统的后端路由器,判断哪些请求可以直接导向哪个组件,进而渲染出这个组件,这就是前端路由器的作用。但是带个 # 号的 url 看起来比较丑,你看哪些传统的 web 应用的 url 就没有,那么有没有一种办法来去掉这个 # 号呢?
有的,Vue-Router 提供了这个 History模式,你只需要做如下设置

const router = new VueRouter({
mode: 'history',
routes: [...]
})

url 就会和正常的 url 一样没有 # 号,就对 url 进行了美化处理
但是这个还需要后端配置,官方文档是这么说的

不过这种模式要玩好,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问 http://oursite.com/user/id 就会返回 404,这就不好看了。
所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。

导航守卫

什么是导航守卫呢?这其实就相当于一个 拦截器,在路由发生变化的之中通过这层拦截来实现一些业务需求。一般来说,我们认为当你点击 a 链接进行 重定向式跳转 时路由就产生 变化 了。
但是这里需要注意,params 或者 query 的变化并不会触发 enter/leave 这类路由拦截器,你可以通过

const User = {
template: '...',
watch: {
'$route' (to, from) {
// 对路由变化作出响应...
}
}
}

或者

const User = {
template: '...',
beforeRouteUpdate (to, from, next) {
// 对路由变化作出响应...
}
}

这样的方式来监听以上的变化

导航守卫有几个 api,分别是

  • 全局的守卫 beforeEach,beforeResolve,afterEach
  • 路由独享守卫 beforeEnter
  • 组件内的守卫 beforeRouterEnter,beforeRouterUpdate,beforeRouterLeave

首先我们应该先看看一个完整的导航解析过程是怎样的

           (失活组件)beforeRouterLeave
1. 导航触发 ---------------------------> beforeEach
(复用组件)beforeRouterUpdate
2. ---------------------------> beforeEnter

3. ---------------------------> 解析组件
(激活组件)beforeEnter
4. ---------------------------> beforeResolve

5. 导航确认 ---------------------------> afterEach

6. 触发 DOM 更新

基本上解析过程就代表了它们之间的区别了
注意 beforeEnter可以 在路由配置上定义

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

以及组件内的路由用法如下

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

路由懒加载

路由的懒加载是什么呢?一般我们使用 webpack 进行打包时,js 代码会变得很大,这样会对页面加载的性能造成影响,如何提升性能呢?就是 按需加载,当路由被访问的时候才加载对应组件。具体要怎么做呢?就是分离组件代码。
这里需要分两步来做

  1. 将异步组件定义为返回一个 Promise 的工厂函数

    const Foo = () => Promise.resolve({ /* 组件定义对象 */ })
  2. 在 webpack 中 import

    import('./Foo.vue').then((module)=> {
    //...
    })

还有其他常用 API

常用 API 有 router-link/router-view/this.$router.push/this.$router.replace/this.$route.params

  • router-link

一般写在需要进行路由跳转的地方,它最终渲染的是一个 a 标签

<!-- 使用字符串 -->
<router-link to="xxx">xxx</router-link>

<!-- 使用 v-bind 绑定变量 -->
<router-link :to="xxx">xxx</router-link>

<!-- 绑定对象 -->
<router-link :to="{
path: 'xxx'
}">xxx</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>

  • router-view

如果 router-view 设置了名称,则会渲染对应的路由配置中 components 下的相应组件,一般我们是这么写

<!-- 渲染一个 name 为 main 的组件 -->
<router-view name="main"></router-view>
<!-- 渲染一个 name 为 slidebar 的组件 -->
<router-view name="slidebar"></router-view>

然后在路由中有如下配置

const router = new VueRouter({
routes: [
{
name:'post',
path:'/post',
components:{
main: Article,
slidebar: SlideBar
}
}
]
})

  • this.$router.push

一个 router-link 的替代品,作用就是 向 history 添加记录,可以有如下用法

// 字符串
router.push('home')

// 对象
router.push({ path: 'home' })

// 命名的路由
router.push({ name: 'user', params: { userId: '123' }})

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

注意,若写了 path ,则 params 会被忽略掉(query 除外),所以你应该这么写

const userId = '123'
router.push({ name: 'user', params: { userId }}) // -> /user/123
router.push({ path: `/user/${userId}` }) // -> /user/123

  • this.$router.replace

跟上面的 push 方法很像,不过作用是 替换掉当前的 history 记录,用法跟上面的相同

  • this.$router.params

就是一个 key/value 对象,之前在 动态路由匹配 中说过,它里面保存的就是路由参数

总结

Vue-Router 是前端 SPA 项目的利器,它可以提升用户的体验,增加开发效率。本文只是简单梳理了一下 Vue-Router 开发经常会用到的东西,其他小细节还得多去翻翻 官方文档