Skip to content

第四章:穿透导航流程与守卫机制

在前几章中,我们深入探讨了 Vue Router 的本质、路由模式和路径匹配机制。现在,让我们深入研究 Vue Router 最核心的功能之一——导航流程与守卫机制。我们将学习导航守卫如何工作、异步流程如何控制,以及如何实现复杂的路由权限控制。

导航流程(Navigation Flow)

Vue Router 的导航流程是一个复杂但有序的过程,涉及多个步骤和守卫函数的执行。理解这个流程对于掌握 Vue Router 至关重要。

完整的导航解析流程

javascript
// 简化的导航流程实现
class NavigationFlow {
  constructor(router) {
    this.router = router
    this.pendingLocation = null
  }
  
  // 导航到指定位置
  async navigate(to, from, options = {}) {
    // 1. 触发导航开始
    const targetLocation = this.router.resolve(to)
    
    // 如果是相同路由且参数相同,直接返回
    if (isSameRouteLocation(from, targetLocation)) {
      return
    }
    
    // 2. 设置待处理导航
    this.pendingLocation = targetLocation
    
    // 3. 执行导航守卫
    try {
      // 全局前置守卫
      await this.runGuards(this.router.beforeEachGuards, targetLocation, from)
      
      // 路由独享守卫
      await this.runRouteGuards(targetLocation, from)
      
      // 组件内守卫
      await this.runComponentGuards(targetLocation, from)
      
      // 全局解析守卫
      await this.runGuards(this.router.beforeResolveGuards, targetLocation, from)
      
      // 4. 确认导航
      this.confirmNavigation(targetLocation, from, options)
      
      // 5. 执行后置守卫
      this.router.afterEachGuards.forEach(guard => {
        guard(targetLocation, from)
      })
      
      // 6. 清除待处理导航
      this.pendingLocation = null
      
      return { redirected: false }
    } catch (error) {
      // 导航被取消或出错
      this.pendingLocation = null
      
      // 执行错误处理
      this.router.errorHandlers.forEach(handler => {
        handler(error)
      })
      
      throw error
    }
  }
  
  // 执行守卫队列
  async runGuards(guards, to, from) {
    for (const guard of guards) {
      const result = await new Promise((resolve, reject) => {
        // 守卫函数可以接受 next 回调或返回 Promise
        const next = (result) => {
          if (result === false) {
            // 中断导航
            reject(new NavigationAborted())
          } else if (typeof result === 'string' || 
                     (typeof result === 'object' && result.path)) {
            // 重定向
            reject(new NavigationRedirected(result))
          } else {
            // 继续导航
            resolve(result)
          }
        }
        
        // 执行守卫
        const guardResult = guard(to, from, next)
        
        // 如果守卫返回 Promise,则等待其完成
        if (guardResult instanceof Promise) {
          guardResult.then(next).catch(reject)
        } else if (guardResult !== undefined) {
          // 如果守卫同步返回结果
          next(guardResult)
        }
        // 如果守卫没有返回任何内容且没有调用 next,等待其调用 next
      })
      
      if (result instanceof NavigationRedirected) {
        throw result
      }
    }
  }
  
  // 执行路由独享守卫
  async runRouteGuards(to, from) {
    const matched = to.matched
    for (const record of matched) {
      if (record.beforeEnter) {
        const guards = Array.isArray(record.beforeEnter) 
          ? record.beforeEnter 
          : [record.beforeEnter]
          
        await this.runGuards(guards, to, from)
      }
    }
  }
  
  // 执行组件内守卫
  async runComponentGuards(to, from) {
    // 这里简化处理,实际实现需要处理组件的异步加载等
    const matched = to.matched
    for (const record of matched) {
      if (record.component && record.component.beforeRouteEnter) {
        await new Promise((resolve, reject) => {
          const next = (result) => {
            if (result === false) {
              reject(new NavigationAborted())
            } else {
              resolve(result)
            }
          }
          
          record.component.beforeRouteEnter(to, from, next)
        })
      }
    }
  }
  
  // 确认导航
  confirmNavigation(to, from, options) {
    // 更新当前路由
    this.router.currentRoute.value = to
    
    // 更新浏览器历史记录
    if (!options.replace) {
      this.router.history.push(to.path, {
        ...to,
        from: from.path
      })
    } else {
      this.router.history.replace(to.path, {
        ...to,
        from: from.path
      })
    }
    
    // 触发响应式更新
    this.router.triggerReactivity()
  }
}

