Code前端首页关于Code前端联系我们

vue-router hooks怎么用?从基础到实战一次讲透

terry 2小时前 阅读数 8 #Vue
文章标签 router hooks

咱做Vue项目时,路由跳转的控制、权限验证、数据预加载这些需求,基本都得靠vue-router hooks(导航守卫)来实现,但刚接触时,很多同学会疑惑:这些钩子分哪几类?怎么在不同场景下用?执行顺序容易搞混咋办?今天就用问答形式,把vue-router hooks从基础到实战的知识点拆明白,解决你开发里的实际问题。

先搞懂:vue-router hooks是啥?不同类型有啥区别?

vue-router hooks 叫“导航守卫”,作用是在路由跳转的不同阶段插入逻辑,跳转前验证权限”“进入组件前加载数据”“离开页面时提示保存”,按作用范围和触发时机,分成三类:

  • 全局钩子:作用于整个路由系统,所有路由跳转都会触发,像 router.beforeEach 这种,适合处理“登录验证”“全局埋点”这类项目级需求。
  • 路由独享钩子:只对单个路由生效,写在路由配置里(beforeEnter),适合“某路由专属的权限验证”“单个页面的数据预加载”。
  • 组件内钩子:写在 .vue 组件的选项里(beforeRouteEnter),和组件生命周期深度绑定,能直接操作组件实例,适合“组件自身的路由交互”(比如表单离开提示、根据路由参数更新数据)。

举个栗子:全局钩子像小区大门的保安,所有住户进出都要查;路由独享像某栋楼的门禁,只拦这栋楼的人;组件内钩子像你家的门,只管你自己进出时的操作~

全局钩子怎么用?能解决哪些实际问题?

全局钩子主要有 beforeEach(跳转前拦截)、beforeResolve(组件解析后拦截)、afterEach(跳转后操作)这几个,每个钩子的场景和用法都很明确:

(1)beforeEach:“跳转前的第一道拦截”

导航触发后,所有其他钩子执行前,它先拦下来,最经典的场景是登录态验证

// router/index.js
router.beforeEach((to, from, next) => {
  const isLogin = localStorage.getItem('token') // 假设token存登录状态
  // 路由元信息meta里,给需要登录的页面加requiresAuth标记
  if (to.meta.requiresAuth && !isLogin) { 
    next('/login') // 没登录?先跳登录页
  } else {
    next() // 正常放行
  }
})

路由配置里给需要权限的页面加标记:

{ 
  path: '/profile', 
  component: Profile, 
  meta: { requiresAuth: true } 
}

除了登录,还能做国际化切换(跳转前改语言)、广告拦截(某些页面强制跳广告页)这些全局逻辑。

(2)beforeResolve:“跳转前的最后一道拦截”

beforeEach 逻辑类似,但它会等异步路由组件加载完再执行,适合“确保组件准备好后,再做最后判断”。

router.beforeResolve((to, from, next) => {
  // 假设某些页面需要加载动态配置后才能进
  if (to.meta.needConfig && !window.globalConfig) {
    fetchConfig().then(() => next()) // 加载配置后放行
  } else {
    next()
  }
})

(3)afterEach:“跳转后的后置操作”

导航完成后执行,不影响跳转流程,适合做“埋点统计”“关闭加载动画”,比如配合NProgress进度条:

import NProgress from 'nprogress'
router.beforeEach(() => { NProgress.start() }) // 跳转前启动进度条
router.afterEach(() => { NProgress.done() })   // 跳转后结束进度条

路由独享钩子(beforeEnter)适合哪些场景?

路由独享钩子写在单个路由的配置项里,作用范围更“精准”,适合两类场景:

(1)单路由的额外权限验证

比如后台管理页,全局验证登录后,还要确认是管理员,路由配置里加 beforeEnter

{
  path: '/admin',
  component: Admin,
  beforeEnter: (to, from, next) => {
    const role = localStorage.getItem('role')
    if (role !== 'admin') {
      next('/403') // 不是管理员?跳权限不足页
    } else {
      next()
    }
  }
}

(2)单路由的数据预加载

比如活动页需要先拉取配置,再渲染组件。beforeEnter 里发请求:

{
  path: '/activity',
  component: Activity,
  beforeEnter: async (to, from, next) => {
    const config = await fetchActivityConfig()
    to.meta.config = config // 把配置存在路由元信息,组件里能拿到
    next()
  }
}

组件里直接用 this.$route.meta.config 取数据,不用等组件创建后再请求~

组件内的钩子有啥独特优势?怎么结合组件生命周期用?

组件内钩子写在 .vue 组件的 export default 里,有 beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave 三个,优势是和组件自身状态深度绑定,能直接操作组件实例(除了 beforeRouteEnter 早期阶段)。

