路由中间件 middleware
本教程共 50 篇 · 第 19 篇 · 更新于 2026-08-08 · 约 5 分钟阅读
本节目标:理解路由中间件的作用,能写全局/命名/匿名中间件,并用它做登录鉴权拦截。
19-1
有些逻辑,你希望「在进入某个页面之前」先跑一遍:比如检查用户有没有登录,没登录就拦去登录页;或者根据当前网址设置一下页面标题;又或者记录一条访问日志。把这些逻辑写进每个页面太重复,路由中间件(route middleware)就是放这类代码的统一地方。
它和服务器中间件名字像,但完全是两回事:路由中间件跑在 Nuxt 的 Vue 部分(客户端+SSR 渲染前),而服务器中间件跑在 Nitro 服务端、拦截 API 请求。本章只讲路由中间件。
Important路由中间件不会作用于服务器路由(如
/api/*)。要给 API 请求加拦截,请用服务器中间件(server/middleware)。
19-2
所有路由中间件都用 defineNuxtRouteMiddleware 这个宏来定义,默认导出。它接收一个处理函数,参数 to 是目标路由、from 是来源路由,两个都来自 Vue Router。
export default defineNuxtRouteMiddleware((to, from) => {
// to.path 是即将进入的网址,from.path 是离开的网址
console.log('准备从', from.path, '去往', to.path)
})
19-3
1. 匿名(内联)中间件:直接写在页面的 definePageMeta 里,不用单独建文件。
<script setup lang="ts">
definePageMeta({
middleware (to, from) {
if (!isLoggedIn()) return navigateTo('/login')
},
})
</script>
2. 命名中间件:放在 app/middleware/ 目录,文件名就是中间件名。在页面里用字符串引用,Nuxt 会在用到时异步加载它。
export default defineNuxtRouteMiddleware((to, from) => {
if (!isLoggedIn()) {
return navigateTo('/login')
}
})
<script setup lang="ts">
definePageMeta({
middleware: 'auth',
})
</script>
Note中间件名会被归一化成短横线写法。文件叫
auth.ts,引用就写'auth';文件叫someMiddleware.ts,引用要写'some-middleware'。记不住就保持文件名和引用一致最简单。
3. 全局中间件:文件名带 .global 后缀(如 app/middleware/logger.global.ts),会对每一次路由变化自动执行,不用在页面里引用。
export default defineNuxtRouteMiddleware((to, from) => {
console.log('全局:从', from.path, '到', to.path)
})
想给一个页面挂多个命名中间件,传数组即可:middleware: ['auth', 'logVisit']。
19-4
最常见的中间件用途就是登录鉴权。思路:中间件检查登录状态,未登录就 return navigateTo('/login') 把人踢走;已登录就什么都不做,Nuxt 继续放行。
function isAuthenticated (): boolean {
// 实际项目里这里读 cookie / token / 状态
return false
}
export default defineNuxtRouteMiddleware((to, from) => {
if (isAuthenticated() === false) {
return navigateTo('/login')
}
})
<script setup lang="ts">
definePageMeta({
middleware: 'auth',
})
</script>
<template>
<h1>欢迎来到你的仪表盘</h1>
</template>
Tip用
navigateTo跳转时,一定要return它(或在函数里直接return navigateTo(...))。不 return,Nuxt 以为你放行了,会继续渲染目标页,拦截就失效了。
19-5
中间件不仅在客户端点击导航时跑,在 SSR/SSG 服务端渲染页面时也会跑。所以如果你的中间件里用到了只有浏览器才有的 API(比如 localStorage、window),要防止它在服务端报错。用 import.meta.server 判断并跳过:
export default defineNuxtRouteMiddleware((to) => {
// 在服务端直接跳过,等价于「只在客户端执行」
if (import.meta.server) return
const token = window.localStorage.getItem('token')
if (!token) return navigateTo('/login')
})
关于顺序:全局中间件先跑(按文件名字母序),之后才是页面指定的中间件(按数组里的顺序)。如果你的逻辑依赖先后顺序,把这个记牢。
19-6
路由中间件的三种类型在 Nuxt 3 与 Nuxt 4 一致,仅目录从 middleware/ 变为 app/middleware/。自 Nuxt 3.11 起,还可以在 nuxt.config 里用 routeRules 集中配置客户端中间件,但仅作用于客户端,不影响 Nitro 服务端路由。
19-7
除了「拦去登录页」,有时你只是想直接返回一个错误,而不是跳走。可以用 abortNavigation(配合 createError)终止导航并展示错误页:
export default defineNuxtRouteMiddleware((to) => {
if (to.path === '/forbidden') {
return abortNavigation('该页面暂不可访问')
}
})
abortNavigation 会阻止本次跳转,Nuxt 转而渲染错误页(具体错误页做法属于后续章节)。它和 navigateTo 的区别是:一个「停在原地报错」,一个「换地方走人」。
Note
abortNavigation与navigateTo都来自 Nuxt 自动导入,无需手动 import。在中间件里,两者是最常用的「收尾动作」。
19-8
中间件文件建议用「动作含义」命名,比如 auth.ts、log-visit.ts、require-admin.ts,一眼能看出它干什么。多个页面需要同样的前置检查时,抽成命名中间件复用,比在每个页面写匿名函数更易维护。全局中间件文件务必带 .global 后缀,否则不会自动运行。
19-9
要清楚边界:路由中间件只管「页面导航前」,它管不了 API 请求、也管不了服务端数据流。需要拦截 HTTP 请求请走服务器中间件。也别在中间件里塞过重的业务逻辑——它每次导航都会跑,写得臃肿会拖慢每一次跳转。把「轻量判断 + 必要时跳转」作为设计原则最稳。
19-10
把三种中间件的顺序记成一条流水线,最不容易乱:
- 先跑所有全局中间件,按文件名字母序(如
auth.global.ts早于logger.global.ts)。 - 再跑页面指定的命名中间件,按数组里写的顺序(如
['auth', 'logVisit']先auth后logVisit)。 - 最后跑页面里的匿名中间件(直接写在
definePageMeta里的函数)。
任何一步只要 return navigateTo(...) 或 abortNavigation(...),后续步骤就不再执行。所以如果你在全局中间件里就把未登录用户踢去登录页,那个页面的命名中间件根本不会跑——这正是我们想要的结果,省掉一遍无意义的检查。
Note因为全局中间件最先跑,它适合做「对所有页面都成立」的通用判断(比如全站登录态、全站访问日志)。只针对某几个页面的逻辑,放命名或匿名中间件更精准,也避免影响无关页面。
19-11
真实登录拦截常要多做一件事:用户被踢到 /login 后,登录成功应该跳回他原本想去的页面,而不是永远停在首页。做法是把「目标地址」编码进登录页的 query 参数:
export default defineNuxtRouteMiddleware((to) => {
const isLoggedIn = false // 实际项目里读登录态
if (!isLoggedIn) {
// 把想去的地址编码进 redirect 参数
return navigateTo(`/login?redirect=${encodeURIComponent(to.fullPath)}`)
}
})
登录页在提交成功后,读取 route.query.redirect,再 return navigateTo(redirect) 即可。这里用 to.fullPath 而不是 to.path,是因为 fullPath 还带上了 query 和 hash,回跳时不会丢参数。
19-12
中间件函数可以是异步的。比如进入某些页面前,要先去接口确认用户权限,就直接 await:
export default defineNuxtRouteMiddleware(async (to) => {
const user = await fetchCurrentUser()
if (user.role !== 'admin') {
return abortNavigation('需要管理员权限')
}
})
Nuxt 会等中间件里的 Promise 跑完,再决定放不放行。注意:异步中间件在服务端渲染和客户端导航时都会执行,里面的请求要在两端都能跑通,别在 SSR 阶段去读只有浏览器才有的东西(这种情况用前面讲过的 import.meta.server 判断跳过)。
Tip中间件里
await一个慢接口,会直接拖慢进入页面的速度。只把「必须前置校验」的请求放进去;能延后的数据获取,一律挪到页面组件里按需加载,让页面先出来、内容再补全。
19-13
- 访问日志 / 埋点:每次路由变化都记一笔,放
.global最合适,不用每个页面引用。 - 全站语言或主题判断:根据 URL 或 cookie 统一设置一次,全局生效。
反过来,只跟某个业务板块相关的判断(比如「订单页要登录」),就别放全局——放进对应页面的命名中间件,逻辑边界更清晰,也方便别人读代码时一眼看出「这个页面有哪些前置条件」。
19-14
路由中间件是「进页面前先跑」的钩子:匿名写在 definePageMeta、命名放 app/middleware 用字符串引用、全局加 .global 后缀自动全跑。执行顺序是「全局→命名→匿名」,任一步拦住就中止。鉴权就是「不满足条件就 return navigateTo 拦走」。下一章我们讲页面之间怎么跳转。