// 导航异常类
class NavigationAborted extends Error {
  constructor() {
    super('Navigation aborted')
    this.name = 'NavigationAborted'
  }
}

class NavigationRedirected extends Error {
  constructor(location) {
    super('Navigation redirected')
    this.name = 'NavigationRedirected'
    this.location = location
  }
}

导航守卫(Guards)

导航守卫是 Vue Router 提供的强大功能,允许我们在路由导航过程中执行各种检查和操作。

beforeEach(to, from, next):全局前置守卫

全局前置守卫在每次导航之前执行,是实现权限控制和路由拦截的主要手段:

javascript
// 全局前置守卫实现
class Router {
  constructor() {
    this.beforeEachGuards = []
  }
  
  // 注册全局前置守卫
  beforeEach(guard) {
    this.beforeEachGuards.push(guard)
    
    // 返回移除守卫的函数
    return () => {
      const index = this.beforeEachGuards.indexOf(guard)
      if (index > -1) {
        this.beforeEachGuards.splice(index, 1)
      }
    }
  }
}

// 使用示例:权限控制
router.beforeEach((to, from, next) => {
  // 检查路由是否需要认证
  if (to.meta.requiresAuth) {
    // 检查用户是否已登录
    if (isAuthenticated()) {
      next() // 允许导航
    } else {
      // 重定向到登录页
      next('/login')
    }
  } else {
    next() // 允许导航
  }
})

// 使用示例:加载状态
router.beforeEach((to, from, next) => {
  // 显示加载指示器
  showLoadingIndicator()
  next()
})

// 使用示例:日志记录
router.beforeEach((to, from, next) => {
  console.log(`Navigating from ${from.path} to ${to.path}`)
  next()
})

异步守卫:如何支持 async 函数?

Vue Router 支持异步守卫,允许执行异步操作(如 API 调用):

javascript
// 异步守卫支持
router.beforeEach(async (to, from, next) => {
  // 异步检查用户权限
  try {
    const user = await fetchUser()
    if (user && hasPermission(to.meta.permission)) {
      next()
    } else {
      next('/unauthorized')
    }
  } catch (error) {
    console.error('Failed to check user permissions:', error)
    next('/error')
  }
})

// 使用 Promise 的守卫
router.beforeEach((to, from, next) => {
  // 返回 Promise
  return fetchUser()
    .then(user => {
      if (user) {
        next()
      } else {
        next('/login')
      }
    })
    .catch(error => {
      next('/error')
    })
})

beforeResolve:在导航确认前执行

beforeResolve 守卫在所有组件内守卫和异步路由组件解析之后,导航被确认之前执行:

javascript
// beforeResolve 守卫
router.beforeResolve(async (to, from, next) => {
  // 在这里可以执行需要确保所有组件都已解析的操作
  try {
    // 数据预取
    await Promise.all(
      to.matched
        .filter(record => record.meta && record.meta.dataFetcher)
        .map(record => record.meta.dataFetcher(to))
    )
    
    next()
  } catch (error) {
    console.error('Data fetching failed:', error)
    next('/error')
  }
})

afterEach(to, from):全局后置钩子

全局后置钩子在导航完成后执行,不接受 next 函数也不会改变导航本身:

javascript
// 全局后置钩子
router.afterEach((to, from) => {
  // 隐藏加载指示器
  hideLoadingIndicator()
  
  // 更新页面标题
  if (to.meta.title) {
    document.title = to.meta.title
  }
  
  // 发送页面浏览分析
  sendPageView(to.path)
  
  // 滚动到顶部
  window.scrollTo(0, 0)
})

组件内守卫

Vue Router 还提供了组件内的守卫函数:

javascript
// 组件内守卫示例
export default {
  name: 'UserProfile',
  
  // 在路由进入之前调用
  async beforeRouteEnter(to, from, next) {
    try {
      // 注意:此时无法访问 this
      const userData = await fetchUserData(to.params.id)
      next(vm => {
        // 通过回调访问组件实例
        vm.userData = userData
      })
    } catch (error) {
      next('/error')
    }
  },
  
  // 在当前路由改变,但是该组件被复用时调用
  async beforeRouteUpdate(to, from, next) {
    // 可以访问 this
    try {
      this.userData = await fetchUserData(to.params.id)
      next()
    } catch (error) {
      next('/error')
    }
  },
  
  // 在导航离开该组件的对应路由时调用
  beforeRouteLeave(to, from, next) {
    // 可以访问 this
    if (this.hasUnsavedChanges) {
      const answer = window.confirm('您有未保存的更改,确定要离开吗?')
      if (answer) {
        next()
      } else {
        next(false)
      }
    } else {
      next()
    }
  }
}

