摘要

  • 明确定义什么是导航失败,以及我们如何和在哪里捕获它们。
  • 改变基于 Promise 的 router.push(和 router.replace 的扩展)何时 resolve 和 reject。
  • 使 router.pushrouter.afterEachrouter.onError 一致。

基本范例

router.push

如果有一个未处理的错误或一个错误被传递到 next

  1. // any other navigation guard
  2. router.beforeEach((to, from, next) => {
  3. next(new Error())
  4. // or
  5. throw new Error()
  6. // or
  7. return Promise.reject(new Error())
  8. })

那么 router.push 返回的 promise 是在 reject 状态的:

  1. router.push('/url').catch((err) => {
  2. // ...
  3. })

在所有其他请下,promise 是在 resolve 状态。我们可以通过检查 resolve 后的值知道导航是否失败:

  1. router.push('/dashboard').then((failure) => {
  2. if (failure) {
  3. failure instanceof Error // true
  4. failure.type // NavigationFailure.canceled
  5. }
  6. })

router.afterEach

这是全局性的,相当于 router.push().then()。

router.onError

这也是全局性的,相当于 router.push().catch()。

动机

目前 Vue Router 关于 push 返回 promise 的行为与 router.afterEachrouter.onError 不一致。理想情况下,我们应该能够在全局和本地捕获所有成功和失败的导航,但我们只能在本地做到这一点。

  • onError 只在抛出的错误和 next(new Error()) 中才会触发
  • afterEach 只有在导航时才会被调用
  • 当涉及到 router.pushrouter.afterEach/router.onError 的调用时,重定向的行为应该与导航守卫中的 next("/url") 相同。唯一的区别是,redirect 只会触发被重定向位置的 leave 守卫和其他之前的守卫,而不是原来的位置。

Promise 的 resolve/reject 和 router.afterEachrouter.onError 不一致并且令人困惑。

具体设计

其中一个要点是要能够在全局和本地一致的处理失败的导航:

  • 失败的导航:
    • 触发 router.afterEach
    • router.push 返回 resolve 状态的 Promise
  • 未捕获的错误,next(new Error())
    • 触发 router.onError
    • router.push 返回 reject 状态的 Promise

需要注意的是,这两组没有重叠:如果导航守卫中存在未处理的错误,它将触发 router.onError 以及由 router.push 返回 reject 状态的 Promise,但不会触发 router.afterEach。用 next(false) 取消一个导航不会触发 router.onError,但会触发 router.afterEach

Promise resolve 和 reject 的更改

push 这样的导航方法返回一个 Promise,一旦导航成功或失败,这个 promise 就会变为 resolve。只有在出现未处理的错误时,它才会变为 reject。如果它变为 reject,它也将触发 router.onError

为了区分成功的导航和失败的导航,push 返回的 Promise 将 resolve 一个 undefinedNavigationFailure

  1. import { NavigationFailureType, isNavigationFailure } from 'vue-router'
  2. router.push('/').then((failure) => {
  3. if (failure) {
  4. // Having an Error instance allows us to have a Stacktrace and trace back
  5. // where the navigation was cancelled. This will, in many cases, lead to a
  6. // Navigation Guard and the corresponding `next()` call that cancelled the
  7. // navigation
  8. failure instanceof Error // true
  9. if (isNavigationFailure(failure, NavigationFailureType.canceled)) {
  10. // ...
  11. }
  12. }
  13. })
  14. // using async/await
  15. let failure = await router.push('/')
  16. if (failure) {
  17. // ...
  18. }

通过在导航失败时 Promise 不是 reject,我们避免了未捕获(in promise)的错误,同时仍然保持了检查导航是否失败的可能性。

导航失败

有几个不同的导航错误,能够在你的代码中做出不同的反应

导航失败可能通过一个 type 属性来区分,尽管你不需要直接检查它。所有可能的值都由一个 menu 来保持,即 NavigationFailure

  • aborted:当前导航正在进行时,发生了一个新的导航。
  • cancelled:在一个导航守卫中执行了 next(false)。
  • duplicated:导航到与当前导航相同的位置将取消导航,并且不调用任何导航守卫。

type 属性的基础上,导航失败也暴露了 fromto 属性,与 router.afterEach 一样。

重定向

next('/url') 在一个导航守卫中重定向本身并不是一个导航失败,因为导航仍然在进行,并在某个地方结束。为了检测导航守卫,特别是在 SSR 期间,在 currentRoute 上有一个 redirectedFrom 属性可以访问。

例如:想象一下,当用户没有经过认证时,一个导航守卫会重定向到 /login

  1. router.beforeEach((to, from, next) => {
  2. // redirect to the login page if the target location requires authentication
  3. if (to.meta.requiresAuth && !isAuthenticated) next('/login')
  4. else next()
  5. })

当导航到一个需要认证的位置时,我们可以通过 redirectedFrom 检索到用户试图访问原始位置:

  1. // user is not authenticated
  2. await router.push('/profile/dashboard')
  3. // `redirectedFrom` is a RouteLocationNormalized, like `currentRoute` but we are omitting
  4. // most properties to make the example readable
  5. router.currentRoute // { path: '/login', redirectedFrom: { path: '/profile/dashboard' } }

router.afterEach 的更改

由于 router.afterEach 在导航失败时也会被触发,我们需要一种方法来知道导航是成功还是失败。为了做到这一点,我们引入了一个额外的参数,其中包含了我们可以在已 resolved 的导航中找到的同样的导航失败:

  1. import { NavigationFailureType, isNavigationFailure } from 'vue-router'
  2. router.afterEach((to, from, failure) => {
  3. if (isNavigationFailure(failure)) {
  4. // ...
  5. }
  6. })

区分导航失败的情况

我们可以使用 isNavigationFailure helper,而不是直接检查 type 属性:

  1. import { NavigationFailureType, isNavigationFailure } from 'vue-router'
  2. router.afterEach((to, from, failure) => {
  3. // Any kind of navigation failure
  4. if (isNavigationFailure(failure)) {
  5. // ...
  6. }
  7. // Only duplicated navigations
  8. if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
  9. // ...
  10. }
  11. // Aborted or canceled navigations
  12. if (
  13. isNavigationFailure(
  14. failure,
  15. NavigationFailureType.aborted | NavigationFailureType.canceled
  16. )
  17. ) {
  18. // ...
  19. }
  20. })

缺点

  • 破坏性变化虽然迁移相对简单,在许多情况下,将允许开发人员删除现有代码。

备选方案

N/A

采纳策略

  • 在 vue-router@3 中暴露 NavigationFailureTypeisNavigationFailure,这样就可以将导航错误和普通的错误区分开来。
  • afterEachonError 在导航中失败时不会 reject。任何依赖捕获错误的代码都应该 await promise 的结果。

没有解决的问题

N/A