最近再刷Vue周边生态的官方文档,因为之前的学习都是看视频配合着文档,但主要还是通过视频学习,所以很多知识点都没有了解,至从上次刷了Vuex的官方文档就体会到了通读文档的好处,学习一门技术最好的还是去看它的官方文档,这样对于这门技术你就会了解的比较透彻,知识点也比较全面,所以在刷完Vuex文档之后写了篇《深入Vuex最佳实践》,然后花了两天(上班没时间摸鱼,都是晚上学习)的时间刷完了Vue-router官方文档,所以有了这篇文章,所以后续还会一篇关于Vue-cli相关的配置文章,所以整篇文章主要从实践角度切入,可能不会有那么多源码解析(有点标题党的味道,哈哈~),但也会涉及到核心功能的源码解读
在线卑微,如果觉得这篇文章对你有帮助的话欢迎大家点个赞
tip: 文章首发于掘金并做了排版美化推荐掘金阅读体验更好 戳我跳转
Vue-router 是 Vue.js
官方的路由管理器。它和 Vue.js
的核心深度集成,让构建单页面应用变得易如反掌
先来了解两点
单页面应用
单页面应用程序将所有的活动局限于一个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
的时候展示该组件
User1 - 单个路径参数
User2
用户访问 /#/user/xxx/post/xxx
的时候展示该组件
User2 - 多段路径参数路由
那么问题来了,我们怎么知道用户访问的是那个动态参数路由呢?这个时候就要用到响应路由参数的变化
两种方式: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路由参数变化时两种方式都监听到了,我们可以在这两个函数中做一些路由状态变化时的操作
上面在
模板中通过$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
布尔模式
User1 -{{username}}
User2
对象模式
User2 - {{username}}
User3
函数模式
User3 - {{username}}
从上面我们可以看出因为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 的 中
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 的 中
component: () => import('../components/UserHome')
},
]
},
{
path: '/footer',
name: 'Foo',
component: () => import('../components/Footer')
}
]
export default new Router({
routes
})
声明式 | 编程式 |
---|---|
|
router.replace(...) |
to footer
router.push(location, onComplete?, onAbort?)
router.replace(location, onComplete?, onAbort?)
这两种的方式一样, 唯一区别在于 push
会产生路由的历史记录, 而repalce
不会产生, 这对于window中的history
是一致的
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
的组件内守卫。
你可以在路由配置上直接定义 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
传递回调的唯一守卫。对于 beforeRouteUpdate
和 beforeRouteLeave
来说,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)
}
}
上面讲了那么多相信大家也是懵懵的,这些路由调用的时机是怎么样的,顺序又是怎么样的,下面我们按照官方给的解释实践一下
完整的导航解析流程
beforeRouteLeave
守卫。beforeEach
守卫。beforeRouteUpdate
守卫 (2.2+)。beforeEnter
。beforeRouteEnter
。beforeResolve
守卫 (2.5+)。afterEach
钩子。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
Home
to Index
Index.vue
Index
返回
toUser
User.vue
User - {{id}}
跳转动态路由
演示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
上面动图可能过于快了, 将其截图下来每一步做下分析
上面标的数子是对应官方给的顺序
从Home.vue跳转到Index.vue触发的路由守卫
beforeRouterLeave
, 表示离开该组件beforeEach
, 从route对象中可以获取我们跳转前和跳转后的路由信息从Index.vue
跳转到user/ck
触发的路由守卫
beforeEach
befreEnter
afterEach
钩子。beforeRouteEnter
守卫中传给 next
的回调函数并且将创建好的实例传递进去了/user/ck
-> user/c
重用同一个组件所以触发beforeRoteUpdate
案列涉及到到了
戳我去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)
// => 所以我们就可以在全局上使用 这两个内置组件
因为是是实践文,所以这整篇文章都是通过代码的方式来讲的,对于一些概念性和基础性语法的东西讲的比较少。如果 这篇文章对你有帮助请点个赞
如果你觉得我的文章对你挺有帮助,我想请你帮我两个小忙: