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

Vue Router里的中间件到底指啥?

terry 2小时前 阅读数 7 #Vue
文章标签 Vue Router;中间件

不少刚接触Vue Router的同学,总会疑惑中间件到底是干啥的、怎么在项目里用起来,其实Vue Router本身没直接提供“中间件”这个官方API,但我们能通过导航守卫等机制,自己实现类似中间件的逻辑,用来处理权限验证、数据预加载这些常见需求,接下来就从基础概念到实战场景,把Vue Router中间件相关的问题拆开来聊~

先类比后端,比如Express里的中间件是请求到达路由前执行的通用逻辑(比如日志、鉴权),Vue Router里的“中间件”是**类比这个概念**——在路由导航的过程中(比如从/home跳转到/profile时),插入一段可复用的逻辑,完成权限检查、数据加载、行为统计这些事。

Vue Router本身提供的是「导航守卫」(像全局的beforeEach、路由独享的beforeEnter、组件内的beforeRouteEnter这些钩子),而“中间件”是我们基于这些守卫,把通用逻辑抽成独立函数,让不同路由能共享这些逻辑,举个简单例子:做登录验证时,不需要每个路由的beforeEnter都写一遍“判断token是否存在,不存在跳登录页”,而是把这段逻辑写成一个checkAuth函数,在全局守卫里调用,这就是中间件思维。

为啥要在Vue Router里搞中间件逻辑?

简单说,**为了让路由相关的通用逻辑“复用起来更爽、维护起来更轻”**,具体看几个场景:
  • 权限控制:必须登录才能进个人中心、订单页”,把“检查登录态”抽成中间件后,所有需要登录的路由都能复用,不用重复写判断代码。
  • 数据预加载:进入文章详情页前,得先根据文章ID把内容请求回来,用中间件在路由跳转前加载数据,能避免页面先渲染再加载导致的“空白闪烁”。
  • 埋点统计:用户进入每个路由时,要上报“用户进入XX页面”的行为,把埋点逻辑写成中间件,所有路由能统一处理。
  • 页面跳转前的“拦截”:比如表单页面没保存就想离开,要弹出“是否确认离开”的提示,中间件能在路由切换前统一处理这类交互。

如果不搞中间件,这些逻辑要么散落在各个路由的守卫里(重复代码一堆),要么塞在组件内的生命周期里(耦合度高,换个路由还要重新写),所以中间件是「把路由级的通用逻辑模块化」的好办法。

怎么自己实现Vue Router中间件?

核心思路是**“用导航守卫承载逻辑,把重复代码抽成函数,再按顺序执行这些函数”**,下面分三种常见实现方式讲:

全局中间件:所有路由都要经过的逻辑

全局登录验证”“全局埋点”这类逻辑,适合用全局守卫(router.beforeEach)+ 抽函数的方式。

举个登录验证的例子:

// 第一步:把验证逻辑抽成中间件函数
function checkAuth(to, from, next) {
  const isLogin = localStorage.getItem('token') // 假设用localStorage存token
  if (isLogin) {
    next() // 已登录,继续跳转到目标路由
  } else {
    next('/login') // 没登录,跳登录页
  }
}
// 第二步:在全局守卫里执行中间件
const router = createRouter({ ... })
router.beforeEach((to, from, next) => {
  checkAuth(to, from, next) // 执行登录验证中间件
})

如果有多个全局中间件(比如还要埋点、还要设置页面标题),可以把它们放进数组,用递归或循环执行:

const globalMiddlewares = [checkAuth, setPageTitle, trackPageView]
router.beforeEach((to, from, next) => {
  let currentIndex = 0 // 记录当前执行到第几个中间件
  function runNextMiddleware() {
    if (currentIndex >= globalMiddlewares.length) {
      return next() // 所有中间件执行完,继续导航
    }
    const currentMiddleware = globalMiddlewares[currentIndex]
    currentIndex++
    currentMiddleware(to, from, runNextMiddleware) // 执行当前中间件,完成后执行下一个
  }
  runNextMiddleware()
})

路由独享中间件:只给特定路由用的逻辑

有些逻辑不是全局的,进入管理员页面需要管理员权限”“进入文章详情页要预加载文章数据”,这类路由专属的逻辑,适合用「路由配置的meta字段 + 全局守卫判断」来实现。

步骤:

  • 在路由的meta里配置中间件列表
  • 全局守卫里读取meta,执行对应的中间件

举个“文章详情页预加载数据”的例子:

