Skip to content

Nuxt中间件与插件注入边界重构

这次整理的对象主要来自下面这些本地素材:

  • 11Vue学习/Nuxt4/note/nuxt-lifecycle.md
  • 11Vue学习/Nuxt4/核心概念/2Lifecycle.md
  • 11Vue学习/组件化思维/认证请求与路由权限链路重构.md
  • 11Vue学习/组件化思维/错误边界与客户端降级渲染重构.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
  • 构造请求上下文
  • 请求级日志记录
  • 服务端鉴权前置检查

它不适合直接处理这些问题:

  • 当前页面应该跳去哪里
  • 某个页面应该展示什么布局
  • 某个组件如何消费认证信息

更稳的做法,是把请求级上下文先准备好,再让后续层去消费。

ts
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 工作。

ts
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 客户端
  • 埋点系统
  • 全局通知能力
  • 当前用户状态
  • 页面级默认请求
  • 权限判断
  • 错误上报

最后插件就会失去边界。

更稳的原则是:插件只初始化和注入“应用级能力”,不直接承担具体业务页面逻辑。

ts
export default defineNuxtPlugin(() => {
  const api = createApiClient()

  return {
    provide: {
      api,
    },
  }
})

在这套结构里:

  • 插件只负责把能力放进应用容器
  • composable 负责消费并组织调用方式
  • 页面负责在业务上下文里使用它

这样插件层就不会继续膨胀成全局业务收纳箱。

.server.client、无后缀插件要按运行环境切清楚

Nuxt 插件最大的优势之一,就是可以天然按运行环境切分。但如果不提前定义规则,后面会很容易混乱。

更稳的切分方式通常是:

适合 .server 插件的

  • 只在服务端记录请求日志
  • 只在服务端操作请求上下文
  • 只在服务端注入与 Node 环境相关的能力

适合 .client 插件的

  • 只在浏览器中初始化 SDK
  • 只在客户端接入埋点、可视化或性能采集
  • 只在客户端使用 windowdocumentperformance

适合无后缀插件的

  • 双端都要可用的轻量注入能力
  • 运行环境无强依赖的应用级能力
  • 只做注入,不做浏览器或服务端专属副作用

如果这个规则不先定下来,就会出现:

  • 本该只在服务端执行的逻辑跑到客户端
  • 本该只在客户端初始化的逻辑进入 SSR 路径
  • 双端插件里夹带运行环境判断,读起来越来越重

页面消费层要只消费能力,不理解底层顺序

很多 Nuxt 项目后期会出现一个信号:页面组件越来越懂“底层运行时顺序”。

比如页面里开始知道:

  • 哪个插件先执行
  • 哪个中间件会改什么状态
  • 什么时候请求上下文准备完成
  • 当前能力到底是服务端注入还是客户端注入

这其实说明边界已经失守了。

更稳的目标应该是:页面只消费稳定的能力接口,而不理解底层顺序细节。

ts
export function useApiClient() {
  return useNuxtApp().$api
}

一旦页面只依赖 useApiClient()useUserSession() 这种稳定入口,底层中间件和插件顺序就能被藏在更低层里,不会继续污染消费层。

应用级钩子和页面级副作用要分开

Nuxt 生命周期素材里还有一个特别值得沉淀的点:不是所有副作用都应该写在页面里。

更稳的判断方式通常是:

  • 应用启动、运行时初始化、全局监听:更适合放插件和应用钩子
  • 页面进入后的业务请求和局部交互:更适合放页面 / composable
  • 请求级前置处理:更适合放 Nitro 中间件

如果这些副作用不拆开,就会出现:

  • 页面里写全局监听逻辑
  • 插件里写页面特定请求
  • 服务端请求逻辑和客户端行为初始化耦合在一起

这类问题通常不是代码能不能运行的问题,而是后续维护时谁都不敢改的问题。

更适合现代 Nuxt 的组织方式

如果把这类“中间件、插件、页面逻辑混写”的结构迁到现代 Nuxt,更推荐的目录边界通常是:

  • server/middleware/*.ts
  • app/middleware/*.ts
  • app/plugins/*.ts
  • composables/runtime/*.ts
  • pages/**/*.vue

在这套结构里:

  • server/middleware 只处理请求级前置逻辑
  • app/middleware 只处理路由导航准入
  • app/plugins 只处理应用级能力注入
  • composables/runtime 只负责消费这些能力
  • 页面只负责业务装配

这和前面几个案例的方法是完全一致的:

  • 先找到复杂度中心
  • 先稳定运行时边界
  • 再拆能力层与消费层
  • 最后再优化页面内部实现

这类项目最值得先检查的 6 个问题

以后再遇到类似“Nuxt 页面很多、插件很多、中间件很多”的项目,可以先检查这几个问题:

  1. 哪些逻辑属于请求级、导航级、应用级、页面级
  2. Nitro 中间件和路由中间件是否已经职责重叠
  3. 插件是不是只在做能力注入,而不是混入业务逻辑
  4. .server.client、无后缀插件是否已经按运行环境分层
  5. 页面是否只消费稳定能力,而不是理解底层顺序
  6. 应用级钩子和页面级副作用是否已经分开

如果这 6 个问题答不清楚,就说明当前 Nuxt 运行时边界还没有真正收口。

这篇案例最后沉淀出的核心方法

这轮最重要的不是多建几个 middlewareplugin 文件,而是沉淀出一条可复用的 Nuxt 运行时重构思路:

  • 先把请求级、导航级、应用级、页面级职责拆开
  • 再把插件从业务逻辑里解耦出来
  • 再让页面只消费稳定能力入口
  • 最后让运行时顺序被封装在底层,而不是暴露给页面

这样以后面对后台工作台、内容平台、多权限系统、Nuxt 全栈应用这些场景时,中间件、插件和运行时注入边界都能落在同一套稳定框架里,而不会每个项目都重新发散一套目录和逻辑。

共 20 个模块,1301 篇 Markdown 文档。