深入Vue-router最佳实践

前言

最近再刷Vue周边生态的官方文档,因为之前的学习都是看视频配合着文档,但主要还是通过视频学习,所以很多知识点都没有了解,至从上次刷了Vuex的官方文档就体会到了通读文档的好处,学习一门技术最好的还是去看它的官方文档,这样对于这门技术你就会了解的比较透彻,知识点也比较全面,所以在刷完Vuex文档之后写了篇《深入Vuex最佳实践》,然后花了两天(上班没时间摸鱼,都是晚上学习)的时间刷完了Vue-router官方文档,所以有了这篇文章,所以后续还会一篇关于Vue-cli相关的配置文章,所以整篇文章主要从实践角度切入,可能不会有那么多源码解析(有点标题党的味道,哈哈~🤣),但也会涉及到核心功能的源码解读

在线卑微,如果觉得这篇文章对你有帮助的话欢迎大家点个赞👻
tip: 文章首发于掘金并做了排版美化推荐掘金阅读体验更好 戳我跳转

简介

Vue-router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌

先来了解两点

  • 单页面应用(SPA)
  • 路由管理器

单页面应用

单页面应用程序将所有的活动局限于一个Web页面中,仅在该Web页面初始化时加载相应的HTML、JavaScript 和 CSS。一旦页面加载完成了,SPA不会因为用户的操作而进行页面的重新加载或跳转。取而代之的是利用 JavaScript 动态的变换HTML的内容,从而实现UI与用户的交互。

路由管理器

这里的路由管理器并不是我们并时生活中的硬件路由器,这里的路由就是单页应用(SPA)的路径管理器,就是为了解决Vue.js开发单页面应用不能进行链接跳转,我们通过路径的的方式来管理不同的页面

了解Vue-router所解决的问题之后, 我们开始学习Vue-router在项目中常用的一些功能

  • 嵌套的路由/视图表
  • 模块化的、基于组件的路由配置
  • 路由参数、查询、通配符
  • 细粒度的导航控制

起步

在开始我们先体会下Vue-router的一些功能:

  • 动态路由匹配
  • 嵌套路由
  • 声明式/编程式导航
  • 命名路由/命名视图
  • 重定向和别名
  • 路由组件传参

tip:本文章所有实例代码仓库地址在文章最后有给出

动态路由匹配

router.js

import Vue from 'vue' // 引入Vue
import Router from 'vue-router' // 引入vue-router
import Home from '@/pages/Home' //引入根目录下的Hello.vue组件
 
// Vue全局使用Router
Vue.use(Router)

/*
	使用 Vue.js + Vue-router构建单页面应用, 只要通过组合组件来组成我们的应用程序, 我们引入Vue-router,只要	将组件映射到路由,告诉Vue-router在那里渲染它们
*/

let routes = [ // 配置路由,这里是个数组
  { // 每一个链接都是一个对象
    path: '/', // 链接路径
    name: 'Home', // 路由名称,
    component: Home // 对应的组件模板
  },
  // 动态路径参数 以冒号开头
  { path: '/user/:username', // 动态路由
    component: () => import('../pages/User1'), // 按需加载路由对应的组件, 需要下载polyfill兼容ES6语法
  },
  {   // 多段路径参数
    path: '/user/:id/post/:post_id', // 动态路由
    component: () => import('../pages/User2'), // 按需加载路由对应的组件, 需要下载polyfill兼容ES6语法
  },
]

export default new Router({
  routes
})

User1

用户访问 /#/user/xxx的时候展示该组件

<template>
  <div class="User1">
    User1 - 单个路径参数
  </div>
</template>

User2

用户访问 /#/user/xxx/post/xxx的时候展示该组件

<template>
  <div class="User2">
    User2 - 多段路径参数路由
  </div>
</template

那么问题来了,我们怎么知道用户访问的是那个动态参数路由呢?这个时候就要用到响应路由参数的变化

两种方式:watch (监测变化) $route 对象, beforeRouteUpdate导航守卫

user1.vue增加下面代码

<template>
  <div class="User1">
    <!-- 通过router对象可以获取到路由属性, 这种方式耦合了,后面会讲路由组件传参的方式 -->
    User1 -{{$route.params.username}} 单个路径参数
  </div>
</template>