// 第一步:抽数据预加载中间件
async function loadArticle(to, from, next) {
  const articleId = to.params.id
  const article = await api.getArticle(articleId) // 假设api是请求工具
  store.commit('setCurrentArticle', article) // 把数据存到Vuex
  next() // 数据加载完,继续导航
}
// 第二步:在路由配置的meta里指定中间件
const routes = [
  {
    path: '/article/:id',
    component: ArticleDetail,
    meta: {
      middlewares: [loadArticle] // 这个路由要执行的中间件
    }
  }
]
// 第三步:全局守卫里处理路由独享中间件
router.beforeEach((to, from, next) => {
  const routeMiddlewares = to.meta.middlewares || [] // 取出当前路由的中间件
  const globalMiddlewares = [checkAuth, setPageTitle] // 全局中间件
  const allMiddlewares = [...globalMiddlewares, ...routeMiddlewares] // 合并
  let currentIndex = 0
  function runMiddleware() {
    if (currentIndex >= allMiddlewares.length) return next()
    const middleware = allMiddlewares[currentIndex]
    currentIndex++
    middleware(to, from, runMiddleware)
  }
  runMiddleware()
})

组件内中间件?(不推荐,但得了解)

组件内的导航守卫(比如beforeRouteEnter)也能写逻辑,但复用性极差——每个组件都要写一遍,完全违背中间件“复用”的初衷,所以除非是组件专属、完全没法通用的逻辑,否则优先用全局或路由级的中间件。

中间件和Vue Router导航守卫有啥区别?

很多同学会把这俩搞混,其实是「机制」和「逻辑组织方式」的区别:
  • 导航守卫是Vue Router提供的底层钩子(比如beforeEach、beforeEnter、beforeRouteEnter),决定了“在路由导航的哪个阶段执行代码”(比如跳转前、跳转后、进入组件前)。
  • 中间件基于导航守卫的上层逻辑封装——把通用的路由逻辑(权限、数据、埋点等)抽成可复用的函数,再通过导航守卫去执行这些函数。

举个通俗例子:导航守卫是“舞台上的不同表演时段”(比如开场前、表演中、谢幕后),中间件是“在某个时段要表演的节目”(比如开场前要检查门票、发纪念品),节目(中间件)得靠时段(守卫)来承载,但节目可以复用,时段是固定的机制。

实际项目里,中间件能解决哪些典型问题?

光讲概念太虚,结合具体场景看更清楚:

场景1:多级权限验证(普通用户、管理员、超级管理员)

不同路由需要不同角色才能进入,

  • /admin 只有管理员能进
  • /super-admin 只有超级管理员能进
  • /user 普通用户和管理员都能进

用中间件实现“按角色拦截”:

// 抽一个通用的角色检查中间件(支持传参)
function checkRole(requiredRole) {
  return (to, from, next) => {
    const userRole = store.state.user.role // 假设从Vuex取角色
    if (userRole === requiredRole) {
      next()
    } else {
      next('/403') // 无权限页面
    }
  }
}
// 路由配置里按需使用
const routes = [
  {
    path: '/admin',
    component: AdminPage,
    meta: {
      middlewares: [checkRole('admin')] // 传参指定需要admin角色
    }
  },
  {
    path: '/super-admin',
    component: SuperAdminPage,
    meta: {
      middlewares: [checkRole('super-admin')]
    }
  }
]

场景2:数据预加载(让页面打开即有数据)

比如进入商品详情页/product/:id前,先把商品数据请求好,避免页面先渲染再加载导致的“空白”。

中间件实现:

async function loadProduct(to, from, next) {
  const productId = to.params.id
  try {
    const product = await api.getProduct(productId)
    store.commit('setCurrentProduct', product) // 存到Vuex
    next() // 数据加载完,继续跳转
  } catch (error) {
    next('/404') // 请求失败跳404
  }
}
// 路由配置
{
  path: '/product/:id',
  component: ProductDetail,
  meta: {
    middlewares: [loadProduct]
  }
}

场景3:页面跳转前的“拦截提示”(防止用户误操作)

比如用户在填写表单的页面(/form),表单内容没保存就想跳走,要弹出确认提示。

中间件实现:

function confirmLeave(to, from, next) {
  const isFormDirty = store.state.form.isDirty // 假设表单脏状态存在Vuex
  if (isFormDirty) {
    const isConfirm = window.confirm('表单还没保存,确定要离开吗?')
    if (isConfirm) {
      next() // 确认离开,继续跳转
    } else {
      next(false) // 取消跳转,留在当前页
    }
  } else {
    next() // 表单没修改,直接跳转
  }
}
// 路由配置(给/form路由加中间件)
{
  path: '/form',
  component: FormPage,
  meta: {
    middlewares: [confirmLeave]
  }
}

场景4:多语言切换同步(路由和i18n联动)

假设路由是/en/about /zh/about 这种格式,切换路由时要同步修改i18n的语言。

中间件实现:

