Nuxt中间件与插件注入边界重构
这次整理的对象主要来自下面这些本地素材:
11Vue学习/Nuxt4/note/nuxt-lifecycle.md11Vue学习/Nuxt4/核心概念/2Lifecycle.md11Vue学习/组件化思维/认证请求与路由权限链路重构.md11Vue学习/组件化思维/错误边界与客户端降级渲染重构.md
和前面的案例一样,这一轮不直接修改任何独立项目,而是把其中最值得复用的“Nuxt 中间件、插件与运行时注入边界”思路整理成一篇案例文档。
这类场景最容易被低估的地方,不是插件怎么写,而是“这段逻辑到底应该放在哪一层执行”:
- 应该放在
server/middleware/,还是app/middleware/ - 应该放在
app/plugins/,还是页面 / composable 内部 - 这段逻辑应该只在服务端运行,还是双端都运行
- 注入到
nuxtApp的能力,应该是谁来消费 - 运行时副作用,应该在哪个钩子里执行
如果这些边界不先理清,项目后面就很容易出现几个典型问题:
- 页面里到处写权限判断和重定向逻辑
- 插件里既做环境初始化,又做业务决策,还顺手发请求
- 服务端中间件和路由中间件职责重叠
- 双端共享逻辑混进客户端专属副作用,最终变成 hydration 或运行时问题
这类场景真正的复杂度中心
Nuxt 中间件与插件场景里,最容易被忽略的复杂度中心,不是目录结构,而是“运行时职责的归属”。
从当前素材可以看到几个非常关键的边界:
- Nitro 中间件处理的是“请求级前置逻辑”
- Nuxt 路由中间件处理的是“导航级准入逻辑”
- Nuxt 插件处理的是“应用级能力初始化与注入”
- 页面与组件处理的是“页面级消费与局部交互”
如果这些职责不先拆开,项目很快会出现:
- 权限逻辑既在中间件里写,又在页面里再判断一次
- 插件变成全局大杂烩,什么都往里注入
- 服务端只该执行一次的逻辑被放进双端插件重复执行
- 页面组件必须理解底层运行时顺序,导致消费层越来越重
所以这类重构的重点,不是先加更多目录,而是先把“请求级、导航级、应用级、页面级”这四类职责收口清楚。
推荐的重构边界
更适合长期维护的结构,通常会把这类场景拆成下面几层:
- 请求边界层:负责每个请求都必须执行的服务端前置逻辑
- 导航边界层:负责页面进入前的路由校验、重定向和准入
- 应用注入层:负责初始化全局能力并注入到
nuxtApp - 页面消费层:只负责调用已注入能力与页面级装配
- 组件交互层:只负责局部交互和展示
如果换成更具体的职责,大概可以这样理解:
server/middleware/auth.ts:负责请求级身份上下文准备app/middleware/auth.ts:负责路由级准入判断app/plugins/api.ts:负责创建并注入统一 API 客户端useUserSession()/useApiClient():负责消费注入能力pages/**:只负责页面级数据与交互组合
这里最重要的一条原则是:
- 请求边界不要承担页面决策
- 路由边界不要承担应用初始化
- 插件不要承担业务页面编排
- 页面不要反向承担底层运行时职责
Nitro 中间件解决的是请求级问题,不是页面级逻辑
很多项目第一次接触 Nuxt 服务端能力时,很容易把 server/middleware/ 当成一个“什么都能提前处理”的地方。
但更稳的理解应该是:Nitro 中间件处理的是每个请求都必须走的服务端前置逻辑。例如:
- 读取 cookie / token
- 构造请求上下文
- 请求级日志记录
- 服务端鉴权前置检查
它不适合直接处理这些问题:
- 当前页面应该跳去哪里
- 某个页面应该展示什么布局
- 某个组件如何消费认证信息
更稳的做法,是把请求级上下文先准备好,再让后续层去消费。
export interface RequestUserContext {
id: string
roles: string[]
}
export function attachRequestUser(event: H3Event, user: RequestUserContext | null) {
event.context.user = user
}这样 Nitro 中间件只负责:
- 有没有 token
- token 能不能解析
- 请求上下文里应该放什么
而不是直接替页面做所有决策。
路由中间件要只处理导航准入,不要和请求边界混写
Nuxt 路由中间件最适合处理的是“当前用户能不能进入这条路由”。
这类逻辑通常包括:
- 是否需要登录
- 是否需要特定角色
- 是否应该重定向到登录页
- 已登录用户是否应该离开登录页
这和 Nitro 中间件最大的区别是:
- Nitro 中间件面向请求
- 路由中间件面向导航
如果这层边界不拆开,就会出现很典型的问题:
- 服务端中间件里直接做页面跳转决策
- 页面组件里再重复做一遍登录态判断
- 某些逻辑在 SSR 首次进入和客户端跳转时表现不一致
更稳的做法,是让路由中间件围绕显式 route meta 工作。
export default defineNuxtRouteMiddleware((to) => {
const session = useUserSession()
if (to.meta.requiresAuth && !session.loggedIn.value) {
return navigateTo('/login')
}
if (to.meta.roles?.length) {
const allowed = to.meta.roles.some((role: string) => {
return session.user.value?.roles.includes(role)
})
if (!allowed) {
return navigateTo('/403')
}
}
})这样页面准入逻辑就变成了:
- route meta 声明需求
- middleware 统一解释需求
- 页面只消费最终准入结果
插件解决的是应用级能力初始化,不是业务逻辑收纳箱
插件是 Nuxt 里最容易被过度使用的一层。
一旦项目规模变大,大家很容易把下面这些东西都塞进插件:
- API 客户端
- 埋点系统
- 全局通知能力
- 当前用户状态
- 页面级默认请求
- 权限判断
- 错误上报
最后插件就会失去边界。
更稳的原则是:插件只初始化和注入“应用级能力”,不直接承担具体业务页面逻辑。
export default defineNuxtPlugin(() => {
const api = createApiClient()
return {
provide: {
api,
},
}
})在这套结构里:
- 插件只负责把能力放进应用容器
- composable 负责消费并组织调用方式
- 页面负责在业务上下文里使用它
这样插件层就不会继续膨胀成全局业务收纳箱。
.server、.client、无后缀插件要按运行环境切清楚
Nuxt 插件最大的优势之一,就是可以天然按运行环境切分。但如果不提前定义规则,后面会很容易混乱。
更稳的切分方式通常是:
适合 .server 插件的
- 只在服务端记录请求日志
- 只在服务端操作请求上下文
- 只在服务端注入与 Node 环境相关的能力
适合 .client 插件的
- 只在浏览器中初始化 SDK
- 只在客户端接入埋点、可视化或性能采集
- 只在客户端使用
window、document、performance
适合无后缀插件的
- 双端都要可用的轻量注入能力
- 运行环境无强依赖的应用级能力
- 只做注入,不做浏览器或服务端专属副作用
如果这个规则不先定下来,就会出现:
- 本该只在服务端执行的逻辑跑到客户端
- 本该只在客户端初始化的逻辑进入 SSR 路径
- 双端插件里夹带运行环境判断,读起来越来越重
页面消费层要只消费能力,不理解底层顺序
很多 Nuxt 项目后期会出现一个信号:页面组件越来越懂“底层运行时顺序”。
比如页面里开始知道:
- 哪个插件先执行
- 哪个中间件会改什么状态
- 什么时候请求上下文准备完成
- 当前能力到底是服务端注入还是客户端注入
这其实说明边界已经失守了。
更稳的目标应该是:页面只消费稳定的能力接口,而不理解底层顺序细节。
export function useApiClient() {
return useNuxtApp().$api
}一旦页面只依赖 useApiClient()、useUserSession() 这种稳定入口,底层中间件和插件顺序就能被藏在更低层里,不会继续污染消费层。
应用级钩子和页面级副作用要分开
Nuxt 生命周期素材里还有一个特别值得沉淀的点:不是所有副作用都应该写在页面里。
更稳的判断方式通常是:
- 应用启动、运行时初始化、全局监听:更适合放插件和应用钩子
- 页面进入后的业务请求和局部交互:更适合放页面 / composable
- 请求级前置处理:更适合放 Nitro 中间件
如果这些副作用不拆开,就会出现:
- 页面里写全局监听逻辑
- 插件里写页面特定请求
- 服务端请求逻辑和客户端行为初始化耦合在一起
这类问题通常不是代码能不能运行的问题,而是后续维护时谁都不敢改的问题。
更适合现代 Nuxt 的组织方式
如果把这类“中间件、插件、页面逻辑混写”的结构迁到现代 Nuxt,更推荐的目录边界通常是:
server/middleware/*.tsapp/middleware/*.tsapp/plugins/*.tscomposables/runtime/*.tspages/**/*.vue
在这套结构里:
server/middleware只处理请求级前置逻辑app/middleware只处理路由导航准入app/plugins只处理应用级能力注入composables/runtime只负责消费这些能力- 页面只负责业务装配
这和前面几个案例的方法是完全一致的:
- 先找到复杂度中心
- 先稳定运行时边界
- 再拆能力层与消费层
- 最后再优化页面内部实现
这类项目最值得先检查的 6 个问题
以后再遇到类似“Nuxt 页面很多、插件很多、中间件很多”的项目,可以先检查这几个问题:
- 哪些逻辑属于请求级、导航级、应用级、页面级
- Nitro 中间件和路由中间件是否已经职责重叠
- 插件是不是只在做能力注入,而不是混入业务逻辑
.server、.client、无后缀插件是否已经按运行环境分层- 页面是否只消费稳定能力,而不是理解底层顺序
- 应用级钩子和页面级副作用是否已经分开
如果这 6 个问题答不清楚,就说明当前 Nuxt 运行时边界还没有真正收口。
这篇案例最后沉淀出的核心方法
这轮最重要的不是多建几个 middleware 或 plugin 文件,而是沉淀出一条可复用的 Nuxt 运行时重构思路:
- 先把请求级、导航级、应用级、页面级职责拆开
- 再把插件从业务逻辑里解耦出来
- 再让页面只消费稳定能力入口
- 最后让运行时顺序被封装在底层,而不是暴露给页面
这样以后面对后台工作台、内容平台、多权限系统、Nuxt 全栈应用这些场景时,中间件、插件和运行时注入边界都能落在同一套稳定框架里,而不会每个项目都重新发散一套目录和逻辑。
