服务器兼容性

构建一个模块,让它既能用于 Nuxt 4,也能用于 Nuxt 5,无论底层使用哪种服务器运行时。

Nuxt 支持多种服务器运行时。Nuxt 3 和 4 以 Nitro v2(nitropack)为目标,它使用 h3 v1;Nuxt 5 默认使用 Nitro v3(nitro),它使用 h3 v2。用户也可以使用基于 Web API、可在各种环境中兼容运行的自定义服务器运行时(nuxt/server)。

为了帮助从 Nuxt 4 升级,针对 Nitro v2 编写的模块代码可以通过临时兼容层继续运行。本指南将介绍该兼容层的作用,以及如何迁移到对 Nuxt 4 和 5 的完整支持

如果你的模块没有服务端代码,则本文内容与你无关。

兼容层

当模块的服务端代码文件从 h3、nitropack、nitropack/runtime、#internal/nitro 或 #imports 导入内容时,就会进入兼容层。(导入 nuxt/server 或 nitro/* 的文件不受影响。)

在此兼容层中,h3 会解析为基于 h3 v2 构建的 h3 v1 辅助工具接口,nitropack 标识符会解析为对应的 Nitro v3 实现,useRuntimeConfig(event) 会保持其按请求运行的行为,并且我们会注入 v1 代码可能会读取的 event.node、event.context.nitro 和 event.context._nitro.routeRules 结构。

为了帮助迁移,Nuxt 会记录应用了兼容层的模块:

WARN [NUXT_B9003] Nitro v2 compatibility was applied to server code from 2 modules, because of what it imports:
  - @nuxtjs/robots (imports `h3`, `nitropack/runtime`, `#imports`)
  - nuxt-site-config (imports `h3`, `nitropack/runtime`)
兼容层涵盖你自己的代码导入的标识符。它不会提供 nitropack v2 恰好提升到服务端打包文件中的包,例如 lru-cache 等。请务必在 package.json 中声明模块的依赖。

推荐的迁移方式

nuxt/server 是基于 Web API 编写通用服务端代码的导入接口,且 Nuxt 4.6+ 已包含该接口。使用它编写的代码可在 nitropack v2、Nitro v3 以及其他服务器构建器(例如 @nuxt/vite-server)下运行。

事件处理程序

如果你要注册事件处理程序,请保留现有处理程序(以兼容 Nuxt <4.6),并在旁边使用 nuxt/server 添加一个新的可移植处理程序,然后将两者都注册。Nuxt 会注册应用能够运行的实现:在 Nitro v3 和其他构建器下使用可移植文件,在仍运行 nitropack v2 的主机上使用 v2 文件。

module.ts
import { addServerHandler, createResolver, defineNuxtModule } from '@nuxt/kit'

export default defineNuxtModule({
  meta: { name: 'my-module' },
  setup () {
    const { resolve } = createResolver(import.meta.url)

    addServerHandler({
      route: '/api/my-module/status',
      handler: {
        nuxt: resolve('./runtime/server/status'),
        nitro2: resolve('./runtime/server/status.legacy'),
      },
    })
  },
})

两个文件的导入内容不同;如果处理程序使用了 h3 v1 辅助函数,或使用了 nuxt/server 处理方式不同的行为,也需要相应调整:

import { defineEventHandler, useRuntimeConfig } from 'nuxt/server'

export default defineEventHandler(() => ({
  version: useRuntimeConfig().public.myModule.version,
}))
查看 h3 v1 与 nuxt/server 之间的所有差异,以及相应的替代写法。

这些键表示每个文件所依赖的服务端 API:

键文件导入内容运行环境
nuxt仅 nuxt/serverNuxt 4.6 起的任何服务器构建器
nitro2h3、nitropack/runtime、#imports直接运行于 nitropack v2,或通过兼容层运行于 Nitro v3
nitro3nitro、nitro/h3,单独使用或与 nuxt/server 一起使用Nitro 服务器构建器,v3

Nuxt 会根据用户使用的服务器构建器选择最合适的处理程序。如果你注册的处理程序无法由用户的服务器构建器运行,该处理程序会被跳过并发出警告。

仅当代码需要 Nitro v3 独有的 API 时才使用 nitro3。使用 nuxt/server 能让你的代码在未来保持更好的可移植性。

使用这种模式意味着你将同时支持旧版(<4.6)和新版 Nuxt(包括 Nuxt 5)。

handler 的对象形式需要在模块的 dependencies 中使用 @nuxt/kit@^4.6,模块起始模板会为你完成此设置。如果 @nuxt/kit 只是 devDependency 并在构建时保持为外部依赖,或是 peerDependency,则会使用应用程序的版本;低于 4.6 的版本无法处理对象形式的 handler。

要对 nuxt 文件进行类型检查,请将 nuxt@>=4.6 安装为 devDependency。要对 nitro3 文件进行类型检查,还需要安装 nitro。

如果模块自己的别名在两个 Nitro 主版本中都能解析到正确的代码,可以为两个键注册同一个文件({ nitro2: file, nitro3: file }),或设置 meta.compatibility.server。