(1)beforeRouteEnter:组件创建前触发(适合“预加载数据”)

因为组件还没实例化,所以this是undefined,但可以通过 next 的回调访问组件实例,比如进入文章详情页,先拉取文章数据:

export default {
  data() { return { article: {} } },
  beforeRouteEnter(to, from, next) {
    axios.get(`/api/article/${to.params.id}`)
      .then(res => {
        // next的回调里,vm是组件实例
        next(vm => {
          vm.article = res.data
        })
      })
  }
}

这样组件渲染时,数据已经准备好了,不会出现“先渲染空页面,再加载数据”的闪烁问题~

(2)beforeRouteUpdate:路由参数变化时触发(适合“组件复用”)

比如路由是 /user/:id,当 id 从1变2,组件实例会被复用(避免重复创建),这时候用 beforeRouteUpdate 处理参数变化:

export default {
  methods: {
    fetchUser(id) { /* 发请求更新用户数据 */ }
  },
  beforeRouteUpdate(to, from, next) {
    this.fetchUser(to.params.id) // 根据新id重新请求数据
    next()
  }
}

(3)beforeRouteLeave:离开组件时触发(适合“表单未保存提示”)

用户编辑表单时,没保存就离开?用这个钩子弹提示:

export default {
  data() { return { formModified: false } },
  beforeRouteLeave(to, from, next) {
    if (this.formModified) { // 表单有修改
      const confirm = window.confirm('表单还没保存,确定离开?')
      if (confirm) next() // 确认就走
      else next(false)    // 取消就留在当前页
    } else {
      next() // 没修改,直接走
    }
  }
}

钩子的执行顺序是怎样的?实际开发容易踩哪些坑?

很多同学搞不清多个钩子同时存在时的执行顺序,导致逻辑冲突或导航卡住,先把顺序理清楚(关键步骤):

  1. 导航被触发(比如点击 <router-link>)→
  2. 调用离开组件的beforeRouteLeave(如果当前组件有这个钩子)→
  3. 调用全局的beforeEach
  4. 调用路由配置里的beforeEnter
  5. 解析异步路由组件(如果有)→
  6. 调用进入组件的beforeRouteEnter
  7. 调用全局的beforeResolve
  8. 导航确认,更新URL→
  9. 调用全局的afterEach
  10. 执行beforeRouteEnter的next回调(此时组件实例已创建)。

容易踩的坑:

  • next没调用,导航卡住:每个钩子都要确保调用 next()next(false)next('/path'),比如写了if判断,但分支里忘了写next,路由就会一直“卡着”,页面没反应。

  • beforeRouteEnter里this为undefined:因为组件还没创建,直接访问 this.xxx 会报错,必须用 next(vm => { vm.xxx = ... }) 来操作组件实例。

  • 异步组件加载时的钩子顺序:如果路由用了 component: () => import('./xxx.vue') 这种异步加载,beforeEnterbeforeRouteEnter 要等组件加载完才执行,处理异步逻辑时,要注意时机。

  • 全局和组件内钩子逻辑冲突:比如全局 beforeEach 里跳登录页,组件内 beforeRouteEnter 又想跳其他页,要注意执行顺序,避免循环跳转。

怎么用hooks实现权限控制?给个完整案例!

权限控制是hooks最常见的场景,一般分“登录态验证”和“角色权限验证”,结合全局钩子+路由元信息,步骤如下:

步骤1:配置路由元信息(meta)

给需要权限的路由加标记,requiresAuth(是否需要登录)、role(需要的角色):

const routes = [
  { path: '/', component: Home },
  { path: '/login', component: Login },
  { 
    path: '/profile', 
    component: Profile, 
    meta: { requiresAuth: true } // 需登录
  },
  { 
    path: '/admin', 
    component: Admin, 
    meta: { requiresAuth: true, role: 'admin' } // 需登录+管理员角色
  },
  { path: '/403', component: Forbidden } // 权限不足页
]

步骤2:全局beforeEach处理权限

router/index.js 里写全局拦截:

router.beforeEach((to, from, next) => {
  const isLogin = localStorage.getItem('token') // 假设token存登录态
  const userRole = localStorage.getItem('role') // 假设role存用户角色
  // 1. 处理“需要登录”的路由
  if (to.meta.requiresAuth) {
    if (!isLogin) {
      next('/login') // 没登录?先跳登录页
      return
    }
    // 2. 处理“角色权限”(如果有role要求)
    if (to.meta.role) {
      if (userRole !== to.meta.role) {
        next('/403') // 角色不符?跳权限不足页
        return
      }
    }
  }
  next() // 所有验证通过,放行
})

