Vue Router 4报错:导航守卫中next回调被多次调用问题排查
解决Vue Router 4导航守卫多次调用next()的错误
问题描述
使用Vue 3搭配Vue Router 4开发项目时,为路由配置中间件后出现以下错误:
The "next" callback was called more than once in one navigation guard when going from "/" to "/protected". It should be called exactly one time in each navigation guard. This will fail in production.
相关代码
路由文件
import auth from '@/middleware/auth' import admin from '@/middleware/admin' import initRoutesMiddleware from '@/routes/middleware' import { createWebHistory, createRouter } from 'vue-router' const routes = [ { path: '/', name: 'dashboard', component: () => import('@/views/DashboardView.vue'), meta: { middleware: [ auth ], }, }, { path: '/protected', name: 'protected', component: () => import('@/views/ProtectedView.vue'), meta: { middleware: [ auth, admin ], }, }, { path: '/access-denied', name: 'accessDenied', component: () => import('@/views/error/AccessDeniedView.vue'), }, ] const router = createRouter({ history: createWebHistory(), routes: routes, }) router.beforeEach((_, __, next) => { setAppLoadingCursor(false) return next() }) initRoutesMiddleware(router) export default router
auth中间件(middleware/auth.ts)
import { ROUTE_LOGIN } from '@/config/routes' import { LOCALSTORAGE_KEY_USER, LOCALSTORAGE_KEY_PATH_BEFORE_LOGIN } from '@/config/app' export default function auth({ to, next }: any) { const user = !!localStorage.getItem(LOCALSTORAGE_KEY_USER) if (!user) { localStorage.setItem(LOCALSTORAGE_KEY_PATH_BEFORE_LOGIN, to.path) return next(ROUTE_LOGIN.path) } return next() }
admin中间件(middleware/admin.ts)
import AppUser from '@/types/appUser' import { LOCALSTORAGE_KEY_USER } from '@/config/app' import { ROUTE_ACCESS_DENIED } from '@/config/routes' export default function admin({ next }: any) { const user = localStorage.getItem(LOCALSTORAGE_KEY_USER) if (!user) return next() const appUser: AppUser = JSON.parse(user) if (appUser.admin !== true) return next(ROUTE_ACCESS_DENIED.path) return next() }
中间件初始化文件(routes/middleware.ts)
import { Router, RouteLocationNormalized, NavigationGuardNext } from 'vue-router' interface RouteContext { from: RouteLocationNormalized next: NavigationGuardNext router: Router to: RouteLocationNormalized } function nextFactory(context: RouteContext, middlewares: Function[], index: number) { const subsequentMiddleware = middlewares[index] if (!subsequentMiddleware) return context.next return (...parameters: any[]) => { context.next(...(parameters as [])) const nextMiddleware = nextFactory(context, middlewares, index + 1) subsequentMiddleware({ ...context, next: nextMiddleware }) } } export default function initRoutesMiddleware(router: Router) { router.beforeEach((to, from, next) => { if (to.meta.middleware) { const middlewares: Function[] = Array.isArray(to.meta.middleware) ? to.meta.middleware : [to.meta.middleware] const context: RouteContext = { from, next, router, to } const nextMiddleware = nextFactory(context, middlewares, 1) return middlewares[0]({ ...context, next: nextMiddleware }) } return next() }) }
问题原因
错误根源在nextFactory函数的实现:每次调用自定义的next时,都会先执行一次原始的context.next(),再调用后续中间件。当存在多个中间件时,每个中间件调用next()都会触发一次原始next,导致重复调用。
比如访问/protected时,auth中间件调用next()会先触发一次原始next,接着执行admin中间件,admin中间件调用next()又会触发一次原始next,最终导致next被调用两次。
解决方案
修改routes/middleware.ts的逻辑,确保只有当所有中间件执行完毕,或者中间件需要跳转路由时,才调用一次原始next:
import { Router, RouteLocationNormalized, NavigationGuardNext } from 'vue-router' interface RouteContext { from: RouteLocationNormalized next: NavigationGuardNext router: Router to: RouteLocationNormalized } export default function initRoutesMiddleware(router: Router) { router.beforeEach((to, from, next) => { if (to.meta.middleware) { const middlewares: Function[] = Array.isArray(to.meta.middleware) ? to.meta.middleware : [to.meta.middleware]; const context: RouteContext = { from, next, router, to }; // 递归执行中间件 const runMiddleware = (index: number) => { const currentMiddleware = middlewares[index]; // 所有中间件执行完成,调用原始next if (!currentMiddleware) return next(); currentMiddleware({ ...context, next: (...args) => { // 如果中间件传入了跳转参数,直接调用原始next终止流程 if (args.length > 0) return next(...args); // 无参数则执行下一个中间件 runMiddleware(index + 1); } }); }; runMiddleware(0); } else { return next(); } }); }
这个实现的核心逻辑:
- 从第一个中间件开始递归执行
- 每个中间件调用
next()时:- 如果带参数(比如跳转路由),直接调用原始
next终止所有后续中间件 - 如果不带参数,继续执行下一个中间件
- 如果带参数(比如跳转路由),直接调用原始
- 所有中间件执行完毕后,才调用一次原始
next
这样就能保证整个导航流程中next只被调用一次,解决错误。
内容的提问来源于stack exchange,提问作者Martin Zeltin
相关产品推荐
相关产品推荐

