vue-router4导航守卫源码解析

1,015 阅读11分钟

vue-router导航守卫

这篇文章我希望讲诉的是

  • vue路由导航守卫是如何被执行的,导航的解析流程
  • vue-router3和vue-router4的导航守卫的实现原理

官方文档上解释了完整的导航解析流程

  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 的回调函数,创建好的组件实例会作为回调函数的参数传入。

vue-router3导航守卫

全局前置守卫

根据官方文档我们可以通过下面代码所示注册全局前置守卫

const router = new VueRouter({ ... })

router.beforeEach((to, from, next) => {
  // ...
})

router.beforeResolve((to, from, next) => {
  // ...
})

router.afterEach((to, from) => {
  // ...
})

全局前置守卫在源码中的实现,从源码里我们可以看到VueRouter类内部创建了beforeHooks,resolveHooks,afterHooks这3个数组变量, 类对外提供了beforeEach,beforeResolve,afterEach三个方法,实际上都是通过调用registerHook来注册全局前置钩子

// 文件位置 vue-router/src/index.js

export default class VueRouter {
    ...省略其余源码...
    beforeHooks: Array<?NavigationGuard>
    resolveHooks: Array<?NavigationGuard>
    afterHooks: Array<?AfterNavigationHook>
    ...省略其余源码...
    constructor (options: RouterOptions = {}) {
        ...省略其余源码...
        this.beforeHooks = []
        this.resolveHooks = []
        this.afterHooks = []
        ...省略其余源码...
    }
    ...省略其余源码...
    /*
    * beforeHooks VueRouter内置属性,前置守卫队列
    * fn 前置守卫函数
    */
    beforeEach (fn: Function): Function {
        return registerHook(this.beforeHooks, fn)
    }

    beforeResolve (fn: Function): Function {
        return registerHook(this.resolveHooks, fn)
    }

    afterEach (fn: Function): Function {
        return registerHook(this.afterHooks, fn)
    }
    ...省略其余源码...
}

registerHook的实现也很简单,就是把传入的导航守卫fn,push到内部hooks队列中,顺带帮你去重,ok,到这里全局前置钩子注册完了

function registerHook (list: Array<any>, fn: Function): Function {
  list.push(fn)
  return () => {
    const i = list.indexOf(fn)
    if (i > -1) list.splice(i, 1)
  }
}

路由独享的守卫

路由配置上直接定义 beforeEnter 守卫

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

路由独享的守卫在源码中的实现,执行new VueRouter,通过createMatcher对路由配置进行分析处理,在createMatcher中通过调用createRouteMap对传入的routes配置进行递归遍历, 在addRouteRecord方法中将传入的routes解析成record:RouteRecord对象,后面我们切换路由渲染视图就是通过这个对象,beforeEnter也是在这里处理完毕

// 文件位置 vue-router/src/index.js

export default class VueRouter {
    ...省略其余源码...
    this.matcher = createMatcher(options.routes || [], this)
    ...省略其余源码...
}

// 文件位置 vue-router/src/create-matcher.js

export function createMatcher (
    routes: Array<RouteConfig>,
    router: VueRouter
): Matcher {
    const { pathList, pathMap, nameMap } = createRouteMap(routes)
    ...省略其余源码...
    return {
        match,
        addRoutes
    }
}

// 文件位置 vue-router/src/create-route-map.js