在 nitropack v2 主机上,Nuxt 始终优先使用 nitro2 变体,因此只有在没有注册其他变体时才会运行 nuxt 变体。请在 Nuxt 5 上测试 nuxt 变体。

在 Nuxt 5 的 playground 和测试固件中,h3 自动导入的 defineEventHandler 不可用,请从 nuxt/server 导入它。

开发服务器处理程序

addDevServerHandler 使用相同的键,但每个变体都是模块代码中的函数,它运行在 Nuxt 进程中,而不是服务器构建中。nuxt/server 仅适用于服务器代码,因此不要从模块代码中导入它,即使是延迟导入也不行。

相反,请针对 Web Request 和 Response 编写处理程序,在 setup 中从 nuxt.options 读取配置,并将其适配到各个服务器 API:

module.ts
import { addDevServerHandler, defineNuxtModule } from '@nuxt/kit'
import { defineEventHandler, toWebRequest } from 'h3'

export default defineNuxtModule({
  meta: { name: 'my-module' },
  setup (_options, nuxt) {
    const greeting = nuxt.options.runtimeConfig.public.myModule.greeting

    function greet (request: Request) {
      const name = new URL(request.url).searchParams.get('name')
      if (!name) {
        return new Response('Missing name', { status: 400 })
      }
      return Response.json({ message: `${greeting}, ${name}!` })
    }

    addDevServerHandler({
      route: '/_my-module/greet',
      handler: {
        nuxt: event => greet(event.req),
        nitro2: defineEventHandler(event => greet(toWebRequest(event))),
      },
    })
  },
})

nuxt 变体接收 RequestEvent 并读取其中的 event.req,nitro2 变体则使用 toWebRequest() 转换 h3 v1 event。由于模块导入 h3 v1,请在模块的 dependencies 中添加 h3@^1。

开发服务器处理程序的路由在各个服务器 API 上并不匹配相同的路径。nuxt 和 nitro3 变体会精确匹配路由。nitro2 处理程序会像 Nitro v2 一样将其作为路径前缀匹配,因此 /_my-module 也会匹配 /_my-module/inter.woff2。Nuxt 将单个函数视为 nitro2 处理程序。