function setLocale(to, from, next) {
  const lang = to.path.split('/')[1] || 'zh' // 从路由里取语言段,默认中文
  i18n.locale = lang // 假设i18n是VueI18n实例
  next()
}
// 全局中间件(所有路由都要执行)
const globalMiddlewares = [setLocale]
router.beforeEach((to, from, next) => {
  // 执行全局中间件...
})

写Vue Router中间件容易踩哪些坑?怎么避?

自己实现中间件时,几个高频踩坑点得注意:

坑1:中间件执行顺序乱了

先执行权限验证,再执行数据预加载”,结果代码里顺序写反了,导致数据还没加载完就开始权限验证(可能拿到空数据)。

解决

  • 全局中间件和路由独享中间件的合并顺序要明确(比如先全局、后路由独享)。
  • 用数组存中间件,通过索引递增的方式顺序执行(像前面例子里的currentIndex++ + 递归调用nextMiddleware)。

坑2:next()调用不当导致死循环

最典型的是“权限验证中间件”和“登录页路由”的循环:用户没登录时,中间件跳转到/login;但/login路由也需要权限验证(又触发中间件),导致无限循环跳转。

解决
在路由的meta里加“忽略验证”的标记,

// 登录页路由配置
{
  path: '/login',
  component: LoginPage,
  meta: {
    ignoreAuth: true // 表示这个路由不需要权限验证
  }
}
// 权限验证中间件里判断
function checkAuth(to, from, next) {
  if (to.meta.ignoreAuth) { // 如果当前路由要忽略验证,直接放行
    next()
    return
  }
  // 正常验证逻辑...
}

坑3:异步中间件没等结果就执行next()

比如数据预加载是异步请求,但中间件里没等请求完成就调用next(),导致页面跳转后数据还没回来,页面渲染空白。

解决
用async/await或Promise处理异步,确保数据加载完再放行:

async function loadData(to, from, next) {
  await fetchData() // 等待请求完成
  next() // 数据加载完再跳转
}

坑4:中间件逻辑重复,没真正复用

比如每个需要权限的路由,都单独写了“判断token”的代码,没有抽成通用函数。

解决
把重复逻辑抽成带参数的函数(像前面的checkRole中间件),通过传参来适配不同场景,而不是复制粘贴代码。

有没有更优雅的中间件实现方案?

如果项目里中间件很多,纯手动写递归、合并数组会有点繁琐,可以封装一个**Vue Router中间件插件**,让代码更模块化。

思路:写一个插件函数,接收全局中间件列表,然后在插件内部处理“全局+路由独享”中间件的执行逻辑。

示例代码:

// 封装中间件插件
function createMiddlewarePlugin(globalMiddlewares = []) {
  return function (router) {
    router.beforeEach((to, from, next) => {
      // 1. 取出当前路由的独享中间件
      const routeMiddlewares = to.meta.middlewares || []
      // 2. 合并全局和路由独享中间件
      const allMiddlewares = [...globalMiddlewares, ...routeMiddlewares]
      let currentIndex = 0 // 记录当前执行到第几个中间件
      // 递归执行中间件
      function runNextMiddleware() {
        if (currentIndex >= allMiddlewares.length) {
          return next() // 所有中间件执行完,继续导航
        }
        const currentMiddleware = allMiddlewares[currentIndex]
        currentIndex++
        currentMiddleware(to, from, runNextMiddleware) // 执行当前中间件,完成后执行下一个
      }
      runNextMiddleware()
    })
  }
}
// 使用插件
const globalMiddlewares = [checkAuth, setLocale] // 全局中间件列表
const middlewarePlugin = createMiddlewarePlugin(globalMiddlewares)
const router = createRouter({
  history: createWebHistory(),
  routes: [...]
})
router.use(middlewarePlugin) // 注册中间件插件

这样一来,全局中间件和路由独享中间件的逻辑被插件封装了,路由配置和守卫代码更简洁,后续新增中间件也只需要往globalMiddlewares数组里加,或者在路由meta里配,维护成本低很多。

Vue Router中间件的核心逻辑

绕了这么多例子和场景,其实Vue Router中间件的本质很简单——**把路由导航过程中要做的通用逻辑,拆成一个个可复用的函数(中间件),再通过导航守卫按顺序执行这些函数**。

不管是简单的登录拦截,还是复杂的“权限验证+数据预载+埋点统计”组合拳,只要记住「逻辑抽成函数、守卫承载执行、顺序控制好、异步处理稳」这几个点,就能把中间件玩得很溜。

要是你刚开始没思路,建议先从单一功能的中间件(比如只做登录验证)写起,慢慢往多中间件组合、插件化封装方向扩展,踩过“执行顺序乱了”“死循环跳转”这些坑后,自然就摸清中间件的脾气了~

版权声明

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

发表评论:

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

热门