# Vue-Router
使用 vue-router 可以实现单页面模式切换 URL 地址。实质是通过 hash 与 History interface 两种 API 实现的,这两种模式都可以实现 URL 地址更新的同时页面无需重新加载。
vue-router 针对两种模式分别封装了两个类:HTML5History,HashHistory
# HashHistory
hash的使用
监听地址
hash的变化:window.addEventListener("hashchange", funcRef, false)主动改变
hash:window.location.hash='/bar',每一次改变hash(window.location.hash),都会在浏览器的访问历史中增加一个记录replacehash:window.location.replace(window.location.href#[hash]),url地址没变,只改变使用hash, 此时不会重新加载页面,只会替换浏览器的访问历史中的最近一个记录
HashHistory.push()
源码定义:
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.transitionTo(location, route => {
pushHash(route.fullPath)
onComplete && onComplete(route)
}, onAbort)
}
function pushHash (path) {
window.location.hash = path
}
# $route/$router属性
每个组件都有一个 $route/$router 属性,这个 $route/$router 是从哪来的咧?
Vue 在安装 vue-router 插件的时候,会调用 vue-router 中的 install 方法,install 中有个 Vue.mixin 方法
Vue.mixin({
beforeCreate: function beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this;
this._router = this.$options.router;
this._router.init(this);
Vue.util.defineReactive(this, '_route', this._router.history.current);
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
}
registerInstance(this, this);
},
destroyed: function destroyed () {
registerInstance(this);
}
});
_routerRoot 是利用 Vue.mixin 方法在每个组件注入的一个属性,注入方式分两种情况:
判断是否存在
this.$options.router,一般来说只有根组件才会这个属性,因为这个属性初始化 Vue 实例时传入的,这个router就是vue-router的实例new Vue({ router, render:(h)=> h(App) }).$mount(root)否则取
$parent父组件上的_routerRoot
所以每个组件都会有一个 _routerRoot 属性
然后在 mixin 下方有这么两行语句
Object.defineProperty(Vue.prototype, '$router', {
get: function get () { return this._routerRoot._router }
});
Object.defineProperty(Vue.prototype, '$route', {
get: function get () { return this._routerRoot._route }
});
可以看到 $route/$router 定义在 Vue 原型中,实际上指向的是当前组件的 this._routerRoot 属性中
# $router实现响应机制
$router 属性的响应机制的添加也是在 install 中添加
if (isDef(this.$options.router)) {
this._routerRoot = this;
this._router = this.$options.router;
this._router.init(this);
Vue.util.defineReactive(this, '_route', this._router.history.current);
}
在根 Vue 实例中使用 Vue.util.defineReactive(this, '_route', this._router.history.current),给 _route 属性添加了监听机制,之后当我们访问 _route 属性时就会去收集相关的 render Watcher
# 依赖收集
上文通过 Vue.util.defineReactive 对 _route 属性进行的监听,以响应式机制的原理可以知道依赖的收集是在访问 _route 属性的时候添加的
var View = {
name: 'RouterView',
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render: function render (_, ref) {
// ....
var route = parent.$route;
}
}
上代码是 RouterView 组件实现的函数式写法,当我们在页面中使用这个组件,并且渲染到 RouterView 组件时会触发对应的 render 方法,接着执行 var route = parent.$route 的时候就会被 Object.defineProperty 定义的 get 方法劫持,此时 Dep.target 就是 RouterView 所在的组件,会把这个组件 render Watcher 收集到 _router 的收集器 Dep 中,这一步就完成了对事件的收集
之后当我们在组件调用 this.$router.push 方法改变路由时就会更新 $route 属性,就会触发依赖的更新方法从而更新视图了(RouteLInk 组件渲染时也会做同样的收集)
# 路由更新(hash模式)
以 push 方法为例
调用 this.$router.push(location, onComplete, onAbort) 时实际上执行的 this.history.push(location, onComplete, onAbort) 方法
// HashHistory.prototype.push
HashHistory.prototype.push = function push (location, onComplete, onAbort) {
var this$1 = this;
var ref = this;
var fromRoute = ref.current;
this.transitionTo(
location,
function (route) {
pushHash(route.fullPath);
handleScroll(this$1.router, route, fromRoute, false);
onComplete && onComplete(route);
},
onAbort
);
};
// pushHash
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path));
} else {
window.location.hash = path;
}
}
// History.prototype.transitionTo
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// 获取匹配的路由信息
const route = this.router.match(location, this.current)
// 确认切换路由
this.confirmTransition(route, () => {
// 以下为切换路由成功或失败的回调
// 更新路由信息,对组件的 _route 属性进行赋值,触发组件渲染
// 调用 afterHooks 中的钩子函数
this.updateRoute(route)
// 添加 hashchange 监听
onComplete && onComplete(route)
// 更新 URL
this.ensureURL()
// 只执行一次 ready 回调
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => { cb(route) })
}
}, err => {
// 错误处理
if (onAbort) {
onAbort(err)
}
if (err && !this.ready) {
this.ready = true
this.readyErrorCbs.forEach(cb => { cb(err) })
}
})
}
// 确认切换路由
confirmTransition(route: Route, onComplete: Function, onAbort?: Function) {
const current = this.current
// 中断跳转路由函数
const abort = err => {
if (isError(err)) {
if (this.errorCbs.length) {
this.errorCbs.forEach(cb => {
cb(err)
})
} else {
warn(false, 'uncaught error during route navigation:')
console.error(err)
}
}
onAbort && onAbort(err)
}
// 如果是相同的路由就不跳转
if (
isSameRoute(route, current) &&
route.matched.length === current.matched.length
) {
this.ensureURL()
return abort()
}
// 通过对比路由解析出可复用的组件,需要渲染的组件,失活的组件
const { updated, deactivated, activated } = resolveQueue(
this.current.matched,
route.matched
)
function resolveQueue(
current: Array<RouteRecord>,
next: Array<RouteRecord>
): {
updated: Array<RouteRecord>,
activated: Array<RouteRecord>,
deactivated: Array<RouteRecord>
} {
let i
const max = Math.max(current.length, next.length)
for (i = 0; i < max; i++) {
// 当前路由路径和跳转路由路径不同时跳出遍历
if (current[i] !== next[i]) {
break
}
}
return {
// 可复用的组件对应路由
updated: next.slice(0, i),
// 需要渲染的组件对应路由
activated: next.slice(i),
// 失活的组件对应路由
deactivated: current.slice(i)
}
}
// 导航守卫数组
const queue: Array<?NavigationGuard> = [].concat(
// 失活的组件钩子
extractLeaveGuards(deactivated),
// 全局 beforeEach 钩子
this.router.beforeHooks,
// 在当前路由改变,但是该组件被复用时调用
extractUpdateHooks(updated),
// 需要渲染组件 enter 守卫钩子
activated.map(m => m.beforeEnter),
// 解析异步路由组件
resolveAsyncComponents(activated)
)
// 保存路由
this.pending = route
// 迭代器,用于执行 queue 中的导航守卫钩子
const iterator = (hook: NavigationGuard, next) => {
// 路由不相等就不跳转路由
if (this.pending !== route) {
return abort()
}
try {
// 执行钩子
hook(route, current, (to: any) => {
// 只有执行了钩子函数中的 next,才会继续执行下一个钩子函数
// 否则会暂停跳转
// 以下逻辑是在判断 next() 中的传参
if (to === false || isError(to)) {
// next(false)
this.ensureURL(true)
abort(to)
} else if (
typeof to === 'string' ||
(typeof to === 'object' &&
(typeof to.path === 'string' || typeof to.name === 'string'))
) {
// next('/') 或者 next({ path: '/' }) -> 重定向
abort()
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
// 这里执行 next
// 也就是执行下面函数 runQueue 中的 step(index + 1)
next(to)
}
})
} catch (e) {
abort(e)
}
}
// 经典的同步执行异步函数
runQueue(queue, iterator, () => {
const postEnterCbs = []
const isValid = () => this.current === route
// 当所有异步组件加载完成后,会执行这里的回调,也就是 runQueue 中的 cb()
// 接下来执行 需要渲染组件的导航守卫钩子
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
const queue = enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, () => {
// 跳转完成
if (this.pending !== route) {
return abort()
}
this.pending = null
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
postEnterCbs.forEach(cb => {
cb()
})
})
}
})
})
}
export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
const step = index => {
// 队列中的函数都执行完毕,就执行回调函数
if (index >= queue.length) {
cb()
} else {
if (queue[index]) {
// 执行迭代器,用户在钩子函数中执行 next() 回调
// 回调中判断传参,没有问题就执行 next(),也就是 fn 函数中的第二个参数
fn(queue[index], () => {
step(index + 1)
})
} else {
step(index + 1)
}
}
}
// 取出队列中第一个钩子函数
step(0)
}
这里先关注 this.confirmTransition 方法第二个回调函数参数,里面会执行以下三个步骤:
this.updateRoute(route)
// 添加 hashchange 监听
onComplete && onComplete(route)
// 确定URL显示是否正确
this.ensureURL()
History.prototype.updateRoute = function updateRoute (route) {
this.current = route;
this.cb && this.cb(route);
};
// this.cb在是 `init()` 方法中设置的回调
this.apps.forEach(function (app) {
app._route = route;
});
在 this.updateRoute(route) 方法将更新 _route 属性,然后就会触发视图的更新
总结一下,从设置路由改变到视图更新的流程如下
$router.push() --> HashHistory.push() --> History.transitionTo() --> History.updateRoute() --> {app._route = route} --> vm.render()
# HashHistory.replace()
replace() 方法与 push() 方法不同之处在于,它并不是将新路由添加到浏览器访问历史的栈顶,而是替换掉当前的路由:
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.transitionTo(location, route => {
replaceHash(route.fullPath)
onComplete && onComplete(route)
}, onAbort)
}
function replaceHash (path) {
const i = window.location.href.indexOf('#')
window.location.replace(
window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path
)
}
# 路由更新(history模式)
history 提供的方法:
History.back(): 前往上一页, 用户可点击浏览器左上角的返回按钮模拟此方法. 等价于history.go(-1)History.forward(): 前往上一页, 用户可点击浏览器左上角的返回按钮模拟此方法. 等价于history.go(1)History.go(): 通过当前页面的相对位置从浏览器历史记录( 会话记录 )加载页面。比如:参数为-1的时候为上一页,参数为1的时候为下一页. 当整数参数超出界限时例如: 如果当前页为第一页,前面/后面 已经没有页面了,传参的值为-1或者1都没有任何效果也不会报错。调用没有参数的go()方法或者不是整数的参数时也没有效果History.pushState(state, title[, url]):按指定的名称和 URL(如果提供该参数)将数据push进会话历史栈const state = { 'page_id': 1, 'user_id': 5 } const title = '' const url = 'hello-world.html' history.pushState(state, title, url)History.replaceState(): 按指定的数据,名称和 URL (如果提供该参数),更新历史栈上最新的入口,用法同History.pushState
History.pushState 和 History.replaceState()都不会刷新页面
history 属性:
History.state: 返回一个表示历史堆栈顶部的状态的值,获取 History.pushState 和 History.replaceState() 添加的 state 值
vue-router 相关的处理逻辑跟 hash 模式差不多,只是更新地址的具体实现方法换成 history 的相关方法罢了
window.location.hash=>history.pushState()window.location.replace()=>history.replaceState()
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
pushState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
replaceState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
// src/util/push-state.js
export function pushState (url?: string, replace?: boolean) {
saveScrollPosition()
// try...catch the pushState call to get around Safari
// DOM Exception 18 where it limits to 100 pushState calls
const history = window.history
try {
if (replace) {
history.replaceState({ key: _key }, '', url)
} else {
_key = genKey()
history.pushState({ key: _key }, '', url)
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url)
}
}
export function replaceState (url?: string) {
pushState(url, true)
}
history 模式下的监听 url 的变化
constructor (router: Router, base: ?string) {
window.addEventListener('popstate', e => {
const current = this.current
this.transitionTo(getLocation(this.base), route => {
if (expectScroll) {
handleScroll(router, route, current, true)
}
})
})
}
需要注意的是调用history.pushState()或history.replaceState()不会触发popstate事件。只有在做出浏览器动作时,才会触发该事件,如用户点击浏览器的回退按钮(或者在Javascript代码中调用 history.back() , history.forward() 或者 history.go() 方法
# RouterView/RouterLink
RouterView/RouterLink 组件能在所有组件中使用也是因为 install 中,对两个组件进行的全局注册
Vue.component('RouterView', View);
Vue.component('RouterLink', Link);
# 路由导航
# 全局前置守卫
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
// ...
})
当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中
每个守卫方法接收三个参数:
to: Route: 即将要进入的目标 路由对象from: Route: 当前导航正要离开的路由next: Function: 一定要调用该方法来resolve这个钩子.执行效果依赖next方法的调用参数next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是confirmed(确认的)。next(false): 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到from路由对应的地址next('/')或者next({ path: '/' }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向next传递任意位置对象,且允许设置诸如replace: true、name: 'home'之类的选项以及任何用在router-link的to prop或router.push中的选项next(error): (2.4.0+)如果传入next的参数是一个Error实例,则导航会被终止且该错误会被传递给router.onError()注册过的回调
# 全局解析守卫
在 2.5.0+ 你可以用 router.beforeResolve 注册一个全局守卫。这和 router.beforeEach 类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。
# 全局后置钩子
你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:
router.afterEach((to, from) => {
// ...
})
# 路由独享的守卫
你可以在路由配置上直接定义 beforeEnter 守卫:
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
这些守卫与全局前置守卫的方法参数是一样的
# 组件内的守卫
最后,你可以在路由组件内直接定义以下路由导航守卫:
beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave
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 => {
// 通过 `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钩子。触发 DOM 更新。
用创建好的实例调用
beforeRouteEnter守卫中传给next的回调函数。
# 总结
先实例一个
router,这个router包含了我们平时使用的$route和$router两个属性在项目初始化传入这个
router实例new Vue({router}),这样就可以this.$options.router访问这个router实例使用
vue-minxin给所有 vue 混入beforeCreate钩子,在beforeCreate钩子中主要有两个关键步骤:给每个组件添加
_routerRoot属性使用
Vue.util.defineReactive给当前的_route($route)添加响应机制
全局注册
RouterView和RouterLink两个组件之后就根
data属性的响应式机制同理:当渲染这两个组件的时候,就会访问$router信息,然后收集当前render Watcher,这里就完成了响应式收集部分
注意一下 $router, _routerRoot, _router 的关系
$router => this._routerRoot._routerthis._routerRoot = this