export function createRouteMap (
  routes: Array<RouteConfig>,
  oldPathList?: Array<string>,
  oldPathMap?: Dictionary<RouteRecord>,
  oldNameMap?: Dictionary<RouteRecord>
): {
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {
    ...省略其余源码...
    routes.forEach(route => {
        // 这里通过递归执行addRouteRecord,把routes树创建,返回pathList,pathMap,nameMap
        addRouteRecord(pathList, pathMap, nameMap, route)
    })
    ...省略其余源码...
    return {
        pathList,
        pathMap,
        nameMap
    }
}

function addRouteRecord (
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>,
  route: RouteConfig,
  parent?: RouteRecord,
  matchAs?: string
) {
    ...省略其余源码...
    const record: RouteRecord = {
        path: normalizedPath,
        regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
        components: route.components || { default: route.component },
        instances: {},
        enteredCbs: {},
        name,
        parent,
        matchAs,
        redirect: route.redirect,
        beforeEnter: route.beforeEnter,
        meta: route.meta || {},
        props:
        route.props == null
            ? {}
            : route.components
            ? route.props
            : { default: route.props }
    }
    // 通过递归把所有的children子节点都解析出来
    if (route.children) {
        ...省略其余源码...
        route.children.forEach(child => {
        const childMatchAs = matchAs
            ? cleanPath(`${matchAs}/${child.path}`)
            : undefined
        addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
        })
    }
    ...省略其余源码...
}

组件内的守卫

beforeRouteLeave,beforeRouteUpdate,beforeRouteEnter这3个方法是定义在VueRouter的静态方法install中的,我们知道Vue提供了全局插件机制, 通过全局方法Vue.use()使用插件,Vue.js 的插件应该暴露一个install方法。这个方法的第一个参数是Vue构造器,第二个参数是一个可选的选项对象。

回顾一下Vue.use

// 文件位置 vue/src/core/global-api/use.js

export function initUse (Vue: GlobalAPI) {
    Vue.use = function (plugin: Function | Object) {
        const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
        // 避免重复注册插件
        if (installedPlugins.indexOf(plugin) > -1) {
            return this
        }
        // additional parameters
        const args = toArray(arguments, 1)
        args.unshift(this)
        // 尝试判断插件是否提供了install方法
        if (typeof plugin.install === 'function') {
            // 插件对外暴露的install方法
            plugin.install.apply(plugin, args)
        } else if (typeof plugin === 'function') {
            // 如果没有install方法,则尝试判断插件是否是一个function
            plugin.apply(null, args)
        }
        installedPlugins.push(plugin)
        return this
    }
}

vue-router的install方法

// 文件位置 vue-router/src/install.js

export function install (Vue) {
    ...省略其余源码...
    Vue.mixin({
        beforeCreate () {
            // 通过mixin全局混入VueRouter实例
            if (isDef(this.$options.router)) {
                this._routerRoot = this
                this._router = this.$options.router
                this._router.init(this)
                // 把_route变成响应式对象
                Vue.util.defineReactive(this, '_route', this._router.history.current)
            } else {
                this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
            }
            registerInstance(this, this)
        },
        destroyed () {
            registerInstance(this)
        }
    })
    ...省略其余源码...
    // Hooks and props are merged as arrays 组件内守卫会被存在一个数组中
    strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

/**
* 这里是一个策略模式
* Option merge strategies (used in core/util/options)
* optionMergeStrategies: Object.create(null)
*/
    const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks

// 文件位置 vue/src/core/util/options.js
    LIFECYCLE_HOOKS.forEach(hook => {
        strats[hook] = mergeHook
    })
    function mergeHook (
        parentVal: ?Array<Function>,
        childVal: ?Function | ?Array<Function>
    ): ?Array<Function> {
        const res = childVal
            ? parentVal
            ? parentVal.concat(childVal)
            : Array.isArray(childVal)
                ? childVal
                : [childVal]
            : parentVal
        return res
            ? dedupeHooks(res)
            : res
    }

执行路由守卫

我们在切换视图是通常会调用push或者replace来渲染页面,而push和replace其实都是通过调用transitionTo来完成,history路由和hash路由都是继承了base.js中 的transitionTo,说了这么多,直接讲核心,导航守卫执行其实就是首先构造一个队列queue,它实际上是一个数组,然后再定义一个迭代器函数iterator,最后再执行runQueue方法来执行这个队列

在切换导航之前,通过resolveQueue,比较当前current和next,去较长的为max,直到current[i] !== next[i],next的i后面的就是激活的路由了,next的0-i之间就是更新的路由, current的i后面就是失活路由

// 文件位置 vue-router/src/history/base.js

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 current = [1, 2, 3]
const next = [1, 2, 3, 4]
resolveQueue(current, next)
{
    activated: [4], // 当前激活的
    deactivated: [], // 失活的为空
    updated: [1, 2, 3] // 更新的
}

const current = [1, 2, 3, 4]
const next = [1, 2, 3]
resolveQueue(current, next)
{
    activated: [], // 当前激活的
    deactivated: [4], // 失活的为空
    updated: [1, 2, 3] // 更新的
}

构建导航守卫队列queue,通过执行extractLeaveGuards,extractUpdateHooks获取路由离开,更新的钩子函数,实际上都是通过调用extractGuards函数, 我们之前定义的record:RouteRecord对象在这里被初始化,实际上是调用了Vue.extend(def),拿到options[key],即组件内守卫

  1. 失活组件的beforeRouteLeave
  2. 全局的beforeEach
  3. 重用更新的beforeRouteUpdate
  4. 组件配置里的beforeEnter
  5. 解析异步组件
const queue: Array<?NavigationGuard> = [].concat(
    // in-component leave guards
    extractLeaveGuards(deactivated),
    // global before hooks
    this.router.beforeHooks,
    // in-component update hooks
    extractUpdateHooks(updated),
    // in-config enter guards
    activated.map(m => m.beforeEnter),
    // async components
    resolveAsyncComponents(activated)
)

这里讲一下解析beofreRouteLeave

  1. 传入失活的record对象,调用extractGuards
  2. extractGuards中执行flatMapComponents,重点还是在这个回调函数中
  3. 回调函数执行extractGuard,其实就是调用Vue.extend创建组件实例,从options中获取守卫钩子
  4. 如果钩子返回是一个数组,处理一下,执行bindGuard,指向组件实例
// 传入失活的record对象,调用extractGuards
function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> {
  return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}
// 
function extractGuards (
    records: Array<RouteRecord>,
    name: string,
    bind: Function,
    reverse?: boolean
): Array<?Function> {
    const guards = flatMapComponents(records, (def, instance, match, key) => {
        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)
}

定义迭代器函数,迭代器函数传入的是第一个参数就是我们在路由配置里写的守卫函数,迭代器的任务就是去执行每一个导航守卫hook,并传入route、current和匿名函数, 这些参数对应文档中的to、from、next

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
                next(to)
            }
        })
    } catch (e) {
        abort(e)
    }
}

