阐述 Vue Router 源码中导航守卫 (Navigation Guards) 的执行流程,包括 `beforeEach`, `beforeResolve`, `afterEach` 的顺序和作用。

Vue Router 导航守卫:一场路由冒险记

各位靓仔靓女们,晚上好!我是今晚的讲师,咱们来聊聊 Vue Router 里那些神出鬼没的“导航守卫”。它们就像路由世界的保安,负责检查你的通行证,决定你能不能去想去的地方。

准备好了吗?系好安全带,我们开始这场路由冒险记!

1. 导航守卫:路由世界的保安

Vue Router 提供了三种全局导航守卫:

  • beforeEach: 路由跳转前置守卫,路由卫兵。
  • beforeResolve: 路由解析守卫,路由最后的检查员。
  • afterEach: 路由跳转后置守卫,路由记录员。

它们都是函数,接收三个参数:

  • to: 即将要进入的目标路由对象。
  • from: 当前导航正要离开的路由对象。
  • next: 这是一个函数,控制导航的进行。

2. next 函数:路由冒险的通行证

next 函数是导航守卫的核心,它决定了路由跳转的命运。它可以接受以下参数:

  • next(): 允许导航继续。
  • next(false): 中断当前的导航。
  • next(path): 重定向到不同的路由。path 可以是字符串或路由对象。
  • next(error): 中断导航,并将错误传递给 router.onError() 注册的回调。

3. 导航守卫的执行顺序:一场精心策划的路由冒险

导航守卫的执行顺序非常重要,它决定了路由跳转过程中发生的事情。简单的说:

  1. beforeEach: 全局前置守卫,就像在进入路由世界前,必须先经过安检,检查你的身份。
  2. 路由组件内的 beforeRouteLeave 守卫: 离开当前路由组件前,组件内的守卫会检查你是否允许离开。
  3. 路由配置中的 beforeEnter 守卫: 进入目标路由前,路由配置中的守卫会检查你是否允许进入。
  4. 异步路由组件解析: 如果路由组件是异步的,Vue Router 会在这里解析它们。
  5. 路由组件内的 beforeRouteUpdate 守卫: 如果当前路由组件被复用 (比如参数变化),组件内的守卫会被调用。
  6. beforeResolve: 全局解析守卫,就像在最终确认进入路由前,再进行一次检查。
  7. 导航被确认: Vue Router 更新 DOM,渲染新的组件。
  8. 路由组件内的 beforeRouteEnter 守卫: 进入目标路由组件时,组件内的守卫会被调用。
  9. afterEach: 全局后置守卫,就像在路由跳转完成后,记录你的行踪。

可以用一个表格来更清晰地展示:

阶段 守卫 作用
准备阶段 全局 beforeEach 在每次路由导航前被调用,用于检查用户身份验证、权限控制等。
离开阶段 组件内 beforeRouteLeave (即将离开的组件) 在离开当前组件的对应路由时被调用,用于阻止用户离开,例如,提示用户保存未保存的更改。
进入阶段 路由独享 beforeEnter 在进入特定路由时被调用,用于验证用户是否具有访问该路由的权限。
解析阶段 异步路由组件解析 Vue Router 会在这里解析异步路由组件。
更新阶段 组件内 beforeRouteUpdate (已复用的组件) 在当前组件的对应路由被复用时调用,例如,当路由参数发生变化时。
确认阶段 全局 beforeResolve 在所有组件内守卫和异步路由组件被成功解析后被调用,确保所有必要的准备工作都已完成。
渲染阶段 Vue Router 更新 DOM,渲染新的组件。
进入组件阶段 组件内 beforeRouteEnter (即将进入的组件) 在进入新组件的对应路由时被调用,但不能访问组件实例 this,因为组件实例尚未创建。可以通过 next 的回调函数访问组件实例。
完成阶段 全局 afterEach 在每次路由导航完成后被调用,用于记录路由访问日志、发送分析数据等。

4. 源码剖析:拨开云雾见天日

让我们深入 Vue Router 的源码,看看这些导航守卫是如何被调用的。

Vue Router 使用 matcher 对象来匹配路由。匹配成功后,会创建一个 Route 对象,包含路由的信息。

createRouteMap 函数中,会遍历路由配置,并为每个路由创建对应的记录 (record)。这些记录包含了路由的 path、component、name 等信息,以及 beforeEnter 守卫。

