Skip to content
路由中间件
基本概念
路由中间件是在路由导航完成之前执行的函数。它拦截每一次导航意图,允许在页面渲染之前插入检查、修正或终止行为。中间件的定位不是通用的业务逻辑容器,而是路由级别的守卫——可以把它看作 Vue Router 的 beforeEach 在 Nuxt 框架层面更结构化的实现:按文件约定注册,或直接在页面中声明,无需手动挂载到 router 实例。
命名中间件与匿名中间件
命名中间件位于 middleware/ 目录,文件名即中间件名称,框架会自动注册。
typescript
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const { isLoggedIn } = useAuth()
if (!isLoggedIn.value) {
return navigateTo('/login')
}
})该函数接收 to 和 from 两个 Vue Router 路由对象,逻辑是检查登录状态,未登录则返回 navigateTo('/login') 触发重定向。navigateTo 是一个辅助函数,会指示 Nuxt 终止当前导航并跳转到指定路径。
匿名中间件不需要单独文件,直接写在页面组件的 definePageMeta 中,作为一次性守卫。
typescript
// pages/dashboard.vue
definePageMeta({
middleware: (to, from) => {
if (process.client && !document.cookie.includes('session')) {
return navigateTo('/login')
}
}
})匿名中间件与命名中间件在功能上完全等价,同样可以使用 useNuxtApp() 获取全局注入的属性。唯一的区别在于匿名中间件无法通过文件名后缀区分客户端或服务端运行环境,但可以在函数体内通过 process.client 或 import.meta.client 判断执行端,实现条件逻辑。
页面级注册与全局注册
命名中间件需要通过 definePageMeta 注册到页面才会执行:
typescript
definePageMeta({
middleware: 'auth'
})也可以按数组顺序注册多个中间件:
typescript
definePageMeta({
middleware: ['auth', 'log']
})如果某个页面不希望执行任何中间件(包括全局中间件),可以传递空数组来覆盖。
全局中间件有两种方式。第一种是使用 .global.ts 后缀的文件:
typescript
// middleware/analytics.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
console.log(`navigating from ${from.path} to ${to.path}`)
}).global.ts 文件会自动应用到所有路由,执行时机早于页面级中间件。第二种是在 nuxt.config.ts 中配置:
typescript
// nuxt.config.ts
export default defineNuxtConfig({
router: {
middleware: ['analytics']
}
})这里通过名称引用已有的命名中间件,同样作用于所有路由。如果同时存在 .global.ts 和 nuxt.config 配置中的全局中间件,执行顺序为:先 .global.ts,再 nuxt.config 中配置的中间件。
执行顺序与上下文
完整的中间件执行顺序如下:
- 全局中间件(
.global.ts文件) nuxt.config.ts中router.middleware配置的中间件- 页面
definePageMeta中声明的中间件,按数组中书写顺序依次执行
同一分组内若存在多个 .global.ts 文件,执行顺序由文件名排序决定,因此应避免全局中间件之间存在隐式依赖。
中间件函数可以调用 useNuxtApp() 获取当前 Nuxt 应用实例,从而访问插件注入的属性、全局状态和工具方法。
typescript
// middleware/permission.ts
export default defineNuxtRouteMiddleware((to, from) => {
const { $permission } = useNuxtApp()
if (!$permission.check(to.path)) {
return abortNavigation({
statusCode: 403,
statusMessage: 'Forbidden'
})
}
})这里 $permission 是由插件注入的,后续插件部分会进一步说明如何注入此类全局属性。
控制导航:重定向、终止与校验
中间件通过返回值控制导航行为:
return navigateTo(path):重定向到新地址return abortNavigation(payload):终止导航,可附带错误信息return true或不显式返回:放行,导航继续
abortNavigation 接收一个可选对象,可设置 statusCode 和 statusMessage。它不会跳转到错误页面,而是抛出异常由框架捕获,再根据状态码显示对应的错误界面。
权限校验是中间件的典型应用。例如检查用户能否访问当前路径:
typescript
// middleware/acl.ts
export default defineNuxtRouteMiddleware((to) => {
const { $acl } = useNuxtApp()
if (!$acl.canAccess(to.path)) {
return abortNavigation({ statusCode: 403 })
}
})如果需要基于动态参数校验(如用户只能编辑自己的文章),可以结合路由参数与异步请求:
typescript
// middleware/owner.ts
export default defineNuxtRouteMiddleware(async (to) => {
const postId = to.params.id
const { data } = await useFetch(`/api/posts/${postId}`)
if (data.value?.authorId !== useAuth().userId) {
return navigateTo('/not-found')
}
})中间件支持异步操作,在异步逻辑完成之前导航不会放行。
插件体系
基本概念
插件在应用初始化阶段执行,运行于 Vue 应用实例创建之后、挂载之前。它的作用是向整个应用注入全局能力,如注册组件、指令、工具方法或第三方库——这与中间件“能不能进页面”的职责不同,插件关注的是“进了之后能用什么”。
注册方式与注入
插件文件放在 plugins/ 目录,使用 defineNuxtPlugin 定义:
typescript
// plugins/hello.ts
export default defineNuxtPlugin((nuxtApp) => {
return {
provide: {
hello: (msg: string) => console.log(`hello, ${msg}`)
}
}
})nuxtApp 对象是插件与 Nuxt 应用的连接点。通过 provide 返回的对象会被注入到整个应用,任何组件或中间件都可以通过 useNuxtApp() 访问:
typescript
const { $hello } = useNuxtApp()
$hello('world')注入的属性名自动添加 $ 前缀是约定而非强制,但遵循该约定有助于在模板和组合式函数中快速识别全局属性。
另一种注入方式是直接调用 nuxtApp.provide:
typescript
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.provide('formatDate', (date: Date) => {
return date.toLocaleDateString()
})
})两种方式效果相同,但返回 { provide } 可以同时注入多个值,使用上更常见。
注册全局组件与指令
通过 nuxtApp.vueApp 可以像在普通 Vue 应用中一样注册全局组件和指令:
typescript
// plugins/ui.ts
import MyButton from '~/components/MyButton.vue'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.component('MyButton', MyButton)
nuxtApp.vueApp.directive('focus', {
mounted(el) {
el.focus()
}
})
})注册后,<MyButton /> 和 v-focus 在所有组件中均可直接使用。
引入第三方库也遵循相同模式:
typescript
// plugins/dayjs.ts
import dayjs from 'dayjs'
export default defineNuxtPlugin(() => {
return {
provide: {
dayjs
}
}
})之后在组件中通过 const { $dayjs } = useNuxtApp() 即可访问 dayjs 实例。
客户端插件与服务端插件
插件默认在服务端和客户端均执行。如果逻辑只适用于一侧,可以通过文件后缀限制运行环境。.client.ts 后缀的插件仅在浏览器端执行:
typescript
// plugins/analytics.client.ts
export default defineNuxtPlugin(() => {
window.addEventListener('error', (e) => {
console.error('client error', e)
})
}).server.ts 后缀的插件仅在服务端执行:
typescript
// plugins/db.server.ts
import { createPool } from 'mysql2'
export default defineNuxtPlugin(() => {
const pool = createPool({ /* config */ })
return {
provide: {
db: pool
}
}
})服务端插件注入的值(如 $db)只在 SSR 过程中可用。中间件在 SSR 阶段运行于服务端,可以访问服务端插件注入的属性;客户端导航时中间件运行在浏览器,则只能拿到客户端插件的注入。这种一致性由 useNuxtApp() 自动处理。
服务端 API 路由
基本概念
server/api/ 目录下的文件会被 Nitro(Nuxt 的后端引擎)自动映射为 API 路由。目录结构直接决定 URL 路径:
server/api/
├── hello.ts → /api/hello
├── posts/index.ts → /api/posts
└── posts/[id].ts → /api/posts/:id每个文件导出一个事件处理器,使用 defineEventHandler 定义:
typescript
// server/api/hello.ts
export default defineEventHandler(() => {
return { message: 'hello' }
})访问 /api/hello 时,返回的 JavaScript 对象会被 Nitro 自动序列化为 JSON 响应。
如果需要区分 HTTP 方法,可以通过 getMethod 判断:
typescript
// server/api/posts/index.ts
export default defineEventHandler(async (event) => {
const method = getMethod(event)
if (method === 'GET') {
return [{ id: 1, title: 'post' }]
}
if (method === 'POST') {
const body = await readBody(event)
return { id: 2, ...body }
}
})动态路由参数通过 getRouterParam 获取:
typescript
// server/api/posts/[id].ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
return { id, title: `Post ${id}` }
})event 对象是 h3 库的核心,封装了本次请求的上下文。
请求解析
h3 提供了一组辅助函数来解析请求数据,避免直接操作原始的 Node.js req 对象。
getQuery(event):解析 URL 查询参数,返回对象readBody(event):解析请求体,支持 JSON 和表单数据,返回 Promise,需要awaitgetRouterParam(event, name):获取动态路由参数
示例:
typescript
// server/api/search.ts
export default defineEventHandler(async (event) => {
const query = getQuery(event) // ?q=nuxt → { q: 'nuxt' }
const body = await readBody(event) // POST body
return { query, body }
})getQuery 不处理多值参数(比如 ?tag=a&tag=b),如果需要多值,需要自行从原始 URL 中解析。readBody 会根据请求的 Content-Type 自动尝试解析,解析失败时不会抛出异常,而是返回 undefined,因此调用后应做空值判断。
状态码与错误处理
默认成功响应的状态码是 200。可以通过 setResponseStatus 修改:
typescript
export default defineEventHandler(async (event) => {
setResponseStatus(event, 201)
return { created: true }
})错误处理有两种方式。一种是利用 createError 抛出错误:
typescript
import { createError } from 'h3'
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
if (!id) {
throw createError({
statusCode: 400,
statusMessage: 'Missing id'
})
}
// ...
})另一种是直接设置状态码并返回错误体:
typescript
export default defineEventHandler(async (event) => {
setResponseStatus(event, 404)
return { error: 'Not found' }
})两种方式在客户端的响应结构不同。createError 抛出的错误会被 h3 捕获并返回标准错误结构;直接设置状态码返回则完全由开发者控制响应格式。
全栈对接:在组件中消费 API
在组件中调用自建的 API 端点主要有 $fetch 和 useFetch 两种方式。
$fetch
$fetch 是 ofetch 的封装,可在任意位置发起 HTTP 请求:
typescript
<script setup>
import { ref } from 'vue'
const data = ref(null)
const error = ref(null)
async function loadPosts() {
try {
data.value = await $fetch('/api/posts')
} catch (e) {
error.value = e
}
}
</script>$fetch 会自动处理 JSON 解析,但不会管理加载状态,需要手动维护 pending、error 等引用。
useFetch
useFetch 组合式函数封装了 $fetch 和 useAsyncData,与 Nuxt 生命周期结合更紧密:
typescript
const { data, pending, error } = await useFetch('/api/posts')在 SSR 模式下,useFetch 会在服务端执行请求并将数据序列化传给客户端,客户端不再重复请求。如果 API 依赖客户端特有的状态(如 cookie、localStorage),则 SSR 阶段的请求可能因缺乏这些状态而失败。此时可以设置 server: false 让请求只在客户端执行:
typescript
const { data } = await useFetch('/api/user', {
server: false
})若请求参数是响应式的,需要以函数形式传入路径,这样 useFetch 能跟踪响应式依赖,参数变化时自动重新请求:
typescript
const postId = ref(1)
const { data } = await useFetch(() => `/api/posts/${postId.value}`)注意点
- 插件与中间件的职责边界清晰:插件负责能力注入,中间件负责路由守卫。在插件中执行重定向是不合理的——插件运行时路由系统可能尚未就绪,
navigateTo的行为不稳定。 - 中间件中应避免进行会阻塞导航的重计算同步操作。如果确实需要复杂计算,应考虑在页面组件内使用
useAsyncData处理,而非在中间件里。 - 服务端 API 路由没有内置认证机制。如需保护端点,应在事件处理器内自行校验,或通过
server/middleware/目录添加服务端中间件(注意与路由中间件是不同的概念)。 - 插件中可以调用
$fetch,但需注意执行环境。服务端插件发出的请求会走 Nitro 内部网络路径,不会经过外部 HTTP,这一优化由 Nitro 自动处理。
参考链接
[1] Nuxt 官方文档 - middleware 目录. https://nuxt.com/docs/guide/directory-structure/middleware
[2] Nuxt 官方文档 - plugins 目录. https://nuxt.com/docs/guide/directory-structure/plugins
[3] Nuxt 官方文档 - server 目录. https://nuxt.com/docs/guide/directory-structure/server
[4] Nuxt 官方文档 - $fetch. https://nuxt.com/docs/api/utils/dollarfetch
[5] Nuxt 官方文档 - useFetch. https://nuxt.com/docs/api/composables/use-fetch
