You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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();
        }
    });
}

这个实现的核心逻辑:

  1. 从第一个中间件开始递归执行
  2. 每个中间件调用next()时:
    • 如果带参数(比如跳转路由),直接调用原始next终止所有后续中间件
    • 如果不带参数,继续执行下一个中间件
  3. 所有中间件执行完毕后,才调用一次原始next

这样就能保证整个导航流程中next只被调用一次,解决错误。

内容的提问来源于stack exchange,提问作者Martin Zeltin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 00:06:17