runQueue

单独讲讲runQueue,vue-router源码里一共执行了2次runQueue,第一次是执行步骤1-6,爹日次是执行步骤7-12

runQueue执行守卫队列

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函数内部定义了一个step方法,通过计数器index一次执行守卫队列,fn(iterator)的回调就是下一个step

// 文件位置 vue-router/src/util/async.js
/**
* queue 一个 NavigationGuard 类型的数组
* fn 是我们传入的迭代器方法
* 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)
}

vue-router4导航守卫

全局前置守卫

我们知道vue-router4创建一个路由是通过createRouter,它没有通过class类函数来创建router对象,而是通过createRouter返回了router, router上包含了beforeEach,beforeResolve,afterEach,看起来跟vue-router3没什么区别

import { createRouter, createWebHashHistory } from 'vue-router'

const router = createRouter({
    history: createWebHashHistory(),
    routes: [
        //...
    ],
})

// 文件位置 vue-router-next/src/router.ts

export function createRouter(options: RouterOptions): Router {
    ...省略其余源码...
    const beforeGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
    const beforeResolveGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
    const afterGuards = useCallbacks<NavigationHookAfter>()
    ...省略其余源码...
    const router: Router = {
        ...省略其余源码...
        beforeEach: beforeGuards.add,
        beforeResolve: beforeResolveGuards.add,
        afterEach: afterGuards.add,
        ...省略其余源码...
    }
    return router
}

> useCallbacks和registerHook异曲同工,这是一个典型的完美的闭包操作

// 文件位置 vue-router-next/src/utils/callbacks.ts
export function useCallbacks<T>() {
    let handlers: T[] = []

    function add(handler: T): () => void {
        handlers.push(handler)
        return () => {
            const i = handlers.indexOf(handler)
            if (i > -1) handlers.splice(i, 1)
        }
    }

    function reset() {
        handlers = []
    }

    return {
        add,
        list: () => handlers,
        reset,
    }
}

路由独享的守卫

vue-router3的路由独享守卫是在执行createRouter时,源码第一行调用createRouterMatcher中配置

export function createRouter(options: RouterOptions): Router {
  const matcher = createRouterMatcher(options.routes, options)
  ...省略其余源码...
}

createRouterMatcher,将我们传入到routes参数通过递归调用addRoute,将routes转化成我们要的路由对象,返回addRoute, resolve, removeRoute, getRoutes, getRecordMatcher

// 文件位置 vue-router-next/src/matcher/index.ts
export function createRouterMatcher(
  routes: RouteRecordRaw[],
  globalOptions: PathParserOptions
): RouterMatcher {
    ...省略其余源码...
    function getRecordMatcher () {}
    function addRoute(
        record: RouteRecordRaw,
        parent?: RouteRecordMatcher,
        originalRecord?: RouteRecordMatcher
    ) {
        // used later on to remove by name
        let isRootAdd = !originalRecord
        let mainNormalizedRecord = normalizeRouteRecord(record)
        ...省略其余源码...
    }
    function removeRoute () {}
    function getRoutes () {}
    function insertMatcher () {}
    function resolve () {}
    // add initial routes
    routes.forEach(route => addRoute(route))

    return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }
}

这里通过normalizeRouteRecord创建的RouteRecordNormalized即和vue-router3中的RouteRecord是相同的道理

export function normalizeRouteRecord(
    record: RouteRecordRaw
    ): RouteRecordNormalized {
    return {
        path: record.path,
        redirect: record.redirect,
        name: record.name,
        meta: record.meta || {},
        aliasOf: undefined,
        beforeEnter: record.beforeEnter,
        props: normalizeRecordProps(record),
        children: record.children || [],
        instances: {},
        leaveGuards: new Set(),
        updateGuards: new Set(),
        enterCallbacks: {},
        components:
        'components' in record
            ? record.components || {}
            : { default: record.component! },
    }
}

组件内的守卫

vue-router4提供了2个Composition API functions,同时页兼容了options配置式开发

import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

export default {
    setup() {
        onBeforeRouteLeave((to, from) => {
            ...
        })
        onBeforeRouteUpdate(async (to, from) => {
            ...
        })
    }
}

源码很清晰,通过调用registerGuard来注册,方法很呆萌,record[name].add(guard),这是一个Set类型

// 文件位置 vue-router-next/src/navigationGuards.ts
export function onBeforeRouteLeave(leaveGuard: NavigationGuard) {
    ...省略其余源码...
    registerGuard(activeRecord, 'leaveGuards', leaveGuard)
}

export function onBeforeRouteUpdate(updateGuard: NavigationGuard) {
  ...省略其余源码...
  registerGuard(activeRecord, 'updateGuards', updateGuard)
}

function registerGuard(
    record: RouteRecordNormalized,
    name: 'leaveGuards' | 'updateGuards',
    guard: NavigationGuard
) {
    const removeFromList = () => {
        record[name].delete(guard)
    }

    onUnmounted(removeFromList)
    onDeactivated(removeFromList)

    onActivated(() => {
        record[name].add(guard)
    })

    record[name].add(guard)
}

执行路由守卫

vue-router4切换视图时执行push或者replace,都会调用pushWithRedirect,导航守卫在navigate函数中被执行,先是执行extractChangingRecords,解析出失活records对象, 更新的records对象,激活的records对象,实现原理跟我们上面在讲vue-router3时相同,具体看源码

function navigate(
    to: RouteLocationNormalized,
    from: RouteLocationNormalizedLoaded
): Promise<any> {
    let guards: Lazy<any>[]

    const [
        leavingRecords,
        updatingRecords,
        enteringRecords,
    ] = extractChangingRecords(to, from)
    ...省略其余源码...
}

function extractChangingRecords(
    to: RouteLocationNormalized,
    from: RouteLocationNormalizedLoaded
) {
    // 创建3个对象分别存放RouteRecordNormalized
    const leavingRecords: RouteRecordNormalized[] = []
    const updatingRecords: RouteRecordNormalized[] = []
    const enteringRecords: RouteRecordNormalized[] = []

    // 获取from,to较长的一个作为len
    const len = Math.max(from.matched.length, to.matched.length)
    for (let i = 0; i < len; i++) {
        const recordFrom = from.matched[i]
        if (recordFrom) {
            if (to.matched.indexOf(recordFrom) < 0) leavingRecords.push(recordFrom)
            else updatingRecords.push(recordFrom)
        }
        const recordTo = to.matched[i]
        if (recordTo) {
            // the type doesn't matter because we are comparing per reference
            if (from.matched.indexOf(recordTo as any) < 0)
                enteringRecords.push(recordTo)
        }
    }
    return [leavingRecords, updatingRecords, enteringRecords]
}

extractComponentsGuards处理beforeRouterLeave,并进行promise化

guards = extractComponentsGuards(
    leavingRecords.reverse(),
    'beforeRouteLeave',
    to,
    from
)

// leavingRecords is already reversed
for (const record of leavingRecords) {
    record.leaveGuards.forEach(guard => {
        guards.push(guardToPromiseFn(guard, to, from))
    })
}
...省略其余源码...
runGuardQueue(guards)
    .then(() => {
        ...
    })

vue-router3的导航守卫支持promise操作,核心就是let guardCall = Promise.resolve(guardReturn).then(next),next接受boolean | RouteLocationRaw | NavigationGuardNextCallback | Error,分别做处理

// 文件位置 vue-router-next/src/navigationGuards.ts
export function guardToPromiseFn(
    guard: NavigationGuard,
    to: RouteLocationNormalized,
    from: RouteLocationNormalizedLoaded,
    record?: RouteRecordNormalized,
    name?: string
): () => Promise<void> {
    ...省略其余源码...
    return () =>
        new Promise((resolve, reject) => {
        const next: NavigationGuardNext = (
            valid?: boolean | RouteLocationRaw | NavigationGuardNextCallback | Error
        ) => {
            if (valid === false)
                reject(
                    createRouterError<NavigationFailure>(
                    ErrorTypes.NAVIGATION_ABORTED,
                    {
                        from,
                        to,
                    }
                    )
                )
            else if (valid instanceof Error) {
                reject(valid)
            } else if (isRouteLocation(valid)) {
                reject(
                    createRouterError<NavigationRedirectError>(
                    ErrorTypes.NAVIGATION_GUARD_REDIRECT,
                    {
                        from: to,
                        to: valid,
                    }
                    )
                )
            } else {
                if (
                    enterCallbackArray &&
                    // since enterCallbackArray is truthy, both record and name also are
                    record!.enterCallbacks[name!] === enterCallbackArray &&
                    typeof valid === 'function'
                )
                    enterCallbackArray.push(valid)
                resolve()
            }
        }

        // wrapping with Promise.resolve allows it to work with both async and sync guards
        const guardReturn = guard.call(
                record && record.instances[name!],
                to,
                from,
                __DEV__ ? canOnlyBeCalledOnce(next, to, from) : next
        )
        let guardCall = Promise.resolve(guardReturn)

        if (guard.length < 3) guardCall = guardCall.then(next)
        ...省略其余源码...
        guardCall.catch(err => reject(err))
    })
}

runGuardQueue

guards队列,通过数组的reduce方法,链式调用,这个就是promise异步串行

function runGuardQueue(guards: Lazy<any>[]): Promise<void> {
    return guards.reduce(
        (promise, guard) => promise.then(() => guard()),
        Promise.resolve()
    )
}

测试下小例子,好了下次面试遇到promise串行的问题可以自信的说,I am Iron Man 了

let s = Date.now();
[
  () => new Promise((r) => {
    setTimeout(() => {
      console.log(1);
      r()
    }, 800)
  }), 
  () => new Promise((r) => {
    setTimeout(() => {
      console.log(2);
      r()
  }, 2000)}), 
  () => new Promise((r) => {
      setTimeout(() => {
        console.log(3);
        r()
  }, 3000)})
].reduce(
  (promise, guard) => promise.then(() => guard()),
  Promise.resolve()
).then(() => {
    console.log(Date.now() - s)
})
// 打印
// 1
// 2
// 3
// 5808