// 简化后的 createRouteMap 函数
function createRouteMap (routes: Array<RouteConfig>, oldPathList?: Array<string>, oldPathMap?: Dictionary<RouteRecord>, oldNameMap?: Dictionary<RouteRecord>): {
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {
  const pathList: Array<string> = oldPathList || []
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)

  routes.forEach(route => {
    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 { path, name } = route
  const normalizedPath = normalizePath(path, parent)

  const record: RouteRecord = {
    path: normalizedPath,
    regex: compileRouteRegex(normalizedPath),
    components: route.components || { default: route.component },
    instances: {},
    name,
    parent,
    matchAs,
    redirect: route.redirect,
    beforeEnter: route.beforeEnter, // 重点:保存 beforeEnter 守卫
    meta: route.meta || {},
    props: route.props == null
      ? {}
      : route.components
        ? route.props
        : { default: route.props },
    children: route.children,
    alias: route.alias || []
  }

  // ... 省略其他逻辑
}

transitionTo 函数中,Vue Router 会执行导航守卫。这个函数负责找到匹配的路由,并调用相应的守卫。

// 简化后的 transitionTo 函数
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    let route;
    try {
      route = this.router.match(location, this.current);
    } catch (e) {
      this.errorCbs.forEach(cb => {
        cb(e);
      });
      // Exception should still be thrown
      throw e;
    }
    const prev = this.current;
    this.confirmTransition(
      route,
      () => {
        this.updateRoute(route);
        onComplete && onComplete(route);
        this.ensureURL();

        // fire ready cbs once
        if (!this.ready) {
          this.ready = true;
          this.readyCbs.forEach(cb => {
            cb(route);
          });
        }
      },
      err => {
        if (onAbort) {
          onAbort(err);
        }
        if (err && !this.suppressRedirect) {
          if (isError(err)) {
            warn(false, 'uncaught error during route navigation:');
            console.error(err);
          }
        }
      }
    );
  }

confirmTransition 函数是执行导航守卫的核心。它会创建一个守卫队列,按照一定的顺序将守卫函数添加到队列中,然后依次执行。

// 简化后的 confirmTransition 函数
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) &&
      // in the case the route map has been dynamically updated
      route.matched.length === current.matched.length
    ) {
      this.ensureURL();
      return abort(createNavigationCancelledError(route, current));
    }

    const queue: Array<?NavigationGuard> = [].concat(
      // in-component leave guards
      extractLeaveGuards(this.current.matched),
      // global before hooks
      this.router.beforeHooks,
      // in-component update hooks
      extractUpdateHooks(route.matched, current.matched),
      // in-config enter guards
      route.matched.map(m => m.beforeEnter),
      // async components
      resolveAsyncComponents(route.matched)
    );

    this.router.resolveHooks.forEach(hook => {
        queue.push(hook)
    })

    queue.push((to, from, next) => {
      this.handleRouteEntered(to);
      next()
    })

    runQueue(queue, iterator, () => {
      const resolvedQueue = [].concat(
          // global after hooks
          this.router.afterHooks.map(hook => {
            return (to, from) => {
              hook && hook(to, from)
            }
          })
      )
      runQueue(resolvedQueue, iterator, () => {
        onComplete(route)
      })

    })
  }

extractLeaveGuardsextractUpdateHooks 函数用于提取组件内的守卫。resolveAsyncComponents 函数用于解析异步组件。

runQueue 函数用于依次执行守卫队列中的函数。

const 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)
}

5. 代码示例:实战演练

让我们通过一些代码示例来加深理解。

5.1 beforeEach:身份验证

const router = new VueRouter({
  routes: [
    {
      path: '/home',
      component: Home
    },
    {
      path: '/login',
      component: Login
    },
    {
      path: '/profile',
      component: Profile,
      meta: { requiresAuth: true } // 需要登录才能访问
    }
  ]
})

router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    // 需要登录才能访问
    if (!localStorage.getItem('token')) {
      // 没有登录,跳转到登录页面
      next({
        path: '/login',
        query: { redirect: to.fullPath } // 登录后重定向到目标页面
      })
    } else {
      // 已经登录,允许访问
      next()
    }
  } else {
    // 不需要登录,允许访问
    next()
  }
})

5.2 beforeResolve:数据预取

const router = new VueRouter({
  routes: [
    {
      path: '/post/:id',
      component: Post,
      props: true
    }
  ]
})

router.beforeResolve((to, from, next) => {
  // 在路由跳转前,预取文章数据
  getPost(to.params.id)
    .then(post => {
      to.params.post = post // 将文章数据添加到路由参数中
      next()
    })
    .catch(error => {
      console.error('Failed to fetch post:', error)
      next(false) // 中断导航
    })
})

5.3 afterEach:页面统计

const router = new VueRouter({
  routes: [...]
})

router.afterEach((to, from) => {
  // 记录页面访问日志
  console.log(`Navigated from ${from.fullPath} to ${to.fullPath}`)
  // 发送页面统计数据
  sendPageView(to.fullPath)
})

6. 总结:路由冒险的收获

通过今天的学习,我们了解了 Vue Router 导航守卫的执行流程和作用。

  • beforeEach 用于全局的身份验证、权限控制等。
  • beforeResolve 用于在路由跳转前进行一些最终的检查和数据预取。
  • afterEach 用于记录页面访问日志、发送统计数据等。

掌握这些导航守卫,可以更好地控制路由跳转的行为,为用户提供更流畅的体验。

希望今天的讲座对你有所帮助。 谢谢大家! 祝大家编码愉快,bug 远离!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注