步骤3:处理登录逻辑(Login组件)

用户登录后,保存 tokenrolelocalStorage,再跳转到目标页:

export default {
  data() { return { username: '', password: '' } },
  methods: {
    login() {
      axios.post('/api/login', { 
        username: this.username, 
        password: this.password 
      }).then(res => {
        localStorage.setItem('token', res.data.token)
        localStorage.setItem('role', res.data.role)
        this.$router.push('/profile') // 跳转到需要权限的页面
      })
    }
  }
}

这样整个权限系统就串联起来了:全局钩子拦截所有需要权限的路由,路由元信息灵活配置权限要求,登录组件处理状态保存,实际项目中还能扩展(比如多角色、动态路由),但核心逻辑都是这么玩的~

数据预加载场景下,hooks怎么配合异步请求?

数据预加载指“进入路由前就把数据准备好,避免组件渲染后再请求导致的闪烁”,hooks在这场景下有三种思路,各有适用场景:

方案1:组件内钩子(beforeRouteEnter)

适合组件专属数据的预加载,比如文章详情页:

export default {
  data() { return { article: {} } },
  beforeRouteEnter(to, from, next) {
    const articleId = to.params.id
    fetchArticle(articleId)
      .then(res => {
        // next的回调传入数据给组件实例
        next(vm => {
          vm.article = res.data
        })
      })
      .catch(err => {
        console.error('获取文章失败', err)
        next('/404') // 失败跳404
      })
  }
}

方案2:路由独享钩子(beforeEnter)

适合单个路由的复杂逻辑,或多个组件复用同一套请求逻辑。

{
  path: '/article/:id',
  component: Article,
  beforeEnter: async (to, from, next) => {
    try {
      const res = await fetchArticle(to.params.id)
      to.meta.article = res.data // 把数据存在路由元信息
      next()
    } catch (err) {
      next('/404')
    }
  }
}
// Article组件里:
export default {
  computed: {
    article() {
      return this.$route.meta.article
    }
  }
}

方案3:全局beforeResolve

适合多路由通用的加载逻辑,比如所有带 dataLoader 的路由,自动执行加载:

router.beforeResolve((to, from, next) => {
  if (to.meta.dataLoader) {
    // dataLoader是路由元信息里的异步函数,返回数据
    to.meta.dataLoader(to)
      .then(() => next())
      .catch(() => next('/404'))
  } else {
    next()
  }
})
// 路由配置:
{
  path: '/product/:id',
  component: Product,
  meta: {
    dataLoader: (to) => fetchProduct(to.params.id).then(res => {
      to.meta.product = res.data
    })
  }
}

核心逻辑是利用钩子在“路由导航完成前”把数据准备好,让组件渲染时直接用数据,避免闪烁~

路由切换时的动画和加载状态,hooks怎么处理?

路由切换时的过渡动画、加载进度条是提升体验的细节,hooks能精准控制“时机”:

(1)配合Vue过渡动画

beforeRouteLeave 控制“离开动画完成后再跳转”。

export default {
  data() { return { leaveAnimation: false } },
  beforeRouteLeave(to, from, next) {
    this.leaveAnimation = true // 触发离开动画的class
    // 监听动画结束事件(假设用了CSS过渡)
    this.$el.addEventListener('animationend', () => {
      next() // 动画结束后,再跳转
    }, { once: true }) // 只监听一次
  }
}

(2)加载进度条(如NProgress)

用全局 beforeEachafterEach 控制进度条的“开始”和“结束”:

import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
router.beforeEach(() => { NProgress.start() }) // 导航开始,启动进度条
router.afterEach(() => { NProgress.done() })   // 导航完成,结束进度条

如果有异步组件或数据预加载,进度条可能“瞬间完成”(因为钩子执行快,实际组件还在加载),这时候用 beforeResolve 加延迟:

router.beforeResolve((to, from, next) => {
  setTimeout(() => {
    NProgress.inc() // 增加进度,模拟加载中
    next()
  }, 300)
})

(3)页面加载状态提示

在组件内用 beforeRouteEnter 显示“加载中”,数据获取后隐藏:

export default {
  data() { return { isLoading: true } },
  beforeRouteEnter(to, from, next) {
    fetchData().then(() => {
      next(vm => {
        vm.isLoading = false
      })
    })
  }
}
// 模板中:
<template>
  <div>
    <div v-if="isLoading">加载中...</div>
    <div v-else>{{ data }}</div>
  </div>
</template>

这些细节让路由切换更丝滑,hooks的核心价值就是“时机控制”——知道什么时候开始动画、什么时候结束加载,让用户感知到流程的连贯性~

vue-router hooks的核心是“流程控制”

不管是全局、路由独享

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门