如果希望每个变体都处理某个路由下的路径,请在路由末尾加上 /**:

module.ts
addDevServerHandler({
  route: '/_my-module/assets/**',
  handler: {
    nuxt: event => serveAsset(event.req),
    nitro2: defineEventHandler(event => serveAsset(toWebRequest(event))),
  },
})

这里的 serveAsset 接收一个 Request 并返回一个 Response,就像前面示例中的 greet。该路由匹配 /_my-module/assets 及其下的所有路径,因此无需再次注册基础路径。在 nitropack v2 主机上,模块的 dependencies 中需要 @nuxt/kit@^4.6.1。

nitro2 处理程序会读取相对于路由基础路径的 event.path,因为 Nitro v2 会移除基础路径。对于 /_my-module/assets/inter.woff2 请求,event.path 是 /inter.woff2。完整 URL 仍可从 toWebRequest() 获取,而 nuxt 变体从 event.req.url 读取它。

服务端插件

服务端插件也支持变体,不过它们专用于 Nitro,因此没有可注册的通用 nuxt 变体。

由于来自 nitro 的 definePlugin() 和来自 nitropack/runtime 的 defineNitroPlugin() 都是恒等函数,你或许可以用类型注解替换它们,从而编写同时适用于 nitro2 和 nitro3 的代码。
module.ts
import { addNitroPlugin, createResolver, defineNuxtModule } from '@nuxt/kit'

export default defineNuxtModule({
  meta: { name: 'my-module' },
  setup () {
    const { resolve } = createResolver(import.meta.url)

    addNitroPlugin({
      nitro3: resolve('./runtime/server/plugin'),
      nitro2: resolve('./runtime/server/plugin.legacy'),
    })
  },
})

其他变体

对于其他指向服务器代码的内容,例如别名、类型模板或读取服务器 event 的应用插件,可以使用 resolveServerVariant 解析适用于当前 Nuxt 版本的变体;如果没有可运行的变体,则返回 undefined。

addServerImports 的 from 属性也接受相同的变体。

module.ts
import { addServerImports, createResolver, defineNuxtModule, resolveServerVariant } from '@nuxt/kit'

export default defineNuxtModule({
  meta: { name: 'my-module' },
  setup (_options, nuxt) {
    const { resolve } = createResolver(import.meta.url)

    const someServerFile = resolveServerVariant({
      nuxt: resolve('./runtime/server'),
      nitro2: resolve('./runtime/server.legacy'),
    })

    if (someServerFile) {
      nuxt.options.alias['#my-module/server'] = someServerFile
    }

    addServerImports({
      name: 'verifyToken',
      from: { nuxt: resolve('./runtime/verify'), nitro2: resolve('./runtime/verify.legacy') },
    })
  },
})

在 nitropack v2 上运行时,nitro2 始终优先于 nuxt;在 Nitro v3 上运行时,nitro3 始终优先于 nuxt。因此,如果需要测试 nuxt 变体,请在测试固件中将 server.builder 设置为非 Nitro 的构建器,例如 @nuxt/vite-server。

声明无法推断的信息

Nuxt 会读取每个已注册文件及其导入文件的导入内容,以判断它使用的 API,因此你无需声明兼容性。类型导入(import type)以及运行时目录中没有被已注册文件导入的文件都会被忽略。但如果这还不够,你可以设置 meta.compatibility.server:

module.ts
export default defineNuxtModule({
  meta: {
    name: 'my-module',
    compatibility: { server: 'nuxt' },
  },
})

如果我们无法判断,代码将被视为 nitro2。

Nuxt 只读取模块自身文件的导入内容,不会读取这些文件所调用的包。如果一个 nuxt 文件调用了内部导入 Nitro 的库,该文件仍会被接受,但在非 Nitro 构建器下会失败。

类型化运行时配置和 Event

要为模块添加的运行时配置键提供类型,请从 nuxt/schema 扩展 RuntimeConfig(或 PublicRuntimeConfig)。这些键随后会在 nuxt、nitro2 和 nitro3 文件中获得相同的类型支持。

module.ts
declare module 'nuxt/schema' {
  interface RuntimeConfig {
    myModule: { apiKey: string }
  }
}

特定于构建器的类型(例如 useRequestEvent() 使用的 H3Event,或 nitro:config 钩子中的完整 Nitro 配置)来自生成的 .nuxt/nuxt.d.ts。没有该文件时,它们会解析为与构建器无关的类型(RequestEvent 和精简的 Nitro 配置)。要支持所有服务器构建器,请让类型工具接收基于 RequestEvent 的 event。

插件、存储和缓存

基于服务器插件、存储或缓存构建的模块没有 nuxt 变体,因为 nuxt/server 不涵盖这些领域。现在将其文件注册为 nitro2,迁移后再添加 nitro3 变体。

Nitro 专属代码

nuxt/server 涵盖请求和响应相关工作:处理程序、错误、路由器参数、查询和请求体的读取与验证、标头、CORS、Cookie、会话、重定向、路由规则、运行时配置、应用配置、使用 serverFetch() 的进程内 fetch,以及使用 useServerHooks() 的运行时钩子。但 Nitro 涵盖的内容要多得多,因此,如果你需要以下任何功能,可能需要在 nitro2 或 nitro3 文件中继续使用 Nitro 自身的导入项。

请使用与文件对应版本的标识符。导入 nitro/* 的文件会被视为 Nitro 3 代码,因此 nitro2 文件必须改为导入 h3 和 nitropack/*:

范围在 nitro3 文件中在 nitro2 文件中
存储:useStorage() 及其驱动程序配置nitro/storagenitropack/runtime
缓存:defineCachedHandler()、defineCachedFunction()nitro/cachenitropack/runtime
服务端插件来自 nitro 的 definePlugin()来自 nitropack/runtime 的 defineNitroPlugin()
Nitro 自身的运行时钩子,例如 request、response 和 error来自 nitro/app 的 useNitroHooks()来自 nitropack/runtime 的 useNitroApp().hooks
应用内相对 fetch来自 nitro 的 serverFetch()来自 nitropack/runtime 的 useNitroApp().localFetch
延迟处理程序:lazyEventHandler()nitro/h3h3
任务:defineTask()、runTask()nitro/tasknitropack/runtime
查看哪些 h3 辅助工具适用于可移植事件,哪些需要 Nitro 专属工具。

Nitro 中值得注意的变更

这些行为在 Nitro v3 中发生了变化,可能需要你进一步更新代码:

  • 路由中间件不再对其子路径运行。 nitropack v2 会将中间件挂载到某个路由上,使其在该路由及其所有下级路径(例如 /path/subpath)上运行,并从 event.path 中移除该路由。Nitro v3 则会像匹配其他处理程序一样进行精确匹配。更新时,如果你希望涵盖这两种情况,请将 route 扩展为 /path/**,并从 event.path 中读取完整路径。
  • 未注册 route 的处理程序会被视为全局中间件。 Nitro v3 要求指定路由。如果需要中间件,请使用 middleware: true 并指定明确的路由,即使你希望它在所有路径 /** 上运行也是如此。
  • beforeResponse 和 afterResponse 不是 Nitro v3 钩子。 它们在兼容层中可以正常工作,但你应该改用 response 钩子,该钩子会接收构建好的 Response。请注意(即使使用兼容层),替换 response.body 或在流式传输期间读取它的钩子也不受支持。
  • Nitro 3 中不存在 globalThis.$fetch。 对于应用的路由,请使用来自 nuxt/server 的 serverFetch(event, path)。你也可以使用 event.$fetch,或从 #imports/server 导入 $fetch。
  • import.meta.url 不再指向服务端入口。 Nitro v2 会将它重写为指向入口的全局值,但 Nitro v3 会输出实际值。请改为从你控制的路径解析资源。