runQueue 与流程控制

Vue Router 内部使用 runQueue 函数来串行执行守卫函数,确保导航流程的正确性。

runQueue(guards, iterator, callback):串行执行守卫函数

javascript
// runQueue 实现
function runQueue(queue, fn, cb) {
  const step = (index) => {
    // 如果所有守卫都已执行完毕
    if (index >= queue.length) {
      cb()
    } else {
      // 执行当前守卫
      fn(queue[index], () => {
        // 递归执行下一个守卫
        step(index + 1)
      })
    }
  }
  
  // 从第一个守卫开始执行
  step(0)
}

// 使用 runQueue 执行守卫
function runGuards(guards, to, from, cb) {
  runQueue(guards, (guard, next) => {
    // 处理守卫执行
    const guardResult = guard(to, from, next)
    
    // 如果守卫返回 Promise
    if (guardResult instanceof Promise) {
      guardResult.then(next).catch(cb)
    } else if (guardResult !== undefined) {
      // 如果守卫同步返回结果
      next()
    }
    // 如果守卫没有返回任何内容,等待其调用 next
  }, cb)
}

如何实现"任意守卫中断则停止后续执行"?

通过在 runQueue 中处理中断信号来实现:

javascript
// 支持中断的 runQueue 实现
function runQueueWithAbort(queue, fn, cb) {
  let aborted = false
  
  const step = (index) => {
    // 如果已被中断,停止执行
    if (aborted) {
      cb(new Error('Navigation aborted'))
      return
    }
    
    // 如果所有守卫都已执行完毕
    if (index >= queue.length) {
      cb()
    } else {
      // 执行当前守卫
      fn(queue[index], (result) => {
        // 检查是否需要中断
        if (result === false) {
          aborted = true
          cb(new Error('Navigation aborted by guard'))
        } else if (typeof result === 'string' || 
                   (typeof result === 'object' && result.path)) {
          // 重定向
          aborted = true
          cb(new NavigationRedirected(result))
        } else {
          // 继续执行下一个守卫
          step(index + 1)
        }
      })
    }
  }
  
  step(0)
}

错误处理:router.onError 如何捕获导航错误?

Vue Router 提供了错误处理机制来捕获导航过程中的错误:

javascript
// 错误处理实现
class Router {
  constructor() {
    this.errorHandlers = []
  }
  
  // 注册错误处理器
  onError(handler) {
    this.errorHandlers.push(handler)
    
    return () => {
      const index = this.errorHandlers.indexOf(handler)
      if (index > -1) {
        this.errorHandlers.splice(index, 1)
      }
    }
  }
  
  // 触发错误处理
  handleError(error) {
    this.errorHandlers.forEach(handler => {
      try {
        handler(error)
      } catch (handlerError) {
        console.error('Error in error handler:', handlerError)
      }
    })
  }
}

// 使用示例
router.onError((error) => {
  if (error.name === 'NavigationAborted') {
    console.log('Navigation was aborted')
  } else if (error.name === 'NavigationRedirected') {
    console.log('Navigation was redirected to:', error.location)
  } else {
    console.error('Navigation error:', error)
    // 显示错误页面
    router.push('/error')
  }
})

小结

在本章中,我们深入探讨了 Vue Router 的导航流程与守卫机制:

  1. 导航流程 - 从导航开始到完成的完整过程
  2. 全局前置守卫 - beforeEach 实现权限控制和路由拦截
  3. 异步守卫支持 - 支持 Promise 和 async/await
  4. 解析守卫 - beforeResolve 在导航确认前执行
  5. 后置钩子 - afterEach 在导航完成后执行
  6. 组件内守卫 - 组件级别的路由控制
  7. 流程控制 - runQueue 实现守卫的串行执行
  8. 错误处理 - router.onError 捕获导航错误

这些机制共同构成了 Vue Router 强大的导航控制能力,使得开发者可以精确控制路由导航的各个方面。

在下一章中,我们将探讨 Vue Router 的生产级功能实现,包括 router-view 组件、router-link 组件、懒加载等。


思考题

  1. 你在项目中如何使用导航守卫实现复杂的权限控制?遇到了哪些挑战?
  2. 对于异步守卫中的错误处理,你通常采用什么策略来确保用户体验?