<script>
export default {
  name: 'User1',
  // 侦听route对象方式
  watch: {
    $route (to, from) {
      this.$message.success(`watch -> ${to.path}, ${from.path}`)
    },
    
  },
  // vue2.2引入的导航守卫,当路由参数发生变化调用
  beforeRouteUpdate (to, from, next) {
    this.$message.info(`导航守卫 -> ${to.path}, ${from.path}`)
    // 一定要调用next让其继续解析下一个管道中的路由组件
    next()
  }
}
</script>

演示

注意上面从ck->ks路由参数变化时两种方式都监听到了,我们可以在这两个函数中做一些路由状态变化时的操作

路由组件传参

上面在<tempate>模板中通过$router.prarams.username方式获取路由传递的参数已经于其对应路由形成高度耦合,限制了其灵活性, 我们可以通过props将组件和路由进行解耦

props传递路由组件参数有三种方式:

  • 布尔模式
  • 对象模式
  • 函数模式

代码

router.js

import Vue from 'vue'
import Router from 'vue-router'
import home from '@/pages/Home'

Vue.use(Router)

let routes = [
  {
    path: '/',
    name: 'Home',
    component: home
  },
  {  // 动态路径参数 以冒号开头
    path: '/user1/:username', // 动态路由
    component: () => import('../pages/User1'),
    props: true  // 布尔模式: 如果 props 被设置为 true,route.params 将会被设置为组件属性。
  },
  { 
    path: '/user2', 
    component: () => import('../pages/User2'),
    props: {username: 'ck'} // 对象模式: 只有静态路由才能有效, 并且参数是写死的
  },
  {
    path: '/user3/:username', 
    component: () => import('../pages/User3'),
    // 返回了用户url中的参数 比如 /user3?username='ck' => {username: 'ck} 最终还是以对象模式的方式返回参数
    props: (route) => ({username: route.query.username}) // 函数模式
  }
]

export default new Router({
  routes
})

User1

布尔模式

<template>
  <div class="User1">
    User1 -{{username}} 
  </div>
</template>

<script>
export default {
  name: 'User1',
  props: ['username']  // 通过props获取路由传递给对应组件的参数
}
</script>

User2

对象模式

<template>
  <div class="User2">
    User2 - {{username}} 
  </div>
</template>

<script>
export default {
  name: 'User2',
  props: ['username']  // 通过props获取路由传递给对应组件的参数
}
</script>

User3

函数模式

<template>
  <div class="User3">
    User3 - {{username}}
  </div>
</template>

<script>
export default {
  name: 'User3',
  props: ['username']  // 通过props获取路由传递给对应组件的参数
}
</script>

演示

从上面我们可以看出因为user2是静态路由所以不支持动态参数而且其对应的路由组件传递过来的参数也是写死的

嵌套路由

实际生活中的应用界面,通常由多层嵌套的组件组合而成。同样地,URL 中各段动态路径也按某种结构对应嵌套的各层组件,例如:

/user/ck/profile                     /user/ks/posts
+------------------+                  +-----------------+
| User             |                  | User            |
| +--------------+ |                  | +-------------+ |
| | Profile      | |  +------------>  | | Posts       | |
| |              | |                  | |             | |
| +--------------+ |                  | +-------------+ |
+------------------+                  +-----------------+

router.js

import Vue from 'vue'
import Router from 'vue-router'
import home from '@/pages/Home'

Vue.use(Router)

let routes = [
  {
    path: '/',
    name: 'Home',
    component: home,
  },
  {
    path: '/user/:username', // 动态路由
    name: 'User',
    component: () => import('../components/User'),
    children: [
      {
       // 当 '/user/:username/profile' 匹配成功, UserProfile 会被渲染在 User 的 <router-view> 中
        path: 'profile', // 可以匹配 /user/ks/profile
        name: 'Profile',
        component: () => import('../components/Profile')
      },
      {
        path: '/user/:usrname/posts', // 这样也可以匹配 /user/ks/posts, 但其实是将其匹配为了根组件的/user:username动态组件下的 posts
        name: 'Posts',
        component: () => import('../components/Posts')
      },
      {
        path: '',
        name: 'UserHome',
        // 当 /user/:username 匹配成功,比如 /user/ks || /user/ck
        // UserHome 会被渲染在 User 的 <router-view> 中
        component: () => import('../components/UserHome')
      },
    ]
  },
  {
    path: '/footer',
    name: 'Foo',
    component: () => import('../components/Footer')
  }
]

