VueRouter 源码解析(四)路由守卫、RouterLink、RouterView
在了解路由守卫之前我们先来看看 VueRouter 官方是怎么解释完整的导航流程
完整的导航解析流程
- 导航被触发。
- 在失活的组件里调用 beforeRouteLeave 守卫。
- 调用全局的 beforeEach 守卫。
- 在重用的组件里调用 beforeRouteUpdate 守卫 。
- 在路由配置里调用 beforeEnter。
- 解析异步路由组件。
- 在被激活的组件里调用 beforeRouteEnter。
- 调用全局的 beforeResolve 守卫 。
- 导航被确认。
- 调用全局的 afterEach 钩子。
- 触发 DOM 更新。
- 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
路由守卫
- 全局前置守卫 [beforeEach]
- 全局解析守卫 [beforeResolve]
- 全局解析守卫 [afterEach]
- 路由独享的守卫 [beforeEnter]
- 组件内的守卫
- beforeRouteEnter
- beforeRouteUpdate
- beforeRouteLeave
详细介绍参见此处
路由守卫的入口
'/src/history/base.js'
- confirmTransition 函数调用了执行钩子函数队列的方法,声明队列并没有 afterEach,afterEach 钩子其实在 onComplete 函数体中,调用了 onComplete 其实就执行了 afterEach 钩子数组。
/**
* 确认跳转
* 完成钩子函数的调用
* @param route 路由
* @param onComplete 完成回调函数
* @param onAbort 中止回调函数【出错的回调】
* */
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
...
// 这里是路由钩子部分
// 解构 updated 、 deactivated 、 activated 用以抽取钩子函数
const { updated, deactivated, activated } = resolveQueue(
this.current.matched,
route.matched
)
// 定义路由钩子队列数组
const queue: Array<?NavigationGuard> = [].concat(
// in-component leave guards
// 失效组件的beforeRouterLeave
extractLeaveGuards(deactivated),
// global before hooks
// beforeEach 钩子
this.router.beforeHooks,
// in-component update hooks
// 重用的组件beforeRouteUpdate
extractUpdateHooks(updated),
// in-config enter guards
// 路由配置的beforeRouteEnter
activated.map(m => m.beforeEnter),
// async components
// 路由组件懒加载
resolveAsyncComponents(activated)
)
/**
* 迭代器函数
* 执行钩子函数队列数组的钩子函数 例如 beforeHooks [fn(to,form,next)=>{...}]
* @param {*} hook hook 函数 例如 beforeEach 函数
* @param {*} next next 回调用以执行 queue 的下一项
*/
const iterator = (hook: NavigationGuard, next) => {
if (this.pending !== route) {
return abort(createNavigationCancelledError(current, route))
}
try {
hook(route, current, (to: any) => {
if (to === false) {
// next(false) -> abort navigation, ensure current URL
this.ensureURL(true)
abort(createNavigationAbortedError(current, route))
} else if (isError(to)) {
this.ensureURL(true)
abort(to)
} else if (
typeof to === 'string' ||
(typeof to === 'object' &&
(typeof to.path === 'string' || typeof to.name === 'string'))
) {
// next('/') or next({ path: '/' }) -> redirect
abort(createNavigationRedirectedError(current, route))
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
// confirm transition and pass on the value
// 这里传值,在 runQueue 方法里并未接受和使用
next(to)
}
})
} catch (e) {
abort(e)
}
}
runQueue(queue, iterator, () => {
// wait until async components are resolved before
// extracting in-component enter guards
const enterGuards = extractEnterGuards(activated)
const queue = enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, () => {
if (this.pending !== route) {
return abort(createNavigationCancelledError(current, route))
}
this.pending = null
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
handleRouteEntered(route)
})
}
})
})
}
runQueue
/src/util/async.js
/* @flow */
/**
* 执行钩子队列
* 通过 step 函数,按顺序调用 queue 队列的函数
* @param {*} queue 存放路由钩子函数的队列数组
* @param {*} fn iterator 函数
* @param {*} cb 回调函数
*/
export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
const step = index => {
if (index >= queue.length) {
cb()
} else {
if (queue[index]) {
fn(queue[index], () => {
step(index + 1)
})
} else {
step(index + 1)
}
}
}
step(0)
}
extractGuards
/src/history/base.js
- extractLeaveGuards 、 extractUpdateHooks 、extractEnterGuards 都是调用 extractGuards 提取守卫方法的
- extractGuards 内部又是调用 extractGuard 方法来提取组件声明的路由钩子
/**
* 提起路由守卫钩子数组
* @param {*} records 路由匹配记录
* @param {*} name 钩子函数名称
* @param {*} bind bindEnterGuard 函数
* @param {*} reverse
*/
function extractGuards (
records: Array<RouteRecord>,
name: string,
bind: Function,
reverse?: boolean
): Array<?Function> {
const guards = flatMapComponents(records, (def, instance, match, key) => {
// def 路由组件 Vue 实例
// match 符合路径的匹配项
// key 路由组件【 router-link 】最后都是当匿名插槽处理所以 key 默认为 default
// 获取路由钩子函数
debugger
const guard = extractGuard(def, name)
if (guard) {// 钩子函数存在
return Array.isArray(guard)
? guard.map(guard => bind(guard, instance, match, key))
: bind(guard, instance, match, key)
}
})
return flatten(reverse ? guards.reverse() : guards)
}
extractGuard
/src/history/base.js
/**
* 提取路由钩子
* 若传递的组件不是一个函数,就使用 Vue.extend 方法拓展为组件
* 从组件 option 将对应的函数返回
* @param {*} def 路由组件 => { template:'<div>Home</div>',data:{...},beforeRouteLeave:()=>{...} }
* @param {*} key 钩子函数名称 => beforeRouteLeave
*/
function extractGuard (
def: Object | Function,
key: string
): NavigationGuard | Array<NavigationGuard> {
if (typeof def !== 'function') {
// extend now so that global mixins are applied.
def = _Vue.extend(def)
}
return def.options[key]
}
flatMapComponents
/src/util/resolve-components.js
/**
* 处理匹配项的路由信息将其作为参数传给 fn 回调
* @param {*} matched 路由匹配项
* @param {*} fn 回调函数
*/
export function flatMapComponents (
matched: Array<RouteRecord>,
fn: Function
): Array<?Function> {
return flatten(matched.map(m => {
return Object.keys(m.components).map(key => fn(
m.components[key],
m.instances[key],
m, key
))
}))
}
bindEnterGuard
/src/history/base.js
/**
* 绑定进入守卫
* 最后都是挂载到 matched.enteredCbs:{default:[fn(){...},...]}
* 这里的 fn 都是 beforeRouteEnter 的 next 回调
* @param {*} guard 守卫
* @param {*} match 匹配记录
* @param {*} key
*/
function bindEnterGuard(
guard: NavigationGuard,
match: RouteRecord,
key: string
): NavigationGuard {
return function routeEnterGuard(to, from, next) {
return guard(to, from, cb => {
if (typeof cb === 'function') {
if (!match.enteredCbs[key]) {
match.enteredCbs[key] = []
}
match.enteredCbs[key].push(cb)
}
next(cb)
})
}
}
router-link
/src/components/link.js
- router-link 是一个 name 为 router-link 的组件,组件内容都被当作插槽处理了,默认渲染为 a 标签,且取消了 a 标签的默认跳转功能。
export default {
name: 'RouterLink',
props: {
// to 目标路径
to: {
type: toTypes,
required: true
},
// 标签名
tag: {
type: String,
default: 'a'
},
custom: Boolean,
exact: Boolean,
exactPath: Boolean,
append: Boolean,
replace: Boolean,
activeClass: String,
exactActiveClass: String,
ariaCurrentValue: {
type: String,
default: 'page'
},
event: {
type: eventTypes,
default: 'click'
}
},
render(h: Function) {
// 获取 router 实例
const router = this.$router
// 获取当前路由
const current = this.$route
const { location, route, href } = router.resolve(
this.to,
current,
this.append
)
// 声明 classes 类
const classes = {}
// 路由样式动态变化
const globalActiveClass = router.options.linkActiveClass
const globalExactActiveClass = router.options.linkExactActiveClass
// Support global empty active class
const activeClassFallback =
globalActiveClass == null ? 'router-link-active' : globalActiveClass
const exactActiveClassFallback =
globalExactActiveClass == null
? 'router-link-exact-active'
: globalExactActiveClass
const activeClass =
this.activeClass == null ? activeClassFallback : this.activeClass
const exactActiveClass =
this.exactActiveClass == null
? exactActiveClassFallback
: this.exactActiveClass
// 是否重定向 是就创建新路由 否则返回结构的路由
const compareTarget = route.redirectedFrom
? createRoute(null, normalizeLocation(route.redirectedFrom), null, router)
: route
classes[exactActiveClass] = isSameRoute(
current,
compareTarget,
this.exactPath
)
classes[activeClass] =
this.exact || this.exactPath
? classes[exactActiveClass]
: isIncludedRoute(current, compareTarget)
const ariaCurrentValue = classes[exactActiveClass]
? this.ariaCurrentValue
: null
// 处理事件
const handler = e => {
if (guardEvent(e)) {
if (this.replace) {
router.replace(location, noop)
} else {
router.push(location, noop)
}
}
}
// 声明 on 对象 【 事件 】
const on = { click: guardEvent }
if (Array.isArray(this.event)) {
this.event.forEach(e => {
on[e] = handler
})
} else {
on[this.event] = handler
}
const data: any = { class: classes }
// 默认作为插槽处理
const scopedSlot =
!this.$scopedSlots.$hasNormal &&
this.$scopedSlots.default &&
this.$scopedSlots.default({
href,
route,
navigate: handler,
isActive: classes[activeClass],
isExactActive: classes[exactActiveClass]
})
// 异常处理部分
if (scopedSlot) {
if (process.env.NODE_ENV !== 'production' && !this.custom) {
!warnedCustomSlot &&
warn(
false,
'In Vue Router 4, the v-slot API will by default wrap its content with an <a> element. Use the custom prop to remove this warning:\n<router-link v-slot="{ navigate, href }" custom></router-link>\n'
)
warnedCustomSlot = true
}
if (scopedSlot.length === 1) {
return scopedSlot[0]
} else if (scopedSlot.length > 1 || !scopedSlot.length) {
if (process.env.NODE_ENV !== 'production') {
warn(
false,
`<router-link> with to="${this.to}" is trying to use a scoped slot but it didn't provide exactly one child. Wrapping the content with a span element.`
)
}
return scopedSlot.length === 0 ? h() : h('span', {}, scopedSlot)
}
}
if (process.env.NODE_ENV !== 'production') {
if ('tag' in this.$options.propsData && !warnedTagProp) {
warn(
false,
`<router-link>'s tag prop is deprecated and has been removed in Vue Router 4. Use the v-slot API to remove this warning: https://next.router.vuejs.org/guide/migration/#removal-of-event-and-tag-props-in-router-link.`
)
warnedTagProp = true
}
if ('event' in this.$options.propsData && !warnedEventProp) {
warn(
false,
`<router-link>'s event prop is deprecated and has been removed in Vue Router 4. Use the v-slot API to remove this warning: https://next.router.vuejs.org/guide/migration/#removal-of-event-and-tag-props-in-router-link.`
)
warnedEventProp = true
}
}
// 插槽内容默认作为 a 标签
if (this.tag === 'a') {
data.on = on
data.attrs = { href, 'aria-current': ariaCurrentValue }
} else {
// find the first <a> child and apply listener and href
// 找到子元素的第一个 a 标签 监听
const a = findAnchor(this.$slots.default)
if (a) { // 若果 a 标签存在
// in case the <a> is a static node
a.isStatic = false
const aData = (a.data = extend({}, a.data))
aData.on = aData.on || {}
// transform existing events in both objects into arrays so we can push later
for (const event in aData.on) {
const handler = aData.on[event]
if (event in on) {
aData.on[event] = Array.isArray(handler) ? handler : [handler]
}
}
// append new listeners for router-link
for (const event in on) {
if (event in aData.on) {
// on[event] is always a function
aData.on[event].push(on[event])
} else {
aData.on[event] = handler
}
}
const aAttrs = (a.data.attrs = extend({}, a.data.attrs))
aAttrs.href = href
aAttrs['aria-current'] = ariaCurrentValue
} else {
// doesn't have <a> child, apply listener to self
// 没有 a 标签的元素就监听自己
data.on = on
}
}
return h(this.tag, data, this.$slots.default)
}
}
router-view
/src/components/view.js
- router-view 是一个 Vue 函数式组件,在内部重写了 data.hook.init/prepatch 方法,在 patch 阶段会调用【 Vue(2.x) 源码 createComponent installComponentHooks 方法】
- data.hook.init 是为了执行 beforeRouterEnter 的回调函数
export default {
name: 'RouterView',
functional: true,
props: {
// 名称
name: {
type: String,
default: 'default'
}
},
render(_, { props, children, parent, data }) {
// 从 context 结构的 { props, children, parent, data }
// used by devtools to display a router-view badge
// 将 routerView 置为 true 标记当前组件是 router-view
data.routerView = true
// directly use parent context's createElement() function
// so that components rendered by router-view can resolve named slots
// 使用父上下文的createElement()函数
const h = parent.$createElement
const name = props.name
const route = parent.$route
const cache = parent._routerViewCache || (parent._routerViewCache = {})
// determine current view depth, also check to see if the tree
// has been toggled inactive but kept-alive.
// 定义一个深度
let depth = 0
// 不活动
let inactive = false
// 通过 parent 属性 判断当前节点是否为最顶层节点
while (parent && parent._routerRoot !== parent) {
const vnodeData = parent.$vnode ? parent.$vnode.data : {}
if (vnodeData.routerView) { // 且是 router-view 组件 深度 ++
depth++
}
if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
inactive = true
}
parent = parent.$parent
}
data.routerViewDepth = depth
// render previous view if the tree is inactive and kept-alive
if (inactive) { // 若是不活动状态 渲染上一个路由
const cachedData = cache[name]
const cachedComponent = cachedData && cachedData.component
if (cachedComponent) { // 缓存的组件
// #2301
// pass props
if (cachedData.configProps) {
fillPropsinData(
cachedComponent,
data,
cachedData.route,
cachedData.configProps
)
}
// 渲染缓存的路由组件
return h(cachedComponent, data, children)
} else {
// render previous empty view
// 渲染一个空组件
return h()
}
}
// 获取匹配项
const matched = route.matched[depth]
// 获取匹配的组件
const component = matched && matched.components[name]
// render empty node if no matched route or no config component
// 若 matched 和 component 都不存在 则渲染空
if (!matched || !component) {
cache[name] = null
return h()
}
// cache component
// 缓存的组件
cache[name] = { component }
// attach instance registration hook
// this will be called in the instance's injected lifecycle hooks
// 注册路由组件实例 => Vue 实例非 VueRouter
data.registerRouteInstance = (vm, val) => {
// val could be undefined for unregistration
const current = matched.instances[name]
if ((val && current !== vm) || (!val && current === vm)) {
matched.instances[name] = val
}
}
// also register instance in prepatch hook
// in case the same component instance is reused across different routes
// Vue 在注册普通组件时会在组件的 data.hook 上 添加 init、prepatch、insert、destroy,在组件的 patch 阶段会被调用,而在函数式组件没有这个操作,这里就是为当前 router-view 也注册了 prepatch,init 方法,在 Vue patch 阶段会调用
;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
matched.instances[name] = vnode.componentInstance
}
// register instance in init hook
// in case kept-alive component be actived when routes changed
data.hook.init = vnode => {
if (
vnode.data.keepAlive &&
vnode.componentInstance &&
vnode.componentInstance !== matched.instances[name]
) {
matched.instances[name] = vnode.componentInstance
}
// if the route transition has already been confirmed then we weren't
// able to call the cbs during confirmation as the component was not
// registered yet, so we call it here.
handleRouteEntered(route)
}
const configProps = matched.props && matched.props[name]
// save route and configProps in cache
if (configProps) {
extend(cache[name], {
route,
configProps
})
fillPropsinData(component, data, route, configProps)
}
return h(component, data, children)
}
}
PS
- 代码仓库地址觉得对你有帮助记得