export default new Router({
  routes
})

演示

声明式/编程式导航

声明式 编程式
<router-link :to="..." replace> router.replace(...)
<template>
  <div class="home">
       <!-- 声明式 -->
    <router-link
  	  to="footer"
      tag="button"
    >
        to footer
    </router-link>  
      
    <!-- 编程式 -->
    <button @click="$router.push('footer')">字符串-写路由路径的方式</button>
    <button @click="$router.push({path: '/footer'})">对象-写路径的方式</button>
    <button @click="$router.push({name: 'Foo', params: {'userId': '123'}})">name和params - 写路由名称携带参数的方式</button>
    <button @click="$router.push({path: '/footer', query: {'userId': '456'}})">queyr和path - 写路由路径携带参数的方式</button>
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: 'home',
  data () {
    return {
    }
  },
  methods: {
  }
}
</script>
<style>
button {
  display: block;
}
</style>

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

这两种的方式一样, 唯一区别在于 push会产生路由的历史记录, 而repalce不会产生, 这对于window中的history是一致的

<!-- router.go方法 -->
<template>
     <button @click="goForward">go(1)-前进一步</button>
    <button @click="goBack">go(-1)-后退一步</button>
    <button @click="gogogo">go(100)-前进一白步</button>   
 </template>

<script>
 export default {
    name: 'home'
  	methods: {
        goForward () {
          // 从历史路由中前进一步相当于 window.history.forward
          this.$router.go(1);
        },
        goBack () {
              // 从历史路由中后退一步相当于 window.history.back
              this.$router.go(-1);
        },
        gogogo () {
              // 历史路由中没有100步, 就啥也不干
              this.$router.go(100);
        }
    }  
 }
</script>

演示

命名路由/命名视图/重定向和别名

router.js

import Vue from 'vue'
import Router from 'vue-router'
import UserSettings from '@/pages/UserSettings'

Vue.use(Router)

let routes = [
  {
    path: '/',
    redirect: '/settings' // 重定向
  },
  {
    path: '/settings',
    name: 'Settings', // 命名路由
    alias: '/a', // 取别名,当url中访问 /a -> 也是访问的 settings组件但是路由匹配的是/a, 就相当于用户访问 /a一样
    // 你也可以在顶级路由就配置命名视图
    component: UserSettings,
    children: [
      {
        path: 'emails',
        component: () => import('../pages/UserEmails')
      }, 
      {
        path: 'profile',
        components: {
          default: () => import('../pages/UserProfile'),
          helper: () => import('../pages/UserProfilePreview')
        }
      }
    ]
  }
]

export default new Router({
  routes
})

UserSetttings

<template>
  <div class="UserSettings">
    <h1>User Settings</h1>
    <NavBar/>
    <router-view/>
    <!-- 命名视图 -->
    <router-view name="helper"/>  
  </div>
</template>

<script>
import NavBar from '../components/NavBar'
export default {
  name: 'UserSettings',
  components: {
    NavBar
  }
}
</script>

通过上面的学习相信大家已经撑握了Vue-router在项目中所常用的功能,下面我们开始学习Vue-router的导航守卫

进阶

导航守卫

“导航”表示路由正在发生改变。记住参数或查询的改变并不会触发进入/离开的导航守卫。你可以通过观察 $route 对象响应路由参数的变化来应对这些变化,或使用 beforeRouteUpdate 的组件内守卫。

全局的守卫

  • 全局前置守卫 (router.beforeEach)
  • 全局解析守卫 (router.breforeResolve)
  • 全局后置钩子 (router.afterEach) 注:这个钩子中不存在next

路由独享的守卫

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

const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      beforeEnter: (to, from, next) => {
        // to -> 要跳转过去的路由信息
        // from -> 当前的路由信息
        // next() => 一个函数,表示解析下一个管道中的路由记录
      }
    }
  ]
})

组件内的守卫

最后,你可以在路由组件内直接定义以下路由导航守卫:

  • beforeRouteEnter
  • beforeRouteUpdate (2.2 新增)
  • beforeRouteLeave
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`
  }
}

beforeRouteEnter 守卫 不能 访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。不过,你可以通过传一个回调给 next来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。

beforeRouteEnter (to, from, next) {
  next(vm => {
    // 在实例创建好之后会调用next传递过去的回调并且将实例当做参数传递进来,所以通过 `vm` 可以访问组件实例
  })
}

注意 beforeRouteEnter 是支持给 next 传递回调的唯一守卫。对于 beforeRouteUpdatebeforeRouteLeave 来说,this 已经可用了,所以不支持传递回调,因为没有必要了。

beforeRouteUpdate (to, from, next) {
  // just use `this`
  this.name = to.params.name
  next()
}

这个离开守卫通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消。

beforeRouteLeave (to, from, next) {
  const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
  if (answer) {
    next()
  } else {
    next(false)
  }
}

实践

上面讲了那么多相信大家也是懵懵的,这些路由调用的时机是怎么样的,顺序又是怎么样的,下面我们按照官方给的解释实践一下

完整的导航解析流程

  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 的回调函数。

router.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/pages/Home'
import {message} from 'ant-design-vue'

Vue.use(VueRouter)

let routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/index',
    name: 'Index',
    component: () => import('../pages/Index'),
  },
  {
    path: '/user/:id',
    name: 'User',
    props: true,
    component: () => import('../pages/User'),
    beforeEnter: (to, from, next) => {
      message.success(`路由独享守卫[beforeEnter] -> 从${from.path} 到 ${to.path}`);
      next()
    }
  }
]

let router = new VueRouter({
  routes
})
router.beforeEach((to, from, next) => {
  message.success(`全局前置守卫[beforeEach] -> 从${from.path} 到 ${to.path}`, 5)
  next();
})

router.beforeResolve((to, from, next) => {
  message.success(`全局解析守卫[beforeResolve] -> 从${from.path} 到 ${to.path}`, 5)
  next();
})

router.afterEach((to, from) =>  {
  // 钩子没有next, 也不会改变导航本身
  message.success(`全局后置钩子[afterEach] -> 从${from.path} 到 ${to.path}`, 5)
})


export default router

Home.vue

<template>
  <div class="Home">
    <div class="title">Home</div>
    <a-button type="primary" @click="toIndexHanlder">
      to Index
    </a-button>
  </div>
</template>

<script>
export default {
  name: 'Home',
  beforeRouteLeave(to, from, next) {
    this.$message.success(`组件内守卫[leave] -> 从${from.path} 到 ${to.path}`, 5);
    next();
  },
  methods: {
    toIndexHanlder() {
      this.$router.push({ path: '/index' });
    },
  },
};
</script>

Index.vue

<template>
  <div class="Index">
    <div class="title">Index</div>
    <a-button class="my-btn" type="primary" @click="BackHanlder">
      返回
    </a-button>
    <a-button class="my-btn" type="primary" @click="toUserHanlder">
      toUser
    </a-button>
  </div>
</template>

<script>
export default {
  name: 'Index',
  beforeRouteLeave (to, from, next) {
    console.log(to);
    next()
  },
  methods: {
    BackHanlder () {
      this.$router.go(-1)
    },
    toUserHanlder () {
      this.$router.push({path: 'user/ck'})
    }
  }
}
</script>

User.vue

<template>
  <div class="User">
    <div class="title">User - {{id}}</div>
    <a-button class="my-btn" type="primary" @click="toUserJump">
      跳转动态路由
    </a-button>
  </div>
</template>

<script>
export default {
  name: 'User',
  data () {
    return {
      names: ['a', 'b', 'c', 'd', 'e', 'f'], // 随机路由
      curNameIndex: 0,
      lastNameIndex: 0,
    }
  },
  props: ['id'],
  beforeRouteUpdate (to, from, next) {
    this.$message.success(`组件内的守卫[beforeRouteUpdate] -> 从${from.path} 到 ${to.path}`, 5)
    next()
  },
  beforeRouteEnter (to, from, next) {
    // 不能获取this, 因为当守卫执行前,组件实例还没被创建, 
    // 但是在这个守卫中next支持传递回调, 在实例创建完毕后调用 
    next((vm) => {
      // vm => 创建好的Vue实例
      vm.$message.success(`组件内的守卫[beforeRouteEnter] -> 从${from.path} 到 ${to.path}`, 5)
    })
  },
  methods: {
    // 获取随机路由的方法
   getRandomArbitrary (min, max) {
        this.curNameIndex = Math.round(Math.random() * (max - min) + min);
        return this.curNameIndex === this.lastNameIndex 
        ? this.getRandomArbitrary(min, max) 
        : this.curNameIndex;
    },
    toUserJump () {
      this.getRandomArbitrary(0, this.names.length -1)
      this.lastNameIndex = this.curNameIndex;
      this.$router.push({path: `/user/${this.names[this.curNameIndex]}`})
    }
  }
}
</script>

演示

上面动图可能过于快了, 将其截图下来每一步做下分析

上面标的数子是对应官方给的顺序

从Home.vue跳转到Index.vue触发的路由守卫

    1. 点击按钮导航被触发
    1. 在失活的组件守卫中(Home.vue)调用的beforeRouterLeave, 表示离开该组件
    1. 调用全局前置守卫beforeEach, 从route对象中可以获取我们跳转前和跳转后的路由信息
    1. 路由解析完毕调用

上面标的数子是对应官方给的顺序

Index.vue跳转到user/ck触发的路由守卫

    1. 调用全局前置守卫beforeEach
    1. 在路由配置(User.vue)中调用befreEnter
    1. 调用全局的 afterEach 钩子。
    1. 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数并且将创建好的实例传递进去了

user/ck跳转到user/c触发的路由守卫

    1. 因为这个组件是动态路由, 在/user/ck -> user/c重用同一个组件所以触发beforeRoteUpdate

案列

该案列涉及到到了

  • 动态修改路由元信息,修改文档标题
  • 基于路由的动态过渡
  • 基于导航守卫对用户登录状态进行拦截
  • 对于没有定义的组件投统一返回404页面
  • 使用路由的离开守卫判对于用户跳转登录页面进行确认

戳我去GitHub仓库地址,欢迎大家点个Start👻

源码解读

查看官方vue-router 源码地址

vue-router 实现原理

vue-router 实例化时会初始化 this.history,传入不同 mode 会对应不同的 history,下面来看下代码

constructor (options: RouterOptions = {}) {
    this.mode = mode // 不传mode, 默认就是hash
    
    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base)
        break
      case 'hash': 
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }
}
// => 上面通过HashHistory初始化之后会得到其实例,我们调用的一些 push、replace、go都是this.history上的方法

这里以 HashHistory 为例,vue-router 的 push 方法实现如下:

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    // $flow-disable-line
    if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        this.history.push(location, resolve, reject)
      })
    } else {
      this.history.push(location, onComplete, onAbort)
    }
}
// 在调用push方法之后调用了this.history的push方法

HashHistory 具体实现了 push 方法:

function pushHash (path) {
  if (supportsPushState) {
    pushState(getUrl(path))
  } else {
    window.location.hash = path // 本质上还是通过对window.location.hash方法进行的封装
  }
}

对路由的监听通过 hash 相应的事件监听实现:

window.addEventListener(
  supportsPushState ? 'popstate' : 'hashchange',
  () => {
    const current = this.current
    if (!ensureSlash()) {
      return
    }
    this.transitionTo(getHash(), route => {
      if (supportsScroll) {
        handleScroll(this.router, route, current, true)
      }
      if (!supportsPushState) {
        replaceHash(route.fullPath)
      }
    })
  }
) 

// 对于路由的监听也是通过监听window对象提供的 popstate、hashchange两个事件对于hash的监听来做出不同的响应

所以,Vue-router最核心的也是通过History来实例相应的功能,而History是由传递进去的mode决定,不同的History调用的底层方法不一样,但底层都是通过window.location提供的一些方法进行实例,比如hash改变就是通过hashchange这个事件监听来支持的,所以Vue-router本质上就是对于原生事件的封装

除此之外,vue-router 还提供了两个组件:

Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
// => 所以我们就可以在全局上使用 <router-view> <router-link> 这两个内置组件

写在最后

因为是是实践文,所以这整篇文章都是通过代码的方式来讲的,对于一些概念性和基础性语法的东西讲的比较少。如果 这篇文章对你有帮助请点个赞🤓

看完两件小事

如果你觉得我的文章对你挺有帮助,我想请你帮我两个小忙:

  1. 关注我的 GitHub 博文,让我们成为长期关系
  2. 关注公众号「前端自学驿站」,所有文章、资料第一时间首发公众号,公众号后台回复「教程」 免费领取我精心整理的前端视频教程

img

posted @ 2020-07-10 15:22  听闻北歌有初心  阅读(317)  评论(0编辑  收藏