# Nitro 中文文档 > 构建可部署到任何地方的全栈服务器,支持零配置部署到任何你喜欢的托管平台。 --- # Home ::page-hero #title 构建 [服务器]{.text-brand} #description Nitro 构建可在任何地方运行的生产就绪服务器。 编写 API 路由,然后无需任何配置即可将同一代码库部署到 Node.js、Bun、Deno 或无服务器环境。 #links :app-hero-links :: ::stat-strip --- # `animateFrom` 仅表示每个计数器开始滚动时的数值。它是一种视觉 # 效果,没有实际含义,绝不代表真实测量值。 stats: - value: "3.8 kB" label: Gzip 压缩后输出 animateFrom: 100 - value: "0" label: 运行时依赖 animateFrom: 35 - value: "25+" label: 部署目标 animateFrom: 0 - value: "~50 ms" label: 冷启动 animateFrom: 350 --- :: ::feature-section --- eyebrow: 路由 link: /docs/routing link-label: 路由文档 points: - 路由会被编译,包中不会包含运行时路由器 - 动态参数、通配符和按方法区分的文件 - 路由组可以整理文件,而不会改变 URL --- #title 文件即路由 #description 将文件放入 `routes/`,它就会成为一个路由。在文件名后追加 HTTP 方法来限定它,将文件夹嵌套以表示路径参数,并用括号包裹文件夹来对路由分组,而不会影响 URL。 #visual :::route-map --- dir: routes/ routes: - file: hello.get.ts method: GET route: /hello - file: hello.post.ts method: POST route: /hello - file: api/test.ts route: /api/test - file: api/[org]/[repo].ts route: /api/:org/:repo - file: (admin)/users.ts route: /users --- ::: :: ::feature-section --- eyebrow: 服务器入口 reverse: true link: /docs/server-entry link-label: 服务器入口文档 --- #title 使用你自己的框架 #description 任何支持 Web `fetch(request): Response` 接口的框架都可以作为你的服务器入口,只需在 `server.ts` 中导出它,它就会在匹配路由之前处理每个请求。Node 风格的 `(req, res)` 框架同样适用:将文件命名为 `server.node.ts`,Nitro 会对其进行适配。 #visual :::tabs ::::tab{label="H3" icon="i-unjs-h3"} ```ts [server.ts] import { H3 } from "h3"; const app = new H3(); app.get("/", () => "⚡️ Hello from H3!"); export default app; ``` :::: ::::tab{label="Hono" icon="i-logos-hono"} ```ts [server.ts] import { Hono } from "hono"; const app = new Hono(); app.get("/", (c) => c.text("🔥 Hello from Hono!")); export default app; ``` :::: ::::tab{label="Elysia" icon="i-skill-icons-elysia-dark"} ```ts [server.ts] import { Elysia } from "elysia"; const app = new Elysia(); app.get("/", () => "🦊 Hello from Elysia!"); export default app.compile(); ``` :::: ::::tab{label="Express" icon="i-simple-icons-express"} ```ts [server.node.ts] import Express from "express"; const app = Express(); app.use("/", (_req, res) => { res.send("Hello from Express with Nitro!"); }); export default app; ``` :::: ::: :: ::feature-section --- eyebrow: 部署 link: /deploy link-label: 所有部署目标 points: - CI 中会自动检测提供商,无需安装适配器 - 通过单个 `preset` 选项切换目标 - 兼容性日期可让提供商行为长期保持稳定 --- #title 一个代码库,适用于所有平台 #description 同一个服务器可以构建到 Node.js、Deno、Bun、边缘 Worker 和无服务器函数。Nitro 会生成每个主机所需的输出格式,因此在它们之间迁移无需修改代码! #visual :::deploy-grid --- more: 25+ 个目标 moreTo: /deploy targets: - name: Node.js icon: i-simple-icons-nodedotjs to: /deploy/runtimes/node - name: Deno icon: i-simple-icons-deno to: /deploy/runtimes/deno - name: Bun icon: i-simple-icons-bun to: /deploy/runtimes/bun - name: Cloudflare icon: i-simple-icons-cloudflare to: /deploy/providers/cloudflare - name: Vercel icon: i-simple-icons-vercel to: /deploy/providers/vercel - name: Netlify icon: i-simple-icons-netlify to: /deploy/providers/netlify - name: AWS Lambda icon: i-simple-icons-awslambda to: /deploy/providers/aws - name: Azure icon: i-simple-icons-microsoftazure to: /deploy/providers/azure - name: Firebase icon: i-simple-icons-firebase to: /deploy/providers/firebase - name: DigitalOcean icon: i-simple-icons-digitalocean to: /deploy/providers/digitalocean - name: Render icon: i-simple-icons-render to: /deploy/providers/render --- ::: :: ::feature-section --- eyebrow: 输出 reverse: true link: /docs/quick-start link-label: 构建你的第一个服务器 --- #title 小到足以阅读 #description 一个最小 Nitro 服务器的生产构建仅包含三个文件,无需在旁边安装任何内容。依赖会被打包并进行 Tree Shaking,因此你部署的就是自己编写的代码,以及让它运行所需的少量代码。 #visual :::bar-chart --- title: 生产服务器输出 data: - label: 未压缩 value: 16.2 display: 16.2 kB - label: 已压缩 value: 8.8 display: 8.8 kB - label: 已压缩 + Gzip 压缩 value: 3.8 display: 3.8 kB highlight: true --- ::: :: ::feature-section --- eyebrow: 缓存 link: /docs/cache link-label: 缓存文档 points: - 默认使用 stale-while-revalidate 响应 - 开箱即用地支持 ETag、last-modified 和 304 处理 - 对同一键的并发请求共享单次调用 --- #title 与存储相匹配的缓存 #description 包装一个处理器或任意异步函数,Nitro 就会在你的应用已经使用的同一存储层上缓存其结果:开发环境中使用内存,生产环境中则使用 Redis、Cloudflare KV、Vercel Blob 存储或文件系统。代码相同,后端不同。 #visual ```ts [routes/stars.ts] import { defineCachedHandler } from "nitro/cache"; export default defineCachedHandler( async () => { const res = await fetch("https://api.github.com/repos/nitrojs/nitro"); const { stargazers_count } = await res.json(); return { stars: stargazers_count }; }, { maxAge: 60 * 60 } ); ``` :: ::feature-grid --- eyebrow: 内置功能 title: 服务器所需的一切 description: 大多数应用都会用到的功能都已随 Nitro 一起提供,并且在每个部署目标上的行为都相同。 features: - title: KV 存储 description: 在文件系统、Redis、Cloudflare KV 等之上提供统一的键值 API。 icon: i-lucide-hard-drive to: /docs/storage - title: 数据库 description: 由 db0 驱动的 SQL 层,已预配置 SQLite。 icon: i-lucide-database badge: 实验性 to: /docs/database - title: 任务 description: 一次性运行时操作,可从 CLI 运行或按 cron 计划运行。 icon: i-lucide-list-checks badge: 实验性 to: /docs/tasks - title: WebSockets description: 基于 crossws 构建的跨运行时 WebSocket 支持。 icon: i-lucide-radio to: /docs/websocket - title: 插件 description: 从 plugins/ 目录接入服务器生命周期。 icon: i-lucide-plug to: /docs/plugins - title: OpenAPI description: 根据你的处理器生成规范,并通过 Scalar 或 Swagger UI 提供服务。 icon: i-lucide-file-json badge: 实验性 to: /docs/openapi - title: 资源 description: 直接提供公共文件,在运行时读取服务器资源。 icon: i-lucide-image to: /docs/assets - title: 渲染器 description: 用于处理未匹配路由的兜底处理器,可用于 SSR、SPA 外壳或纯 HTML。 icon: i-lucide-layout-template to: /docs/renderer - title: 生命周期 description: 请求按顺序经过的每一层,以及拦截请求的位置。 icon: i-lucide-activity to: /docs/lifecycle --- :: ::call-to-action{command="create-nitro-app"} #title 从一条命令开始 #description 创建项目脚手架,或将 Vite 插件添加到你已有的应用中。 #actions :app-hero-links :: ::sponsors --- # 简介 > Nitro 是一个全栈服务器框架,兼容任何运行环境和任何部署目标。 Nitro 为您提供了一个可用于生产环境的服务器,具备文件系统路由、代码拆分,以及对存储、缓存和数据库的内置支持,完全与运行时无关,并且可以部署到任何地方。 ## 什么是 Nitro? 在 `routes/` 目录中创建服务器和 API 路由。每个文件都会直接映射到一个 URL 路径,而 Nitro 会处理其余工作:[路由](/docs/routing)、代码拆分和优化构建。 您也可以通过创建一个 `server.ts` [服务器入口](/docs/server-entry)来完全掌控服务器。Nitro 与运行时无关的方式让您可以使用任何 HTTP 库,例如 [Elysia](https://elysiajs.com/)、[H3](https://h3.dev) 或 [Hono](https://hono.dev)。 ### 性能 Nitro 在构建时编译您的路由,无需运行时路由器。只需加载和执行处理每个传入请求所需的代码。这使其非常适合无服务器托管,无论项目大小如何,都能实现接近 0 毫秒的启动时间。 ### 随处部署 将您的服务器构建到经过优化的 `.output/` 文件夹中,无需任何配置即可兼容 Node.js、Bun、Deno 和许多托管平台:Cloudflare Workers、Netlify、Vercel 等。无需更改一行代码,即可利用 ESR、ISR 和 SWR 等平台功能。 ### 服务端渲染 使用您喜欢的模板引擎渲染 HTML,或直接在服务器上使用 React、Vue 或 Svelte 等组件库。通过客户端注水实现完全通用渲染。Nitro 提供基础架构和渐进式方法来达成您的目标。 ### 存储 Nitro 开箱即用地提供了与运行时无关的键值 [存储层](/docs/storage)。它默认使用内存存储,但您可以连接 20 多种不同的驱动程序(FS、Redis、S3 等),将它们附加到不同的命名空间,并在不更改代码的情况下进行替换。 ### 缓存 Nitro 支持服务器路由和服务器函数的[缓存](/docs/cache),并直接由服务器存储提供支持(通过 `cache` 命名空间)。 ### 数据库 Nitro 还内置了 SQL [数据库](/docs/database)。它默认使用 SQLite,但您可以使用相同的 API 连接并查询 10 多种数据库(Postgres、MySQL、PGLite 等)。 ### 元框架基础 Nitro 可用作构建您自己元框架的基础。Nuxt、SolidStart 和 TanStack Start 等流行框架都完全或部分地利用了 Nitro。 ## Vite 集成 Nitro 以插件形式与 [Vite](https://vite.dev) 无缝集成。如果您正在使用 Vite 构建前端应用,添加 Nitro 后即可获得 API 路由、服务端渲染和完整的生产服务器,并通过 `vite build` 一起完成构建。 ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()], }); ``` 使用 Nitro 时,`vite build` 会生成一个经过优化的 `.output/` 文件夹,其中同时包含您的前端和后端,可随处部署。 准备好尝试了吗?进入 [快速开始](/docs/quick-start)。 --- # 快速开始 > 从一个全新的 Nitro 项目开始,或在现有的 Vite 项目中采用它。 ## 在线试用 Nitro 在我们的游乐场中通过浏览器体验 Nitro。 [在 StackBlitz 中试用 Nitro](https://stackblitz.com/github/nitrojs/starter/tree/v3-vite?file=index.html,server.ts){target="_blank"} ## 创建 Nitro 项目 创建 Nitro 应用最快的方式是使用 `create-nitro-app`。 > [!NOTE] > 请确保你已安装最新的 LTS 版本 [Node.js](https://nodejs.org/en)、[Bun](https://bun.sh/) 或 [Deno](https://deno.com/)。 :pm-x{command="create-nitro-app"}
预览
预览
按照 CLI 指引操作,你就可以启动开发服务器了。 ## 添加到 Vite 项目 你可以将 Nitro 添加到任何现有的 Vite 项目中,以获得 API 路由、服务端渲染等功能。 ::steps{level="3"} ### 安装 `nitro` 和 `vite` :pm-install{name="nitro vite"} ### 向 Vite 添加 Nitro 插件 将 Nitro 插件添加到你的 `vite.config.ts` 中: ```ts [vite.config.ts] {2,6} import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [ nitro() ], }); ``` ### 配置 Nitro 创建 `nitro.config.ts` 来配置服务器目录: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: "./server", }); ``` `serverDir` 选项告诉 Nitro 在哪里查找服务器路由。在此示例中,所有路由都将位于 `server/` 目录中。默认情况下,`serverDir` 为 `false`,因此在设置它(或 `scanDirs`)之前不会扫描任何目录。 ### 创建 API 路由 在 `server/api/test.ts` 创建你的第一个 API 路由: ::code-tree{defaultValue="server/api/test.ts"} ```ts [server/api/test.ts] import { defineHandler } from "nitro"; export default defineHandler(() => { return { message: "Hello Nitro!" }; }); ``` ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: "./server", }); ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()], }); ``` :: 文件路径会直接映射到路由 URL:`server/api/test.ts` 会变成 `/api/test`。处理器可以返回字符串、JSON 对象、`Response` 实例或可读流。 ::tip 作为文件系统路由的替代方案,你可以使用 `routes` 配置选项以编程方式声明路由。查看 [程序化路由处理器](/docs/routing#programmatic-route-handlers) 了解更多详情。 :: ### 启动开发服务器 :pm-run{script="dev"} 你的 API 路由现在可以通过 `http://localhost:3000/api/test` 访问 :sparkles: :: ## 后续步骤 - 在 [路由](/docs/routing) 中了解动态路由、方法和中间件。 - 使用[渲染器](/docs/renderer)为未匹配的路由提供 HTML 或单页应用。 - 通过[服务器入口](/docs/server-entry)完全控制每个请求。 --- # Nitro 渲染器 > 使用渲染器处理所有未匹配的路由,并返回自定义 HTML 或使用模板系统。 渲染器是 Nitro 中的一个特殊处理程序,用于捕获所有未与任何特定 API 或路由处理程序匹配的路由。它通常用于服务端渲染(SSR)、提供单页应用(SPA)或创建自定义 HTML 响应。 ## 配置 渲染器通过 Nitro 配置中的 `renderer` 选项进行配置: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ renderer: { template: './index.html', // Path to HTML template file handler: './renderer.ts', // Path to custom renderer handler static: false, // Force dynamic template processing (auto-detected when unset) } }) ``` | 选项 | 类型 | 描述 | | --- | --- | --- | | `template` | `string` | 用作渲染器模板的 HTML 文件路径。 | | `handler` | `string` | 自定义渲染器处理程序模块的路径。 | | `static` | `boolean` | 当设为 `true` 时,跳过 rendu 模板处理并按原样提供 HTML。如未设置,将根据模板语法自动检测。 | 在配置中设置 `renderer: false` 以完全禁用渲染器(包括自动检测 `index.html`)。 ## HTML 模板 ### 自动检测 `index.html` 默认情况下,Nitro 会自动在项目根目录或扫描目录之一中查找 `index.html` 文件。 如果找到,Nitro 会将其用作渲染器模板,并为所有未匹配的路由提供服务。 ::code-group ```html [index.html] 我的 Vite + Nitro 应用
``` ```ts [routes/api/hello.ts] import { defineHandler } from "nitro"; export default defineHandler((event) => { return { hello: "API" }; }); ``` :: ::tip 当检测到 `index.html` 时,Nitro 会自动在终端中记录:`Using index.html as renderer template.` :: 在此配置下: - `/api/hello` → 由您的 API 路由处理 - `/about`、`/contact` 等 → 通过 `index.html` 提供服务 ### 自定义 HTML 文件 您可以使用 Nitro 配置中的 `renderer.template` 选项来指定自定义 HTML 模板文件。 ::code-group ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ renderer: { template: './app.html' } }) ``` ```html [app.html] 自定义模板
加载中...
``` :: ### 静态模板 默认情况下,Nitro 会自动检测您的 HTML 模板是否包含 [rendu](#超文本预处理器实验性) 语法。如果包含,则会在每次请求时动态处理模板;如果不包含,则作为静态 HTML 提供。 您可以使用 `static` 选项覆盖此行为: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ renderer: { template: './index.html', static: true // 强制静态提供,跳过模板处理 } }) ``` 在生产环境中,静态模板会被内联到服务器包中并直接提供,以获得最佳性能。 ### 超文本预处理器(实验性) 对于动态模板,Nitro 使用 [rendu](https://github.com/h3js/rendu) 超文本预处理器,这是一种使用 JavaScript 表达式创建动态 HTML 模板的简单而强大的方式。 #### 输出表达式 - `{{ expression }}`:HTML 转义输出 - `{{{ expression }}}` 或 ``:原始(未转义)输出 ```html

你好 {{ $URL.pathname }}

{{{ '原始 html' }}}
``` #### 控制流 使用 `` 进行 JavaScript 控制流: ```html

表单已提交!

``` #### 服务端脚本 使用 `
{{ JSON.stringify(data) }}
``` #### 流式内容 使用 `echo()` 函数进行内容流式传输。它接受字符串、函数、Promise、Response 对象或 ReadableStream: ```html ``` #### 全局变量 在模板中访问请求上下文: | 变量 | 描述 | | --- | --- | | `$REQUEST` | 传入的 `Request` 对象 | | `$METHOD` | HTTP 方法(`GET`、`POST` 等) | | `$URL` | 请求 `URL` 对象 | | `$HEADERS` | 请求头 | | `$RESPONSE` | 响应配置对象 | | `$COOKIES` | 包含请求 Cookie 的只读对象 | #### 内置函数 | 函数 | 描述 | | --- | --- | | `htmlspecialchars(str)` | 转义 HTML 字符(在 `{{ }}` 语法中自动应用) | | `setCookie(name, value, options?)` | 在响应中设置 Cookie | | `redirect(url)` | 将用户重定向到另一个 URL | | `echo(content)` | 将内容流式传输到响应 | ```html [index.html] 动态模板

你好 {{ $REQUEST.url }}

欢迎,!

``` :read-more{to="https://github.com/h3js/rendu" title="Rendu 文档"} ## 自定义渲染器处理程序 对于更复杂的场景,您可以创建以编程方式生成响应的自定义渲染器处理程序。 该处理程序是一个默认导出的函数,接收一个 H3 事件对象。您可以通过 `event.req` 访问传入的 `Request`: ```ts [renderer.ts] export default function renderer({ req }: { req: Request }) { const url = new URL(req.url); return new Response( /* html */ ` 自定义渲染器

来自自定义渲染器的问候!

当前路径:${url.pathname}

`, { headers: { "content-type": "text/html; charset=utf-8" } } ); } ``` 然后,在 Nitro 配置中指定渲染器入口: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ renderer: { handler: './renderer.ts' } }) ``` ::note 当设置了 `renderer.handler` 时,它将完全控制渲染。`renderer.template` 选项将被忽略。 :: ## 渲染器优先级 渲染器始终充当捕获所有路由的通配符路由(`/**`),并具有**最低优先级**。这意味着: 1. 特定的 API 路由首先匹配(例如 `/api/users`) 2. 特定的服务器路由随后匹配(例如 `/about`) 3. 渲染器捕获其他所有内容 ```md api/ users.ts → /api/users(首先匹配) routes/ about.ts → /about(其次匹配) renderer.ts → /**(捕获所有其他路由) ``` ::note 如果您定义了自己的捕获所有路由(`[...].ts`),它会与其他 `/**` 处理程序串联,而不是取代它们:捕获所有处理程序会按照注册顺序运行(基于文件的捕获所有处理程序,然后是[服务器入口](/docs/server-entry),最后是渲染器),第一个返回响应的处理程序获胜。渲染器只会处理您的捕获所有路由未作出响应的请求。 :: :read-more{to="/docs/lifecycle" title="生命周期"} ## Vite 集成 将 Nitro 与 Vite 一起使用时,渲染器会与 Vite 的构建管道和开发服务器集成。 ### 开发模式 在开发过程中,每次请求都会从磁盘读取渲染器模板,因此对 `index.html` 的更改会立即生效,无需重启服务器。Vite 的 `transformIndexHtml` 钩子会被应用,以注入 HMR 客户端脚本和其他开发时转换。 ### 使用 `` 的 SSR 在使用带有 `ssr` 服务的 Vite 环境时,您可以在 `index.html` 中添加 `` 注释。Nitro 会在渲染期间将其替换为您的 SSR 入口的输出: ```html [index.html] SSR 应用
``` ### 生产构建 在生产构建期间,Vite 会通过其构建管道处理 `index.html`(解析脚本、CSS 和其他资源),然后 Nitro 将转换后的 HTML 内联到服务器包中。 ## 使用场景 ### 单页应用(SPA) 为所有路由提供 SPA 的 `index.html` 以启用客户端路由: > [!TIP] > 这是 Nitro 与 Vite 一起使用时的默认行为。 --- # 路由 > Nitro 支持文件系统路由,可自动将文件映射到路由。路由会在构建时进行编译和代码分割,因此无需运行时路由器。 ## 路由处理器 路由处理器是一个接收 `event` 对象([H3Event](https://h3.dev/guide/api/h3event#h3event-properties))并返回响应的函数。 ::code-group ```ts [单个函数] import type { H3Event } from "nitro"; export default (event: H3Event) => { return "world"; } ``` ```ts [defineHandler] import { defineHandler } from "nitro"; // 为了更好的类型推断 export default defineHandler((event) => { return "world"; }); ``` :: ## 文件系统路由 文件会自动映射到 [h3 路由](https://h3.dev/guide/basics/routing)。定义路由非常简单,只需在 [`serverDir`](/docs/configuration) 的 `api/` 或 `routes/` 目录中创建文件。 ::note 文件系统路由要求在配置中设置 [`serverDir`](/docs/configuration)(或 `scanDirs`);默认情况下不会扫描任何目录。 :: 每个文件定义一个处理器,你可以在文件名后[追加 HTTP 方法](#specific-request-method),以匹配特定的请求方法。 ``` routes/ api/ test.ts <-- /api/test hello.get.ts <-- /hello (仅 GET) hello.post.ts <-- /hello (仅 POST) vite.config.ts ``` 你可以通过创建子目录来嵌套路由。 ```txt routes/ api/ [org]/ [repo]/ index.ts <-- /api/:org/:repo issues.ts <-- /api/:org/:repo/issues index.ts <-- /api/:org package.json ``` ### 路由组 要组织相关的路由文件而不影响最终生成的 URL,可以将它们放在用括号 `(` 和 `)` 包裹的文件夹中。 例如: ```txt routes/ api/ (admin)/ users.ts <-- /api/users reports.ts <-- /api/reports (public)/ index.ts <-- /api package.json ``` > [!NOTE] 路由组不会成为路由路径的一部分,仅用于组织文件。 ### 静态路由 在 `routes/` 或 `routes/api/` 目录中创建文件。文件路径会成为路由路径。然后导出一个路由处理器: ```ts [routes/api/test.ts] import { defineHandler } from "nitro"; export default defineHandler(() => { return { hello: "API" }; }); ``` ### 动态路由 #### 单参数 要定义带参数的路由,请使用 `[]` 语法,其中 `` 是参数名称。参数可以通过 `event.context.params` 对象访问。 ```ts [routes/hello/[name\\].ts] import { defineHandler } from "nitro"; export default defineHandler((event) => { const { name } = event.context.params; return `Hello ${name}!`; }); ``` 调用 `/hello/nitro` 会返回: ```txt [响应] Hello nitro! ``` #### 多参数 你可以使用 `[]/[]` 语法在路由中定义多个参数,其中每个参数都是一个文件夹。**不能**在单个文件名或文件夹中定义多个参数。 ```ts [routes/hello/[name\\]/[age\\].ts] import { defineHandler } from "nitro"; export default defineHandler((event) => { const { name, age } = event.context.params; return `Hello ${name}! You are ${age} years old.`; }); ``` #### 捕获所有参数 你可以使用 `[...]` 语法捕获 URL 的所有剩余部分。这将把 `/` 包含在参数中。 ```ts [routes/hello/[...name\\].ts] import { defineHandler } from "nitro"; export default defineHandler((event) => { const { name } = event.context.params; return `Hello ${name}!`; }); ``` 调用 `/hello/nitro/is/hot` 会返回: ```txt [响应] Hello nitro/is/hot! ``` ### 特定请求方法 你可以在文件名后追加 HTTP 方法来强制路由仅匹配特定的 HTTP 请求方法,例如 `hello.get.ts` 将只匹配 `GET` 请求。你可以使用任何你想要的 HTTP 方法。 支持的方法:`get`、`post`、`put`、`delete`、`patch`、`head`、`options`、`query`、`connect`、`trace`。 ::note `query` 匹配 [`QUERY` 方法](https://datatracker.ietf.org/doc/draft-ietf-httpbis-safe-method-w-body/),这是一种携带请求体的安全且幂等的 `GET` 替代方法。在代理、CDN 和部署平台中的支持仍不一致,因此对于公共 API,请保留 `GET` 或 `POST` 回退方案。 :: ::code-group ```ts [GET] // routes/users/[id].get.ts import { defineHandler } from "nitro"; export default defineHandler(async (event) => { const { id } = event.context.params; // 对 id 进行一些操作 return `User profile!`; }); ``` ```ts [POST] // routes/users.post.ts import { defineHandler } from "nitro"; export default defineHandler(async (event) => { const body = await event.req.json(); // 对 body 进行一些操作,比如保存到数据库 return { updated: true }; }); ``` :: ### 捕获所有路由 你可以创建一个特殊路由,用于匹配未被其他路由处理的所有请求,适合作为默认路由或回退路由。 要创建捕获所有路由,请创建一个名为 `[...].ts` 的文件。 ```ts [routes/[...\\].ts] import { defineHandler } from "nitro"; export default defineHandler((event) => { return `Hello ${event.url.pathname}!`; }); ``` ::note 对于未命名的捕获所有路由(`[...].ts`),匹配到的通配符片段可以通过 `event.context.params._` 访问。 :: [服务器入口](/docs/server-entry)和[渲染器](/docs/renderer)也会作为捕获所有处理器,并在基于文件的捕获所有路由之后串联执行。 ### 特定环境的处理器 你可以在文件名中添加 `.dev`、`.prod` 或 `.prerender` 后缀,使路由仅包含在特定构建中,例如:`routes/test.get.dev.ts` 或 `routes/test.get.prod.ts`。 该后缀放在方法后缀之后(如果有的话): ```txt routes/ env/ index.dev.ts <-- /env (仅开发环境) index.get.prod.ts <-- /env (GET, 仅生产环境) ``` > [!TIP] > 要针对多个环境,或使用预设名称作为环境,请通过 [`routes`](#routes-config) 配置以编程方式注册路由。 ### 忽略文件 你可以使用 `ignore` 配置选项来排除文件不被路由扫描。它接受相对于服务器目录的 glob 模式数组。 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ ignore: [ "routes/api/**/_*", // 忽略 api/ 中以 _ 开头的文件 "middleware/_*.ts", // 忽略以 _ 开头的中间件 "routes/_*.ts", // 忽略根路由中以 _ 开头的文件 ], }); ``` ### 路由元数据 你可以在路由处理器文件中使用 `defineRouteMeta` 宏,在构建时定义路由元数据。 > [!IMPORTANT] > 此功能目前处于实验阶段。 ```ts [routes/api/test.ts] import { defineRouteMeta } from "nitro"; import { defineHandler } from "nitro"; defineRouteMeta({ openAPI: { tags: ["test"], description: "Test route description", parameters: [{ in: "query", name: "test", required: true }], }, }); export default defineHandler(() => "OK"); ``` ::read-more{to="https://swagger.io/specification/v3/"} 此功能目前可用于指定 OpenAPI 元数据。有关可用的 OpenAPI 选项,请参阅 Swagger 规范。 :: ## 程序化路由处理器 除了文件系统路由外,你还可以使用 `routes` 配置选项以程序化方式注册路由处理器。 ### `routes` 配置 `routes` 选项允许你将路由模式映射到处理器: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ routes: { "/api/hello": "./server/routes/api/hello.ts", "/api/custom": { handler: "./server/routes/api/hello.ts", method: "POST", lazy: true, }, "/virtual": { handler: "#virtual-route", }, }, }); ``` 每个路由条目可以是一个简单的字符串(处理器路径)或一个具有以下选项的对象: | 选项 | 类型 | 描述 | |--------|------|-------------| | `handler` | `string` | 事件处理器文件或虚拟模块 ID 的路径 | | `method` | `string` | 要匹配的 HTTP 方法(`get`、`post` 等) | | `lazy` | `boolean` | 使用懒加载导入处理器 | | `format` | `"web" \| "node"` | 处理器类型。`"node"` 处理器会被转换为兼容 web 的格式 | | `env` | `string \| string[]` | 包含此处理器的环境(`"dev"`、`"prod"`、`"prerender"` 或预设名称) | ### `handlers` 配置 `handlers` 数组适用于注册具有路由匹配控制权的中间件: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ handlers: [ { route: "/api/**", handler: "./server/utils/api-auth.ts", middleware: true, }, ], }); ``` 每个处理器条目支持以下选项: | 选项 | 类型 | 描述 | |--------|------|-------------| | `route` | `string` | HTTP 路径名模式(例如 `/test`、`/api/:id`、`/blog/**`) | | `handler` | `string` | 事件处理器文件或虚拟模块 ID 的路径 | | `method` | `string` | 要匹配的 HTTP 方法(`get`、`post` 等) | | `middleware` | `boolean` | 在路由处理器之前作为中间件运行处理器 | | `lazy` | `boolean` | 使用懒加载导入处理器 | | `format` | `"web" \| "node"` | 处理器类型。`"node"` 处理器会被转换为兼容 web 的格式 | | `env` | `string \| string[]` | 包含此处理器的环境(`"dev"`、`"prod"`、`"prerender"` 或预设名称) | ## 中间件 中间件在路由处理器之前运行,让你可以检查或扩展传入的请求。有关中间件在请求管道中的位置,请参阅[生命周期](/docs/lifecycle)。 ::tip 中间件可以在请求被处理之前修改请求,但不能在请求处理之后修改。 :: 中间件会从 `middleware/` 目录自动注册。 ```md middleware/ auth.ts logger.ts ... routes/ hello.ts ``` ### 简单中间件 中间件的定义方式与路由处理器完全相同,但有一个例外:它们不应返回任何内容。 ```ts [middleware/auth.ts] import { defineHandler } from "nitro"; export default defineHandler((event) => { // Extend or modify the event event.context.user = { name: "Nitro" }; }); ``` `middleware/` 目录中的中间件会自动为所有路由注册。要为特定路由模式注册中间件,请参阅[路由作用域中间件](#route-scoped-middleware)。 ::warning 从中间件返回值会关闭请求:该值会成为响应,并且不会继续运行后续处理器。请避免从中间件返回内容;应改用路由处理器。 :: ### 执行顺序 中间件按目录列表顺序执行。 ```md middleware/ auth.ts <-- 第一个 logger.ts <-- 第二个 ... <-- 第三个 ``` 在中间件文件名前添加数字以控制它们的执行顺序。 ```md middleware/ 1.logger.ts <-- 第一个 2.auth.ts <-- 第二个 3.... <-- 第三个 ``` ::note 文件名会按字符串排序,因此 `10.filename.ts` 会紧跟在 `1.filename.ts` 之后,并排在 `2.filename.ts` 之前。如果同一目录中有超过 10 个中间件,请在 `1-9` 前加上 `0`(例如 `01.filename.ts`)。 :: ### 请求过滤 全局中间件会在每个请求上运行,但你可以添加自定义条件来限定其作用范围。例如,检查 URL,以便将中间件应用于特定路由: ```ts [middleware/auth.ts] import { defineHandler } from "nitro"; export default defineHandler((event) => { // 仅对 /auth 路由执行 if (event.url.pathname.startsWith('/auth')) { event.context.user = { name: "Nitro" }; } }); ``` ### 路由作用域中间件 你可以使用 [`handlers`](#handlers-config) 配置并设置 `middleware` 选项和特定的 `route` 来为特定路由模式注册中间件: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ handlers: [ { route: "/api/**", handler: "./server/utils/api-auth.ts", middleware: true, }, ], }); ``` 与全局中间件(从 `middleware/` 目录自动注册,并匹配 `/**`)不同,路由作用域中间件只会对匹配指定模式的请求运行。 请将路由作用域处理器文件放在 `middleware/` 目录之外(如上例所示),这样目录扫描不会同时将它们注册为全局中间件。 ## 错误处理 你可以使用 [H3 中提供的工具](https://h3.dev/guide/basics/error)来处理路由处理器和中间件中的错误。 错误发送回客户端的方式取决于环境。在开发环境中,`Accept` 标头为 `text/html` 的请求(例如浏览器请求)会收到 HTML 错误页面。在生产环境中,错误始终以 JSON 形式发送。此行为可能会受到请求属性的影响,例如 `Accept` 或 `User-Agent` 标头。 要使用 `error` hook 全局捕获错误,请参阅[生命周期](/docs/lifecycle#error-handling)。 ## 代码分割 Nitro 为每个路由处理程序创建单独的代码块。代码块在首次请求时按需加载,因此 `/api/users` 不会加载 `/api/posts` 的代码。 查看 [`inlineDynamicImports`](/config#inlinedynamicimports) 以将所有内容打包到单个文件中。 ## 路由规则 路由规则允许你直接从配置中将行为(重定向、代理、缓存、身份验证、自定义标头)附加到路由模式,而无需编写处理器代码。 每条规则都会将路由模式(使用 [rou3](https://github.com/h3js/rou3) 语法)映射到一组选项,这些选项基于 [h3 路由规则](https://h3.dev/guide/rules)。设置 `cache` 选项后,匹配的处理器会自动使用 `defineCachedHandler` 包装。请参阅[缓存指南](/docs/cache)了解更多信息。 使用 `routeRules` 配置选项设置路由规则: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ routeRules: { '/blog/**': { swr: true }, '/blog2/**': { swr: 600 }, '/blog3/**': { static: true }, '/blog4/**': { cache: { /* 缓存选项 */ } }, '/assets/**': { headers: { 'cache-control': 's-maxage=0' } }, '/api/v1/**': { cors: true, headers: { 'access-control-allow-methods': 'GET' } }, '/old-page': { redirect: '/new-page' }, '/old-page/**': { redirect: '/new-page/**' }, '/proxy/example': { proxy: 'https://example.com' }, '/proxy/**': { proxy: '/api/**' }, } }); ``` ### 规则合并与覆盖 路由规则按照从最不具体到最具体的顺序匹配。当多个规则匹配一个请求时,它们的选项会合并,更具体的规则优先。 你可以使用 `false` 来禁用由更通用模式设置的规则: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ routeRules: { '/api/cached/**': { swr: true }, '/api/cached/no-cache': { cache: false, swr: false }, '/api/**': { cors: true }, '/api/internal/**': { cors: false }, } }); ``` ### 按方法限定的规则 在规则键前加上大写的 HTTP 方法(后跟一个空格),即可将规则限定为使用该方法的请求。不带方法前缀的键适用于所有方法。按方法限定的规则会合并到匹配相同路径的、与方法无关的规则之上,因此你可以在共享默认配置的基础上叠加特定于方法的行为: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ routeRules: { // 应用于所有方法 '/api/**': { headers: { 'x-api': 'true' } }, // Only POST requests to /api/** get this header 'POST /api/**': { headers: { 'x-write': 'true' } }, // Cache GET requests to /feed only 'GET /feed': { swr: 600 }, } }); ``` ::note 方法匹配由服务器运行时针对每个请求进行解析。平台原生的静态生成(例如 Netlify/Cloudflare 的 `_headers` 和 `_redirects`、Vercel 的 `config.json`)不会按方法进行拆分,因此对于希望平台写入其静态配置的 `headers`/`redirect`/`proxy` 规则,建议使用与方法无关的键。 :: ### Headers 为匹配的路由设置自定义响应头: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ routeRules: { '/api/**': { headers: { 'cache-control': 's-maxage=60' } }, '**': { headers: { 'x-powered-by': 'Nitro' } }, } }); ``` ### 跨域资源共享(CORS) 在运行时使用 `cors` 规则处理 CORS。`cors: true` 会应用宽松的默认设置(来源、方法和允许的请求头均为 `*`):简单请求会获得 `access-control-allow-origin: *` 和 `access-control-expose-headers: *`,而 `OPTIONS` 预检请求会被直接响应(状态码为 `204`),并返回匹配的 `access-control-allow-*` 请求头。 > [!NOTE] > CORS 由正在运行的服务器(h3 的 [`handleCors`](https://h3.dev/utils/security#handlecorsevent-options))应用,而不是由静态/CDN 配置应用。 > 在可以直接从边缘节点提供预渲染/静态资源的平台上,只有当请求到达服务器处理程序时,才会添加 CORS 请求头。 传入对象可以进行更精细的控制:来源允许列表、`credentials`、`maxAge` 等(h3 `CorsOptions`)。将 `credentials: true` 与通配符来源结合使用是无效的,并会在构建时抛出错误: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ routeRules: { // 宽松的默认设置 '/api/public/**': { cors: true }, // 限制为特定来源并启用凭据 '/api/v1/**': { cors: { origin: ['https://app.example.com'], credentials: true }, }, } }); ``` ### 重定向 将匹配的路由重定向到另一个 URL。使用字符串进行简单重定向(默认为 `307` 状态码),或使用对象进行更精细的控制: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ routeRules: { // 简单重定向(307 状态码) '/old-page': { redirect: '/new-page' }, // 自定义状态码的重定向 '/legacy': { redirect: { to: 'https://example.com/', status: 308 } }, // Wildcard redirect, preserves the path after the pattern '/old-blog/**': { redirect: 'https://blog.example.com/**' }, } }); ``` ### 代理 将请求代理到另一个 URL。支持内部和外部目标: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ routeRules: { // 代理到确切 URL '/api/proxy/example': { proxy: 'https://example.com' }, // 代理到内部路由 '/api/proxy/**': { proxy: '/api/echo' }, // Wildcard proxy, preserves the path after the pattern '/cdn/**': { proxy: 'https://cdn.jsdelivr.net/**' }, // 带选项的代理 '/external/**': { proxy: { to: 'https://api.example.com/**', // 其他 H3 代理选项... }, }, } }); ``` ### 身份验证 没有 `auth` 路由规则:身份验证需要可执行逻辑,因此应放在[中间件](#middleware)中。对于单个路由,请使用 h3 的 [`basicAuth`](https://h3.dev/utils/security#basicauthopts) 中间件: ```ts [routes/admin/index.ts] import { defineHandler } from "nitro"; import { basicAuth } from "nitro/h3"; export default defineHandler({ middleware: [basicAuth({ username: 'admin', password: 'supersecret', realm: 'Admin Area' })], handler: (event) => `Hello, ${event.context.basicAuth?.username}!`, }); ``` 对于整个子树,可以将其注册为[路由作用域中间件](#route-scoped-middleware),或注册为 `middleware/` 目录中的全局中间件。 ### 缓存(SWR / 静态) 使用 `cache`、`swr` 或 `static` 选项控制缓存行为: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ routeRules: { // 启用 stale-while-revalidate 缓存 '/blog/**': { swr: true }, // 带 maxAge(秒)的 SWR '/blog/posts/**': { swr: 600 }, // 完整缓存选项 '/api/data/**': { cache: { maxAge: 60, swr: true, // ...其他缓存选项 }, }, // 禁用缓存 '/api/realtime/**': { cache: false }, } }); ``` ::tip `swr: true` 是 `cache: { swr: true }` 的简写,`swr: ` 是 `cache: { swr: true, maxAge: }` 的简写。 :: ### 预渲染 标记路由以在构建时进行预渲染: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ routeRules: { '/about': { prerender: true }, '/dynamic/**': { prerender: false }, } }); ``` 预渲染路由会在构建时生成一次,并作为静态文件提供。它们不会在运行时重新验证。 ::warning 不要在同一路由上同时使用 `prerender` 和 `isr`。预渲染具有更高优先级,因此 `isr` 会被忽略,页面也不会在部署后重新生成。在支持的平台上需要增量再生时,请单独使用 `isr`。 :: ### ISR(Vercel) 为 Vercel 部署配置增量静态再生: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ routeRules: { '/isr/**': { isr: true }, '/isr-ttl/**': { isr: 60 }, '/isr-custom/**': { isr: { expiration: 60, allowQuery: ['q'], group: 1, }, }, } }); ``` ::note ISR 会按需生成页面,并根据你设置的过期时间重新验证页面。它是构建时 `prerender` 的替代方案,而不是可以在同一路由上与其结合使用的功能。 :: ### 路由规则参考 | 选项 | 类型 | 描述 | |--------|------|-------------| | `headers` | `Record` | 自定义响应标头 | | `redirect` | `string \| { to: string, status?: number }` | 重定向到另一个 URL(默认状态码:`307`) | | `proxy` | `string \| { to: string, ...proxyOptions }` | 将请求代理到另一个 URL | | `cors` | `boolean \| CorsOptions` | 通过 h3 的 [`handleCors`](https://h3.dev/utils/security#handlecorsevent-options) 处理 CORS(`true` = 宽松设置) | | `cache` | `object \| false` | 缓存选项(请参阅[缓存指南](/docs/cache)) | | `swr` | `boolean \| number` | `cache: { swr: true, maxAge: number }` 的简写(`false` 会重置继承的缓存规则) | | `static` | `boolean \| number` | 静态缓存的简写 | | `prerender` | `boolean` | 启用/禁用预渲染 | | `isr` | `boolean \| number \| object` | 增量静态再生(Vercel) | ### 运行时路由规则 路由规则可以通过 `runtimeConfig` 提供,允许通过环境变量覆盖而无需重新构建: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ runtimeConfig: { nitro: { routeRules: { '/api/**': { headers: { 'x-env': 'production' } }, }, }, }, }); ``` ## 配置参考 这些配置选项控制路由行为: | 选项 | 类型 | 默认值 | 描述 | |--------|------|---------|-------------| | `baseURL` | `string` | `"/"` | 所有路由的基础 URL | | `apiBaseURL` | `string` | `"/api"` | `api/` 目录中路由的基础 URL | | `apiDir` | `string` | `"api"` | API 路由的目录名称 | | `routesDir` | `string` | `"routes"` | 基于文件的路由的目录名称 | | `serverDir` | `string \| false` | `false` | 用于扫描路由、中间件、插件等的服务器目录 | | `scanDirs` | `string[]` | `[]` | 扫描路由的额外目录 | | `routes` | `Record` | `{}` | 路由到处理程序的映射 | | `handlers` | `NitroEventHandler[]` | `[]` | 程序化处理程序注册(主要用于中间件) | | `routeRules` | `Record` | `{}` | 匹配模式的路由规则 | | `ignore` | `string[]` | `[]` | 文件扫描期间忽略的 glob 模式 | --- # Nitro 服务端入口 > 使用服务端入口处理没有匹配到任何路由的请求 服务端入口是 Nitro 注册为 catch-all(`/**`)路由的特殊处理器。特定路由始终具有优先权,因此服务端入口只会在请求没有匹配到任何路由时、[渲染器](/docs/renderer)运行之前执行。它通常用于在 Nitro 中挂载另一个框架,或为文件系统路由未处理的所有请求实现自定义路由。 ::warning 服务端入口是回退机制,而不是全局中间件:对于已由路由处理的请求,它不会运行。对于必须应用于**每个**请求的横切关注点(身份验证、日志记录、请求预处理),请改用[中间件](/docs/routing#middleware)。 :: ## 自动检测的 `server.ts` 默认情况下,Nitro 会在 `serverDir`(如果已设置)或项目根目录中自动查找 `server.ts`(或 `.js`、`.mjs`、`.mts`、`.tsx`、`.jsx`)文件。 如果找到,Nitro 将使用它作为服务端入口,并针对所有传入请求运行它。 ::code-group ```ts [server.ts] export default { async fetch(req: Request) { const url = new URL(req.url); // 处理特定路由 if (url.pathname === "/health") { return new Response("OK", { status: 200, headers: { "content-type": "text/plain" } }); } // 为所有请求添加自定义请求头 // 不返回内容以继续执行下一个处理器 } } ``` ```ts [routes/api/hello.ts] import { defineHandler } from "nitro"; export default defineHandler((event) => { return { hello: "API" }; }); ``` :: ::tip 当检测到 `server.ts` 时,Nitro 会在终端中输出:``已检测到 `server.ts` 作为服务端入口。`` :: 使用此配置: - `/health` → 由服务端入口处理(返回响应) - `/api/hello` → 由 API 路由处理器直接处理 - `/about` 等 → 服务端入口先运行,如果没有返回响应,则继续执行到渲染器 ## 框架兼容性 服务端入口是与其他框架集成的绝佳方式。任何公开标准 Web `fetch(request: Request): Response` 接口的框架都可以用作服务端入口。 ### Web 兼容框架 实现 Web `fetch` API 的框架可以直接与 `server.ts` 配合使用: ::tabs ::tabs-item{label="H3" icon="i-undocs-h3"} ```ts [server.ts] import { H3 } from "h3"; const app = new H3() app.get("/", () => "⚡️ 来自 H3 的问候!"); export default app; ``` :: ::tabs-item{label="Hono" icon="i-undocs-hono"} ```ts [server.ts] import { Hono } from "hono"; const app = new Hono(); app.get("/", (c) => c.text("🔥 来自 Hono 的问候!")); export default app; ``` :: ::tabs-item{label="Elysia" icon="i-undocs-elysia"} ```ts [server.ts] import { Elysia } from "elysia"; const app = new Elysia(); app.get("/", () => "🦊 来自 Elysia 的问候!"); export default app.compile(); ``` :: :: ### Node.js 框架 对于使用 `(req, res)` 风格处理器(如 [Express](https://expressjs.com/) 或 [Fastify](https://fastify.dev/))的 Node.js 框架,请将服务端入口文件命名为 `server.node.ts` 而不是 `server.ts`。Nitro 将自动检测 `.node.` 后缀,并使用 [`srvx`](https://srvx.h3.dev/) 将 Node.js 处理器转换为兼容 Web 的 fetch 处理器。 ::tabs ::tabs-item{label="Express"} ```ts [server.node.ts] import Express from "express"; const app = Express(); app.use("/", (_req, res) => { res.send("Hello from Express with Nitro!"); }); export default app; ``` :: ::tabs-item{label="Fastify"} ```ts [server.node.ts] import Fastify from "fastify"; const app = Fastify(); app.get("/", () => "Hello, Fastify with Nitro!"); await app.ready(); export default app.routing; ``` :: :: ## 服务端选项 当服务端入口的默认导出为普通对象时,以下服务端选项会传递给由 [Node.js](/deploy/runtimes/node)、[Bun](/deploy/runtimes/bun) 和 [Deno](/deploy/runtimes/deno) 预设启动的 [srvx](https://srvx.h3.dev/) 服务器:`port`、`hostname`、`reusePort`、`protocol`、`tls`、`silent`、`gracefulShutdown`、`maxRequestBodySize`、`trustProxy` 以及运行时特定设置(`node`、`bun`、`deno`)。 使用 `defineServerEntry` 辅助函数获取类型化选项: ```ts [server.ts] import { defineServerEntry } from "nitro"; export default defineServerEntry({ fetch(req) { return new Response("Hello from server entry!"); }, port: 8080, maxRequestBodySize: 1024 * 1024, }); ``` ::read-more{to="https://srvx.h3.dev/guide/options" title="srvx server options"} :: ::note - `NITRO_PORT`/`PORT`、`NITRO_HOST`/`HOST` 和 `NITRO_SSL_CERT`/`NITRO_SSL_KEY` 环境变量的优先级高于 `port`、`hostname` 和 `tls` 选项,因此服务器仍可在运行时进行配置。 - 在开发过程中(`nitro dev`),选项会应用于开发工作进程服务器,但监听器选项(`port`、`hostname`、`protocol`、`tls`、`silent`、`gracefulShutdown`)除外,这些选项由开发服务器和 CLI(`--port`、`--host`)控制。Vite 开发服务器不会应用这些选项。 - 其他 srvx 选项(`middleware`、`plugins`、`error` 和 `manual`)会被忽略。这些选项会改变请求的处理方式,但只适用于这些预设,不适用于无服务器部署或 `nitroApp.fetch`。请改用 [Nitro 中间件](/docs/routing#middleware)和[插件](/docs/plugins)。 - 选项只会从普通对象导出中读取(不会从 `export default app` 这类框架实例中读取),也不会从 Node.js 格式入口(`server.node.ts`)中读取。 :: ## 配置 ### 自定义服务端入口文件 你可以使用 Nitro 配置中的 `serverEntry` 选项指定自定义服务端入口文件: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverEntry: "./nitro.server.ts" }) ``` 你还可以提供一个包含 `handler` 和 `format` 选项的对象: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverEntry: { handler: "./server.ts", format: "node" // "web"(默认)或 "node" } }) ``` ### 处理器格式 `format` 选项控制 Nitro 如何处理服务端入口的默认导出: - **`"web"`**(默认):要求使用具有 `fetch(request: Request): Response` 方法的 Web 兼容处理器 - **`"node"`**:要求使用 Node.js 风格的 `(req, res)` 处理器。Nitro 会自动将其转换为 Web 兼容处理器 自动检测时,格式由文件名决定:`server.node.ts` 使用 `"node"` 格式,而 `server.ts` 使用 `"web"` 格式。 ### 禁用服务端入口 将 `serverEntry` 设置为 `false` 以禁用自动检测,并阻止 Nitro 使用任何服务端入口: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverEntry: false }) ``` ## 使用事件处理器 你可以导出使用 `defineHandler` 创建的事件处理器,而不是 Web `fetch` 处理器,从而获得更好的类型推断并访问 H3 事件对象: ```ts [server.ts] import { defineHandler, HTTPError } from "nitro"; export default defineHandler((event) => { // 仅在请求未匹配到任何路由时运行 if (event.url.pathname.startsWith("/api/")) { throw new HTTPError("Unknown API endpoint", { status: 404 }); } // 为渲染器添加上下文 event.context.requestId = crypto.randomUUID(); // 不返回内容以将请求交给渲染器 }); ``` ::important 返回 `undefined`(或不返回任何内容)会将请求交给[渲染器](/docs/renderer)。如果没有渲染器,Nitro 会返回一个空的 `200` 响应。返回一个值则会在此处结束请求。 :: ## 启动逻辑 服务端入口会被预先导入,因此其中的顶层代码会在**服务器启动时运行一次**,而不是在第一个请求到达时运行。 请注意: - 顶层代码会在[运行时插件](/docs/plugins)**之前**运行,因此插件注册的钩子尚不可用。不要在顶层调用 `useNitroApp()`;应在处理器内部调用它 - 顶层 `await` 会延迟启动:在服务端入口完成求值之前,服务器不会接受请求 - 在无服务器和边缘预设中,“启动”意味着新实例的每次冷启动。某些运行时(例如 Cloudflare Workers)不允许在全局作用域中执行 I/O(例如 `fetch`) - 在开发过程中,每当服务端入口重新加载时,顶层代码都会再次运行 ## 请求生命周期 服务端入口被注册为通配符(`/**`)路由处理器。当特定路由(如 `/api/hello`)匹配到请求时,该路由处理器具有优先权。对于不匹配任何特定路由的请求,服务端入口在渲染器之前运行: ```md 1. Server hook: `request` 2. Route rules (headers, redirects, etc.) 3. Global middleware (static assets first, then middleware/) 4. Route-scoped middleware (handlers config) 5. Route matching: a. Specific routes (routes/) ← if matched, handles the request b. Server entry ← runs for unmatched routes c. Renderer (renderer.ts or index.html) ``` 当服务端入口和[渲染器](/docs/renderer)同时存在时,它们会串联执行:服务端入口先运行,如果没有返回响应,则由渲染器处理请求。 :read-more{to="/docs/lifecycle" title="生命周期"} ## 开发模式 在开发过程中,Nitro 会监视服务端入口文件的变更。当文件被创建、修改或删除时,开发服务器会自动重新加载以获取最新更改。 ## 最佳实践 - 将服务端入口用作没有匹配到任何路由的请求的回退机制,或用于挂载另一个框架 - 对于必须应用于每个请求(包括已匹配路由)的关注点,请使用[中间件](/docs/routing#middleware) - 返回 `undefined` 以继续交给渲染器;返回一个值以结束请求 - 保持服务端入口轻量;它会针对每个未匹配的请求运行 - 使用[运行时插件](/docs/plugins)实现一次性初始化逻辑 - 不要将服务端入口用于特定路由逻辑;[路由处理器](/docs/routing)的性能更高。 --- # 缓存 > Nitro 提供了一个构建于[存储层](/docs/storage)之上的缓存系统,由 [ocache](https://ocache.unjs.io/) 提供支持。 缓存可以存储开销较大的操作结果(渲染后的响应、上游 API 调用、复杂计算),并在之后再次提供这些结果,而无需重新执行操作。Nitro 提供了三种使用方式: - [缓存处理器](#缓存处理器):缓存事件处理器的完整响应。 - [缓存函数](#缓存函数):缓存任意函数的结果,并在多个处理器之间复用。 - [路由规则](#使用路由规则):通过配置为路由模式启用缓存,而无需修改处理器代码。 ## 缓存处理器 要缓存事件处理器,请使用 `defineCachedHandler` 方法。 它的工作方式类似于 `defineHandler`,但多了一个用于传入[缓存选项](#选项)的第二个参数。 ```ts [routes/cached.ts] import { defineCachedHandler } from "nitro/cache"; export default defineCachedHandler((event) => { return "我会被缓存一小时"; }, { maxAge: 60 * 60 }); ``` 在此示例中,响应会缓存 1 小时。缓存过期后,下一个请求会等待处理器解析出新的值,然后再返回响应。如果你希望在后台重新验证时立即提供过期响应,请设置 `swr: true`(请参阅 [SWR 行为](#swr-行为))。 有关可用选项的更多详情,请参阅[选项](#选项)部分。 ::important `maxAge` 默认为 **1 秒**。对于任何确实希望缓存的内容,请务必显式设置此选项。 :: ### 处理器可以看到的内容 对于可缓存的请求,**处理器接收到的请求数据,恰好就是缓存键所涵盖的数据**。在处理器运行前,其他所有内容都会被移除,因此处理器无法根据缓存键无法区分的值来渲染输出,并将其存储在共享键下: - **请求头**会被剥离,除非列在 [`varies`](#仅适用于处理器的选项) 中。这包括 `if-none-match` / `if-modified-since`(Nitro 会自行处理条件请求)以及 `traceparent` 或 `x-request-id` 等追踪头。 - **查询参数**会被剥离,除非列在 [`allowQuery`](#仅适用于处理器的选项) 中。默认情况下,`/page`、`/page?a=1` 和 `/page?utm=x` 都会共用一个条目,并且处理器内部的 `url.searchParams` 为空。 - **Cookie**会被剥离,除非列在 [`allowCookies`](#仅适用于处理器的选项) 中,并且每个缓存响应中的 `set-cookie` 都会被移除。 - **`authorization` / `proxy-authorization`** 会被剥离,除非设置了 [`allowAuthorization`](#仅适用于处理器的选项)。 - **`host`** 是唯一会被重写而非移除的请求头:它携带已解析请求源的主机名,该主机名属于缓存键的一部分。因此,为多个主机名提供服务的处理器会为每个主机名生成一个条目。 对于必须原样到达处理器的请求,请使用 [`shouldBypassCache`](#通用选项)。绕过缓存的请求会保留其请求头、Cookie、查询字符串和请求体,且其响应不会被存储。 ::read-more{to="https://ocache.unjs.io/docs/handler#headers-the-handler-cant-see"} ocache:处理器无法看到的请求头。 :: ### 自动 HTTP 头 使用 `defineCachedHandler` 时,Nitro 会自动管理缓存响应上的 HTTP 缓存头: - **`etag`**:如果处理器尚未设置,则根据响应体哈希生成弱 ETag(`W/"..."`)。 - **`cache-control`**:根据实际应用于条目的有效期生成,除非处理器已设置此项(请参阅下表)。 - **`vary`**:`varies` 中的每个名称都会合并到响应的 `Vary` 中;使用 `allowCookies` 时也会加入 `Cookie`。`allowQuery` 不会添加任何内容,因为查询参数已经包含在 URL 中。 - **`x-cache`**:类似 CDN 的缓存状态头(`HIT`、`STALE`、`REVALIDATED` 或 `MISS`)。可通过 [`cacheStatusHeader`](#仅适用于处理器的选项) 选项自定义。 | 选项 | 合成的 `cache-control` | | ------- | --------------------------- | | `{ maxAge: 60 }` | `max-age=60` | | `{ maxAge: 60, swr: true }` | `max-age=60, s-maxage=60` | | `{ maxAge: 60, swr: true, staleMaxAge: 600 }` | `max-age=60, s-maxage=60, stale-while-revalidate=600` | 只有设置了 `staleMaxAge` 时,才会声明 `stale-while-revalidate`,因为该指令需要一个以秒为单位的数值。设置 `swr: true` 而未设置 `staleMaxAge` 时,过期窗口没有限制,因此下游缓存会在 `max-age` 之后重新验证,而 Nitro 会使用其过期副本响应。 要仅在服务器端缓存,而不向客户端和 CDN 声明 `cache-control`,请设置 [`sendCacheControl: false`](#仅适用于处理器的选项)。 ::note **永远不会生成 `last-modified`。** 条目填充的时间并不等于内容更改的时间,因此当处理器知道实际修改时间时,应自行设置此请求头。同样,处理器设置的 `cache-control` 也永远不会被覆盖。 :: ### 条件请求(304 Not Modified) 缓存处理器会自动支持条件请求。当客户端发送的 `if-none-match` 与缓存的 `etag` 匹配,或者 `if-modified-since` 的时间等于或晚于处理器设置的 `last-modified` 时,Nitro 会返回不含响应体的 `304 Not Modified` 响应。如果两者都存在,则以 `if-none-match` 为准。 请求验证器会在请求被缩减范围之前捕获,因此**处理器永远不会看到它们**,也无法自行处理条件请求。 ### 可缓存的请求 只有 `GET` 和 `HEAD` 请求会被缓存。所有其他 HTTP 方法(`POST`、`PUT`、`DELETE` 等)都会自动绕过缓存并直接调用处理器;携带 `Range` 请求头的请求也会如此。 `GET` 和 `HEAD` 会使用**单独的条目**进行缓存,因为 `HEAD` 响应不包含响应体。即使使用自定义 `getKey`,方法仍然是缓存键的一部分。 ### 请求去重 当缓存正在解析时,如果有多个并发请求命中相同的缓存键,只会运行一次处理器调用。所有并发请求都会等待并共享相同的结果。 共享解析受 [`maxResolveTime`](#通用选项) 限制(默认 30 秒)。超时后,所有等待者都会因 `TimeoutError` 而被拒绝,正在刷新的条目会被移除,并且该键可以再次使用。此外,处理器还会通过 `event.req.signal` 收到此截止时间,因此你可以将其传递给上游 `fetch` 调用。 ## 缓存函数 你也可以使用 `defineCachedFunction` 缓存函数。这适用于缓存非事件处理器函数的结果,但该函数是事件处理器的一部分,并且需要在多个处理器中复用的情况。 例如,你可能想将 API 调用的结果缓存一小时: ```ts [routes/api/stars/[...repo\\].ts] import { defineCachedFunction } from "nitro/cache"; import { defineHandler, type H3Event } from "nitro"; export default defineHandler(async (event) => { const { repo } = event.context.params; const stars = await cachedGHStars(repo).catch(() => 0) return { repo, stars } }); const cachedGHStars = defineCachedFunction(async (repo: string) => { const data = await fetch(`https://api.github.com/repos/${repo}`).then(res => res.json()); return data.stargazers_count; }, { maxAge: 60 * 60, name: "ghStars", getKey: (repo: string) => repo }); ``` 星标数会使用[缓存存储](#缓存存储)(默认为内存)进行缓存。缓存键由组、名称和 `getKey` 的结果生成,而 `value` 则是星标数。 ```json {"expires":1677851092249,"value":43991,"mtime":1677847492540,"integrity":"ZUHcsxCWEH"} ``` ::important 由于缓存数据会被序列化为 JSON,因此缓存函数不能返回无法序列化的内容,例如 Symbols、Maps、Sets…… 对于**本身就是**字节的值(`Uint8Array`、`ArrayBuffer` 或任何类型化数组),无需使用钩子即可处理,并且始终会作为 `Uint8Array` 返回。其*存储*方式取决于后端:声明了 `binary` 的后端会原样保存字节,而执行序列化的后端(包括 Nitro 的[默认缓存存储](#缓存存储))会将其存储为 base64。对象中嵌套的字节不会采用此处理方式;对于这类数据结构,请使用 [`serialize`/`transform`](#仅适用于函数的选项)。 :: ::note 如果你使用 edge workers 托管应用,应遵循以下说明。 在 edge workers 中,实例会在每个请求完成后销毁。Nitro 会自动使用 `event.waitUntil`,以便在响应发送给客户端的同时,实例仍能保持运行并更新缓存。 为确保缓存函数在 edge workers 中按预期工作,**应始终将 `event` 作为使用 `defineCachedFunction` 定义的函数的第一个参数传入。** ```ts [routes/api/stars/[...repo\\].ts] {5,10,17} import { defineCachedFunction } from "nitro/cache"; import { defineHandler, type H3Event } from "nitro"; export default defineHandler(async (event) => { const { repo } = event.context.params; const stars = await cachedGHStars(event, repo).catch(() => 0) return { repo, stars } }); const cachedGHStars = defineCachedFunction(async (event: H3Event, repo: string) => { const data = await fetch(`https://api.github.com/repos/${repo}`).then(res => res.json()); return data.stargazers_count; }, { maxAge: 60 * 60, name: "ghStars", getKey: (event: H3Event, repo: string) => repo }); ``` 这样,函数就能在更新缓存时保持实例运行,同时不会拖慢返回给客户端的响应。 对于请求不携带 `waitUntil` 的平台,请改用 [`waitUntil` 选项](#通用选项)传递平台钩子。 :: ## 选项 `defineCachedHandler` 和 `defineCachedFunction` 函数接受以下选项: ### 通用选项 以下选项可用于 `defineCachedHandler` 和 `defineCachedFunction`: ::field-group ::field{name="base" type="string | string[]"} 用于缓存键的存储基路径(前缀),通常与存储挂载点相匹配。 :br 默认为 `/cache`(存储在 `cache:` 前缀下)。如果提供数组,读取时会按顺序尝试每个基路径,写入时则会写入所有基路径(多层缓存)。 :: ::field{name="name" type="string"} 如果未提供,则根据函数名称猜测;否则回退为 `anon_`(根据函数代码计算)。 :: ::field{name="group" type="string"} 处理器默认为 `'nitro/handlers'`,函数默认为 `'nitro/functions'`。 :: ::field{name="getKey()" type="(...args) => string"} 接受与原始函数相同参数并返回缓存键(`String`)的函数。 :br 如果未提供,则会使用内置哈希函数,根据函数参数生成键。对于缓存处理器,键由请求源、路径和方法生成。 :br :br 自定义 `getKey` 会替代生成的键,但**不会**替代请求范围缩减:处理器可以看到哪些内容,仍由 `allowQuery`、`allowCookies` 和 `varies` 决定。 :: ::field{name="integrity" type="string"} 更改时会使缓存失效的值。 :br 默认情况下,它根据**函数代码和选项**计算,因此更改其中任一项都会静默忽略旧条目,而不是提供旧条目。 :: ::field{name="maxAge" type="number"} 缓存有效的最长时间,以秒为单位。 :br 默认为 `1`(秒)。显式设置为 `0` 会禁用该处理器或函数的缓存;省略或设为 `undefined` 时,则回退到默认值。 :: ::field{name="staleMaxAge" type="number"} 在 `maxAge` 过期后,后台重新验证期间仍可提供过期值的最长时间,以秒为单位。仅在启用 `swr` 时适用。 :br 设置为 `0` 时,永远不会提供过期值(等同于禁用 `swr`)。未设置时,过期值可以无限期提供,并且**不会写入存储 TTL**,因此条目会一直保留,直到后端将其清除。 :: ::field{name="swr" type="boolean"} 启用 `stale-while-revalidate` 行为,在异步重新验证的同时提供过期的缓存值。 :br 启用后,过期的缓存值会立即返回,同时在后台进行重新验证。禁用后,调用方会在前台等待值重新验证完成。 :br 默认为 `false`。请参阅 [SWR 行为](#swr-行为)。 :: ::field{name="getMaxAge()" type="(entry: CacheEntry) => number | { maxAge?, staleMaxAge? }"} 根据解析后的值为每个条目推导缓存有效期,并覆盖该条目的静态 `maxAge` / `staleMaxAge` 选项。返回以秒为单位的数值(`maxAge` 的简写),或返回可同时覆盖 `staleMaxAge` 的对象。 :br 适用于携带自身过期时间的值,例如访问令牌:`getMaxAge: (entry) => entry.value.expires_in`。解析后的值 `<= 0` 时,会禁用该条目的缓存。对于处理器,`entry.value` 是 `Response`,合成的 `cache-control` 会遵循返回的有效期。 :: ::field{name="maxResolveTime" type="number"} 一次共享解析的截止时间,以秒为单位。默认为 `30`。 :br 超时后,所有等待的调用方都会因 `TimeoutError` 而被拒绝,并且正在刷新的条目会被移除。设置为 `Infinity` 或 `0` 可禁用此限制;对于确实运行缓慢的解析器,请调高此值。 :: ::field{name="storage" type="StorageInterface | () => StorageInterface"} 覆盖此处理器或函数所使用的后端。 :br 默认为 Nitro 的[缓存存储](#缓存存储),它是 Nitro 存储层中 `cache:` 前缀的适配器。 :: ::field{name="waitUntil()" type="(promise: Promise) => void"} 将后台工作(缓存写入、SWR 刷新、清除操作)交给宿主运行时处理。 :br 优先级高于 `event.req.waitUntil`。仅在请求不携带此方法的无服务器平台上需要使用。 :: ::field{name="shouldInvalidateCache()" type="(...args) => boolean | Promise"} 返回 `boolean` 的函数,用于使当前缓存失效并创建新缓存。 :: ::field{name="shouldBypassCache()" type="(...args) => boolean | Promise"} 返回 `boolean` 的函数,用于绕过当前缓存但不使现有条目失效。 :br 对于处理器,绕过缓存的请求会以**未更改的状态**到达处理器(包括凭据、Cookie 和完整查询字符串),其响应既不会被存储,也不会带有缓存头。 :: ::field{name="onError()" type="(error: unknown) => void"} 缓存函数抛出错误时调用的自定义错误处理器。 :br 默认情况下,错误会记录到控制台,并由 Nitro 错误处理器捕获。 :: :: ### 仅适用于处理器的选项 以下选项仅适用于 `defineCachedHandler`: ::field-group ::field{name="headersOnly" type="boolean"} 为 `true` 时,跳过完整响应缓存,仅处理条件请求头(`if-none-match`、`if-modified-since`),用于返回 `304 Not Modified` 响应。每个请求都会调用处理器,并以处理器自身设置的 `etag` / `last-modified` 作为条件,因此处理器必须自行设置这些值。此模式下不会生成 `x-cache` 请求头。 :: ::field{name="varies" type="string[]"} 作为缓存键变化依据的请求头名称数组。此处列出的请求头在缓存解析期间会保留在请求中,并纳入缓存键,使缓存按请求头值的不同组合进行区分。它们也会合并到响应的 `Vary` 请求头中。 :br :br 未列在 `varies` 中的请求头会在调用处理器之前从请求中剥离,以确保缓存命中一致。 :br :br 如果响应自身的 `Vary` 列出了键中未包含的请求头,则该响应会返回,但**不会被存储**,因此请确保两个列表保持同步。 :br :br 多租户配置现在无需在此处添加 `['host']`:请求源已经是生成的键的一部分。只有当处理器在代理后会读取 `x-forwarded-host` 时,才需将其添加进来。 :: ::field{name="allowQuery" type="boolean | string[]"} 允许作为缓存键变化依据的查询参数名称白名单。**默认情况下,任何查询参数都不会影响缓存键,也不会传递给处理器**,因此 `/page?a=1` 和 `/page?utm=x` 会共用一个条目,并且处理器内部的 `url.searchParams` 为空。 :br :br 设置数组可指定允许的名称(区分大小写,不区分顺序);设置为 `true` 则会根据完整查询字符串生成键,适用于参数无法预先确定的代理和搜索端点。 :: ::field{name="allowCookies" type="string[]"} 参与缓存的 Cookie 名称白名单。列出的 Cookie 会影响缓存键,并保留在处理器所见的 `cookie` 请求头中。 :br :br **默认情况下不允许任何 Cookie**:处理器运行前会剥离 `cookie` 请求头,并且响应中的任何 `set-cookie` 请求头都会在缓存或返回前被丢弃,因此每个用户专属的 Cookie(例如会话 ID)永远不会通过缓存泄漏给其他用户。只将其值可安全地在所有命中同一缓存键的用户之间共享的 Cookie 加入白名单(例如 `theme` 或 `locale` 偏好),切勿加入用户专属的机密信息。 :br :br 设置此选项会生成 `Vary: Cookie`,这可能会降低 CDN 和共享代理的命中率。可以考虑改用 `allowQuery` 将选择放入 URL,或与 `sendCacheControl: false` 配合使用。 :: ::field{name="allowAuthorization" type="boolean"} 允许 `authorization` 和 `proxy-authorization` 到达处理器,并使缓存键随其变化。默认为 `false`,此时可缓存请求中的这两个请求头都会被剥离。 :br :br 发送相同凭据值的所有客户端会共用一个条目。如果响应绝不能共享,请改用 `shouldBypassCache`。 :: ::field{name="sendCacheControl" type="boolean"} 是否自动设置 `cache-control` 响应头。默认为 `true`。 :br 设置为 `false` 可仅在服务器端缓存:响应仍会存储并从缓存提供,但不会向客户端和 CDN 声明 `cache-control`。此设置不会生成 `no-store`,因此下游缓存仍可能根据自身启发式规则存储响应。 :: ::field{name="cacheStatusHeader" type="boolean | string"} 添加缓存状态响应头(`X-Cache: HIT | STALE | REVALIDATED | MISS`)。默认为 `true`。传入字符串可使用自定义请求头名称,传入 `false` 可禁用。 :: ::field{name="stream" type="boolean"} 直接流式传输用于填充缓存条目的响应,而不是先进行缓冲。默认为 `false`。 :br 这可以改善流式渲染的首字节时间,但会导致无法生成合成的 `etag`(摘要需要完整的响应体),并且会影响响应体中途出错时的恢复。任何不完整内容都不会被存储,后续请求仍会像往常一样使用已存储的条目。 :: ::field{name="maxBodySize" type="number"} 可缓冲并用于存储的最大响应体大小,以字节为单位。更大的响应会像绕过缓存的请求一样直接流式传输,不会被缓存。 :br Nitro 的默认存储未声明单个条目的大小上限,因此**默认没有限制**。对于代理大小不受你控制的上游响应的任何路由,请设置此选项。 :: ::field{name="shouldCache()" type="(entry: ResponseCacheEntry) => boolean | Promise"} 在[内置检查](#哪些内容会被缓存)的基础上,额外判断响应是否可缓存(内置检查始终适用,且只能进一步收窄缓存范围)。返回 `false` 可跳过响应缓存;响应仍会返回给调用方,只是不予存储。 :br 此函数在读取和写入时都会运行,可以是异步函数,并且会以安全失败的方式处理:钩子抛出错误时会将响应视为“不可缓存”,并交由 `onError` 处理。 :: :: ### 仅适用于函数的选项 以下选项仅适用于 `defineCachedFunction`: ::field-group ::field{name="transform()" type="(entry: CacheEntry, ...args) => any"} 在返回缓存条目之前对其进行转换。返回值会替代缓存值。 :br 条目还会携带每次调用对应的 `entry.status`(`"hit"`、`"stale"`、`"revalidated"` 或 `"miss"`),用于说明值的提供方式,适用于指标统计或条件逻辑。 :: ::field{name="serialize()" type="(entry: CacheEntry, ctx: { args }) => any"} 在将条目写入存储之前,准备好已解析的值(对应于 `transform` 的写入侧操作)。适用于函数返回无法原样存储的内容(例如流或类实例)的情况:`serialize` 会在写入时转换数据,而 `transform` 会在读取时恢复数据。 :br 每次解析只会运行一次,且去重调用会共享该结果,因此可以安全地读取流等只能使用一次的数据源。 :: ::field{name="validate()" type="(entry: CacheEntry, ctx: { args }) => boolean | Promise"} 验证缓存条目。返回 `false`(或解析为 `false` 的 Promise)会将条目视为无效并触发重新解析。第二个参数包含当前调用的 `args`,因此可以根据这些参数验证条目。 :: :: ## SWR 行为 `stale-while-revalidate`(SWR)模式需要通过 `swr` 选项**显式启用**(默认禁用)。了解它与其他选项的交互方式: | `swr` | `maxAge` | 行为 | |-------|----------|----------| | `false`(默认) | `3600` | 缓存 1 小时;过期时等待新值 | | `true` | `3600` | 缓存 1 小时;重新验证时提供过期值 | | `true` | `3600`,且设置 `staleMaxAge: 600` | 缓存 1 小时;重新验证时最多再提供 10 分钟的过期值 | | `true` | `3600`,且设置 `staleMaxAge: 0` | 缓存 1 小时;永远不提供过期值(等同于 `swr: false`) | 启用 `swr` 且存在已过期的缓存值时: 1. 立即将过期的缓存值返回给客户端。 2. 在后台调用函数或处理器以刷新缓存。 3. 在 edge workers 上,使用 `event.waitUntil` 保持后台刷新继续运行。 禁用 `swr`(默认)且缓存值已过期时: 1. 客户端会等待函数或处理器解析出新值。 2. 调用返回前会替换该条目(状态为 `REVALIDATED`)。 ::tip 当允许数据略微过期时(例如内容页面、列表、第三方 API 镜像),请启用 `swr`,这样即使缓存过期,响应时间也能保持稳定。对于调用方绝不能收到过期数据的情况(例如访问令牌或逐请求授权检查),请保持禁用。 :: ::warning 设置 `swr: true` 且**未设置** `staleMaxAge` 时,条目不会携带存储 TTL:最后一个有效值会一直提供,直到后端将其清除。这正是 ISR 模式,但也意味着缓存增长受后端容量而非时间限制。请设置 `staleMaxAge`,以便最终清理条目。 :: ## 缓存键 使用 `defineCachedFunction` 或 `defineCachedHandler` 函数时,缓存键会按照以下模式生成: ```ts `${options.base}:${options.group}:${options.name}:${options.getKey(...args)}.json` ``` `group` 和 `name` 在进入键之前会经过**转义**:移除 `[A-Za-z0-9_]` 之外的字符;如果值因此发生变化,则会附加原始值的哈希。这就是为什么 Nitro 的 `nitro/functions` 组会显示为 `nitrofunctions.`,并且不会与 `:` 键结构混淆。`getKey` 的结果是最后一个片段,并且会按原样存储。 例如,以下函数: ```ts import { defineCachedFunction } from "nitro/cache"; const getAccessToken = defineCachedFunction(() => { return String(Date.now()) }, { maxAge: 10, name: "getAccessToken", getKey: () => "default" }); ``` 将存储在类似以下的键下: ```ts cache:nitrofunctions.3aFYDY_G88ZLdElv-hEB0r8snSViMYJ1NbRtlNP9o_0:getAccessToken:default.json ``` 默认的 `base` 为 `/cache`,存储层会将其规范化为 `cache:` 前缀(与处理键中其他 `/` 的方式相同)。 ::note 对于缓存处理器,生成的键涵盖请求的**源**(协议、主机和端口)、URL 路径和方法,以及 [`varies`](#仅适用于处理器的选项)、[`allowQuery`](#仅适用于处理器的选项) 和 [`allowCookies`](#仅适用于处理器的选项) 对应值的哈希。`GET` 和 `HEAD` 会分别存储。 :: ::important 如果未提供 `name`,则会回退为**函数源码**的哈希;这种方式无法区分由工厂或循环生成的处理器或函数:它们的源码相同,但闭包变量不同,因此会共享同一个键并相互覆盖条目。对于这类情况,请务必显式传入 `name`(或 `getKey`)。 :: ::note 如果未提供 `getKey`,参数会被哈希。不透明值(`Blob`、`ReadableStream`、`Promise`、`Request`、`WeakMap`)只会贡献其类型,因此两个不同的值也会共用一个条目;嵌套深度超过 128 层的参数会抛出 `RangeError`。出现上述任一情况时,请传入一个只读取结果所依赖字段的 `getKey`。 :: ### 哪些内容会被缓存 存储响应之前,会进行以下检查。未通过任一检查的响应仍会返回给调用方,只是永远不会被存储或从缓存提供: | 检查 | 原因 | | ----- | ------ | | 状态不是 `200`、`203`、`301` 或 `308` | 只有这些状态对应完整、可复用的表示。错误、`204`/`304`、`206` 范围响应和临时重定向都会被排除。 | | 处理器通过 `cache-control` 选择退出 | 包括 `no-store`、`private`、`no-cache`,或共享有效期为零(如果存在 `s-maxage` 则使用它,否则使用 `max-age`)。 | | `Vary: *` | 除 `no-store` 之外最强的“禁止共享”信号。 | | `Vary` 中指定了缓存键之外的请求头 | 存储此响应会将某个变体重放给所有客户端。请将该请求头添加到 [`varies`](#仅适用于处理器的选项) 中。 | | 缺少响应体 | 没有可供重放的内容。空的(零字节)`200` 响应是允许的,并会正常缓存。 | | `etag` 或 `last-modified` 等于字符串字面值 `"undefined"` | 这是将缺失值转换为字符串后通常产生的结果,会破坏条件请求。 | 这些检查始终适用。使用 [`shouldCache`](#仅适用于处理器的选项) 可以进一步收窄缓存范围。内置检查拒绝的响应也**不会**获得合成的 `cache-control`,因此前置 CDN 无法继续缓存错误;而被你自己的 `shouldCache` 拒绝的响应仍会获得该请求头。 ::note `cache-control: must-revalidate` 不会选择退出缓存。响应会被存储,并在新鲜时提供,但过期后永远不会提供,因此仅对该响应禁用 SWR。 :: ::read-more{to="https://ocache.unjs.io/docs/cache-control"} ocache:完整了解 `cache-control` 和缓存资格。 :: ## 缓存失效 可以在运行时以编程方式使缓存条目失效(例如,在底层数据更改时通过 webhook 触发),而无需等待 `maxAge` 过期。 ### `.invalidate()` 和 `.expire()` 方法 通过 `defineCachedFunction` 创建的每个函数都会公开按需重新验证方法: - **`.invalidate(...args)`**:完全移除缓存条目。下次调用时会重新调用函数,并等待新值。 - **`.expire(...args)`**:将缓存条目标记为过期,但不移除它。启用 `swr` 时,仍会提供过期值,而下次调用会触发后台刷新;未启用 SWR 时,下次调用会在返回前重新解析。 - **`.resolveKeys(...args)`**:解析条目所缓存到的存储键(每个 `base` 前缀对应一个)。 参数会传递给 `getKey`,以生成缓存键。 ```ts import { defineCachedFunction } from "nitro/cache"; const cachedGHStars = defineCachedFunction(async (repo: string) => { const data = await fetch(`https://api.github.com/repos/${repo}`).then(res => res.json()); return data.stargazers_count; }, { maxAge: 60 * 60, name: "ghStars", getKey: (repo: string) => repo, }); await cachedGHStars("unjs/nitro"); // populates the cache await cachedGHStars.expire("unjs/nitro"); // marks the entry as stale await cachedGHStars.invalidate("unjs/nitro"); // removes the entry await cachedGHStars("unjs/nitro"); // re-invokes the function ``` 如果没有缓存条目与给定参数匹配,`.invalidate()` 和 `.expire()` 会正常完成且不报错,并且不会更改存储。两者也会取消该键上正在进行的解析,因此清除前的值不会在清除后写入。 ::note Nitro 的 `defineCachedHandler` 目前会返回普通事件处理器,并且不会公开这些方法,因此无法按需使缓存路由失效。如果需要此功能,请将开销较大的操作移入由普通 `defineHandler` 调用的 `defineCachedFunction`,然后使该函数失效。 :: ## 缓存存储 缓存条目使用[存储层](/docs/storage)存储在 `cache:` 前缀下(该前缀源自默认的 `base: "/cache"` 选项)。 默认情况下,没有为其配置专用挂载点:条目存储在根存储中,该存储使用内存驱动程序,并且在开发和生产环境中**不会跨重启持久化**。 要使用持久化后端,请通过 `kv` 选项设置 `cache` 挂载点: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ kv: { cache: { driver: 'redis', /* redis 连接器选项 */ } } }) ``` 在开发环境中,你也可以使用 `$development` 配置键覆盖缓存挂载点: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ kv: { cache: { // 生产环境缓存存储 }, }, $development: { kv: { cache: { // development cache storage } } } }) ``` 若要在预渲染时也覆盖它,请同时在 `$prerender` 中设置。 ::note 条目会通过 unstorage 序列化为 JSON,因此 Nitro 的默认缓存存储不会声明 `binary`。可解码为 UTF-8 的响应体(HTML、JSON、纯文本)无论如何都会以文本形式存储;只有**不是**有效 UTF-8 的响应体(图片、字体、PDF)才会进行 base64 编码,字节值的缓存函数也会如此。传入你自己的、声明了 `binary` 的 [`storage`](#通用选项)(例如基于驱动程序的 `getItemRaw` / `setItemRaw` 创建 ocache 的 `createBlobStorage`),即可将这些内容保留为字节,并跳过编码、解码和 4/3 倍膨胀。 :br :br 默认后端也不会声明单个条目的大小上限,因此缓存处理器会缓冲任意大小的响应体,除非你设置了 [`maxBodySize`](#仅适用于处理器的选项)。 :: ::read-more{to="https://ocache.unjs.io/docs/storage"} ocache:了解存储接口、二进制数据和分层后端。 :: ::read-more{to="/docs/storage"} 进一步了解 Nitro 存储层和可用驱动程序。 :: ## 使用路由规则 路由规则允许你直接通过配置,为匹配 glob 模式的所有路由启用缓存。对于应用中某个部分实施全局缓存策略,而无需修改处理器代码时,这尤其有用。 将所有博客路由缓存 1 小时: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ routeRules: { "/blog/**": { cache: { maxAge: 60 * 60 } }, }, }); ``` `cache` 规则接受上文记录的相同[处理器选项](#选项)(`swr`、`staleMaxAge`、`varies`、`allowQuery`、`allowCookies`、`allowAuthorization`、`sendCacheControl`、`cacheStatusHeader`、`stream`、`maxBodySize`、`headersOnly`、`base`、`group`、`name`、`integrity`、`maxResolveTime`)。`shouldCache` 等钩子选项仅适用于 `defineCachedHandler`。 如果要使用[自定义缓存存储](#缓存存储)挂载点,可以使用 `base` 选项。 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ kv: { redis: { driver: "redis", url: "redis://localhost:6379", }, }, routeRules: { "/blog/**": { cache: { maxAge: 60 * 60, base: "redis" } }, }, }); ``` ### 路由规则快捷方式 你可以使用 `swr` 快捷方式为路由规则启用 `stale-while-revalidate` 缓存。`swr: 3600` 是 `cache: { swr: true, maxAge: 3600 }` 的简写。设置为 `true` 时,SWR 会使用默认的 `maxAge` **1 秒**,因此建议传入明确的数值。 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ routeRules: { "/blog/**": { swr: 3600 }, "/api/**": { swr: 60 }, }, }); ``` 要显式禁用某个路由的缓存,请在更具体的模式上设置 `cache: false`(或 `swr: false`)。这也会移除从范围更广的模式继承的 `cache` 规则: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ routeRules: { "/api/**": { swr: 60 }, "/api/realtime/**": { cache: false }, }, }); ``` ::note 使用路由规则时,缓存处理器会使用组 `'nitro/route-rules'`,而不是默认的 `'nitro/handlers'`;生成的 `name` 会将条目限定到匹配的路由处理器、HTTP 方法、规则模式和匹配的路由。 :: ::warning 生成的范围在每个进程中都是唯一的。对于**持久化**缓存存储,重启或启动第二个工作进程都会从全新的条目开始,而不会复用已存储的条目。如果路由规则缓存条目需要跨进程共享,请在规则上设置显式的 `name`。 :: ::read-more{to="/docs/routing#route-rules"} 路由规则的用途远不止缓存:还包括请求头、重定向、代理等。 :: --- # KV 存储 > Nitro 提供了一个内置存储层,可以抽象文件系统、数据库或任何其他数据源。 Nitro 内置了与 [unstorage](https://unstorage.unjs.io) 的集成,以提供与运行时无关的持久化层:你的代码始终调用相同的键值 API,而底层驱动(内存、文件系统、Redis 等)则通过配置进行切换。 ## 用法 要使用存储层,可以使用 `useKV()` 工具函数访问存储实例。 ```ts import { useKV } from "nitro/kv"; // Default storage (in-memory) await useKV().setItem("test:foo", { hello: "world" }); const value = await useKV().getItem("test:foo"); // You can specify a base prefix with useKV(base) const testStorage = useKV("test"); await testStorage.setItem("foo", { hello: "world" }); await testStorage.getItem("foo"); // { hello: "world" } // You can use generics to type the return value await useKV<{ hello: string }>("test").getItem("foo"); await useKV("test").getItem<{ hello: string }>("foo"); ``` :read-more{to="https://unstorage.unjs.io"} ::note `useKV` 之前名为 `useStorage`,并从 `nitro/storage` 导出;`kv` 配置选项之前名为 `storage`。旧名称仍可作为已弃用的别名使用。已弃用的 `devStorage` 选项已被 `$development`(以及 `$prerender`)中的 `kv` 替代。 :: ### 可用方法 `useKV()` 返回的存储实例提供以下方法: | 方法 | 描述 | |---|---| | `getItem(key)` | 获取键的值。如果键不存在则返回 `null`。 | | `getItems(items)` | 一次获取多个项目。接受键数组或 `{ key, options }` 对象。 | | `getItemRaw(key)` | 获取键的原始值,不进行解析。适用于二进制数据。 | | `setItem(key, value)` | 设置键的值。 | | `setItems(items)` | 一次设置多个项目。接受 `{ key, value }` 对象数组。 | | `setItemRaw(key, value)` | 设置键的原始值,不进行序列化。 | | `hasItem(key)` | 检查键是否存在。返回布尔值。 | | `removeItem(key)` | 从存储中移除键。 | | `getKeys(base?)` | 获取所有键,可选按基础前缀过滤。 | | `clear(base?)` | 清除所有键,可选按基础前缀过滤。 | | `getMeta(key)` | 获取键的元数据(例如 `mtime`、`atime`、`ttl`)。 | | `setMeta(key, meta)` | 设置键的元数据。 | | `removeMeta(key)` | 移除键的元数据。 | | `mount(base, driver)` | 在基础路径动态挂载存储驱动。 | | `unmount(base)` | 从基础路径卸载存储驱动。 | | `watch(callback)` | 监视变更。回调接收 `(event, key)`,其中 event 为 `"update"` 或 `"remove"`。 | | `unwatch()` | 停止监视变更。 | 还提供了简写别名:`get`、`set`、`has`、`del`、`remove`、`keys`。 ```ts import { useKV } from "nitro/kv"; // Get all keys under a prefix const keys = await useKV("test").getKeys(); // Check if a key exists const exists = await useKV().hasItem("test:foo"); // Remove a key await useKV().removeItem("test:foo"); // Get raw binary data const raw = await useKV().getItemRaw("assets:server:image.png"); // Get metadata (type, etag, mtime, etc.) const meta = await useKV("assets:server").getMeta("file.txt"); ``` ## 配置 你可以使用 `kv` 选项挂载一个或多个自定义存储驱动。 键是挂载点名称,值是驱动名称和配置。 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ kv: { redis: { driver: "redis", /* redis 连接器选项 */ } } }) ``` 然后,你可以使用 `useKV("redis")` 函数来使用 Redis 存储。 ::read-more{to="https://unstorage.unjs.io/"} 你可以在 [unstorage 文档](https://unstorage.unjs.io/) 上找到驱动列表及其配置。 :: ::important 挂载选项会被序列化到构建输出中,因此必须是可进行 JSON 序列化的。如果挂载需要不可序列化的选项,请使用[运行时配置](#runtime-configuration)。 :: ### 驱动依赖 某些驱动依赖第三方库(例如,`redis` 需要 [`ioredis`](https://www.npmjs.com/package/ioredis))。 Nitro 会检测挂载驱动所需的库,并提示安装缺失的库(在 CI 中会自动安装)。安装的库随后会通过其 `lib` 选项显式传递给驱动,以便打包器能够静态解析它们。因此,`lib` 选项无法在 `kv` 挂载中配置(将其设置为 `null` 可选择不注入导入)。 ### 开发存储 你可以使用 `$development` 配置键在开发期间覆盖 `kv` 挂载。 当你的生产驱动在开发环境中不可用时(例如托管的 Redis 实例),这很有用。 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ kv: { db: { driver: "redis", host: "prod.example.com", } }, $development: { kv: { db: { driver: "fs", base: "./.data/db" } } } }) ``` 在开发模式下运行时,`$development.kv` 挂载会合并到 `kv` 挂载之上,让你在开发时可以使用本地文件系统驱动或内存驱动。使用不同 `driver` 的挂载会替换整个挂载(上例中的 `host` 不会传递给 `fs` 驱动)。使用相同 `driver` 的挂载则会深度合并其选项。 `$development` 仅适用于开发服务器。在[预渲染](/docs/prerendering)生产构建时,则会依次应用 `$production` 和 `$prerender` 覆盖项。使用 `$prerender` 仅在预渲染期间覆盖挂载: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ kv: { db: { driver: "redis", host: "prod.example.com" } }, $development: { kv: { db: { driver: "fs", base: "./.data/db" } } }, $prerender: { kv: { db: { driver: "fs", base: "./.data/db" } } } }) ``` ::tip 一种常见配置是在生产环境的 `kv` 中使用托管驱动(例如 `redis`),并在 `$development.kv` 中使用 `fs` 驱动,这样开发数据便于在本地 `.data/` 中检查,也无需依赖外部服务。 :: ## 内置挂载点 Nitro 自动挂载以下存储路径: ### `/assets` 服务器资源挂载在 `/assets` 基础路径下(参见[服务器资源](#server-assets))。在生产环境中,此挂载为只读,并且仅支持 `getKeys()`、`hasItem()`、`getItem()`、`getItemRaw()` 和 `getMeta()`。 ```ts import { useKV } from "nitro/kv"; // Access server assets via the /assets mount const content = await useKV("assets:server").getItem("my-file.txt"); ``` ### 默认(内存中) 根存储(无基础路径)默认使用内存驱动。此处存储的数据在重启后不会持久化。 ```ts import { useKV } from "nitro/kv"; // In-memory by default, not persisted await useKV().setItem("counter", 1); ``` 要持久化数据,请使用 `kv` 配置选项挂载具有持久化后端的驱动(例如 `fs`、`redis` 等)。 ::note [缓存层](/docs/cache)也会将其条目存储在此根存储中(位于 `cache:` 前缀下),除非你配置了专用的 `cache` 挂载点,因此默认情况下,缓存数据在重启后也不会持久化。 :: ## 服务器资源 Nitro 允许你从 `assets/` 目录中捆绑文件。这些文件可以在运行时通过 `assets:server` 存储挂载访问。 当设置了 [`serverDir`](/config#serverdir) 时,目录相对于该目录解析;否则相对于项目根目录解析: ``` my-project/ server/ assets/ data.json templates/ welcome.html routes/ index.ts ``` ```ts [server/routes/index.ts] import { defineHandler } from "nitro"; import { useKV } from "nitro/kv"; export default defineHandler(async () => { const serverAssets = useKV("assets:server"); const keys = await serverAssets.getKeys(); const data = await serverAssets.getItem("data.json"); const template = await serverAssets.getItem("templates/welcome.html"); return { keys, data, template }; }); ``` ### 自定义资源目录 你可以使用 `serverAssets` 配置选项注册其他资源目录。每个 `dir` 都相对于 `rootDir`(而不是 `serverDir`)解析: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverAssets: [ { baseName: "templates", dir: "./templates", } ] }) ``` 自定义资源目录可通过 `assets:` 访问: ```ts import { useKV } from "nitro/kv"; const templates = useKV("assets:templates"); const keys = await templates.getKeys(); const html = await templates.getItem("email.html"); ``` ### 资源元数据 服务器资源包含元数据,例如内容类型、ETag 和修改时间: ```ts import { useKV } from "nitro/kv"; const serverAssets = useKV("assets:server"); const meta = await serverAssets.getMeta("image.png"); // { type: "image/png", etag: "\"...\"", mtime: "2024-01-01T00:00:00.000Z" } // 适用于设置响应头 const raw = await serverAssets.getItemRaw("image.png"); ``` ::note 在开发环境中(以及预渲染期间),服务器资源会直接使用 `fs` 驱动从文件系统读取,因此 `getMeta()` 返回的是文件系统统计信息(`mtime`、`size` 等),而不是 `type` 和 `etag`。在生产环境中,资源会被捆绑到构建输出中,其元数据会在构建时预先计算。 :: ::read-more{to="/docs/assets"} 详细了解公共资源和服务器资源。 :: ## 运行时配置 在挂载点配置直到运行时才知道的场景中,Nitro 可以使用[插件](/docs/plugins)在启动期间动态添加挂载点。 ```ts [plugins/storage.ts] import { useKV } from "nitro/kv"; import { definePlugin } from "nitro"; import redisDriver from "unstorage/drivers/redis"; export default definePlugin(() => { const storage = useKV() // Dynamically pass credentials from runtime config or elsewhere const driver = redisDriver({ base: "redis", host: process.env.REDIS_HOST, port: Number(process.env.REDIS_PORT), // Pass the driver library explicitly so that it is bundled lib: () => import("ioredis"), /* other redis connector options */ }) // Mount the driver storage.mount("redis", driver) }) ``` ::important 以这种方式挂载的驱动不在 Nitro 的[驱动依赖检测](#driver-dependencies)范围内:请自行安装第三方库(上例中的 `ioredis`),并通过驱动的 `lib` 选项传入。如果不传入,驱动会回退到动态 `import("ioredis")`,而打包器无法静态解析该导入,导致挂载在运行时失败,并出现 `Cannot import ioredis` 错误。 :: --- # 资源 Nitro 支持三种类型的资源:直接提供给客户端的**公共资源**、内联到服务器包中的**导入文件**,以及打包到服务器中以供编程访问的**服务器资源**。 ## 公共资源 Nitro 通过 `public/` 目录处理资源。 `public/` 目录中的所有资源将被自动提供。这意味着您可以直接从浏览器访问它们,无需任何特殊配置。 ```md public/ image.png <-- /image.png video.mp4 <-- /video.mp4 robots.txt <-- /robots.txt ``` ### 缓存和请求头 公共资源会自动携带 `ETag` 和 `Last-Modified` 响应头,以支持条件请求。当客户端发送 `If-None-Match` 或 `If-Modified-Since` 请求头时,Nitro 会返回 `304 Not Modified` 响应。 对于从非根目录 `baseURL`(如 `/build/`)提供的资源,Nitro 会阻止请求穿透到应用处理器。如果请求匹配了公共资源基础路径但文件未找到,将立即返回 `404`。 ### 生产环境公共资源 构建 Nitro 应用时,`public/` 目录将被复制到 `.output/public/`,同时会创建一个包含元数据的清单文件并嵌入到服务器包中。 ```json { "/image.png": { "type": "image/png", "etag": "\"4a0c-6utWq0Kbk5OqDmksYCa9XV8irnM\"", "mtime": "2023-03-04T21:39:45.086Z", "size": 18956 }, "/robots.txt": { "type": "text/plain; charset=utf-8", "etag": "\"8-hMqyDrA8fJ0R904zgEPs3L55Jls\"", "mtime": "2023-03-04T21:39:45.086Z", "size": 8 }, "/video.mp4": { "type": "video/mp4", "etag": "\"9b943-4UwfQXKUjPCesGPr6J5j7GzNYGU\"", "mtime": "2023-03-04T21:39:45.085Z", "size": 637251 } } ``` 这使得 Nitro 无需扫描目录即可了解公共资源,配合缓存响应头提供高性能。 ### 自定义公共资源目录 您可以使用 `publicAssets` 配置选项配置额外的公共资源目录。每个条目支持以下属性: - `dir` -- 目录路径(相对于 `rootDir` 解析) - `baseURL` -- 用于提供资源的 URL 前缀(默认值:`"/"`) - `maxAge` -- 以秒为单位的缓存 `max-age`。设置后,将通过路由规则应用 `Cache-Control: public, max-age=, immutable` 响应头。仅当 `fallthrough` 为 `false` 时生效(非根目录的默认值)。 - `fallthrough` -- 当资源未找到时,请求是否应继续交由应用处理器处理。顶层(`baseURL: "/"`)目录默认为 `true`;非根目录默认为 `false` - `ignore` -- 传入 `false` 以禁用忽略模式,或传入 glob 模式数组以覆盖全局 `ignore` 选项 ```js [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ publicAssets: [ { baseURL: "build", dir: "public/build", maxAge: 3600, }, ], }); ``` 在此示例中,`public/build/` 目录下的文件在 `/build/` 路径下提供,具有一小时缓存且不会穿透到应用处理器。 ### 压缩公共资源 Nitro 可以在构建期间生成公共资源的预压缩版本。当客户端发送 `Accept-Encoding` 请求头时,服务器将提供压缩版本(如果可用)。支持的压缩编码包括 gzip(`.gz`)、brotli(`.br`)和 zstd(`.zst`)。 设置 `compressPublicAssets: true` 以启用所有编码: ```js [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ compressPublicAssets: true, }); ``` 或选择特定编码: ```js [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ compressPublicAssets: { gzip: true, brotli: true, zstd: false, }, }); ``` ::note 仅压缩可压缩的 MIME 类型(文本、JavaScript、JSON、XML、WASM、字体、SVG 等)且文件大小至少为 1 KB 的文件。源映射文件(`.map`)会被排除。zstd 压缩要求在构建时使用 Node.js 22+(`zlib.zstdCompress`),在较旧版本上会被静默跳过 :: ## 导入文件 任何文件都可以通过 `type` import 属性导入。其内容会以内联形式添加到服务器包中,可以作为带有 [`type: "bytes"`](https://github.com/tc39/proposal-import-bytes) 的 `Uint8Array`,也可以作为带有 [`type: "text"`](https://github.com/tc39/proposal-import-text) 的 UTF-8 `string`: ```ts [routes/logo.ts] import { defineHandler } from "nitro"; import logo from "./logo.png" with { type: "bytes" }; // Uint8Array import readme from "./README.md" with { type: "text" }; // string export default defineHandler(() => { return new Response(logo, { headers: { "content-type": "image/png" } }); }); ``` 文件类型会被忽略:任何文件都可以作为字节或文本导入。 动态导入同样受支持: ```ts const { default: logo } = await import("./logo.png", { with: { type: "bytes" } }); ``` 或者,`raw:` 前缀会将文件作为 `string`(对于文本文件,根据文件类型确定)或 `Uint8Array`(对于二进制文件)导入: ```ts import readme from "raw:./README.md"; // string import logo from "raw:./logo.png"; // Uint8Array ``` ::note 导入的文件会以内联形式添加到服务器包中(二进制文件会进行 base64 编码)。对于较大的文件,建议使用[服务器资源](#server-assets) :: ::note TypeScript 尚不支持 `bytes` 和 `text` 属性。在此之前,导入未知文件类型时需要添加 `// @ts-ignore` 注释 :: ## 服务器资源 `assets/` 目录中的所有资源(如果设置了 [`serverDir`](/config#serverdir),则相对于该目录,否则相对于项目根目录)都会添加到服务器包中。构建应用后,可以在 `.output/server/chunks/raw/` 目录中找到这些资源。请注意资源大小,因为它们会与服务器包一起打包。 ::tip 如果您的代码从未使用 `useKV()`,服务器资源将不会包含在服务器包中。 :: 它们可以通过 `assets:server` 挂载点使用[存储层](/docs/storage)来访问。 例如,您可以将 json 文件存储在 `assets/data.json` 中,并在处理器中检索它: ```js import { defineHandler } from "nitro"; import { useKV } from "nitro/kv"; export default defineHandler(async () => { const data = await useKV("assets:server").get("data.json"); return data; }); ``` ### 自定义服务器资源 为了从自定义目录添加资源,您需要在 nitro 配置中定义路径。这允许您从 `assets/` 目录之外的目录添加资源。 `serverAssets` 中的每个条目支持以下属性: - `baseName` -- 用作存储挂载点的名称(通过 `assets:` 访问) - `dir` -- 目录路径(相对于 `rootDir` 解析) - `pattern` -- 文件包含的 glob 模式(默认:`"**/*"`) - `ignore` -- 用于排除文件的 glob 模式数组 ```js [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverAssets: [ { baseName: "templates", dir: "./templates", }, ], }); ``` 然后您可以使用 `assets:templates` 基础路径来检索您的资源。 ```ts [handlers/success.ts] import { defineHandler } from "nitro"; import { useKV } from "nitro/kv"; export default defineHandler(async (event) => { const html = await useKV("assets:templates").get("success.html"); return html; }); ``` ::note 在开发期间,服务器资源会使用 `fs` unstorage 驱动程序直接从文件系统读取。在生产环境中,它们会作为带有预计算元数据(MIME 类型、ETag、修改时间)的延迟导入打包到服务器中 :: ::read-more{to="/docs/storage"} 服务器资源通过存储层访问。有关所有可用方法的完整列表,请参阅存储文档 :: --- # 配置 > 自定义并扩展 Nitro 默认配置。 ::read-more{to="/config"} 查看 [配置参考](/config) 了解可用选项。 :: ## 配置文件 你可以使用配置文件自定义 Nitro。根据你的设置,在 `nitro.config.ts` 中定义选项,或者使用 Vite 插件时在 `vite.config.ts` 的 `nitro` 键下定义选项。 ::code-group ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ // Nitro options }) ``` ```ts [vite.config.ts] import { defineConfig } from 'vite' import { nitro } from 'nitro/vite' export default defineConfig({ plugins: [ nitro() ], nitro: { // Nitro 选项 } }) ``` :: ### 支持的配置文件 Nitro 使用项目根目录中的 [c12](https://github.com/unjs/c12) 加载配置(该目录由传递给 `nitro` CLI 的目录决定,默认为当前工作目录)。系统会按以下顺序检查位置,并使用第一个匹配的文件: - `nitro.config.{js,ts,mjs,cjs,mts,cts,json,jsonc,json5,yaml,yml,toml}` - `.config/nitro.{js,ts,mjs,cjs,mts,cts,json,jsonc,json5,yaml,yml,toml}` - `.config/nitro.config.{js,ts,mjs,cjs,mts,cts,json,jsonc,json5,yaml,yml,toml}` 推荐使用 `nitro.config.ts` 这一约定。 同一目录下没有扩展名的 `.nitrorc` 文件也会被加载(使用 `key=value` 语法),并以低于主配置文件的优先级进行合并。 使用 Vite 插件时,也可以在 `vite.config.ts` 的 `nitro` 键中直接传入选项(如上所示)。 ### 环境特定配置 使用 [c12](https://github.com/unjs/c12) 约定,你可以使用 `$development` 和 `$production` 键提供特定于环境的覆盖: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ logLevel: 3, $development: { // 仅在开发模式下应用的选项 debug: true, }, $production: { // 仅在生产构建中应用的选项 minify: true, }, }) ``` 环境名称在 `nitro dev` 期间为 `"development"`,在 `nitro build` 期间为 `"production"`。 ### 扩展配置 你可以使用 `extends` 键从其他配置或预设进行扩展: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ extends: "./base.config", }) ``` ## 目录选项 Nitro 提供了多个选项来控制目录结构: | 选项 | 默认值 | 描述 | | --- | --- | --- | | `rootDir` | `.`(当前目录) | 项目的根目录。 | | `serverDir` | `false` | 服务器源代码目录(设置为 `"server"` 或 `"./"` 以启用)。 | | `buildDir` | `node_modules/.nitro` | 构建产物的目录。 | | `output.dir` | `.output` | 生产输出目录。 | | `output.serverDir` | `.output/server` | 服务器输出目录。 | | `output.publicDir` | `.output/public` | 公共资源输出目录。 | ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: "server", buildDir: "node_modules/.nitro", output: { dir: ".output", }, }) ``` ::note `srcDir` 选项已弃用。请改用 `serverDir`。 :: :read-more{to="/config#directories"} ## 环境变量 某些 Nitro 行为可以使用环境变量进行配置。这些变量会在构建和启动时配置 Nitro 本身。若要向应用公开你自己的值,请改用[运行时配置](#runtime-configuration)。 | 变量 | 描述 | | --- | --- | | `NITRO_PRESET` | 覆盖部署预设。 | | `NITRO_COMPATIBILITY_DATE` | 设置兼容性日期。 | | `NITRO_APP_BASE_URL` | 覆盖基础 URL(默认值:`/`)。 | | `NITRO_BUILDER` | 选择用于构建的打包器(`rollup`、`rolldown` 或 `vite`)。 | | `NITRO_ENV_PREFIX` | 设置运行时配置环境覆盖的自定义次要前缀(默认值:`_`)。 | | `NITRO_ENV_EXPANSION` | 启用运行时配置值中的环境变量展开。 | ## 运行时配置 运行时配置允许你向应用公开配置值,并使用环境变量在运行时覆盖这些值。这对于在不同环境(开发、预发布、生产)之间有所不同的值非常有用,例如 API 端点或功能标志。 首先,在配置文件中定义运行时配置。 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ runtimeConfig: { apiToken: "dev_token", // `dev_token` 是默认值 } }); ``` 你现在可以使用 `useRuntimeConfig()` 访问运行时配置。 ```ts [api/example.get.ts] import { defineHandler } from "nitro"; import { useRuntimeConfig } from "nitro/runtime-config"; export default defineHandler((event) => { return useRuntimeConfig().apiToken; // 返回 `dev_token` }); ``` ### 嵌套对象 运行时配置支持嵌套对象。任何深度的键都使用 `NITRO_` 前缀和 `UPPER_SNAKE_CASE` 转换映射到环境变量: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ runtimeConfig: { database: { host: "localhost", port: 5432, }, }, }); ``` ```bash [.env] NITRO_DATABASE_HOST="db.example.com" NITRO_DATABASE_PORT="5433" ``` ::note 只有在配置文件的 `runtimeConfig` 中定义的键才会被考虑。你无法仅通过环境变量引入新键。 :: ### 序列化 运行时配置值必须是可序列化的(字符串、数字、布尔值、普通对象和数组)。不可序列化的值(类实例、函数等)将在构建时触发警告。 配置中值为 `undefined` 或 `null` 的项将作为后备替换为空字符串(`""`)。 ### 本地开发 你可以使用环境变量更新运行时配置。在本地可以使用 `.env` 或 `.env.local` 文件,在生产环境中可以使用平台变量(见下文)。 在你的项目根目录创建一个 `.env` 文件: ```bash [.env] NITRO_API_TOKEN="123" ``` 重启开发服务器,访问 `/api/example` 端点,你应该会看到响应为 `123`,而不是 `dev_token`。 ::note `.env` 和 `.env.local` 文件会在 Nitro 解析配置时加载,在 `nitro dev` 和 `nitro build` 中都是如此,因此 `nitro.config.ts` 可以通过 `process.env` 读取它们。环境中已经存在的变量优先级高于 `.env` 中的值。它们**不会**在运行时由构建后的服务器加载:在生产环境中,请使用平台原生的环境变量机制。 :: 你仍然可以使用 `import.meta.env` 或 `process.env` 直接访问环境变量,但应避免在环境全局上下文中(模块顶层)读取它们,以防止出现意外行为。 ### 生产环境 你可以在生产环境中定义变量以更新运行时配置。 ::warning 所有变量必须以 `NITRO_` 为前缀才能应用到运行时配置。它们将覆盖你在 `nitro.config.ts` 文件中定义的运行时配置变量。 :: ```bash NITRO_API_TOKEN="123" ``` 运行时配置中的键采用 camelCase,环境变量中采用 UPPER_SNAKE_CASE。 ```ts { helloWorld: "foo" } ``` ```bash NITRO_HELLO_WORLD="foo" ``` ### 自定义环境前缀 你可以使用 `nitro.envPrefix` 运行时配置键配置次要的环境变量前缀。除了默认的 `NITRO_` 前缀外,还会检查此前缀: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ runtimeConfig: { nitro: { envPrefix: "APP_", }, apiToken: "", }, }); ``` 使用此配置,`NITRO_API_TOKEN` 和 `APP_API_TOKEN` 都将被检查作为覆盖值。 ::note 未配置自定义前缀时,默认的次要前缀为 `_`。例如,`_API_TOKEN` 也会覆盖 `apiToken`。 :: ### 环境变量展开 启用后,运行时配置字符串值中使用 `{{VAR_NAME}}` 语法的环境变量引用将在运行时展开: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ experimental: { envExpansion: true, }, runtimeConfig: { url: "https://{{APP_DOMAIN}}/api", }, }); ``` ```bash APP_DOMAIN="example.com" ``` 在运行时,`useRuntimeConfig().url` 将解析为 `"https://example.com/api"`。 --- # 数据库 > Nitro 提供了一个内置的轻量级 SQL 数据库层。 > Nitro 提供了一个内置的轻量级 SQL 数据库层。 默认数据库连接已通过 [SQLite](https://db0.unjs.io/connectors/sqlite) **预配置**,可在开发模式和任何兼容 Node.js 的生产部署中开箱即用。默认情况下,数据将存储在 `.data/db.sqlite` 中。 :read-more{to="https://db0.unjs.io" title="DB0 文档"} ::important 数据库支持目前处于实验阶段。 请参阅 [db0 issues](https://github.com/unjs/db0/issues) 了解状态并报告错误。 :: 数据库层默认不启用。启用实验性功能标志: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ experimental: { database: true } }) ``` ## 使用 ```ts [server.ts] import { defineHandler } from "nitro"; import { useDatabase } from "nitro/database"; export default defineHandler(async () => { const db = useDatabase(); // 创建用户表 await db.sql`DROP TABLE IF EXISTS users`; await db.sql`CREATE TABLE IF NOT EXISTS users ("id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT)`; // 添加新用户 const userId = String(Math.round(Math.random() * 10_000)); await db.sql`INSERT INTO users VALUES (${userId}, 'John', 'Doe', '')`; // 查询用户 const { rows } = await db.sql`SELECT * FROM users WHERE id = ${userId}`; return { rows, }; }); ``` ### `useDatabase` 使用 `useDatabase` 获取数据库实例。它接受一个可选的连接名称(默认为 `"default"`)。 ```ts import { useDatabase } from "nitro/database"; // 使用默认连接 const db = useDatabase(); // 使用命名连接 const usersDb = useDatabase("users"); ``` 数据库实例会在首次使用时延迟创建,并在后续使用相同连接名称调用时进行缓存。如果连接名称未配置,将抛出错误。 ### `db.sql` 使用带标签的模板字面量和自动参数绑定执行 SQL 查询: ```ts const db = useDatabase(); // 使用参数化值插入(防止 SQL 注入) const id = "1001"; await db.sql`INSERT INTO users VALUES (${id}, 'John', 'Doe', 'john@example.com')`; // 使用参数查询 const { rows } = await db.sql`SELECT * FROM users WHERE id = ${id}`; // 结果包含行、变更计数和最后插入的 ID const result = await db.sql`INSERT INTO posts (title) VALUES (${"Hello"})`; // result.rows, result.changes, result.lastInsertRowid ``` ### `db.exec` 直接执行原始 SQL 字符串: ```ts const db = useDatabase(); await db.exec("CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, name TEXT)"); ``` ### `db.prepare` 准备 SQL 语句以供重复执行: ```ts const db = useDatabase(); const stmt = db.prepare("SELECT * FROM users WHERE id = ?"); const result = await stmt.bind("1001").all(); ``` ## 配置 你可以使用 `database` 配置更改默认连接,或为任意[受支持的数据库](https://db0.unjs.io/connectors/sqlite)定义其他命名连接,并将数据库实例与任意[受支持的 ORM](https://db0.unjs.io/integrations)集成。 每个连接都是一个包含 `connector` 名称和可选 `options` 对象的 `DatabaseConnectionConfig`。特定连接器的设置(例如 `url`、`host` 或 `name`)应放在 `options` 下,而不是连接配置的顶层。 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ database: { default: { connector: "sqlite", options: { name: "db" }, }, users: { connector: "postgresql", options: { url: "postgresql://username:password@hostname:port/database_name", }, }, analytics: { connector: "mysql2", options: { host: "localhost", port: 3306, user: "root", password: "password", database: "analytics", }, }, }, }); ``` 请参阅 [db0 连接器文档](https://db0.unjs.io/connectors),了解每个连接器接受的 `options`。 ### 开发数据库 使用 `devDatabase` 配置来**仅在开发模式下**覆盖数据库配置。这对于在开发期间使用本地 SQLite 数据库,同时在生产环境中使用不同的数据库非常有用。 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ database: { default: { connector: "postgresql", options: { url: "postgresql://username:password@hostname:port/database_name" } } }, devDatabase: { default: { connector: "sqlite", options: { name: "dev-db" } } } }); ``` ::note 在开发环境中,提供的 `devDatabase` 会完全**替换** `database` 配置;连接不会按名称合并。请在 `devDatabase` 中包含开发期间所需的每个连接。 :: ::tip 启用 `experimental.database` 且未提供 `database` 或 `devDatabase` 配置时,Nitro 会自动配置一个默认的 SQLite 连接。在开发模式下,数据会存储在相对于项目根目录的位置。在 Node.js 生产环境中,则使用默认的 SQLite 路径。 :: ## 连接器 Nitro 支持所有 [db0 连接器](https://db0.unjs.io/connectors)。数据库配置中的 `connector` 字段接受以下任意值: | 连接器 | 描述 | |---|---| | `sqlite` | Node.js 内置 SQLite(`node-sqlite` 的别名)| | `node-sqlite` | Node.js 内置 SQLite | | `better-sqlite3` | [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) | | `sqlite3` | [sqlite3](https://github.com/TryGhost/node-sqlite3) | | `bun` / `bun-sqlite` | Bun 内置 SQLite | | `libsql` / `libsql-node` | [libSQL](https://github.com/tursodatabase/libsql)(Node.js) | | `libsql-http` | 基于 HTTP 的 libSQL | | `libsql-web` | 用于 Web 环境的 libSQL | | `libsql-core` | 使用用户提供客户端的 libSQL | | `postgresql` | [PostgreSQL](https://github.com/brianc/node-postgres) | | `mysql2` | [MySQL](https://github.com/sidorares/node-mysql2) | | `pglite` | [PGlite](https://github.com/electric-sql/pglite)(嵌入式 PostgreSQL) | | `planetscale` | [PlanetScale](https://github.com/planetscale/database-js) 无服务器数据库 | | `neon` | [Neon](https://github.com/neondatabase/serverless) 无服务器数据库 | | `cloudflare-d1` | [Cloudflare D1](https://developers.cloudflare.com/d1/) | | `cloudflare-hyperdrive-mysql` | 使用 MySQL 的 Cloudflare Hyperdrive | | `cloudflare-hyperdrive-postgresql` | 使用 PostgreSQL 的 Cloudflare Hyperdrive | ### 连接器依赖 某些连接器依赖第三方库(例如,`postgresql` 需要 [`pg`](https://www.npmjs.com/package/pg))。 Nitro 会检测已配置连接所需的库,并提示安装缺失的库(在 CI 中会自动安装)。随后,已安装的库会通过其 `lib` 选项显式传递给连接器,以便打包器能够静态解析它们。 --- # 生命周期 > 了解 Nitro 如何运行并为你的应用提供传入请求服务。 ## 服务器启动 当服务器启动时,在接受任何请求之前,Nitro 会: 1. 执行[服务器入口](/docs/server-entry#startup-logic)中的顶层代码。 2. 创建 Nitro 应用,并按顺序运行[运行时插件](/docs/plugins)。 在无服务器和边缘预设中,这会在每次冷启动时发生。 ## 请求生命周期 请求可以在以下任何层中被拦截和终止(无论是否有响应),按以下顺序: ::steps ### `request` 钩子 `request` 钩子是每个传入请求运行的第一段代码。它通过[运行时插件](/docs/plugins)进行注册: ```ts [plugins/request-hook.ts] import { definePlugin } from "nitro"; export default definePlugin((nitroApp) => { nitroApp.hooks.hook("request", (event) => { console.log(`Incoming request on ${event.req.url}`); }); }); ``` ::note 在 `request` 钩子中抛出的错误会被 [`error` 钩子](#error-handling)捕获,不会终止请求管道。 :: ### 路由规则 接下来执行 Nitro 配置中的匹配[路由规则](/docs/routing#route-rules)。路由规则作为中间件运行,在所有全局中间件之前执行,并且其中大多数会修改响应而不会终止请求(例如添加标头或设置缓存策略)。匹配的规则会在任何中间件运行之前解析,并且在每个中间件中都可通过 `event.context.routeRules` 访问。 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ routeRules: { '/**': { headers: { 'x-nitro': 'first' } } } }) ``` :read-more{to="/docs/routing#route-rules" title="路由 > 路由规则"} ### 静态资源 启用静态资源服务时(大多数预设的默认设置),Nitro 会将静态资源处理程序注册为**第一个全局中间件**。它会在其他全局中间件和路由处理程序运行之前,检查请求是否匹配 `public/` 目录中的文件。 如果找到匹配项,静态文件会立即使用适当的 `Content-Type`、`ETag`、`Last-Modified` 和 `Cache-Control` 标头进行响应。请求会被终止,不会再执行其他中间件或路由。 静态资源还通过 `Accept-Encoding` 标头支持对预压缩文件(gzip、brotli、zstd)的内容协商。 ### 全局中间件 接下来会运行 `middleware/` 目录中定义的所有全局中间件: ```ts [middleware/info.ts] import { defineHandler } from "nitro"; export default defineHandler((event) => { event.context.info = { name: "Nitro" }; }); ``` ::warning 从中间件返回将关闭请求,应尽可能避免。 :: ::read-more{to="/docs/routing#middleware"} 了解有关 Nitro 中间件的更多信息。 :: ### 路由中间件 针对特定路由模式注册的中间件(通过 [`handlers` 配置](/docs/routing#route-scoped-middleware))会在全局中间件之后运行。附加到匹配路由本身的任何中间件会最后运行,就在路由处理程序之前。 ### 路由 Nitro 会将传入请求与 `routes/` 文件夹中定义的路由进行匹配。 ```ts [routes/api/hello.ts] export default (event) => ({ world: true }) ``` ::read-more{to="/docs/routing#filesystem-routing"} 详细了解 Nitro 文件系统路由。 :: ### 服务器入口 如果定义了服务器入口,它会捕获所有未匹配任何路由的请求,充当 `/**` 路由处理程序。 ```ts [server.ts] import { defineHandler } from "nitro"; export default defineHandler((event) => { if (event.url.pathname === "/") { return "Home page"; } }); ``` ::read-more{to="/docs/server-entry"} 详细了解 Nitro 服务器入口。 :: ### 渲染器 如果没有匹配的路由,Nitro 将查找渲染器处理程序(已定义或自动检测)来处理请求。 ::read-more{to="/docs/renderer"} 了解有关 Nitro 渲染器的更多信息。 :: ### `response` 钩子 在创建响应后(来自上述任何层),`response` 钩子运行。此钩子接收最终的 `Response` 对象和事件,可用于检查或修改响应头: ```ts [plugins/response-hook.ts] import { definePlugin } from "nitro"; export default definePlugin((nitroApp) => { nitroApp.hooks.hook("response", (res, event) => { console.log(`Response ${res.status} for ${event.req.url}`); }); }); ``` ::note `response` 钩子为每个响应运行,包括静态资源、中间件终止的请求和错误响应。 :: :: ## 错误处理 当在请求生命周期的任何点发生错误时,Nitro: 1. 调用 `error` 钩子,传入错误和上下文(包括事件和源标签)。 2. 将错误传递给**错误处理程序**,将其转换为 HTTP 响应。 ### 抛出错误 从任何处理程序或中间件中抛出 `HTTPError`,以使用特定的状态码、消息和可选数据结束请求: ```ts [routes/signup.ts] import { defineHandler, HTTPError } from "nitro"; export default defineHandler((event) => { // Message and details throw new HTTPError("Invalid user input", { status: 400 }); // Status code shortcut throw HTTPError.status(400, "Bad Request"); // Full object form throw new HTTPError({ status: 400, message: "Invalid user input", data: { field: "email" }, }); }); ``` ::note 任何其他抛出的值(例如普通的 `new Error()`)都会被视为**未处理**错误:响应始终为 `500`,并且出于安全考虑,消息、数据和堆栈不会发送给客户端。 :: ### 默认错误响应 在生产环境中,默认错误处理程序始终返回 JSON: ```json { "error": true, "status": 400, "message": "Invalid user input", "data": { "field": "email" } } ``` 在开发环境中,程序化请求会收到相同的 JSON 格式;但当请求的 `Accept` 标头包含 `text/html` 时(浏览器请求),Nitro 会渲染一个带有源映射堆栈跟踪的交互式 HTML 错误页面。 ### 自定义错误处理程序 要控制错误响应格式,请在 Nitro 配置中将 `errorHandler` 设置为一个文件,该文件使用 `defineErrorHandler` 导出处理程序: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ errorHandler: "~/error", }); ``` ```ts [error.ts] import { defineErrorHandler } from "nitro"; export default defineErrorHandler((error, event) => { return new Response(`[custom error] ${error.message}`, { status: error.status || 500, headers: { "Content-Type": "text/plain" }, }); }); ``` 返回 `Response` 以将其发送给客户端,或不返回任何内容以继续交由下一个处理程序处理。`errorHandler` 还接受按顺序运行的处理程序数组:第一个返回响应的处理程序获胜,并且内置默认处理程序始终作为最后一项追加,因此始终存在备用响应: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ errorHandler: ["~/error/known-errors", "~/error/fallback"], }); ``` 如果处理程序自身抛出错误,则会记录该失败,并继续使用下一个处理程序。每个处理程序还会在其第三个参数中以 `{ defaultHandler }` 的形式接收内置的 `defaultHandler`,因此你可以在自己的逻辑中复用默认渲染。 ::read-more{to="/examples/custom-error-handler"} 查看完整的自定义错误处理程序示例。 :: 要仅自定义开发服务器的错误渲染(不影响生产环境构建),请将处理程序函数作为 `devErrorHandler` 传入: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; import errorHandler from "./error.ts"; export default defineConfig({ devErrorHandler: errorHandler, }); ``` ### 观察错误 要在不更改响应的情况下观察错误,请使用来自[插件](/docs/plugins)的 `error` 运行时钩子: ```ts [plugins/errors.ts] import { definePlugin } from "nitro"; export default definePlugin((nitroApp) => { nitroApp.hooks.hook("error", (error, context) => { console.error("Captured error:", error); // context.event - H3 事件(如果可用) // context.tags - 错误源标签,如 "request"、"response"、"plugin" }); }); ``` 你还可以通过 [`nitroApp.captureError`](/docs/plugins#programmatic-error-capture) 以编程方式将错误送入同一管道。错误也会按请求记录在 `event.req.context.nitro.errors` 中,以便在后续钩子中检查。 此外,进程级别的未处理 Promise 拒绝和未捕获异常会自动捕获到 `error` 钩子中,并带有 `"unhandledRejection"` 和 `"uncaughtException"` 标签。 ## 服务器关闭 当 Nitro 服务器关闭时,会调用 `close` 钩子。使用它来清理资源,如数据库连接、定时器或外部服务句柄: ```ts [plugins/cleanup.ts] import { definePlugin } from "nitro"; export default definePlugin((nitroApp) => { nitroApp.hooks.hook("close", async () => { // 清理资源 }); }); ``` ::note HTTP 服务器停止接受连接后会等待该钩子完成。长时间运行的服务器预设(Node.js、Bun 和 Deno)会在 `SIGINT` 和 `SIGTERM` 上调用它。无服务器和边缘预设没有关闭信号,因此只有在平台关闭运行时的情况下才会运行该钩子。 :: ## 钩子参考 所有运行时钩子都通过[运行时插件](/docs/plugins)使用 `nitroApp.hooks.hook()` 注册。 | 钩子 | 签名 | 运行时机 | | --- | --- | --- | | `request` | `(event: HTTPEvent) => void \| Promise` | 每个请求开始时,路由之前。 | | `response` | `(res: Response, event: HTTPEvent) => void \| Promise` | 响应创建后、发送前。 | | `error` | `(error: Error, context: { event?, tags? }) => void` | 在生命周期期间捕获到任何错误时。 | | `close` | `() => void` | 当 Nitro 服务器关闭时。 | ::note `NitroRuntimeHooks` 接口是可扩展的。部署预设(如 Cloudflare)可以用平台特定的钩子来扩展它。 :: ::read-more{to="/docs/plugins"} 了解有关 Nitro 插件和钩子使用示例的更多信息。 :: --- # OpenAPI > Nitro 可以根据你的路由处理器自动生成 [OpenAPI](https://www.openapis.org/) 规范,并提供交互式 API 文档。 Nitro 会扫描所有路由处理器,提取通过 `defineRouteMeta` 定义的元数据,并生成一份 [OpenAPI 3.1.0](https://spec.openapis.org/oas/v3.1.0) 规范。内置的 [Scalar](https://scalar.com/) 和 [Swagger UI](https://swagger.io/tools/swagger-ui/) 界面让你可以直接在浏览器中浏览并测试 API。 ::important OpenAPI 支持目前处于实验阶段。 :: ## 启用 OpenAPI 在 Nitro 配置中启用 OpenAPI: ::code-group ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ experimental: { openAPI: true, }, }); ``` :: 启用后,开发环境中会提供以下端点: | 端点 | 说明 | |---|---| | `/_openapi.json` | OpenAPI 3.1.0 JSON 规范 | | `/_scalar` | Scalar API 参考界面 | | `/_swagger` | Swagger UI | ## 路由元数据 在路由处理器文件中使用 `defineRouteMeta` 宏,为每个路由提供 OpenAPI 元数据。`openAPI` 属性接收标准的 OpenAPI [Operation Object](https://spec.openapis.org/oas/v3.1.0#operation-object)。 ```ts [routes/api/hello.ts] import { defineRouteMeta, defineHandler } from "nitro"; defineRouteMeta({ openAPI: { tags: ["greeting"], description: "返回问候消息", responses: { 200: { description: "问候成功" }, }, }, }); export default defineHandler(() => { return { message: "你好,世界!" }; }); ``` ::note `defineRouteMeta` 是一个构建期宏。元数据会在构建过程中被静态提取,不会为处理器增加任何运行时开销。 :: ### 参数 路由参数(如 `:id`、`[id]`)会自动转换为 OpenAPI 路径参数。你也可以在 `parameters` 数组中定义额外的查询参数或请求头参数: ```ts [routes/api/users/[id].get.ts] import { defineRouteMeta, defineHandler } from "nitro"; defineRouteMeta({ openAPI: { tags: ["users"], description: "根据 ID 获取用户", parameters: [ { in: "query", name: "include", description: "要包含的关联资源列表,使用逗号分隔", schema: { type: "string" }, }, ], responses: { 200: { description: "已找到用户" }, 404: { description: "未找到用户" }, }, }, }); export default defineHandler((event) => { const { id } = event.context.params; return { id, name: "小明" }; }); ``` 在这个示例中,`id` 路径参数会根据路由模式自动推断,因此只需要声明额外的 `include` 查询参数。 ### 响应 Schema 使用标准的 OpenAPI `responses` 对象来定义响应的内容类型和 Schema: ```ts [routes/api/status.ts] import { defineRouteMeta, defineHandler } from "nitro"; defineRouteMeta({ openAPI: { description: "返回当前服务器状态", responses: { 200: { description: "服务器状态", content: { "application/json": { schema: { type: "object", properties: { status: { type: "string", enum: ["healthy", "degraded"] }, uptime: { type: "number" }, }, }, }, }, }, }, }, }); export default defineHandler(() => { return { status: "healthy", uptime: process.uptime() }; }); ``` ### 全局组件 使用 `$global` 属性定义可复用的 Schema,它们会被提升到 OpenAPI 规范顶层的 `components` 区域。这样你就可以在多个路由之间通过 `$ref` 复用这些共享 Schema。 ```ts [routes/api/users.get.ts] import { defineRouteMeta, defineHandler } from "nitro"; defineRouteMeta({ openAPI: { tags: ["users"], description: "列出所有用户", responses: { 200: { description: "用户列表", content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/User" }, }, }, }, }, }, $global: { components: { schemas: { User: { type: "object", properties: { id: { type: "string" }, name: { type: "string" }, email: { type: "string", format: "email" }, }, }, }, }, }, }, }); export default defineHandler(() => { return [{ id: "1", name: "小明", email: "xiaoming@example.com" }]; }); ``` 定义之后,`User` Schema 就可以在任意其他路由中通过 `{ $ref: "#/components/schemas/User" }` 引用,而无需重复声明。 ### 自动标签 路由会根据路径前缀自动分配标签: | 路径前缀 | 标签 | |---|---| | `/api/` | API 路由 | | `/_` | 内部 | | 其他 | 应用路由 | 你也可以在 `openAPI` 元数据中显式指定 `tags` 来覆盖自动标签。 ## 配置 通过顶层 `openAPI` 选项配置 OpenAPI 的行为: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ experimental: { openAPI: true, }, openAPI: { meta: { title: "My API", description: "我的超棒 API", version: "2.0.0", }, }, }); ``` ### `meta` 设置 API 元数据,这些内容会出现在规范的 `info` 对象中: | 属性 | 默认值 | 说明 | |---|---|---| | `title` | `"Nitro Server Routes"` | API 标题 | | `description` | — | API 描述 | | `version` | `"1.0.0"` | API 版本 | ### `route` - 默认值:`"/_openapi.json"` 覆盖 OpenAPI JSON 规范的服务路径: ```ts [nitro.config.ts] export default defineConfig({ openAPI: { route: "/_docs/openapi.json", }, }); ``` ### `ui` 配置或禁用内置的 API 文档界面: ```ts [nitro.config.ts] export default defineConfig({ openAPI: { ui: { scalar: { route: "/_docs/scalar", theme: "purple", }, swagger: { route: "/_docs/swagger", }, }, }, }); ``` 将任一 UI 设置为 `false`{lang=ts} 即可禁用: ```ts [nitro.config.ts] export default defineConfig({ openAPI: { ui: { swagger: false, }, }, }); ``` :read-more{to="https://github.com/scalar/scalar" title="Scalar 配置"} ## 生产环境 默认情况下,OpenAPI 端点只在开发环境中可用。若要在生产环境中启用,请设置 `production` 选项: ```ts [nitro.config.ts] export default defineConfig({ openAPI: { production: "runtime", }, }); ``` | 值 | 行为 | |---|---| | `false`{lang=ts} | 在生产环境中禁用(默认) | | `"runtime"`{lang=ts} | 在每次请求时运行时生成规范 | | `"prerender"`{lang=ts} | 在构建时生成规范,并作为静态文件提供 | 如果规范在不同部署之间不会变化,建议使用 `"prerender"`{lang=ts} 以获得最佳性能;如果你需要动态服务器信息或中间件访问能力,则使用 `"runtime"`{lang=ts}。 ::warning 如果你在生产环境中启用了 OpenAPI,请务必使用合适的鉴权或访问控制来保护这些端点。 :: --- # 插件 > 使用运行时插件扩展 Nitro 的运行时行为。 运行时插件会在服务器启动期间**执行一次**,因此它们适合用于一次性初始化以及注册[生命周期钩子](/docs/lifecycle)。每个插件都会接收 `nitroApp` 上下文。 插件会从 `plugins/` 目录自动注册并同步运行。插件函数本身必须是同步的(返回 `void`),但它们注册的钩子可以是异步的。 ```ts [plugins/test.ts] import { definePlugin } from "nitro"; export default definePlugin((nitroApp) => { console.log('Nitro plugin', nitroApp) }) ``` 如果你有其他目录中的插件,可以使用 `plugins` 选项: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ plugins: ['my-plugins/hello.ts'] }) ``` 插件按以下顺序运行: 1. 按 `plugins` 选项中列出的顺序运行其中列出的插件。 2. 运行从 `plugins/` 目录自动注册的插件。目录按照 [`scanDirs`](/config#scandirs) 的顺序处理,每个目录中的文件则按路径排序。 ## `nitroApp` 上下文 插件函数接收一个具有以下属性的 `nitroApp` 对象: | 属性 | 类型 | 描述 | | --- | --- | --- | | `hooks` | [`HookableCore`](https://github.com/unjs/hookable) | 用于注册生命周期回调的钩子系统。 | | `h3` | `H3Core` | 底层的 [H3](https://github.com/h3js/h3) 应用实例。 | | `fetch` | `(req: Request) => Response \| Promise` | 应用的内部 fetch 处理器。 | | `captureError` | `(error: Error, context) => void` | 以编程方式将错误捕获到错误钩子管道中。 | ::note H3 会在第一个请求时组合一次中间件链。插件在启动期间添加到 `nitroApp.h3["~middleware"]` 的中间件会被包含在内,但如果它们位于数组前部,则会在 Nitro 的路由规则之前运行。如果插件在处理第一个请求后修改了该数组,则还必须将 `nitroApp.h3["~dispatch"]` 和 `nitroApp.h3["~composed"]` 重置为 `undefined`,以便重新组合该链。 :: ## Nitro 运行时钩子 使用 Nitro [钩子](https://github.com/unjs/hookable) 在[请求生命周期](/docs/lifecycle)中的特定时间点运行自定义函数。在插件内部使用 `nitroApp.hooks.hook()` 注册它们: ```ts [plugins/close.ts] import { definePlugin } from "nitro"; export default definePlugin((nitroApp) => { nitroApp.hooks.hook("close", async () => { // 当 nitro 关闭时运行 }); }) ``` ### 可用的钩子 | 钩子 | 签名 | 描述 | | --- | --- | --- | | `request` | `(event: HTTPEvent) => void \| Promise` | 在每个请求开始时调用。 | | `response` | `(res: Response, event: HTTPEvent) => void \| Promise` | 在响应创建后调用。 | | `error` | `(error: Error, context: { event?: HTTPEvent, tags?: string[] }) => void` | 当错误被捕获时调用。 | | `close` | `() => void` | 当 Nitro 服务器关闭时调用。 | > [!NOTE] > `NitroRuntimeHooks` 接口是可扩展的。部署预设(如 Cloudflare)可以使用平台特定的钩子(如 `cloudflare:scheduled` 和 `cloudflare:email`)来扩展它。 ### 注销钩子 `hook()` 方法返回一个注销函数,可以调用该函数来移除钩子: ```ts [plugins/example.ts] import { definePlugin } from "nitro"; export default definePlugin((nitroApp) => { const unregister = nitroApp.hooks.hook("request", (event) => { // ...... }); // 稍后,移除该钩子 unregister(); }); ``` ## 示例 ### 捕获错误 你可以使用插件来捕获所有应用错误。 ```ts [plugins/errors.ts] import { definePlugin } from "nitro"; export default definePlugin((nitroApp) => { nitroApp.hooks.hook("error", async (error, { event }) => { console.error(`${event?.req.url} Application error:`, error) }); }) ``` `context` 对象包含一个可选的 `tags` 数组,用于标识错误来源(例如 `"request"`、`"response"`、`"cache"`、`"plugin"`、`"unhandledRejection"`、`"uncaughtException"`)。 ### 以编程方式捕获错误 你可以使用 `captureError` 手动将错误送入错误钩子管道: ```ts [plugins/capture.ts] import { definePlugin } from "nitro"; export default definePlugin((nitroApp) => { nitroApp.captureError(new Error("something went wrong"), { tags: ["startup"], }); }); ``` ### 优雅关闭 服务器会优雅地关闭,等待所有使用 `event.waitUntil` 启动的待处理后台任务。使用 `close` 钩子清理你自己的资源: ```ts [plugins/shutdown.ts] import { definePlugin } from "nitro"; export default definePlugin((nitroApp) => { nitroApp.hooks.hook("close", async () => { // 清理资源、关闭连接等。 }); }); ``` ### 请求和响应生命周期 你可以使用插件注册在[请求生命周期](/docs/lifecycle)中运行的钩子: ```ts [plugins/lifecycle.ts] import { definePlugin } from "nitro"; export default definePlugin((nitroApp) => { nitroApp.hooks.hook("request", (event) => { console.log("on request", event.req.url); }); nitroApp.hooks.hook("response", (res, event) => { // 修改或检查响应 console.log("on response", res.status); }); }); ``` ### 修改响应头 ```ts [plugins/headers.ts] import { definePlugin } from "nitro"; export default definePlugin((nitroApp) => { nitroApp.hooks.hook("response", (res, event) => { const { pathname } = new URL(event.req.url); if (pathname.endsWith(".css") || pathname.endsWith(".js")) { res.headers.append("Vary", "Origin"); } }); }); ``` --- # 预渲染 > 在构建时渲染路由,并将其作为静态资源提供 启用预渲染后,Nitro 会在构建期间获取指定路由,并将响应写入 `.output/public` 目录。预渲染的路由随后会作为静态文件提供。请求时不会进行服务器渲染,因此这些路由速度快、可缓存,并且可以部署到任意静态托管服务或 CDN。 预渲染适用于内容不会随请求变化的路由:落地页、文档、博客文章,或输出内容固定的 JSON 端点。动态路由仍然可以继续由服务器提供,并且你可以在同一个应用中自由混合预渲染路由和服务器渲染路由。 ## 预渲染路由 ### `prerender.routes` 配置 使用 `prerender.routes` 选项列出要预渲染的路由: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ prerender: { routes: ["/", "/about", "/api/content.json"], }, }); ``` HTML 响应会作为 `index.html` 文件写入子文件夹中(`/about` 会生成 `about/index.html`),而其他响应会保留其路径(`/api/content.json` 会生成 `api/content.json`)。请参阅 [`autoSubfolderIndex`](#options) 以更改此行为。 ### `prerender` 路由规则 或者,使用 `prerender` [路由规则](/docs/routing#route-rules)将路由标记为预渲染: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ routeRules: { "/about": { prerender: true }, }, }); ``` ::warning 路由规则只会将**完全匹配的路径**添加到预渲染列表中。像 `"/blog/**": { prerender: true }` 这样的通配符模式**不会**自动展开,因为 Nitro 无法知道哪些路由与该模式匹配。要预渲染一组动态路由,请使用[链接爬取](#crawling-links)或 [`prerender:routes` hook](#hooks)。 :: ## 爬取链接 除了(或作为补充)列出每个路由之外,还可以启用 `crawlLinks`,让 Nitro 通过跟随预渲染 HTML 页面中的链接来发现路由: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ prerender: { crawlLinks: true, routes: ["/"], }, }); ``` 爬虫会从每个预渲染 HTML 页面中提取 `href` 属性,并将发现的路由加入预渲染队列。外部链接、哈希链接,以及指向带扩展名文件的链接(`.json` 除外)都会被跳过。相对链接会根据其所在页面进行解析。 如果启用了 `crawlLinks` 且未指定路由,爬取会从 `/` 开始。 ::tip 处理程序可以通过将 `x-nitro-prerender` 响应标头设置为以逗号分隔的路径列表,来为预渲染添加其他路由。即使禁用了 `crawlLinks`,此功能仍然有效。 :: ## 忽略路由 使用 `prerender.ignore` 跳过路由。每个条目可以是字符串前缀、正则表达式,或接收路径的函数: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ prerender: { crawlLinks: true, ignore: [ "/admin", // Ignore routes starting with /admin /\/preview$/, // Ignore routes ending with /preview (path) => path.includes("draft"), // Ignore routes containing "draft" ], }, }); ``` 你也可以使用 `prerender: false` 路由规则排除路由。与启用预渲染不同,通配符模式**可以**用于排除,因为它们会针对每个候选路由进行匹配: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ routeRules: { "/dynamic/**": { prerender: false }, }, }); ``` ### 公共资源 由带有基础 URL 前缀的[公共资源](/docs/assets)提供的路由始终会被跳过。此外,你可以设置 `prerender.ignoreUnprefixedPublicAssets: true`,跳过已经由不带前缀的公共资源提供的路由,从而避免输出中出现重复文件: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ prerender: { crawlLinks: true, ignoreUnprefixedPublicAssets: true, }, }); ``` ## 选项 | 选项 | 默认值 | 描述 | |--------|---------|-------------| | `failOnError` | `false` | 当路由无法预渲染时使构建失败。适用于 CI。 | | `retry` | `3` | 失败路由的重试次数。传入 `Infinity` 可无限重试。 | | `retryDelay` | `500` | 每次重试之间的延迟时间,单位为毫秒。 | | `concurrency` | `1` | 并行预渲染的最大路由数。 | | `interval` | `0` | 预渲染请求之间的延迟时间,单位为毫秒。 | | `autoSubfolderIndex` | `true` | 将 HTML 路由写入子文件夹索引文件。 | ::note `retry` 和 `retryDelay` 选项可以在配置中接受,但当前的 v3 预渲染器尚未应用它们。 :: `concurrency` 和 `interval` 选项控制预渲染速度。提高 `concurrency` 可以加快大型网站的速度,或者添加 `interval`,以避免路由调用外部 API 时触发速率限制。 `autoSubfolderIndex` 控制 HTML 文件写入 `.output/public` 的方式: ```bash # autoSubfolderIndex: true (default) /about -> .output/public/about/index.html # autoSubfolderIndex: false /about -> .output/public/about.html ``` 当你的托管服务提供商不允许配置尾部斜杠行为时,将其设置为 `false`。 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ prerender: { crawlLinks: true, failOnError: true, concurrency: 8, autoSubfolderIndex: false, }, }); ``` ## Hooks 对于高级用例,你可以在构建时接入预渲染生命周期。这些 hooks 可供[模块](/docs/modules)和 `hooks` 配置选项使用: | Hook | 描述 | |------|-------------| | `prerender:routes` | 使用初始的路由 `Set` 调用,在预渲染开始前添加或移除条目。 | | `prerender:config` | 使用用于预渲染的内部 Nitro 实例配置调用。 | | `prerender:generate` | 在写入每个路由的文件之前调用,用于检查或修改路由对象(`route.contents`、`route.fileName`……),或设置 `route.skip = true`。 | | `prerender:route` | 每个路由生成后调用。 | | `prerender:done` | 预渲染完成后调用一次,并传入已预渲染和失败路由的列表。 | 一个常见用例是在运行时动态注册路由,例如从 CMS 或数据库中注册: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ modules: [ (nitro) => { nitro.hooks.hook("prerender:routes", async (routes) => { const posts = await fetch("https://api.example.com/posts").then((res) => res.json()); for (const post of posts) { routes.add(`/blog/${post.slug}`); } }); }, ], }); ``` ## 静态托管 要发布一个不包含服务器包的完全预渲染网站,请使用 `static`、`github_pages` 或 `gitlab_pages` 等静态预设。这些预设会跳过服务器构建、启用 `crawlLinks`,并且只输出 `public` 目录,可直接上传到任意静态托管服务。详情请参阅[部署文档](/deploy)以及 [GitHub Pages](/deploy/providers/github-pages) 等提供商的文档。 ::note `static: true` 配置选项只会禁用服务器构建;它本身不会启用预渲染。直接使用该选项而不是静态预设时,请配置 `prerender` 选项(通常为 `crawlLinks: true`)来生成页面。 :: --- # 任务 > Nitro tasks 允许在运行时执行一次性操作:例如数据库迁移、缓存清理或内容同步等命名的服务器端操作,你可以按需、按计划或从 CLI 运行这些操作 ## 选择加入实验性功能 ::important Tasks support is currently experimental. See [nitrojs/nitro#1974](https://github.com/nitrojs/nitro/issues/1974) for the relevant discussion. :: 为了使用 tasks API,你需要启用实验性功能标志。Tasks 会从你的 [`serverDir`](/docs/configuration) 中扫描,同时也必须设置该配置: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: "./server", experimental: { tasks: true } }) ``` 本页面上的所有示例都假设使用此配置。 ## 定义任务 可以在 [`serverDir`](/docs/configuration) 内的 `tasks/[name].ts` 文件中定义任务。 ::note 与路由和插件一样,只有在设置了 [`serverDir`](/docs/configuration)(或 `scanDirs`)时才会扫描任务;默认不会扫描任何目录 :: 支持嵌套目录。任务名称将使用 `:` 连接。 ```txt server/ tasks/ cleanup.ts <-- cleanup db/ migrate.ts <-- db:migrate nitro.config.ts ``` **示例:** ```ts [server/tasks/db/migrate.ts] import { defineTask } from "nitro/task"; export default defineTask({ meta: { name: "db:migrate", description: "Run database migrations", }, run({ payload, context }) { console.log("Running DB migration task..."); return { result: "Success" }; }, }); ``` ### 任务接口 `defineTask` 辅助函数接受一个具有以下属性的对象: - **`meta`**(可选):一个包含可选的 `name` 和 `description` 字符串字段的对象,用于在开发服务器和 CLI 中显示。 - **`run`**(必需):一个接收 [`TaskEvent`](#taskevent) 并返回(或解析为)一个包含可选 `result` 属性的对象的函数。 ```ts interface Task { meta?: { name?: string; description?: string }; run(event: TaskEvent): { result?: RT } | Promise<{ result?: RT }>; } ``` ### `TaskEvent` `run` 函数接收一个具有以下属性的 `TaskEvent` 对象: - **`name`**:正在执行的任务的名称。 - **`payload`**:一个包含传递给任务的任何数据的对象(`Record`)。 - **`context`**:一个 `TaskContext` 对象(根据运行时的不同,可能包含 `waitUntil`)。 ```ts interface TaskEvent { name: string; payload: TaskPayload; context: TaskContext; } ``` ### 通过配置注册任务 除了基于文件的扫描之外,还可以直接在 Nitro 配置中注册任务。这对于向扫描到的任务添加描述,或用于由模块提供并指向自定义处理程序路径的任务非常有用。 ```ts [nitro.config.ts] import { fileURLToPath } from "node:url"; import { defineConfig } from "nitro"; export default defineConfig({ serverDir: "./server", experimental: { tasks: true }, tasks: { // Describe a task scanned from `server/tasks/db/migrate.ts` "db:migrate": { description: "Run database migrations" }, // Register a task from a custom location "db:seed": { handler: fileURLToPath(new URL("scripts/seed.ts", import.meta.url)), description: "Seed the database" } } }) ``` ::warning 与路由处理程序不同,任务的 `handler` 会按原样导入,**不会**根据项目根目录解析,因此类似 `"./scripts/seed.ts"` 的路径会在构建时解析失败。请使用绝对路径(例如通过 `fileURLToPath(new URL(..., import.meta.url))`),或使用包名称 :: 如果某个任务同时从 `tasks/` 目录扫描到,并且在配置中定义,则配置中定义的 `handler` 优先级更高。 ## 定时任务 你可以使用 Nitro 配置定义定时任务,使其在指定的 cron 格式时间自动运行。 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ scheduledTasks: { // 每分钟运行 `cms:update` 任务 '* * * * *': ['cms:update'], // 运行单个任务(字符串简写形式) '0 * * * *': 'db:cleanup' } }) ``` `scheduledTasks` 配置将 cron 表达式映射为单个任务名称(字符串)或任务名称数组。当多个任务被分配到同一个 cron 表达式时,它们会并行运行。 ::tip 你可以使用 [crontab.guru](https://crontab.guru/) 轻松生成和理解 cron 表达式 :: 当定时任务运行时,它会自动接收一个 `payload`,其中 `scheduledTime` 设置为当前时间戳(`Date.now()`)。 ::note 当你的部署目标支持时,优先使用 `scheduledTasks`(参见[平台支持](#platform-support))。Nitro 会为你连接平台的原生调度器。如果你的平台不受支持,仍然可以通过从经过身份验证的 HTTP 端点调用 [`runTask`](#programmatically-run-tasks),由外部调度器触发任务 :: ### 平台支持 - **`dev`**、**`node_server`**、**`node_cluster`**、**`node_middleware`**、**`bun`** 和 **`deno_server`** 预设由 [croner](https://croner.56k.guru/) 引擎支持。 - **`cloudflare_module`** 和 **`cloudflare_pages`** 预设原生集成了 [Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/)。Nitro 会在构建时自动在 wrangler 配置中生成 cron triggers,无需手动设置 wrangler - **`vercel`** 预设原生集成了 [Vercel Cron Jobs](https://vercel.com/docs/cron-jobs)。Nitro 会在构建时自动生成 cron job 配置,无需手动设置 `vercel.json`。你可以通过设置 `CRON_SECRET` 环境变量来保护 cron 端点 - 计划支持更多预设(包括原生 primitives 支持)! ## `waitUntil` 在运行后台任务时,你可能需要确保服务器或工作进程等待任务完成。 根据运行时的不同,一个可选的 `context.waitUntil` 函数 _可能_ 可用。 ```ts import { defineTask } from "nitro/task"; export default defineTask({ run({ context }) { const promise = fetch(...) context.waitUntil?.(promise); await promise; return { result: "Success" }; }, }); ``` ## 以编程方式运行任务 要手动运行任务,你可以使用 `nitro/task` 中的 `runTask(name, { payload?, context? })` 函数。 **示例:** ```ts [server/api/migrate.ts] import { defineHandler } from "nitro"; import { runTask } from "nitro/task"; export default defineHandler(async (event) => { // 重要:验证用户身份并验证 payload! const payload = Object.fromEntries(event.url.searchParams); const { result } = await runTask("db:migrate", { payload }); return { result }; }); ``` ### 错误处理 `runTask` 在以下情况下会抛出 HTTP 错误: - 任务不存在(状态 `404`)。 - 任务没有处理程序实现(状态 `501`)。 在任务的 `run` 函数中抛出的任何错误都会传播给调用者。 ## 使用开发服务器运行任务 Nitro 的内置开发服务器公开了任务,以便无需编程即可轻松执行。 ### 使用 API 路由 #### `/_nitro/tasks` 此端点返回可用任务名称及其元数据的列表。 ```json // [GET] /_nitro/tasks { "tasks": { "db:migrate": { "description": "Run database migrations" }, "cms:update": { "description": "Update CMS content" } }, "scheduledTasks": [ { "cron": "* * * * *", "tasks": [ "cms:update" ] } ] } ``` #### `/_nitro/tasks/:name` 此端点执行任务。你可以使用查询参数和 JSON 请求体来提供 payload。在 JSON 请求体中发送的 payload 必须位于 `"payload"` 属性下。 ::code-group ```ts [server/tasks/echo/payload.ts] import { defineTask } from "nitro/task"; export default defineTask({ meta: { name: "echo:payload", description: "Returns the provided payload", }, run({ payload, context }) { console.log("Running echo task..."); return { result: payload }; }, }); ``` ```json [GET] // [GET] /_nitro/tasks/echo:payload?field=value&array=1&array=2 { "field": "value", "array": ["1", "2"] } ``` ```json [POST] /** * [POST] /_nitro/tasks/echo:payload?field=value * 请求体: { * "payload": { * "answer": 42, * "nested": { * "value": true * } * } * } */ { "field": "value", "answer": 42, "nested": { "value": true } } ``` :: ::note JSON 请求体中指定的键会覆盖查询参数中存在的任何值 :: ### 使用 CLI ::important 只有在 **dev 服务器正在运行** 时才能运行这些命令。你应该在第二个终端中运行它们 :: #### 列出任务 ```sh nitro task list ``` #### 运行任务 ```sh nitro task run db:migrate --payload "{}" ``` `--payload` 标志接受一个 JSON 字符串,该字符串将被解析并传递给任务。如果该值不是有效的 JSON 对象,任务将在没有 payload 的情况下运行。 ::read-more{to="/docs/cli#nitro-task"} 有关所有 `nitro task` 子命令和标志,请参阅 CLI 参考 :: ## 注意事项 ### 并发 任务运行会按**任务名称**去重:每个任务在每个服务器实例中最多只能有一个正在运行的实例。对已在运行的任务调用 `runTask` 不会启动新的运行:所有并发调用者,即使传入不同的 payload,也会共享第一个正在运行的任务结果 --- # WebSocket > Nitro 基于 [CrossWS](https://crossws.h3.dev/) 和 [H3](https://h3.dev/) 提供跨平台的 WebSocket 支持。 WebSocket 支持客户端与服务器之间进行实时双向通信。Nitro 的 WebSocket 集成适用于所有受支持的部署目标,包括 Node.js、Bun、Deno、Vercel 和 Cloudflare Workers。 ::tip 如果你只需要单向的服务器到客户端更新(通知、实时信息流),可以考虑改用 [Server-Sent Events](#server-sent-events-sse)。它们使用普通 HTTP,并会自动重新连接。 :: :read-more{to="https://crossws.h3.dev/" title="CrossWS 文档"} ## 启用 WebSocket 在 Nitro 配置中启用 WebSocket 支持: ::code-group ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ features: { websocket: true, }, }); ``` :: ## 用法 使用 `defineWebSocketHandler` 创建一个 WebSocket 处理器,并从路由文件中导出。WebSocket 处理器与普通请求处理器一样,遵循相同的[文件路由](/docs/routing)规则。 ```ts [routes/_ws.ts] import { defineWebSocketHandler } from "nitro"; export default defineWebSocketHandler({ open(peer) { console.log("已连接:", peer.id); }, message(peer, message) { console.log("收到消息:", message.text()); peer.send("来自服务器的问候!"); }, close(peer, details) { console.log("已断开:", peer.id, details.code, details.reason); }, error(peer, error) { console.error("错误:", error); }, }); ``` ::tip 你可以为 WebSocket 处理器使用任意路由路径。例如,`routes/chat.ts` 会处理 `/chat` 上的 WebSocket 连接。 :: ### 从客户端连接 使用浏览器的 [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) 进行连接: ```js const ws = new WebSocket("ws://localhost:3000/_ws"); ws.addEventListener("open", () => { console.log("已连接!"); ws.send("来自客户端的问候!"); }); ws.addEventListener("message", (event) => { console.log("收到:", event.data); }); ``` ## 钩子 WebSocket 处理器支持以下生命周期钩子: ### `upgrade` 在 WebSocket 连接建立之前调用。你可以用它来校验请求、设置命名空间,或给 peer 附加上下文数据。 ```ts [routes/chat.ts] import { defineWebSocketHandler } from "nitro"; export default defineWebSocketHandler({ upgrade(request) { const url = new URL(request.url); const token = url.searchParams.get("token"); if (!isValidToken(token)) { throw new Response("未授权", { status: 401 }); } return { context: { userId: getUserId(token) }, }; }, open(peer) { console.log("用户已连接:", peer.context.userId); }, // ... }); ``` `upgrade` 钩子可以返回一个对象,包含: | 属性 | 类型 | 说明 | |---|---|---| | `headers` | `HeadersInit` | 要包含在升级响应中的 Response headers | | `protocol` | `string` | 为连接选择 WebSocket 子协议(必须是客户端在 `Sec-WebSocket-Protocol` 中提供的值之一) | | `namespace` | `string` | 覆盖此连接的发布/订阅命名空间 | | `context` | `object` | 附加到 `peer.context` 的数据 | | `handled` | `boolean` | 为 `true` 时,表示钩子已经自行完成升级,因此适配器会跳过自身的升级处理 | 抛出一个 `Response` 即可拒绝升级请求。 ### `open` 当 WebSocket 连接建立完成,且 peer 已可收发消息时调用。 ```ts open(peer) { peer.send("欢迎!"); } ``` ### `message` 在收到 peer 发来的消息时调用。 ```ts message(peer, message) { const text = message.text(); const data = message.json(); } ``` ### `close` 当 WebSocket 连接关闭时调用。会收到一个 `details` 对象,其中可能包含 `code` 和 `reason`。 ```ts close(peer, details) { console.log(`已关闭:${details.code} - ${details.reason}`); } ``` ### `error` 在 WebSocket 连接发生错误时调用。 ```ts error(peer, error) { console.error("WebSocket 错误:", error); } ``` ::note 根据适配器的不同,可能还会提供其他钩子,例如 `drain`、`ping` 和 `pong`。请参阅 [CrossWS 兼容性表](https://crossws.h3.dev/guide/peer#compatibility)。 :: ## Peer `peer` 对象表示一个已连接的 WebSocket 客户端。除 `upgrade` 外,其他所有钩子中都可以访问它。 ### 属性 | 属性 | 类型 | 说明 | |---|---|---| | `id` | `string` | 当前 peer 的唯一标识 | | `namespace` | `string` | 当前 peer 所属的发布/订阅命名空间 | | `context` | `object` | 在 `upgrade` 阶段设置的任意上下文数据 | | `request` | `Request` | 原始升级请求 | | `peers` | `Set` | 同一命名空间下的所有已连接 peer | | `topics` | `Set` | 当前 peer 订阅的主题 | | `remoteAddress` | `string?` | 客户端 IP 地址(取决于适配器) | | `websocket` | `WebSocket` | 底层的 WebSocket 实例 | ### 方法 #### `peer.send(data, options?)` 直接向当前 peer 发送消息。支持字符串、对象(会序列化为 JSON)以及二进制数据。 ```ts peer.send("你好!"); peer.send({ type: "greeting", text: "你好!" }); ``` #### `peer.subscribe(topic)` 让当前 peer 订阅一个发布/订阅主题。 ```ts peer.subscribe("notifications"); ``` #### `peer.unsubscribe(topic)` 取消当前 peer 对某个主题的订阅。 ```ts peer.unsubscribe("notifications"); ``` #### `peer.publish(topic, data, options?)` 向同一命名空间内订阅了该主题的所有 peer 广播消息。发送消息的 peer **不会**收到这条消息。 ```ts peer.publish("chat", { user: "小明", text: "大家好!" }); ``` #### `peer.close(code?, reason?)` 优雅地关闭 WebSocket 连接。 ```ts peer.close(1000, "正常关闭"); ``` #### `peer.terminate()` 立即终止连接,不发送关闭帧。 ## Message `message` 钩子中的 `message` 对象提供了多种方法,用于以不同格式读取传入数据。 | 方法 | 返回类型 | 说明 | |---|---|---| | `text()` | `string` | 将消息作为 UTF-8 字符串读取 | | `json()` | `T` | 将消息解析为 JSON | | `uint8Array()` | `Uint8Array` | 将消息读取为字节数组 | | `arrayBuffer()` | `ArrayBuffer` | 将消息读取为 ArrayBuffer | | `blob()` | `Blob` | 将消息读取为 Blob | ```ts message(peer, message) { // 按文本解析 const text = message.text(); // 按带类型的 JSON 解析 const data = message.json<{ type: string; payload: unknown }>(); } ``` ## 发布/订阅 发布/订阅(pub/sub)允许你通过主题向一组已连接的 peer 广播消息。peer 订阅主题后,就能收到发布到这些主题的消息。 ```ts [routes/chat.ts] import { defineWebSocketHandler } from "nitro"; export default defineWebSocketHandler({ open(peer) { peer.subscribe("chat"); peer.publish("chat", { system: `${peer} 加入了聊天室` }); peer.send({ system: "欢迎来到聊天室!" }); }, message(peer, message) { // 广播给其他所有订阅者 peer.publish("chat", { user: peer.toString(), text: message.text(), }); // 回显给发送者 peer.send({ user: "你", text: message.text() }); }, close(peer) { peer.publish("chat", { system: `${peer} 离开了聊天室` }); }, }); ``` ::note `peer.publish()` 会将消息发送给该主题的所有订阅者,但**不包括**发送消息的 peer 自己。如果也要发给发布者,请使用 `peer.send()`。 :: ### 命名空间 命名空间为 WebSocket 连接提供隔离的发布/订阅分组。每个 peer 都属于某一个命名空间,而 `peer.publish()` 只会向同一命名空间内的 peer 广播。 默认情况下,命名空间根据请求 URL 的路径名派生。它与[动态路由](/docs/routing#dynamic-routes)配合使用时非常自然:每个路径都有自己的隔离命名空间: ```ts [routes/rooms/[room].ts] import { defineWebSocketHandler } from "nitro"; export default defineWebSocketHandler({ open(peer) { peer.subscribe("messages"); peer.publish("messages", `${peer} 加入了 ${peer.namespace}`); }, message(peer, message) { // 只会发送给同一房间内的 peer peer.publish("messages", `${peer}: ${message.text()}`); }, close(peer) { peer.publish("messages", `${peer} 离开了`); }, }); ``` 在此示例中,连接到 `/rooms/game` 的客户端与连接到 `/rooms/lobby` 的客户端相互隔离;每个路径都是自己的命名空间。 如果要覆盖默认命名空间,可以在 `upgrade` 钩子中返回自定义的 `namespace`: ```ts [routes/chat.ts] import { defineWebSocketHandler } from "nitro"; export default defineWebSocketHandler({ upgrade(request) { // 按查询参数分组连接,而不是按路径名 const url = new URL(request.url); const channel = url.searchParams.get("channel") || "general"; return { namespace: `chat:${channel}`, }; }, open(peer) { peer.subscribe("messages"); peer.publish("messages", `${peer} 加入了`); }, message(peer, message) { peer.publish("messages", `${peer}: ${message.text()}`); }, close(peer) { peer.publish("messages", `${peer} 离开了`); }, }); ``` ## Server-Sent Events(SSE) [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) 在只需要服务端到客户端单向流式推送时,是一种更简单的替代方案。与 WebSocket 不同,SSE 使用标准 HTTP,并支持自动重连。 ```ts [routes/sse.ts] import { defineHandler } from "nitro"; import { createEventStream } from "nitro/h3"; export default defineHandler((event) => { const stream = createEventStream(event); const interval = setInterval(async () => { await stream.push(`Message @ ${new Date().toLocaleTimeString()}`); }, 1000); stream.onClosed(() => { clearInterval(interval); }); return stream.send(); }); ``` 客户端可以通过 [EventSource API](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) 连接: ```js const source = new EventSource("/sse"); source.onmessage = (event) => { console.log(event.data); }; ``` ### 结构化消息 SSE 消息支持可选的 `id`、`event` 和 `retry` 字段: ```ts [routes/events.ts] import { defineHandler } from "nitro"; import { createEventStream } from "nitro/h3"; export default defineHandler((event) => { const stream = createEventStream(event); let id = 0; const interval = setInterval(async () => { await stream.push({ id: String(id++), event: "update", data: JSON.stringify({ value: Math.random() }), retry: 3000, }); }, 1000); stream.onClosed(() => { clearInterval(interval); }); return stream.send(); }); ``` :read-more{to="https://h3.dev/" title="H3 文档"} --- # 模块 > 使用模块扩展 Nitro 的构建时行为 Nitro 模块是在 **Nitro 实例初始化时运行一次**的函数(在开发、构建和预渲染过程中)。它们接收 `nitro` 构建上下文,可用于修改选项、注册构建时钩子、添加虚拟文件或注册路由处理器 ::note 模块在 **构建时**运行。要扩展服务器的**运行时**行为(请求[生命周期](/docs/lifecycle)、错误处理、关闭),请改用[插件](/docs/plugins) :: | | 模块 | [插件](/docs/plugins) | | --- | --- | --- | | 运行时机 | 一次,在初始化和构建期间 | 一次,在服务器启动时 | | 接收对象 | `nitro`(构建上下文) | `nitroApp`(运行时应用) | | 典型用途 | 修改配置、添加处理器和虚拟文件、接入构建流程 | 接入请求/响应生命周期 | | 是否打包到输出中 | 否 | 是 | ## 使用模块 使用 `modules` 配置数组注册模块: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ modules: [ // Path to a local module (relative to the project root) "./modules/my-module.ts", // Name of an npm package "my-nitro-module", // Inline module object { name: "inline-module", setup(nitro) { nitro.logger.info("Inline module setup"); }, }, // Bare setup function (nitro) => { nitro.hooks.hook("compiled", () => { // ... }); }, // Plugin object with a `nitro` key { name: "plugin-shaped-module", nitro: { setup(nitro) { nitro.logger.info("Plugin module setup"); }, }, }, ], }); ``` 每个条目可以采用以下形式之一: | 形式 | 示例 | 描述 | | --- | --- | --- | | 路径或包字符串 | `"./modules/my-module.ts"`、`"my-nitro-module"` | 从项目根目录解析并导入。使用其**默认导出**作为模块 | | 模块对象 | `{ name?, setup }` | 一个包含 `setup(nitro)` 函数和可选 `name` 的对象 | | 裸函数 | `(nitro) => { ... }` | `{ setup }` 的简写形式 | | 插件对象 | `{ name?, nitro: { setup } }` | 一个包含 `nitro` 键的对象,该键中存放模块。它与 Rollup/Vite 插件形式一致,因此一个导出既可以是打包器插件,也可以是 Nitro 模块 | `modules/` 目录中的文件会自动注册为模块(类似于用于[运行时插件](/docs/plugins)的 `plugins/`) 只有以下两种模式会自动注册(支持任意以下扩展名:`.ts`、`.mts`、`.cts`、`.js`、`.mjs`、`.cjs`、`.tsx`、`.jsx`): - `modules/*.ts`(单文件模块) - `modules/*/index.ts`(目录模块) `modules/` 中不匹配这些模式的任何其他文件(例如模块自身的 `runtime/` 文件)都会被扫描器忽略 通过路径或包名引用的模块只会被解析和安装**一次**;指向同一文件的重复条目会被跳过 ::tip 在 [Vite](/docs/vite) 项目中,`vite.config.ts` 中任何带有 `nitro` 键的插件都会自动注册为 Nitro 模块。无需再将其列入 `modules` :: ## 编写模块 模块是一个包含 `setup` 函数的对象,该函数接收 [`Nitro`](https://github.com/nitrojs/nitro/blob/main/src/types/nitro.ts) 实例。使用 `nitro/types` 中的 `NitroModule` 类型来确保类型安全: ```ts [modules/hello.ts] import type { NitroModule } from "nitro/types"; export default { name: "hello", setup(nitro) { // Add a virtual module usable anywhere in the server code as `#hello` nitro.options.virtual["#hello"] = `export const hello = "world";`; // Register a route handler pointing to the virtual module nitro.options.handlers.push({ route: "/_hello", handler: "#hello-handler", }); nitro.options.virtual["#hello-handler"] = /* ts */ ` import { defineHandler } from "nitro"; import { hello } from "#hello"; export default defineHandler(() => ({ hello })); `; }, } satisfies NitroModule; ``` `setup` 函数可以是异步函数。`nitro` 实例上的实用属性: | 属性 | 描述 | | --- | --- | | `nitro.options` | 解析后的配置(可变):`handlers`、`virtual`、`plugins`、`runtimeConfig` 以及其他所有选项 | | `nitro.hooks` | 构建时[钩子](#build-time-hooks)(`hookable` 实例) | | `nitro.logger` | 用于构建时输出的带标签 [consola](https://github.com/unjs/consola) 日志记录器 | | `nitro.meta` | Nitro 版本信息(`version`、`majorVersion`) | ## 构建时钩子 模块可以通过 `nitro.hooks.hook()` 接入构建生命周期: ```ts [modules/build-info.ts] import type { NitroModule } from "nitro/types"; export default { name: "build-info", setup(nitro) { nitro.hooks.hook("compiled", () => { nitro.logger.info(`Server built in ${nitro.options.output.dir}`); }); }, } satisfies NitroModule; ``` 最有用的钩子: | 钩子 | 签名 | 运行时机 | | --- | --- | --- | | `build:before` | `(nitro) => void` | 构建开始之前,在创建打包器配置之前。 | | `rollup:before` | `(nitro, config) => void` | 打包器(Rollup/Rolldown)配置解析完成后、即将打包时。最后一次修改配置的机会。 | | `vite:compile:before` | `(nitro) => void` | 仅适用于 Vite 构建器。在复制公共资源并完成预渲染后、构建输出最终确定之前(服务器包会被编译,静态构建则会跳过编译)。可用于向公共输出目录写入额外文件。 | | `compiled` | `(nitro) => void` | 服务器包写入输出目录后。 | | `dev:start` | `() => void` | 开发环境中,开发工作线程启动时。 | | `dev:reload` | `(payload?) => void` | 开发环境中,每次重新构建后开发工作线程重新加载时。 | | `dev:error` | `(cause?) => void` | 开发环境中,发生构建错误时。 | | `prerender:routes` | `(routes: Set) => void` | 预渲染之前。添加或移除要预渲染的路由。 | | `prerender:config` | `(config) => void` | 创建预渲染器的 Nitro 配置时。 | | `prerender:generate` | `(route, nitro) => void` | 每个路由生成之前。 | | `prerender:route` | `(route) => void` | 每个路由生成之后。 | | `prerender:done` | `({ prerenderedRoutes, failedRoutes }) => void` | 预渲染完成后。 | | `close` | `() => void` | Nitro 实例关闭时。 | 所有钩子都可以是异步的。完整列表和确切签名请参阅[钩子源码](https://github.com/nitrojs/nitro/blob/main/src/types/hooks.ts),有关 `prerender:*` 钩子的具体用法,请参阅[预渲染指南](/docs/prerendering#hooks) ::tip 这里不提供运行时钩子(`request`、`response`、`error`);它们属于[插件](/docs/plugins)。模块仍然可以通过向 `nitro.options.plugins` 推送插件文件来添加运行时行为 :: ## 最佳实践 - **为每个模块提供一个 `name`。** 这样可以使模块在配置中更易识别;对于插件对象,Rollup 和 Vite 也要求通过它来识别插件 - **优先使用插件对象形式。** `{ name, nitro: { setup } }` 同时可被 Nitro、Rollup 和 Vite 理解,因此一个导出即可同时适用于三者。Vite 应用的用户将其放入 `plugins`,独立 Nitro 应用的用户将其放入 `modules`,无论采用哪种方式,模块都能运行 - **保持模块幂等。** 在开发环境中,配置变更时可能会重新处理选项,因此在推送重复的处理器、插件或虚拟条目之前进行检查 - **优先使用钩子而不是打补丁。** 使用构建钩子响应生命周期事件,而不是修改 Nitro 内部实现或覆盖函数 - **尊重用户选项。** 从 `nitro.options` 中读取现有值并在其基础上进行扩展,而不是替换它们。让用户可以选择退出模块的行为 - **使用 `nitro.logger`** 输出构建时日志,而不是使用 `console` - **注意运行时包。** 模块添加的任何处理器或插件都会为每个部署目标打包,因此应保持运行时代码精简且与运行时环境无关 ## 分发模块 要将模块作为 npm 包共享,请将模块对象作为**默认导出**,用户即可通过包名引用它: ```ts [my-nitro-module/src/index.ts] import type { NitroModule } from "nitro/types"; export default { name: "my-nitro-module", setup(nitro) { // ... }, } satisfies NitroModule; ``` ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ modules: ["my-nitro-module"], }); ``` 将 `nitro` 添加为对等依赖,并且只从 `nitro/types` 导入类型,这样你的包就不会将 Nitro 自身打包进去 ### 同时作为打包器插件的模块 如果你的包已经提供 Rollup 或 Vite 插件,请将 Nitro 模块放在插件对象的 `nitro` 键下,而不是发布两个入口: ```ts [my-plugin/src/index.ts] import type { Plugin } from "vite"; import type { NitroModule } from "nitro/types"; export default function myPlugin(): Plugin & { nitro: NitroModule } { return { name: "my-plugin", transform(code, id) { // ... }, nitro: { setup(nitro) { nitro.options.virtual["#my-plugin"] = `export const value = "hello";`; }, }, }; } ``` 同一个导出随后即可用于两个配置文件: ::code-group ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; import myPlugin from "my-plugin"; export default defineConfig({ plugins: [nitro(), myPlugin()], }); ``` ```ts [nitro.config.ts] import { defineConfig } from "nitro"; import myPlugin from "my-plugin"; export default defineConfig({ modules: [myPlugin()], }); ``` :: 打包器会忽略额外的 `nitro` 键,而 Nitro 会忽略插件钩子,因此双方都无需了解对方的存在。 --- # CLI > Nitro 提供了 `nitro` 命令,用于开发、构建、预览和部署项目。 当 `nitro` 作为项目依赖安装时,通过包管理器运行它(`npx nitro`、`pnpm nitro`、`yarn nitro`、`bun nitro`、……),或者将其添加到 `package.json` 脚本中: ```json [package.json] { "scripts": { "dev": "nitro dev", "build": "nitro build", "preview": "nitro preview" } } ``` 然后按照[快速开始](/docs/quick-start)指南中的说明使用 `npm run dev`、`pnpm dev` 等命令。 ::tip 每个命令都支持使用 `--help` 查看其参数概览(例如 `nitro build --help`),而 `nitro --version` 会打印已安装的 Nitro 版本。 :: 所有命令都接受项目根目录,可以将其作为位置参数(`nitro build ./my-app`),也可以使用 `--dir` 标志(推荐)。默认使用当前目录。 ::note 将 Nitro 用作 [Vite 插件](/docs/vite)?建议使用 Vite CLI(`vite dev`、`vite build`、`vite preview`);Nitro 开发服务器不支持 Vite builder。 :: ## `nitro dev` 启动支持热重载的开发服务器。 ```sh npx nitro dev [--dir ] [--port ] [--host ] ``` | 标志 | 描述 | | --- | --- | | `--dir` | 项目根目录。 | | `--port` | 要监听的端口。 | | `--host` | 要监听的主机名。 | 开发服务器会监视 Nitro 配置:对 `runtimeConfig` 或 `routeRules` 的更改会热重载,而其他配置更改会触发开发服务器完全重启。 **示例:** ```sh npx nitro dev --port 4000 --host 0.0.0.0 ``` ## `nitro build` 为生产环境构建项目。Nitro 会准备构建目录、复制公共资源、预渲染所有已配置的路由,并将服务器打包到输出目录(默认为 `.output/`)。 ```sh npx nitro build [--dir ] [--preset ] [--minify] [--builder ] [--compatibility-date ] ``` | 标志 | 描述 | | --- | --- | | `--dir` | 项目根目录。 | | `--preset` | 要构建的部署[preset](/deploy)(也可以使用 `NITRO_PRESET` 环境变量)。 | | `--minify` | 压缩输出,覆盖 preset 默认设置。使用 `--no-minify` 可禁用压缩。 | | `--builder` | 要使用的打包器:`rollup`、`rolldown` 或 `vite`(也可以使用 `NITRO_BUILDER` 环境变量)。 | | `--compatibility-date` | 用于 preset 兼容性的日期(也可以使用 `NITRO_COMPATIBILITY_DATE` 环境变量)。 | **示例:** ```sh npx nitro build --preset cloudflare_module ``` ### `NITRO_PRESET` 与 `--preset` 两者都会设置部署 preset,而 `--preset` 标志的优先级高于 `NITRO_PRESET` 环境变量。在部署平台和 CI 中,环境变量非常方便,因为你通常无法更改构建命令: ```sh NITRO_PRESET=cloudflare_module npm run build ``` ## `nitro preview` 启动本地服务器,以预览生产环境构建输出。 ```sh npx nitro preview [--dir ] [--port ] [--host ] ``` | 标志 | 描述 | | --- | --- | | `--dir` | 项目根目录。 | | `--port` | 要监听的端口。 | | `--host` | 要监听的主机名。 | **示例:** ```sh npx nitro build npx nitro preview ``` ## `nitro deploy` 构建项目,然后运行所选 preset 的部署命令。 ```sh npx nitro deploy [--dir ] [--prebuilt] [build flags...] [-- ] ``` | 标志 | 描述 | | --- | --- | | `--prebuilt` | 跳过构建步骤,部署现有的构建输出。 | 所有 [`nitro build`](#nitro-build) 标志(`--preset`、`--minify`、`--builder`、`--compatibility-date`、……)也会被接受,并应用于构建步骤。`--` 后的任何参数都会附加到底层部署命令(或者作为 `args` 传递给函数部署命令)。 ::note 并非所有 preset 都支持 `nitro deploy`。如果所选 preset 未定义部署命令(或未找到构建输出),命令会报错失败。在这种情况下,请使用其他 preset,在 Nitro 配置中配置部署命令,或按照提供商的[部署指南](/deploy)手动部署。 :: **示例:** ```sh # 使用 cloudflare_module preset 构建,然后运行 wrangler deploy npx nitro deploy --preset cloudflare_module # 部署现有构建,而不重新构建 npx nitro deploy --prebuilt ``` ## `nitro task` 操作 [Nitro 任务](/docs/tasks)(实验性功能)。 ::important 任务支持处于实验阶段,需要使用 `experimental.tasks` 标志。`task` 子命令针对**当前正在运行的开发服务器**执行,因此请在 `nitro dev` 运行时于第二个终端中执行这些命令。详情请参阅[任务文档](/docs/tasks)。 :: ### `nitro task list` 列出可用任务及其描述。 ```sh npx nitro task list [--dir ] ``` | 标志 | 描述 | | --- | --- | | `--dir` | 项目根目录。 | ### `nitro task run` 按名称运行任务,也可以选择传入 JSON 负载。 ```sh npx nitro task run [--dir ] [--payload ] ``` | 标志 | 描述 | | --- | --- | | `--dir` | 项目根目录。 | | `--payload` | 解析为 JSON 字符串,并作为负载传递给任务。 | **示例:** ```sh npx nitro task run db:migrate --payload '{"force": true}' ``` ## `nitro docs` 从终端浏览 Nitro 文档(由 [mdzilla](https://github.com/pi0/mdzilla) 提供支持),也可以选择从特定页面路径开始。 ```sh npx nitro docs [page] ``` --- # Vite 集成 > 使用 Nitro 作为 Vite 插件构建全栈应用,并选择用于打包服务器的构建器 Nitro 以插件形式与 [Vite](https://vite.dev) 集成。将其添加到 Vite 项目后,除了前端之外,还能获得完整的服务器功能:API 路由、服务器端渲染,以及可部署到任意位置的生产构建。 ## Nitro 作为 Vite 插件 将插件添加到 Vite 配置中: ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()], }); ``` 启用插件后,所有操作都使用 Vite CLI: - **`vite dev`**:为前端和后端提供一个开发服务器。Nitro 提供服务器路由和资源,而 Vite 处理客户端模块。服务器代码在隔离的 worker 中运行,并在服务器文件发生变化时自动重新加载;客户端代码则继续使用 Vite 的热模块替换(HMR) - **`vite build`**:使用任意[部署预设](/deploy)将客户端和服务器一起构建到单个可部署的 `.output/` 目录中 - **`vite preview`**:通过 Nitro 在本地提供生产构建,包括静态资源和 WebSocket 支持 ::tip 请按照[快速开始](/docs/quick-start#add-to-a-vite-project)中的步骤,将 Nitro 添加到现有的 Vite 项目中 :: ::note 使用 Vite 插件时,优先使用 `vite dev` 而不是 `nitro dev`;Nitro CLI 开发服务器不支持 Vite 构建器。`nitro build` 可以正常工作,并会在底层运行 Vite 构建 :: ## 前端框架 Nitro 插件可以与其他 Vite 插件组合使用,因此你可以使用偏好的前端框架,通过客户端水合实现服务器端渲染: ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [nitro(), react()], }); ``` Nitro 会自动检测名为 `entry-server.(ts|js|tsx|jsx|mjs)` 的文件(位于项目根目录、`app/`、`src/` 或服务器目录中),并将其用作 SSR 入口。 无需逐一重新解释每种设置,请参阅以下可运行示例: - [使用 React 的 SSR](/examples/vite-ssr-react) - [使用 Vue Router 的 SSR](/examples/vite-ssr-vue-router) - [使用 Solid 的 SSR](/examples/vite-ssr-solid) - [使用 Preact 的 SSR](/examples/vite-ssr-preact) - [React Server Components](/examples/vite-rsc) - [使用纯 HTML 的 SSR](/examples/vite-ssr-html) ::read-more{to="/docs/renderer"} 请在 [renderer](/docs/renderer) 和[服务器入口](/docs/server-entry)指南中进一步了解未匹配路由的渲染方式 :: ## 使用 Vite 配置 Nitro Nitro 配置可以位于以下三个位置,并且都接受相同的[选项](/config): 1. `nitro.config.ts` 文件(大多数项目推荐) 2. Vite 配置中的 `nitro` 键 3. 传递给 `nitro()` 的内联插件选项 ::code-group ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: "./server", }); ``` ```ts [vite.config.ts (nitro key)] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()], nitro: { serverDir: "./server", }, }); ``` ```ts [vite.config.ts (inline)] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [ nitro({ serverDir: "./server", }), ], }); ``` :: 内联插件选项的优先级高于 Vite 配置中的 `nitro` 键,而这两者的优先级都高于 `nitro.config.ts`。`dev` 模式和 `rootDir` 会从 Vite 推断,无法覆盖。 Vite 插件还可以通过在插件对象上暴露 `nitro` 键(一个 [Nitro 模块](/docs/modules)),以编程方式扩展 Nitro。请参阅 [Vite Nitro 插件示例](/examples/vite-nitro-plugin)。 ### 使用特定的 Vite 软件包 Nitro 本身不依赖 `vite`:它会解析你项目中安装的 Vite。以编程方式运行 Vite 的框架应通过 `vite.path` 选项传入自己的 `vite` 软件包,以便开发模块运行器从实际运行的 Vite 实例创建(在 monorepo 中,另一个版本可能会提升至应用旁边): ```ts import { createServer } from "vite"; import { nitro } from "nitro/vite"; const server = await createServer({ plugins: [nitro({ vite: { path: import.meta.resolve("vite") } })], }); ``` 该选项接受指向 `vite` 软件包目录或入口的路径或 `file://` URL。 ### 实验性插件选项 该插件还接受 `experimental.vite` 下的 Vite 专用标志: ::field{name="assetsImport" type="boolean"} 启用 `?assets` 导入,以从入口点收集 CSS 和 JS 资源(默认值:`true`) :: ::field{name="serverReload" type="boolean"} 当仅服务器模块发生变化时重新加载服务器(以及浏览器)(默认值:`true`) :: ::field{name="services" type="Record"} 注册其他服务器环境,使其成为可获取的服务 :: ## 构建器 Nitro 可以使用以下三种构建器之一打包服务器: | 构建器 | 描述 | | --- | --- | | `rolldown` | 默认构建器。基于 Rust 的打包器,随 Nitro 一起提供,无需额外安装 | | `rollup` | 久经考验且拥有成熟插件生态系统的打包器。需要安装 `rollup` | | `vite` | 通过 Vite 插件进行全栈构建。需要安装 `vite` | ### 构建器的选择方式 构建器按以下顺序解析: 1. Nitro 配置中的 `builder` 选项 2. `NITRO_BUILDER` 环境变量 3. 自动检测:如果已安装 `vite`,且你的 `vite.config` 使用了 `nitro()` 插件,则使用 `vite` 构建器 4. 否则,Nitro 默认为 `rolldown` ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ builder: "rollup", }); ``` 或者无需修改配置文件: ```bash NITRO_BUILDER=rollup nitro build ``` 如果你明确选择了 `rollup` 或 `vite`,但未安装相应的软件包,Nitro 会提示将其作为开发依赖安装。 ### 应选择哪个构建器 - 对于独立服务器和 API,选择 **`rolldown`**。它是速度最快的选项,开箱即用 - 如果依赖 Rollup 专用插件,或需要最大的生态系统兼容性,请选择 **`rollup`** - 只要前端使用 Vite 构建,就使用 **`vite`**(通常通过自动检测)。它是唯一能将客户端和服务器一起打包的构建器 ### 自定义打包器 对于高级场景,你可以通过 `rollupConfig` 或 `rolldownConfig`,直接向底层打包器传递额外配置: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ rolldownConfig: { // Extra Rolldown options and plugins }, rollupConfig: { // Extra Rollup options and plugins }, }); ``` 每个选项仅应用于与其匹配的构建器。使用 `vite` 构建器时,根据 Vite 运行在 Rolldown(`rolldown-vite`)还是 Rollup 上,两者都会合并到服务器打包配置中。 ::warning 这些选项属于应急手段。在可用时优先使用 Nitro 内置选项,因为底层打包器覆盖配置可能会在不同构建器之间失效 :: ## 需要了解的事项 - **服务器重新加载而非 HMR**:仅服务器模块发生变化时,会触发服务器重新加载(以及浏览器刷新),而不是热模块替换。在客户端和服务器之间共享的模块仍会保留正常的 Vite HMR - **开发服务器端口**:端口依次从 `PORT` 环境变量、Vite 的 `server.port`,以及 Nitro 的 `devServer.port` 中解析(默认值:`3000`) - **环境文件**:在开发和构建过程中,Nitro 都会遵循 [Vite 约定](https://vite.dev/guide/env-and-mode#env-files)加载 `.env`、`.env.local` 以及特定模式的变体(`.env.[mode]`、`.env.[mode].local`)。现有环境变量的优先级高于 `.env` 值 - **Vite 环境**:Nitro 使用 [Vite 环境 API](https://vite.dev/guide/api-environment),并注册 `client` 和 `nitro` 环境(存在 SSR 入口时还会注册 `ssr`)。你可以通过 Vite 配置中的 `environments.client.build.rollupOptions.input` 配置客户端入口点,如 [SSR 示例](/examples/vite-ssr-react)所示。 --- # 服务器工具 > 可在服务器代码中从 `nitro` 及其子路径导入的所有内容的参考 Nitro **没有自动导入**:每个工具都必须从主 `nitro` 入口或其子路径之一显式导入。本页面列出了每个导出项对应的导入路径。 ## 快速参考 | 你想要执行的操作 | 导入 | | --- | --- | | 定义事件处理器 | `import { defineHandler } from "nitro"`{lang=ts} | | 定义中间件 | `import { defineMiddleware } from "nitro"`{lang=ts} | | 抛出 HTTP 错误 | `import { HTTPError } from "nitro"`{lang=ts} | | 读取请求体、查询参数、Cookie 等 | `import { readBody, getQuery } from "nitro/h3"`{lang=ts} | | 在内部调用自己的路由 | `import { serverFetch } from "nitro"`{lang=ts} | | 处理 WebSocket 连接 | `import { defineWebSocketHandler } from "nitro"`{lang=ts} | | 添加运行时插件 | `import { definePlugin } from "nitro"`{lang=ts} | | 自定义错误响应 | `import { defineErrorHandler } from "nitro"`{lang=ts} | | 缓存处理器或函数 | `import { defineCachedHandler, defineCachedFunction } from "nitro/cache"`{lang=ts} | | 读取/写入键值存储 | `import { useKV } from "nitro/kv"`{lang=ts} | | 查询 SQL 数据库 | `import { useDatabase } from "nitro/database"`{lang=ts} | | 访问运行时配置 | `import { useRuntimeConfig } from "nitro/runtime-config"`{lang=ts} | | 定义和运行任务 | `import { defineTask, runTask } from "nitro/task"`{lang=ts} | | 访问应用实例和钩子 | `import { useNitroApp, useNitroHooks } from "nitro/app"`{lang=ts} | | 配置 Nitro(`nitro.config.ts`) | `import { defineConfig } from "nitro"`{lang=ts} | | 将 Nitro 用作 Vite 插件 | `import { nitro } from "nitro/vite"`{lang=ts} | ## `nitro` 主入口包含日常路由处理器所需的一切内容。 | 导出项 | 描述 | | --- | --- | | `defineHandler` | 为[路由或中间件文件](/docs/routing)定义事件处理器 | | `defineMiddleware` | 定义一个在路由处理器之前运行的[中间件](/docs/routing#middleware) | | `defineWebSocketHandler` | 为路由文件定义一个 [WebSocket 处理器](/docs/websocket) | | `definePlugin` | 定义一个在服务器启动时运行的[运行时插件](/docs/plugins) | | `defineErrorHandler` | 定义自定义[错误处理器](/config#errorhandler),替代内置错误页面 | | `defineConfig` | 定义 Nitro [配置](/docs/configuration)(用于 `nitro.config.ts`,不用于运行时代码) | | `defineRouteMeta` | 附加路由元数据,例如 [OpenAPI 规范](/docs/openapi)(构建时宏) | | `HTTPError` | 使用状态码抛出 HTTP 错误:`throw new HTTPError("Not found", { status: 404 })`{lang=ts} | | `HTTPResponse` | 从处理器返回带有自定义状态码和标头的响应体 | | `html` | 返回 HTML 响应(`text/html` 内容类型)的带标签模板字面量 | | `serverFetch` | 在内部调用自己的路由,无需经过网络往返 | | `fetch` | 类似全局 `fetch`,但以 `/` 开头的路径会路由到自己的服务器 | 常用类型也会导出:`H3Event`、`EventHandlerRequest` 和 `EventHandlerWithFetch`。 ```ts [server/routes/hello.ts] import { defineHandler, HTTPError } from "nitro"; export default defineHandler((event) => { const name = event.url.searchParams.get("name"); if (!name) { throw new HTTPError("Missing name", { status: 400 }); } return { hello: name }; }); ``` ### 内部 fetch `serverFetch` 直接调用自己应用中的路由:请求会经过 Nitro 应用(路由规则、中间件、插件),而不会接触网络: ```ts [server/routes/summary.ts] import { defineHandler, serverFetch } from "nitro"; export default defineHandler(async () => { const res = await serverFetch("/api/stats"); // returns a web Response return { stats: await res.json() }; }); ``` `fetch` 导出项是一个通用变体:绝对路径(以 `/` 开头)会通过 `serverFetch` 在内部处理,其他路径则回退到全局 `fetch`: ```ts import { fetch } from "nitro"; await fetch("/api/hello"); // handled by your own server await fetch("https://example.com"); // regular network fetch ``` ### 错误处理器 将 [`errorHandler` 配置](/config#errorhandler)指向一个导出自定义错误处理器的文件: ```ts [error.ts] import { defineErrorHandler } from "nitro"; export default defineErrorHandler((error, event) => { return new Response(`[custom error] ${error.message}`, { status: error.status, headers: { "Content-Type": "text/plain" }, }); }); ``` ### 运行时插件 插件会在服务器启动时运行一次,并且可以接入[运行时生命周期](/docs/lifecycle): ```ts [server/plugins/log.ts] import { definePlugin } from "nitro"; export default definePlugin((nitroApp) => { nitroApp.hooks.hook("request", (event) => { console.log("request:", event.url.pathname); }); }); ``` 更多信息请参阅[插件指南](/docs/plugins)。 ## `nitro/h3` 重新导出所有 [H3 v2](https://h3.dev) 工具:`readBody`、`getQuery`、`getCookie`、`setCookie`、`proxy`、`redirect` 等。 ```ts [server/routes/form.post.ts] import { defineHandler } from "nitro"; import { readBody, getCookie } from "nitro/h3"; export default defineHandler(async (event) => { const body = await readBody(event); const session = getCookie(event, "session"); return { body, session }; }); ``` ::note 完整列表请参阅 [H3 工具文档](https://h3.dev/utils)。 :: ## `nitro/cache` | 导出项 | 描述 | | --- | --- | | `defineCachedHandler` | 为事件处理器包装缓存 | | `defineCachedFunction` | 缓存任意(异步)函数的结果 | ```ts import { defineCachedHandler } from "nitro/cache"; export default defineCachedHandler(() => `Generated at ${new Date().toISOString()}`, { maxAge: 60, }); ``` 有关选项、缓存键和失效处理,请参阅[缓存指南](/docs/cache)。 ## `nitro/kv` | 导出项 | 描述 | | --- | --- | | `useKV` | 访问键值[存储层](/docs/storage),也可指定作用域基准(例如 `useKV("data")`{lang=ts})。 | ```ts import { useKV } from "nitro/kv"; await useKV("data").set("visits", 42); ``` 有关挂载点和驱动程序,请参阅 [KV 存储指南](/docs/storage)。 ::note `nitro/storage` 中的 `useStorage` 仍可作为 `useKV` 的弃用别名使用。 :: ## `nitro/database` | 导出项 | 描述 | | --- | --- | | `useDatabase` | 访问已配置的 SQL [数据库](/docs/database)连接(默认:`useDatabase()`{lang=ts},命名数据库:`useDatabase("users")`{lang=ts}) | ```ts import { useDatabase } from "nitro/database"; const db = useDatabase(); const { rows } = await db.sql`SELECT * FROM users`; ``` 有关连接器和配置,请参阅[数据库指南](/docs/database)。 ## `nitro/runtime-config` | 导出项 | 描述 | | --- | --- | | `useRuntimeConfig` | 访问[运行时配置](/docs/configuration#runtime-configuration),可使用 `NITRO_*` 环境变量覆盖 | ```ts import { useRuntimeConfig } from "nitro/runtime-config"; const { apiToken } = useRuntimeConfig(); ``` ## `nitro/task` | 导出项 | 描述 | | --- | --- | | `defineTask` | 在 `server/tasks/` 文件中定义一个[任务](/docs/tasks) | | `runTask` | 在服务器代码的任意位置按名称运行任务 | ```ts import { runTask } from "nitro/task"; const { result } = await runTask("db:migrate", { payload: { force: true } }); ``` 请参阅[任务指南](/docs/tasks)。任务处于实验阶段,需要启用 `experimental.tasks` 标志。 ## `nitro/app` 对正在运行的 Nitro 应用进行更底层的访问。 | 导出项 | 描述 | | --- | --- | | `useNitroApp` | 访问当前的 [Nitro 应用实例](/docs/lifecycle)(`fetch`、`hooks` 等) | | `useNitroHooks` | 访问[运行时钩子](/docs/plugins#nitro-runtime-hooks)实例,以便在插件外注册钩子 | | `getRouteRules` | 获取方法和路径名对应的已解析[路由规则](/docs/routing#route-rules):`getRouteRules("GET", "/blog/post")`{lang=ts} | | `serverFetch`、`fetch` | 与[主入口导出项](#internal-fetch)相同 | ```ts import { useNitroHooks } from "nitro/app"; useNitroHooks().hook("error", (error) => { console.error(error); }); ``` ## `nitro/context` | 导出项 | 描述 | | --- | --- | | `useRequest` | 在请求生命周期内的任意位置访问当前请求,无需在各处传递 `event` | ```ts import { useRequest } from "nitro/context"; const request = useRequest(); ``` ::warning `useRequest` 处于实验阶段:它需要 `experimental.asyncContext: true`{lang=ts} 配置标志,并且仅在支持 `AsyncLocalStorage` 的运行时(Node.js 和兼容运行时)上有效。 :: ## `nitro/config` | 导出项 | 描述 | | --- | --- | | `defineConfig` | 与主 `nitro` 入口导出的 `defineConfig` 相同(也别名为 `defineNitroConfig`) | 建议直接从 `"nitro"` 导入 `defineConfig`。 ## `nitro/types` 仅类型入口,包含所有公开的 Nitro 类型,包括 `NitroConfig`、`Nitro`、`NitroApp`、`NitroAppPlugin`、`NitroErrorHandler`、`NitroRuntimeConfig`、`NitroRuntimeHooks` 和 `Task`。 ```ts import type { NitroRuntimeConfig } from "nitro/types"; ``` ## `nitro/vite` | 导出项 | 描述 | | --- | --- | | `nitro` | Nitro [Vite 插件](/docs/vite) | ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()], }); ``` 同时也会导出 `NitroPluginConfig` 和 `ServiceConfig` 类型。请参阅 [Vite 集成指南](/docs/vite)。 ## `nitro/builder` 面向无需 CLI 即可驱动 Nitro 的工具的编程式 API(构建包装器、框架集成)。 | 导出项 | 描述 | | --- | --- | | `createNitro` | 根据内联配置(以及 `rootDir` 中的 `nitro.config`)创建 Nitro 实例 | | `loadOptions` | 加载并解析 Nitro 选项,但不创建实例 | | `prepare` | 清理输出目录 | | `copyPublicAssets` | 将公共资源复制到输出目录 | | `prerender` | 预渲染路由 | | `build` | 构建服务器包 | | `createDevServer` | 为 Nitro 实例创建开发服务器 | | `getBuildInfo` | 读取上次构建的 `nitro.json`(接受 `rootDir` 或 `{ rootDir, outputDir }`) | | `startPreview` | 启动构建的进程内预览(`{ rootDir, outputDir?, loader? }` 返回 `{ fetch, upgrade?, close }`) | | `deploy` | 为已有构建运行预设的部署命令(`deploy(nitro, { args? })`) | ```ts import { build, copyPublicAssets, createNitro, deploy, prepare, prerender } from "nitro/builder"; const nitro = await createNitro({ rootDir: ".", dev: false, preset: "cloudflare_module" }); await prepare(nitro); await copyPublicAssets(nitro); await prerender(nitro); await build(nitro); await deploy(nitro, { args: ["--env", "staging"] }); await nitro.close(); ``` `deploy` 使用已解析选项中的 `commands.deploy`。它可以是 shell 命令(从 `rootDir` 运行,并将 `./` 路径重写为输出目录),也可以是以编程方式执行部署的预设所使用的函数(例如 `zephyr`)。缺少部署命令的预设会抛出错误。 ## 其他入口 其余子路径(用于获取包版本和路径的 `nitro/meta`、`nitro/vite/runtime`、`nitro/tsconfig`)属于内部入口,在应用代码中很少需要使用。 --- # TypeScript > Nitro 以 TypeScript 为先:无需任何配置,即可使用 TypeScript 编写配置、处理程序和服务端代码 Nitro 使用 TypeScript 编写,并会打包你的服务端代码,因此 `.ts` 文件在开发和生产环境中都可以开箱即用,无需单独的编译步骤。TypeScript 编译器仅在类型检查时需要(例如在 CI 中执行 `tsc --noEmit`)。 所有公共工具都附带类型定义: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ // Fully typed config with completions }); ``` ```ts [server/routes/hello.ts] import { defineHandler } from "nitro"; export default defineHandler((event) => { return { hello: "world" }; // Typed event and inferred return type }); ``` 类型(例如 `NitroConfig`、`NitroModule` 或 `NitroRuntimeConfig`)从 `nitro/types` 导出: ```ts import type { NitroModule } from "nitro/types"; ``` ## `tsconfig.json` 设置 Nitro 提供了一个可共享的 TypeScript 配置,你可以从中进行扩展: ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` 它启用了与 Nitro 打包代码方式相匹配的现代严格默认设置:`strict` 类型检查、打包器模块解析、`noEmit`、`verbatimModuleSyntax` 和 `allowImportingTsExtensions`(因此你可以在导入中使用显式的 `.ts` 扩展名)。你可以通过在其基础上添加自己的 `compilerOptions` 来覆盖任何选项: ```json [tsconfig.json] { "extends": "nitro/tsconfig", "compilerOptions": { "jsx": "react-jsx" } } ``` ## `typescript` 配置选项 | 选项 | 默认值 | 描述 | | --- | --- | --- | | `tsConfig` | 项目的 `tsconfig.json` | 打包器用于 JSX 选项和路径别名的 TypeScript 配置 | Nitro 会读取项目根目录中的 `tsconfig.json`,以获取 JSX 设置(`jsx`、`jsxFactory`、`jsxFragmentFactory`、`jsxImportSource`)和路径别名。设置 `typescript.tsConfig` 可覆盖该配置: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ typescript: { tsConfig: { compilerOptions: { jsx: "react-jsx", }, }, }, }); ``` --- # 迁移指南 > Nitro v3 引入了有意的不向后兼容更改。本指南将帮助你从 Nitro v2 迁移。 > [!NOTE] > 这是一份关于从 Nitro 2 迁移到 3 的动态文档。在使用测试版期间,请定期查看。 大多数迁移遵循相同的顺序: 1. [将 `nitropack` 依赖重命名为 `nitro`](#nitropack-is-renamed-to-nitro),并升级到 [Node.js 20+](#minimum-supported-nodejs-version-20)。 2. [添加显式导入](#auto-imports-are-removed),并更新 [类型](#type-imports) 和 [子路径](#changed-nitro-subpath-imports) 导入。 3. 更新配置:[启用服务器目录扫描](#server-directory-scanning-is-opt-in),并查看[已移除的功能](#app-config-support-removed)。 4. 查看[路由规则](#route-rules)和[运行时工具](#runtime-utilities)。 5. 根据 [H3 v2](#h3-v2) API 更改更新处理器。 6. 检查你的[部署预设](#preset-updates)和[平台特定访问方式](#cloudflare-bindings-access)。 ## `nitropack` 已重命名为 `nitro` NPM 包 [nitropack](https://www.npmjs.com/package/nitropack)(v2)已重命名为 [nitro](https://www.npmjs.com/package/nitro)(v3)。 **迁移:** 在 `package.json` 中将 `nitropack` 依赖更新为 `nitro`: ::code-group ```diff [release channel] { "dependencies": { -- "nitropack": "latest" ++ "nitro": "latest" } } ``` ```diff [nightly channel] { "dependencies": { -- "nitropack": "latest" ++ "nitro": "npm:nitro-nightly@latest" } } ``` :: 有关 nightly 构建的更多信息,请参阅 [nightly channel](/docs/nightly) 指南。 **迁移:** 搜索你的代码库,并将所有 `nitropack` 实例重命名为 `nitro`: ```diff -- import { defineNitroConfig } from "nitropack/config" ++ import { defineConfig } from "nitro" ``` ## 最低支持的 Node.js 版本:20 Nitro 现在要求最低 Node.js 版本为 20,因为 Node.js 18 已于 [2025 年 4 月](https://nodejs.org/en/about/previous-releases)结束生命周期。 请升级到[最新的 LTS](https://nodejs.org/en/download) 版本(>= 20)。 **迁移:** - 使用 `node --version` 检查本地 Node.js 版本,并在必要时进行更新。 - 如果你使用 CI/CD 系统进行部署,请确保你的流水线运行的是 Node.js 20 或更高版本。 - 如果你的托管服务商管理 Node.js 运行时,请确保其设置为版本 20、22 或更高版本。 ## 自动导入已移除 Nitro v2 会自动让 `defineEventHandler`、`useStorage`、`useRuntimeConfig`、`defineCachedFunction` 和 `defineNitroPlugin` 等工具在服务器代码中无需导入即可使用(由 [unimport](https://github.com/unjs/unimport) 提供支持),同时还会公开 `server/utils/` 的导出内容和虚拟的 `#imports` 模块。 Nitro v3 中已**移除**自动导入。`imports` 配置选项、`#imports` 模块和生成的 `nitro-imports.d.ts` 声明均不再可用。使用这些全局变量将导致构建或运行时错误。 **迁移:** 在所有位置添加显式导入。常用工具及其来源: ```ts import { defineHandler, definePlugin, defineErrorHandler, HTTPError } from "nitro"; import { useKV } from "nitro/kv"; import { useRuntimeConfig } from "nitro/runtime-config"; import { defineCachedFunction, defineCachedHandler } from "nitro/cache"; import { useDatabase } from "nitro/database"; import { defineTask, runTask } from "nitro/task"; import { useNitroApp, useNitroHooks } from "nitro/app"; import { getQuery, getCookie } from "nitro/h3"; ``` 你自己的 `utils/` 目录中的工具也需要显式的(相对)导入: ```ts [server/routes/index.ts] import { defineHandler } from "nitro"; import { makeGreeting } from "../utils/hello.ts"; export default defineHandler(() => makeGreeting("Nitro")); ``` ## 类型导入 Nitro 类型现在仅从 `nitro/types` 导出。 **迁移:** 从 `nitro/types` 导入类型,而不是从 `nitro` 导入: ```diff -- import { NitroRuntimeConfig } from "nitropack" ++ import { NitroRuntimeConfig } from "nitro/types" ``` ## 更改后的 Nitro 子路径导入 Nitro v2 引入了多个子路径导出,其中一些已被移除或更新: - `nitropack/rollup`、`nitropack/core`(使用 `nitro/builder`) - `nitropack/runtime/*`(使用 `nitro/*`) - `nitropack/kit`(已移除) - `nitropack/presets`(已移除) 此前引入了实验性的 `nitropack/kit`,但现在已将其移除。未来可能会推出独立的 Nitro Kit 包,并明确其目标。 **迁移:** - 使用来自 `nitro/types` 的 `NitroModule`,而不是 kit 中的 `defineNitroModule`。 - 优先使用内置的 Nitro 预设(外部预设仅用于评估)。 ## 服务器目录扫描需选择性启用 在 Nitro v2 中,`srcDir` 默认为项目根目录,并且始终会扫描 `routes/`、`api/`、`middleware/`、`plugins/` 和 `tasks/` 等目录。 在 Nitro v3 中: - `srcDir` 选项已弃用,推荐使用 `serverDir`(继续使用 `srcDir` 仍然有效,但会记录警告)。 - `serverDir` 默认为 `false`,这意味着在显式启用之前**不会进行目录扫描**。否则,扫描到的路由、中间件、插件和任务不会被注册。 **迁移:** 在 Nitro 配置中设置 `serverDir`: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: "./server", // or "." to keep the v2 root layout }); ``` 将 `serverDir` 设置为 `true` 等同于设置为 `"server"`。路径将相对于项目根目录解析。 ## 已移除 App Config 支持 Nitro v2 支持捆绑的 app config,可以在 `app.config.ts` 中定义配置,并通过 `useAppConfig()` 在运行时访问这些配置。 此功能已被移除。 **迁移:** 在服务器目录中使用常规的 `.ts` 文件,并直接导入它。 ## 路由规则 路由规则的匹配和处理已移至 [h3-rules](https://github.com/h3js/h3-rules)。`redirect`、`proxy`、`cors`、`headers`、`cache` 和 `swr` 的规则形状现在由 h3-rules 定义,而 `isr`、`prerender` 和 `static` 仍是 Nitro 特有的规则。 `NitroRouteConfig` 和 `NitroRouteRules` 类型已弃用,推荐使用从 `nitro/types` 重新导出的 `RouteRuleConfig` 和 `RouteRules`: ```diff -- import { NitroRouteConfig, NitroRouteRules } from "nitropack/types" ++ import { RouteRuleConfig, RouteRules } from "nitro/types" ``` ## 运行时工具 运行时工具已移至单独的 `nitro/*` 子路径导出。有关用法,请参阅文档。 ```diff -- import { useStorage } from "nitropack/runtime/storage" ++ import { useKV } from "nitro/kv" ``` | 工具 | Nitro v3 导入 | |---------------------------------------------------|------------------------| | `useKV`(原为 `useStorage`) | `nitro/kv` | | `defineCachedFunction`、`defineCachedHandler` | `nitro/cache` | | `useDatabase` | `nitro/database` | | `useRuntimeConfig` | `nitro/runtime-config` | | `defineTask`、`runTask` | `nitro/task` | | `useNitroApp`、`useNitroHooks`、`getRouteRules` | `nitro/app` | ::note `useStorage` 已重命名为 `useKV`,`nitro/storage` 已重命名为 `nitro/kv`。`storage` 配置选项已重命名为 `kv`,而 `devStorage` 已由 `$development` 中的 `kv` 替代(如果你在预渲染期间依赖 `devStorage`,也请将其添加到 `$prerender` 中)。旧名称仍可作为已弃用的别名使用,并将在未来版本中移除。 :: ## 内部服务器请求 要向你自己的服务器发起内部请求(v2 的 `useNitroApp().localFetch` 模式),请使用从 `nitro` 导出的 `serverFetch` 工具。此外,还有一个 `fetch` 导出,它会将绝对路径(以 `/` 开头)路由到服务器,并将其他所有内容路由到全局 `fetch`。 ```ts import { serverFetch } from "nitro"; const res = await serverFetch("/api/hello"); // returns a web Response ``` ## 插件 `defineNitroPlugin` 工具已重命名为 `definePlugin`,现在从 `nitro` 中导入。 ```diff -- export default defineNitroPlugin((nitroApp) => { ++ import { definePlugin } from "nitro" ++ ++ export default definePlugin((nitroApp) => { ``` ## 可选钩子 如果你之前在 Nitro 插件之外使用 `useNitroApp().hooks`,它可能是未定义的。使用新的 `useNitroHooks()` 可以确保获得一个实例。 ```ts import { useNitroHooks } from "nitro/app"; useNitroHooks().hook("request", (event) => { /* ... */ }); ``` ## 错误处理器 `defineNitroErrorHandler` 工具已重命名为 `defineErrorHandler`,现在从 `nitro` 导入。 ```diff -- export default defineNitroErrorHandler((error, event) => { ++ import { defineErrorHandler } from "nitro" ++ ++ export default defineErrorHandler((error, event) => { ``` ## H3 v2 Nitro v3 升级到 [H3 v2](https://h3.dev),其中包含 API 更改。所有 H3 工具从 `nitro/h3` 导入。 ### Web 标准 H3 v2 基于 Web 标准原语([`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL)、[`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers)、[`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) 和 [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response))重写。 仅在 Node.js 运行时中可访问 `event.node.{req,res}`。`event.web` 已重命名为 `event.req`(Web [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) 的实例)。 ### 响应处理 你应该始终显式地 **return** 响应体或 **throw** 一个错误: ```diff -- import { send, sendRedirect, sendStream } from "nitro/h3" -- send(event, value) -- sendStream(event, stream) -- sendRedirect(event, location, code) ++ import { redirect } from "nitro/h3" ++ return value ++ return stream ++ return redirect(event, location, code) ``` 其他更改: - `sendError(event, error)` → `throw error`(或 `throw new HTTPError(...)`) - `sendNoContent(event)` → `return noContent(event)` - `sendProxy(event, target)` → `return proxy(event, target)` ### 请求体 大多数 body 工具可以被原生的 `event.req` 方法替代: ```diff -- import { readBody, readRawBody, readFormData } from "nitro/h3" ++ // 使用原生 Request 方法 ++ const json = await event.req.json() ++ const text = await event.req.text() ++ const formData = await event.req.formData() ++ const stream = event.req.body ``` ### 请求头 H3 现在使用标准 Web [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers)。请求头的值始终是纯 `string`(没有 `null`、`undefined` 或 `string[]`)。 ```diff -- import { getHeader, setHeader, getResponseStatus } from "nitro/h3" -- getHeader(event, "x-foo") -- setHeader(event, "x-foo", "bar") ++ event.req.headers.get("x-foo") ++ event.res.headers.set("x-foo", "bar") ++ event.res.status // 替代 getResponseStatus(event) ``` ### 处理器工具 ```diff -- import { eventHandler, defineEventHandler } from "nitro/h3" ++ import { defineHandler } from "nitro" ``` - `lazyEventHandler` → `defineLazyEventHandler` - `useBase` → `withBase` ### 错误工具 ```diff -- import { createError, isError } from "nitro/h3" ++ import { HTTPError } from "nitro" ++ throw new HTTPError({ status: 404, message: "未找到" }) ++ HTTPError.isError(error) ``` ### Node.js 工具 ```diff -- import { defineNodeListener, fromNodeMiddleware, toNodeListener } from "nitro/h3" ++ import { defineNodeHandler, fromNodeHandler, toNodeHandler } from "nitro/h3" ``` ## 预设更新 Nitro 预设已针对最新兼容性进行更新。 一些(旧版)预设已被移除或重命名。 | 旧预设 | 新预设 | |------------------------------|-------------------------------| | `node`(Node.js 中间件) | `node_middleware`(导出已更改为 `middleware`) | | `cloudflare`、`cloudflare_worker`、`cloudflare_module_legacy` | `cloudflare_module` | | `deno-server-legacy` | 使用 Deno v2 的 `deno_server` | | `netlify-builder` | `netlify` 或 `netlify_edge` | | `vercel-edge` | 启用 Fluid compute 的 `vercel` | | `azure`、`azure_functions` | `azure_swa` | | `firebase` | `firebase_app_hosting` | | `iis` | `iis_handler` | | `deno`(Deno Deploy) | `deno_deploy` | | `edgio` | 已停止维护 | | `cli` | 因缺乏使用而移除 | | `service_worker` | 因不稳定而移除 | ::warning `node` 和 `deno` 预设名称在 v3 中仍然存在,但它们现在指向不同的目标: - 在 v2 中,`node` 预设会生成可复用的 Node.js 中间件。在 v3 中,`node` 是 `node_server` 的别名,会构建一个完整的独立 Node.js 服务器。如果保留 `preset: "node"`,现在将获得一个独立服务器。使用 `node_middleware` 以保留 v2 的中间件输出。 - 在 v2 中,`deno` 预设面向 Deno Deploy。在 v3 中,`deno` 是 `deno_server` 的别名,会构建一个自托管的 Deno 服务器。使用 `deno_deploy` 以继续部署到 Deno Deploy。 :: ## Cloudflare 绑定访问 在 Nitro v2 中,可以通过 `event.context.cloudflare.env` 访问 Cloudflare 环境变量和绑定。 在 Nitro v3 中,Cloudflare 运行时上下文改为附加到请求的运行时对象上。 **迁移:** ```diff -- const { cloudflare } = event.context -- const binding = cloudflare.env.MY_BINDING ++ const { env } = event.req.runtime.cloudflare ++ const binding = env.MY_BINDING ``` --- # 每日构建通道 > 在正式发布之前,尝试最新的 Nitro 更改,每次向 `main` 分支提交都会自动发布构建版本。 你可以通过更新 `package.json` 加入 nightly 发布通道: ```json { "devDependencies": { "nitro": "npm:nitro-nightly@latest" } } ``` 删除锁文件(`package-lock.json`、`yarn.lock`、`pnpm-lock.yaml`、`bun.lock` 或 `bun.lockb`)并重新安装依赖。 在 mono-repo 中使用 **Bun 作为包管理器** 时,需要确保 nitro 包被正确提升: ```toml [bunfig.toml] [install] publicHoistPattern = ["nitro*"] ``` 避免使用 ` install nitro-nightly`;它无法正确安装。 如果遇到问题,请删除你的 `node_modules` 和锁文件,然后按照上述步骤操作。 要同时从 Nitro v2 迁移?请参阅[迁移指南](/docs/migration)。 --- # 部署 > 了解更多关于 Nitro 部署提供商的信息。 Nitro 可以从同一代码库为不同的托管提供商生成不同的输出格式。使用内置预设,你几乎无需额外的代码或配置即可调整输出格式。 ## 默认输出 默认的生产输出预设是 [Node.js 服务器](/deploy/runtimes/node)。 在开发模式下,Nitro 始终使用特殊的 `nitro-dev` 预设,在隔离的 worker 环境中通过 Node.js(ESM)运行你的应用,尽可能贴近生产环境的行为。 ## 零配置提供商 使用 CI/CD 部署到生产环境时,Nitro 会尝试检测提供商环境并自动设置正确的预设,无需任何配置。以下提供商可以在零配置的情况下自动检测: - [AWS Amplify](/deploy/providers/aws-amplify) - [Azure](/deploy/providers/azure) - [Cloudflare](/deploy/providers/cloudflare) - [Firebase App Hosting](/deploy/providers/firebase#firebase-app-hosting) - [Netlify](/deploy/providers/netlify) - [Stormkit](/deploy/providers/stormkit) - [Vercel](/deploy/providers/vercel) - [Zeabur](/deploy/providers/zeabur) ::warning 对于 Turborepo 用户,其 Strict Environment Mode 可能会干扰零配置检测。你可能需要显式允许检测变量,或使用 Loose Environment Mode(`--env-mode=loose` 标志)。 :: 其他内置提供商可以通过显式预设使用,包括 [Zephyr](/deploy/providers/zephyr)。 ## 更改部署预设 要为特定提供商进行构建,请设置 `NITRO_PRESET` 环境变量,更新 Nitro 的[配置](/docs/configuration),或使用 `--preset` 参数。 对于 CI/CD 部署,推荐使用环境变量方式。 **示例:** 使用 `NITRO_PRESET` 环境变量或 `--preset` 参数 ```bash NITRO_PRESET=cloudflare_pages nitro build # or nitro build --preset cloudflare_pages ``` **示例:** 更新 `nitro.config.ts` 文件 ```ts import { defineConfig } from "nitro"; export default defineConfig({ preset: 'cloudflare_pages' }) ``` ## 兼容性日期 部署提供商会定期更新其运行时行为。Nitro 预设会更新以支持这些新功能。 为避免破坏现有部署,Nitro 使用兼容性日期。这些日期让你可以在项目创建时锁定行为。准备就绪后,你也可以选择启用未来的更新。 当你创建新项目时,`compatibilityDate` 会被设置为当前日期。此设置保存在你的项目配置中。 你应该定期更新兼容性日期。更新后始终彻底测试你的部署。下面是一系列关键日期及其影响。 --- # Node.js > 使用 Node.js 运行时运行 Nitro 应用。 **预设:** `node_server` Node.js 是生产构建的默认 Nitro 输出预设。 使用 Nitro CLI 构建项目: ```bash nitro build ``` 使用 Node 服务器预设时,构建输出是一个入口点,用于启动一个可直接运行的 Node 服务器。要在本地试用该输出: ```bash $ node .output/server/index.mjs Listening on http://localhost:3000 ``` 现在,你可以将完全独立的 `.output` 目录部署到你选择的托管平台。 ## 环境变量 你可以使用以下环境变量自定义服务器行为: - `NITRO_PORT` 或 `PORT`(默认为 `3000`) - `NITRO_HOST` 或 `HOST` - `NITRO_UNIX_SOCKET` - 设置为套接字文件路径时,服务器会监听所提供的 UNIX 套接字 - `NITRO_SSL_CERT` 和 `NITRO_SSL_KEY` - 同时设置这两个变量时,服务器会以 HTTPS 模式启动。这仅用于测试;在生产环境中,请将 Nitro 服务器运行在终止 SSL 的反向代理之后,例如 nginx 或 Cloudflare - `NITRO_SHUTDOWN_DISABLED` - 设置为 `'true'` 以跳过优雅关闭。默认为 `'false'` - `NITRO_SHUTDOWN_SIGNALS` - 触发优雅关闭的信号列表,以空格分隔。默认为 `'SIGINT SIGTERM'` - `NITRO_SHUTDOWN_TIMEOUT` - 强制关闭发生前的时间(以毫秒为单位)。默认为 `'30000'` - `NITRO_SHUTDOWN_FORCE` - 设置为 `'true'` 时,在关闭过程结束时调用 `process.exit()`。设置为 `'false'` 时,进程会直接等待事件循环清空。默认为 `'true'` ## 集群模式 **预设:** `node_cluster` 要利用多个 CPU 核心来提升性能,请使用集群预设。 ### 环境变量 除了上述 `node_server` 环境变量之外: - `NITRO_CLUSTER_WORKERS`:集群工作进程数量(默认为可用 CPU 核心数) ## 处理器(高级) **预设:** `node_middleware` Nitro 还提供了一个更底层的预设,可以导出一个中间件函数,以便你将其接入自定义服务器。 使用 Node 中间件预设时,构建输出是一个导出中间件处理器的入口点。 **示例:** ```js import { createServer } from 'node:http' import { middleware } from './.output/server' const server = createServer(middleware) server.listen(8080) ``` --- # Bun > 使用 Bun 运行 Nitro 应用。 **预设:** `bun` Nitro 输出与 Bun 运行时兼容。虽然默认的 [Node.js](/deploy/runtimes/node) 输出也可以在 Bun 中运行,但使用 `bun` 预设进行构建可以启用 Bun 特定的优化。 使用 `bun` 预设构建后,使用以下命令启动生产服务器: ```bash bun run ./.output/server/index.mjs ``` :read-more{to="https://bun.sh"} --- # Deno > 使用 [Deno](https://deno.com/) 运行时运行 Nitro 应用。 **预设:** `deno_server` 你可以将 Nitro 应用构建为在 [Deno runtime](https://deno.com/runtime) 中运行的自定义服务器。 ```bash # Build with the Deno preset NITRO_PRESET=deno_server npm run build # Start production server deno run --allow-net --allow-read --allow-env .output/server/index.mjs ``` ## Deno Deploy :read-more{to="/deploy/providers/deno-deploy"} --- # Standard > 在任何运行时上运行 Nitro 应用,并使用符合 Web 标准的入口。 **Preset:** `standard` `standard` preset 会生成可移植且与运行时无关的构建产物,并提供符合 Web 标准的入口。服务器入口默认导出一个带有 [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) 处理程序的对象,与任何支持 Web 标准服务器约定的运行时或平台兼容。 ```bash # Build with the standard preset NITRO_PRESET=standard npm run build ``` 生成的入口如下: ```js [.output/server/index.mjs] export default { fetch(request) { // ... } } ``` ## 预览 你可以使用 [srvx](https://srvx.h3.dev) 在本地预览输出: ```bash cd .output npx srvx --prod ./ ``` ::note 使用此 preset 时,服务器不会提供静态资源。公共资源会输出到构建输出的 `public/` 目录中,应由托管平台或单独的静态文件服务器提供服务。 :: --- # Alwaysdata > 将 Nitro 应用部署到 alwaysdata。 **预设:** `alwaysdata` :read-more{to="https://alwaysdata.com"} ## 设置应用 ### 先决条件 1. 如果你还没有 alwaysdata 平台的个人资料,请[注册一个新的个人资料](https://www.alwaysdata.com/en/register/)。 2. 获取免费的 100MB 套餐来托管你的应用。 ::note 你的*账户名称*将用于生成默认 URL,格式为 `account_name.alwaysdata.net`,因此请谨慎选择。你也可以稍后将已有域名关联到你的账户,或者根据需要在个人资料下注册任意数量的账户。 :: ### 本地部署 1. 使用 `npm run build -- --preset alwaysdata` 在本地构建你的项目。 2. 将[你的应用上传](https://help.alwaysdata.com/en/remote-access/)到你账户中的独立目录(例如 `$HOME/www/my-app`)。你可以使用任何你喜欢的协议(SSH/FTP/WebDAV…)来完成此操作。 3. 在管理面板中,为你的应用[创建一个新站点](https://admin.alwaysdata.com/site/add/),并配置以下参数: - *地址*:`[account_name].alwaysdata.net` - *类型*:Node.js - *命令*:`node .output/server/index.mjs` - *工作目录*:`www/my-app`(根据你的部署路径进行调整) - *环境*: ```ini NITRO_PRESET=alwaysdata ``` - *Node.js 版本*:`默认版本`即可;建议选择不低于 `20.0.0` 的版本(你也可以[全局设置你的 Node.js 版本](https://help.alwaysdata.com/en/languages/nodejs/configuration/#supported-versions)) - *热重启*:`SIGHUP` :read-more{title="获取有关 alwaysdata Node.js 站点类型的更多信息" to="https://help.alwaysdata.com/en/languages/nodejs"} 4. 你的应用现在已在 `http(s)://[account_name].alwaysdata.net` 上线。 --- # AWS Amplify > 将 Nitro 应用部署到 AWS Amplify Hosting。 **预设:** `aws_amplify` :read-more{title="AWS Amplify 托管服务" to="https://aws.amazon.com/amplify"} ## 部署到 AWS Amplify Hosting ::tip 与此提供商集成时无需进行[零配置](/deploy#zero-config-providers) :: 1. 登录 [AWS Amplify Hosting 控制台](https://console.aws.amazon.com/amplify/) 2. 点击“Get Started” > Amplify Hosting(Host your web app) 3. 选择并授权访问您的 Git 仓库提供商,然后选择主分支 4. 为您的应用选择名称,确保自动检测构建设置,并可选地在高级部分设置所需的环境变量 5. 可选地,选择 Enable SSR logging,以启用将服务器端日志记录到您的 Amazon CloudWatch 账户 6. 确认配置并点击“Save and Deploy” ## 高级配置 You can configure advanced options of this preset using the `awsAmplify` option. ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ awsAmplify: { // 捕获所有静态回退:true, // 图片优化:{ path: "/_image", cacheControl: "public, max-age=3600, immutable" }, // 图片设置:{ ... }, // 运行时: "nodejs24.x", // 默认值: "nodejs20.x" | "nodejs22.x" | "nodejs24.x" } }) ``` ### `amplify.yml` 对于高级配置,您可能需要自定义的 `amplify.yml` 文件。以下是两个模板示例: ::code-group ```yml [amplify.yml] version: 1 frontend: phases: preBuild: commands: - nvm use 24 && node --version - corepack enable && npx --yes nypm install build: commands: - pnpm build artifacts: baseDirectory: .amplify-hosting files: - "**/*" ``` ```yml [amplify.yml (monorepo)] version: 1 applications: - frontend: phases: preBuild: commands: - nvm use 24 && node --version - corepack enable && npx --yes nypm install build: commands: - pnpm --filter website1 build artifacts: baseDirectory: apps/website1/.amplify-hosting files: - '**/*' buildPath: / appRoot: apps/website1 ``` :: --- # AWS Lambda > 将 Nitro 应用部署到 AWS Lambda。 **预设:** `aws_lambda` :read-more{title="AWS Lambda" to="https://aws.amazon.com/lambda/"} Nitro 提供了一个内置预设,用于生成与 [AWS Lambda](https://aws.amazon.com/lambda/) 兼容的输出。`.output/server/index.mjs` 中的输出入口点与 [AWS Lambda 格式](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html) 兼容。 它可以以编程方式使用,或作为部署的一部分。 ```ts import { handler } from './.output/server' // 以编程方式使用 const { statusCode, headers, body } = handler({ rawPath: '/' }) ``` ## 内联代码块 默认情况下,Nitro 输出使用动态代码块,仅在需要时才延迟加载代码。不过,这并不总是有利于性能(请参阅 [nitrojs/nitro#650](https://github.com/nitrojs/nitro/pull/650) 中的讨论)。你可以使用 [`inlineDynamicImports`](/config#inlinedynamicimports) 配置启用代码块内联。 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ inlineDynamicImports: true }); ``` ## 响应流式传输 :read-more{title="AWS Lambda 响应流式传输简介" to="https://aws.amazon.com/blogs/compute/introducing-aws-lambda-response-streaming/"} :read-more{title="介绍 AWS Lambda 响应流式传输" to="https://aws.amazon.com/blogs/compute/introducing-aws-lambda-response-streaming/"} 要启用响应流式传输,请设置 `awsLambda.streaming` 标志: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ awsLambda: { streaming: true } }); ``` --- # Azure > 将 Nitro 应用部署到 Azure Static Web Apps。 ## Azure Static Web Apps **预设:** `azure_swa` :read-more{title="Azure Static Web Apps" to="https://azure.microsoft.com/en-us/products/app-service/static"} ::note 与此提供商的集成可以通过[零配置](/deploy#zero-config-providers)实现。 :: [Azure Static Web Apps](https://azure.microsoft.com/en-us/products/app-service/static) 专为在 [GitHub Actions 工作流](https://docs.microsoft.com/en-us/azure/static-web-apps/github-actions-workflow)中持续部署而设计。Nitro 会检测此部署环境,并自动启用 `azure_swa` 预设。 ### 本地预览 要在本地进行测试,请安装 [Azure Functions Core Tools](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local),然后构建并启动本地预览环境: ```bash NITRO_PRESET=azure_swa npm run build npx @azure/static-web-apps-cli start .output/public --api-location .output/server ``` ### 配置 Azure Static Web Apps 使用 `staticwebapp.config.json` 文件进行[配置](https://learn.microsoft.com/en-us/azure/static-web-apps/configuration)。 每当使用 `azure_swa` 预设构建应用程序时,Nitro 都会自动生成此配置文件。 Nitro 会自动设置以下属性: | 属性 | 条件 | 默认值 | | --- | --- | --- | | **[platform.apiRuntime](https://learn.microsoft.com/en-us/azure/static-web-apps/configuration#platform)** | 根据 `package.json` 中的 `engines.node` 字段设置为 `node:20` 或 `node:22`。 | `node:20` | | **[navigationFallback.rewrite](https://learn.microsoft.com/en-us/azure/static-web-apps/configuration#fallback-routes)** | 始终为 `/api/server` | `/api/server` | | **[routes](https://learn.microsoft.com/en-us/azure/static-web-apps/configuration#routes)** | 添加所有预渲染路由。此外,如果没有 `index.html` 文件,则会创建一个空文件以实现兼容,并将对 `/index.html` 的请求重定向到根目录(由 `/api/server` 处理)。 | `[]` | ### 自定义配置 你可以使用 `azure.config` 选项修改 Nitro 生成的配置。 自定义路由会首先添加和匹配。如果发生冲突(两个对象具有相同的 route 属性),自定义路由会覆盖生成的路由。 ### 通过 GitHub Actions 从 CI/CD 部署 当你将 GitHub 仓库链接到 Azure Static Web Apps 时,仓库中会添加一个工作流文件。 当你被要求选择框架时,选择 custom 并提供以下信息: | 输入 | 值 | | --- | --- | | **app_location** | '/' | | **api_location** | '.output/server' | | **output_location** | '.output/public' | 如果你错过了此步骤,你可以在工作流中找到构建配置部分并更新构建配置: ```yaml [.github/workflows/azure-static-web-apps-.yml] ###### 仓库/构建配置 ###### app_location: '/' api_location: '.output/server' output_location: '.output/public' ###### 仓库/构建配置结束 ###### ``` 就是这样!现在 Azure Static Web Apps 将在推送时自动部署你的 Nitro 驱动应用。 如果你使用 `runtimeConfig`,可能需要在 [Azure 上配置相应的环境变量](https://docs.microsoft.com/en-us/azure/static-web-apps/application-settings)。 --- # Cleavr > 将 Nitro 应用部署到 Cleavr。 **预设:** `cleavr` :read-more{title="cleavr.io" to="https://cleavr.io"} ::note 与此提供商集成时无需进行[零配置](/deploy#zero-config-providers) :: ## 设置你的 Web 应用 在你的项目中,将 Nitro 预设设置为 `cleavr`。 ::code-group ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ preset: "cleavr", }); ``` ```ts [nuxt.config.ts] export default defineNuxtConfig({ nitro: { preset: "cleavr", }, }); ``` :: 将更改推送到你的代码仓库。 **在你的 Cleavr 面板中:** 1. 置备一台新服务器 2. 添加一个网站,选择 **Nuxt 3** 作为应用类型 3. 在 Web 应用 > 设置 > 代码仓库中,指向你项目的代码仓库 一切就绪,现在可以部署你的项目了! --- # Cloudflare > 将 Nitro 应用部署到 Cloudflare。 ## Cloudflare Workers **预设:** `cloudflare_module` :read-more{title="Cloudflare Workers" to="https://developers.cloudflare.com/workers/"} ::note 通过与该提供商的集成支持 [workers builds (beta)](https://developers.cloudflare.com/workers/ci-cd/builds/),可以实现[零配置](/deploy#zero-config-providers)。 :: 下面展示了用于将 Nitro 应用部署到 Cloudflare Workers 的示例 `nitro.config.ts` 文件。 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ preset: "cloudflare_module" }) ``` ### 本地预览 你可以使用 [Wrangler](https://github.com/cloudflare/workers-sdk/tree/main/packages/wrangler) 在本地预览你的应用: :pm-run{script="build"} :pm-x{command="wrangler dev"} ### 手动部署 构建应用后,你可以使用 Wrangler 手动部署。 首先确保你已登录到你的 Cloudflare 账户: :pm-x{command="wrangler login"} 然后你可以使用以下命令部署应用: :pm-x{command="wrangler deploy"} ### 运行时钩子 你可以使用下面的[运行时钩子](/docs/plugins#nitro-runtime-hooks)来扩展 [Worker 处理程序](https://developers.cloudflare.com/workers/runtime-apis/handlers/)。 :read-more{to="/docs/plugins#nitro-runtime-hooks"} - [`cloudflare:scheduled`](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/) - [`cloudflare:email`](https://developers.cloudflare.com/email-routing/email-workers/runtime-api/) - [`cloudflare:queue`](https://developers.cloudflare.com/queues/configuration/javascript-apis/#consumer) - [`cloudflare:tail`](https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/) - `cloudflare:trace` - `cloudflare:durable:init`(仅适用于 [`cloudflare_durable`](#cloudflare-workers-with-durable-objects) 预设) - [`cloudflare:durable:alarm`](https://developers.cloudflare.com/durable-objects/api/alarms/)(仅适用于 [`cloudflare_durable`](#cloudflare-workers-with-durable-objects) 预设) ::note `cloudflare:queue` 钩子会将消息批次作为 `batch` 接收,而 `cloudflare:email` 钩子会将收到的消息作为 `message` 接收。较旧的 `event` 字段已弃用,不再适用于这两个钩子。 :: ### 额外导出 你可以将 `exports.cloudflare.ts` 文件添加到项目根目录,以从 Cloudflare Worker 入口点导出其他处理程序或属性。 ```ts [exports.cloudflare.ts] export class MyWorkflow extends WorkflowEntrypoint { async run(event: WorkflowEvent, step: WorkflowStep) { // ... } } ``` Nitro 将自动检测此文件并将其导出包含在最终构建中。 ::warning `exports.cloudflare.ts` 文件不能包含默认导出。 :: 你还可以使用 `nitro.config.ts` 中的 `cloudflare.exports` 选项来自定义入口点文件位置: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ cloudflare: { exports: "custom-exports-entry.ts" } }) ``` ### 定时任务(Cron 触发器) 当使用带有 `scheduledTasks` 的 [Nitro 任务](/docs/tasks)时,Nitro 会在构建时自动在 wrangler 配置中生成 [Cron 触发器](https://developers.cloudflare.com/workers/configuration/cron-triggers/)。 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ preset: "cloudflare_module", experimental: { tasks: true, }, scheduledTasks: { "* * * * *": ["cms:update"], "0 15 1 * *": ["db:cleanup"], } }) ``` 无需手动配置 Wrangler,Nitro 会为你处理。 ## Cloudflare Workers with Durable Objects **预设:** `cloudflare_durable` :read-more{title="Durable Objects" to="https://developers.cloudflare.com/durable-objects/"} 此预设扩展了 `cloudflare_module`,并通过 [Durable Object](https://developers.cloudflare.com/durable-objects/) 实例路由请求,从而支持有状态功能,例如 WebSocket 支持(通过 [CrossWS](https://crossws.h3.dev/adapters/cloudflare#durable-objects))以及可跨请求持久存在的内存状态。 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ preset: "cloudflare_durable" }) ``` 该预设入口会导出一个 `$DurableObject` 类。你需要在 wrangler 配置中声明 Durable Object 绑定和迁移: ```json [wrangler.json] { "durable_objects": { "bindings": [ { "name": "$DurableObject", "class_name": "$DurableObject" } ] }, "migrations": [ { "tag": "v1", "new_classes": ["$DurableObject"] } ] } ``` 你可以使用 `cloudflare:durable:init` 运行时钩子,在 Durable Object 初始化时运行代码,并使用 `cloudflare:durable:alarm` 钩子处理 [alarms](https://developers.cloudflare.com/durable-objects/api/alarms/)。 ### 追踪 **🧪 实验性功能!** 启用实验性的 [`tracingChannel`](/config#tracingchannel) 选项后,Cloudflare 预设会将 Nitro 的追踪通道事件(h3 路由和中间件、srvx、unstorage 操作等)作为[自定义 span](https://developers.cloudflare.com/workers/observability/traces/custom-spans/)进行报告,同时还会报告 Cloudflare 的自动插桩事件(fetch 调用、KV 读取、D1 查询等),无需 OpenTelemetry SDK。 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ preset: "cloudflare_module", tracingChannel: true, }); ``` 必须在 Worker 上启用追踪,才能记录 span: ```jsonc [wrangler.jsonc] { "observability": { "traces": { "enabled": true } } } ``` ## Cloudflare Pages **预设:** `cloudflare_pages` :read-more{title="Cloudflare Pages" to="https://pages.cloudflare.com/"} ::note 与该提供商的集成支持[零配置](/deploy#zero-config-providers)。 :: ::warning Cloudflare [Workers](#cloudflare-workers) 是目前推荐用于部署的新预设。如果你需要 Pages 特有的功能,请考虑使用 Cloudflare Pages。 :: 以下展示了用于将 Nitro 应用部署到 Cloudflare Pages 的示例 `nitro.config.ts` 文件。 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ preset: "cloudflare_pages" }) ``` Nitro 自动生成一个 `_routes.json` 文件,用于控制哪些路由由文件提供,哪些由 Worker 脚本提供。自动生成的路由文件可以通过配置选项 `cloudflare.pages.routes` 覆盖([了解更多](https://developers.cloudflare.com/pages/platform/functions/routing/#functions-invocation-routes))。 ### 本地预览 你可以使用 [Wrangler](https://github.com/cloudflare/workers-sdk/tree/main/packages/wrangler) 在本地预览你的应用: :pm-run{script="build"} :pm-x{command="wrangler pages dev"} ### 手动部署 构建应用后,你可以使用 Wrangler 手动部署。 首先确保你已登录到你的 Cloudflare 账户: :pm-x{command="wrangler login"} 然后你可以使用以下命令部署应用: :pm-x{command="wrangler pages deploy"} ## 使用 GitHub Actions 在 CI/CD 中部署 无论你使用的是 Cloudflare Pages 还是 Cloudflare Workers,都可以使用 [Wrangler GitHub actions](https://github.com/marketplace/actions/deploy-to-cloudflare-workers-with-wrangler) 来部署应用。 ::note 请记得[指示 Nitro 使用正确的预设](/deploy#changing-the-deployment-preset)。所有预设都需要这样做,包括 `cloudflare_pages`。 :: ## 环境变量 Nitro 允许你通过 `process.env`、`import.meta.env` 或运行时配置,以统一的方式访问环境变量。 ::note 请确保只在**事件生命周期内**访问环境变量,而不要在全局上下文中访问,因为 Cloudflare 只会在请求生命周期内提供这些变量,而不会在此之前提供。 :: **示例:**如果你已设置 `SECRET` 和 `NITRO_HELLO_THERE` 环境变量,可以通过以下方式访问它们: ```ts import { defineHandler } from "nitro"; import { useRuntimeConfig } from "nitro/runtime-config"; console.log(process.env.SECRET) // 注意:这里位于全局作用域!因此它实际上不会生效,变量会是 undefined! export default defineHandler((event) => { // 注意:下面这些方式都可以访问上面的变量 useRuntimeConfig().helloThere useRuntimeConfig().secret process.env.NITRO_HELLO_THERE import.meta.env.SECRET }); ``` ### 在开发模式中指定变量 对于开发,你可以使用 `.env` 或 `.env.local` 文件来指定环境变量: ```ini NITRO_HELLO_THERE="captain" SECRET="top-secret" ``` ::note 请确保将 `.env` 和 `.env.local` 添加到 `.gitignore` 文件中,以免提交它们,因为其中可能包含敏感信息。 :: ### 为本地预览指定变量 构建完成后,当你使用 `wrangler dev` 或 `wrangler pages dev` 在本地试用项目时,请在项目根目录的 `.dev.vars` 文件中指定环境变量(如 [Pages](https://developers.cloudflare.com/pages/functions/bindings/#interact-with-your-environment-variables-locally) 和 [Workers](https://developers.cloudflare.com/workers/configuration/environment-variables/#interact-with-environment-variables-locally) 文档中所述)。 如果你在开发时使用 `.env` 或 `.env.local` 文件,你的 `.dev.vars` 应该与之一致。 ::note 请确保将 `.dev.vars` 添加到 `.gitignore` 文件中,以免提交它,因为其中可能包含敏感信息。 :: ### 为生产环境指定变量 对于生产环境,请使用 Cloudflare 仪表板或 [`wrangler secret`](https://developers.cloudflare.com/workers/wrangler/commands/#secret) 命令来设置环境变量和密钥。 ### 使用 `wrangler.toml`/`wrangler.json` 指定变量 你可以指定自定义的 `wrangler.toml`/`wrangler.json` 文件并在其中定义变量。 ::warning 不建议使用此方式存储密钥等敏感数据。 :: **示例:** ::code-group ```ini [wrangler.toml] # 共享 [vars] NITRO_HELLO_THERE="general" SECRET="secret" # 为 `--env production` 用法覆盖值 [env.production.vars] NITRO_HELLO_THERE="captain" SECRET="top-secret" ``` ```json [wrangler.json] { "vars": { "NITRO_HELLO_THERE": "general", "SECRET": "secret" }, "env": { "production": { "vars": { "NITRO_HELLO_THERE": "captain", "SECRET": "top-secret" } } } } ``` :: ## 直接访问 Cloudflare 绑定 绑定允许你与 Cloudflare 平台中的资源进行交互,例如键值数据存储([KV](https://developers.cloudflare.com/kv/))和无服务器 SQL 数据库([D1](https://developers.cloudflare.com/d1/))。 ::read-more 有关绑定及其使用方式的更多详细信息,请参阅 Cloudflare 的 [Pages](https://developers.cloudflare.com/pages/functions/bindings/) 和 [Workers](https://developers.cloudflare.com/workers/configuration/bindings/#bindings) 文档。 :: ::tip Nitro 为 [KV Storage](/docs/storage) 和 [Database](/docs/database) 等基础功能提供了高级 API。为了保持使用稳定性,优先使用这些 API,而不是直接依赖底层平台 API。 :: :read-more{title="数据库层" to="/docs/database"} :read-more{title="KV 存储" to="/docs/storage"} 你可以通过 `event.req.runtime.cloudflare.env` 从请求事件中访问绑定。例如,以下是访问 D1 绑定的方式: ```ts import { defineHandler } from "nitro"; defineHandler(async (event) => { const { env } = event.req.runtime.cloudflare const stmt = await env.MY_D1.prepare('SELECT id FROM table') const { results } = await stmt.all() }) ``` ### 在本地开发中访问绑定 在开发模式下,Nitro 使用 [Miniflare](https://miniflare.dev/) 模拟 Cloudflare 环境(这是 Wrangler 和生产环境中的 Cloudflare Workers 使用的相同 [`workerd`](https://github.com/cloudflare/workerd) 运行时)。这意味着绑定可以直接从请求事件中原生访问,无需单独的代理。 [`miniflare`](https://www.npmjs.com/package/miniflare) 软件包由你的项目负责管理:Nitro 会从你的 `node_modules` 中解析它,并在首次使用时提供安装选项。 要在开发模式下访问绑定,请先定义绑定。你可以在 `wrangler.jsonc`/`wrangler.json`/`wrangler.toml` 文件中完成此操作(读取 `wrangler.jsonc`、`wrangler.toml` 和 `.dev.vars` 需要安装 `wrangler` 软件包): ::code-group ```ini [wrangler.toml] [vars] MY_VARIABLE="my-value" [[kv_namespaces]] binding = "MY_KV" id = "xxx" ``` ```json [wrangler.json] { "vars": { "MY_VARIABLE": "my-value", }, "kv_namespaces": [ { "binding": "MY_KV", "id": "xxx" } ] } ``` :: 或者,你也可以在 `nitro.config.ts` 中使用 `cloudflare.wrangler` 选项内联定义绑定(它接受与 `wrangler.json` 相同的结构): ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ preset: "cloudflare_module", cloudflare: { wrangler: { vars: { MY_VARIABLE: "my-value", }, kv_namespaces: [{ binding: "MY_KV", id: "xxx" }], }, }, }) ``` 从现在开始,运行 :pm-run{script="dev"} 时,你可以按照上面的示例从请求事件中访问 `MY_VARIABLE` 和 `MY_KV`。 与 `wrangler dev` 相比,有以下几点不同: - 绑定(KV、D1、R2 等)的本地数据会持久化到 `.wrangler/state/v3`,并与 `wrangler dev` 共享 - 无论 `compatibility_date` 如何设置,都会使用已安装的 `miniflare` 所支持的最新兼容性日期 - 静态资源由 Nitro 提供服务,并且无法使用指向其他 Worker 的绑定(`services`、`tail_consumers`)以及指向类或处理程序的绑定(`durable_objects`、`workflows`、队列使用者) - 设置 `cloudflare.wrangler` 后,它会与最近的 Wrangler 配置文件合并 #### Wrangler environments 如果你有多个 Wrangler 环境,你可以使用 `cloudflare.wranglerEnv` 选项指定在本地开发模拟时要使用的环境: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ preset: 'cloudflare_module', cloudflare: { wranglerEnv: 'preview' } }) ``` --- # Deno Deploy > 将 Nitro 应用部署到 [Deno Deploy](https://deno.com/deploy)。 **预设:** `deno_deploy` :read-more{title="Deno Deploy" to="https://deno.com/deploy"} ## 使用 CLI 部署 你可以使用 [deployctl](https://deno.com/deploy/docs/deployctl) 来部署你的应用。 登录 [Deno Deploy](https://dash.deno.com/account#access-tokens) 以获取 `DENO_DEPLOY_TOKEN` 访问令牌,并将其设置为环境变量。 ```bash # Build with the deno_deploy preset NITRO_PRESET=deno_deploy npm run build # 确保从输出目录运行 deployctl 命令 cd .output deployctl deploy --project=my-project server/index.ts ``` ## 使用 GitHub Actions 在 CI/CD 中部署 将 deployctl GitHub Action 作为一个步骤包含在工作流中。 无需任何密钥,但你需要将 GitHub 仓库关联到 Deno Deploy 项目,并在 [Deno Deploy](https://dash.deno.com) 的项目设置中选择“GitHub Actions”部署模式。 在你的 `.github/workflows` 目录中创建以下工作流文件: ```yaml [.github/workflows/deno_deploy.yml] name: deno-deploy on: push: branches: - main pull_request: branches: - main jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - run: corepack enable - uses: actions/setup-node@v6 with: node-version: 20 cache: pnpm - run: pnpm install - run: pnpm build env: NITRO_PRESET: deno_deploy - name: 部署到 Deno Deploy uses: denoland/deployctl@v1 with: project: my-project entrypoint: server/index.ts root: .output ``` ## Deno 运行时 :read-more{to="/deploy/runtimes/deno"} --- # DigitalOcean > 将 Nitro 应用部署到 DigitalOcean。 **预设:** `digital_ocean` :read-more{title="DigitalOcean App Platform" to="https://docs.digitalocean.com/products/app-platform/"} ## 设置应用 1. 按照[指南](https://docs.digitalocean.com/products/app-platform/how-to/create-apps/)创建一个新的 DigitalOcean 应用。 1. 接下来,配置环境变量。在您的应用设置中,确保设置了以下应用级环境变量: ```bash NITRO_PRESET=digital_ocean ``` [更多信息](https://docs.digitalocean.com/products/app-platform/how-to/use-environment-variables/). 1. 在应用的 `package.json` 中设置 `engines.node` 字段,以便 DigitalOcean 使用受支持的 Node.js 版本: ```json { "engines": { "node": "20.x" } } ``` [查看更多信息](https://docs.digitalocean.com/products/app-platform/languages-frameworks/nodejs/#node-version). 1. 在 `package.json` 中添加启动脚本,以便 DigitalOcean 知道构建后要运行的命令: ```json { "scripts": { "start": "node .output/server/index.mjs" } } ``` 1. 最后,将此启动脚本添加到 DigitalOcean 应用的运行命令中。转到 `Components > Settings > Commands`,点击“Edit”,然后添加 `npm run start`。 现在,您的应用应该已经通过 DigitalOcean 生成的 URL 上线,您可以继续阅读[其余的 DigitalOcean 部署指南](https://docs.digitalocean.com/products/app-platform/how-to/manage-deployments/)。 --- # EdgeOne Pages > 将 Nitro 应用部署到 EdgeOne Pages。 **预设:** `edgeone_pages` :read-more{to="https://pages.edgeone.ai/"} ## 使用控制台 1. 在 [EdgeOne Pages 控制台](https://console.tencentcloud.com/edgeone/pages)中,点击 **创建项目** 2. 选择 **导入 Git 仓库** 作为部署方式。EdgeOne 支持从 GitHub、GitLab、Gitee 和 CNB 进行部署 3. 选择包含应用代码的 **仓库** 和 **分支** 4. 在设置过程中,添加一个值为 `edgeone_pages` 的 `NITRO_PRESET` 环境变量(此步骤为必需步骤) 5. 点击 **部署** 按钮 ## 使用 EdgeOne CLI 你也可以使用 [EdgeOne CLI](https://pages.edgeone.ai/document/edgeone-cli) 进行部署(有关安装和使用方法,请参阅其文档)。 配置完成后,运行 `edgeone pages deploy` 以部署项目。CLI 会先自动构建项目,然后上传并发布构建产物。 --- # Firebase > 将 Nitro 应用部署到 Firebase。 ::note 你需要订阅 [**Blaze 计费方案**](https://firebase.google.com/pricing)(即用即付)才能开始使用。 :: ## Firebase App Hosting **预设:** `firebase_app_hosting` :read-more{title="Firebase 应用托管" to="https://firebase.google.com/docs/app-hosting"} ::tip 你可以使用[零配置](/deploy#zero-config-providers)将此提供程序集成到项目中。 :: ### 项目设置 1. 前往 Firebase [控制台](https://console.firebase.google.com/)并设置一个新项目。 2. 在侧边栏中选择 **构建 > App Hosting**。 - 你可能需要在此步骤升级计费方案。 3. 点击**开始使用**。 - 选择一个区域。 - 导入 GitHub 仓库(你需要关联 GitHub 账户)。 - 配置部署设置(项目根目录和分支),并启用自动发布。 - 为后端选择一个唯一 ID。 4. 点击**完成并部署**以创建首次发布。 使用 Firebase App Hosting 部署时,App Hosting 预设会在构建时自动启用。 ### 运行配置 你可以使用 Nitro 配置中的 `firebase.appHosting` 选项自定义生成的 [App Hosting 输出包](https://firebase.google.com/docs/app-hosting/build-config)运行配置: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ firebase: { appHosting: { cpu: 1, memoryMiB: 512, concurrency: 80, minInstances: 0, maxInstances: 10, }, }, }); ``` 支持的值包括 `runCommand`、`environmentVariables`、`concurrency`、`cpu`、`memoryMiB`、`minInstances` 和 `maxInstances`。 --- # Flightcontrol > 通过 Flightcontrol 将 Nitro 应用部署到 AWS。 **预设:** `flight_control` :read-more{title="flightcontrol.dev" to="https://flightcontrol.dev?ref=nitro"} ## 设置你的 Flightcontrol 账户 概括来说,首次部署项目需要完成以下步骤: 1. 在 [Flightcontrol](https://app.flightcontrol.dev/signup?ref=nitro) 创建账户 2. 在 [AWS](https://portal.aws.amazon.com/billing/signup) 创建账户(如果你还没有账户) 3. 将你的 AWS 账户关联到 Flightcontrol 4. 授权 Flightcontrol GitHub App 访问你选择的仓库,可以是公开仓库或私有仓库 5. 创建一个 Flightcontrol 项目,可以通过 Dashboard 或 `flightcontrol.json` 进行配置 ### 通过 Dashboard 创建项目 1. 从 Dashboard 创建一个 Flightcontrol 项目。为源码选择一个仓库 2. 选择 `GUI` 配置类型 3. 选择 Nuxt 预设。此预设同样适用于任何基于 Nitro 的应用 4. 选择你偏好的 AWS 服务器规格 5. 提交新项目表单 ### 通过 `flightcontrol.json` 创建项目 1. 从你的 Dashboard 创建一个 Flightcontrol 项目。为源码选择一个仓库。 2. 选择 `flightcontrol.json` 配置类型。 3. 在仓库根目录添加一个名为 `flightcontrol.json` 的新文件。以下是一个为你的应用创建 AWS Fargate 服务的示例配置: ```json [flightcontrol.json] { "$schema": "https://app.flightcontrol.dev/schema.json", "environments": [ { "id": "production", "name": "Production", "region": "us-west-2", "source": { "branch": "main" }, "services": [ { "id": "nitro", "buildType": "nixpacks", "name": "My Nitro site", "type": "fargate", "domain": "www.yourdomain.com", "outputDirectory": ".output", "startCommand": "node .output/server/index.mjs", "cpu": 0.25, "memory": 0.5 } ] } ] } ``` 4. 提交新项目的表单。 ::read-more{to="https://www.flightcontrol.dev/docs?ref=nitro"} 了解更多关于 Flightcontrol 的 [配置](https://www.flightcontrol.dev/docs?ref=nitro)。 ::: --- # Genezio > 将 Nitro 应用部署到 Genezio。 **预设:** `genezio` :read-more{title="Genezio" to="https://genezio.com"} ::warning 此预设目前处于实验阶段。 :: ## 1. 项目设置 创建一个 `genezio.yaml` 文件: ```yaml # 项目的名称。 name: nitro-app # 要解析的 Genezio YAML 配置版本。 yamlVersion: 2 backend: # 后端的根目录。 path: .output/ # 后端编程语言的信息。 language: # 编程语言的名称。 name: js # 后端使用的包管理器。 packageManager: npm # 后端函数的信息。 functions: # 函数的名称(标签)。 - name: nitroServer # 函数代码的路径。 path: server/ # 函数处理程序的名称 handler: handler # 函数的入口点。 entry: index.mjs ``` ::read-more{to="https://genezio.com/docs/project-structure/genezio-configuration-file/"} 如需根据您的需求进一步自定义该文件,您可以查阅 [官方文档](https://genezio.com/docs/project-structure/genezio-configuration-file/)。 :: ## 2. 部署您的项目 使用 `genezio` 预设进行构建: ```bash NITRO_PRESET=genezio npm run build ``` 使用 [`genezio`](https://npmjs.com/package/genezio) CLI 进行部署: :pm-x{command="genezio deploy"} ::read-more{title="后端环境变量" to="https://genezio.com/docs/project-structure/backend-environment-variables"} 如需设置环境变量,请参阅 [Genezio - 环境变量](https://genezio.com/docs/project-structure/backend-environment-variables)。 :: ## 3. 监控您的项目 您可以通过 [Genezio 应用控制面板](https://app.genez.io/dashboard) 监控和管理您的应用。控制面板 URL 也会在部署完成后显示,您可以通过它查看项目的状态和日志。 --- # GitHub Pages > 将 Nitro 应用部署到 GitHub Pages。 **预设:** `github_pages` :read-more{title="GitHub Pages" to="https://pages.github.com/"} ## 设置 按照[创建 GitHub Pages 站点](https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-github-pages-site)的步骤操作。 ## 部署 以下是一个使用 `github_pages` 预设将您的站点部署到 GitHub Pages 的 GitHub Actions 工作流示例: ```yaml [.github/workflows/deploy.yml] # https://github.com/actions/deploy-pages#usage name: Deploy to GitHub Pages on: workflow_dispatch: push: branches: - main jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - run: corepack enable - uses: actions/setup-node@v6 with: node-version: "20" - run: npx nypm install - run: npm run build env: NITRO_PRESET: github_pages - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: ./.output/public # 部署任务 deploy: # 添加对构建任务的依赖 needs: build # 授予 GITHUB_TOKEN 部署到 Pages 所需的权限 permissions: pages: write # 用于部署到 Pages id-token: write # 用于验证部署是否来自适当的来源 # 部署到 github_pages 环境 environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} # 指定运行器 + 部署步骤 runs-on: ubuntu-latest steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 ``` --- # GitLab Pages > 将 Nitro 应用部署到 GitLab Pages。 **预设:** `gitlab_pages` :read-more{title="GitLab Pages" to="https://docs.gitlab.com/ee/user/project/pages/"} ## 设置 按照步骤[创建 GitLab Pages 站点](https://docs.gitlab.com/ee/user/project/pages/#getting-started)。 ## 部署 以下是一个用于将站点部署到 GitLab Pages 的 GitLab CI/CD 配置示例: ```yaml [.gitlab-ci.yml] image: node:lts before_script: - npx nypm install pages: cache: paths: - node_modules/ variables: NITRO_PRESET: gitlab_pages script: - npm run build artifacts: paths: - .output/public publish: .output/public rules: # 这确保只有推送到默认分支的操作 # 才会触发页面部署 - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH ``` --- # Heroku > 将 Nitro 应用部署到 Heroku。 **预设:** `heroku` :read-more{title="heroku.com" to="https://heroku.com/"} ## 使用 Heroku CLI 1. 创建一个新的 Heroku 应用。 ```bash heroku create myapp ``` 1. 配置 Heroku 使用 nodejs 构建包。 ```bash heroku buildpacks:set heroku/nodejs ``` 1. 配置你的应用。 ```bash heroku config:set NITRO_PRESET=heroku ``` 1. 确保在 `package.json` 文件中有 `start` 和 `build` 命令。 ```json5 "scripts": { "build": "nitro build", // 如果使用 nuxt 则为 `nuxt build` "start": "node .output/server/index.mjs" } ``` ## 使用 Nginx 1. 添加 [Heroku nginx buildpack](https://github.com/heroku/heroku-buildpack-nginx.git) 1. 将 Nitro 配置中的预设更改为 `node_middleware` ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ preset: "node_middleware", }) ``` 或者,如果你使用的是 Nuxt: ```ts [nuxt.config.ts] export default defineNuxtConfig({ nitro: { preset: "node_middleware", }, }) ``` 1. 如构建包文档的**现有应用**部分所述,需要完成以下两个关键步骤才能正常运行: - 在 `/tmp/nginx.socket` 上监听套接字 - 在应用准备好接受连接时创建文件 `/tmp/app-initialized` 1. 创建一个自定义应用运行器(例如,在项目根目录下创建 `apprunner.mjs`)。在此文件中,使用 `node_middleware` 预设生成的中间件创建服务器,然后按照构建包文档中的详细说明监听套接字: ```ts import { createServer } from 'node:http' import { middleware } from './.output/server/index.mjs' const server = createServer(middleware) server.listen('/tmp/nginx.socket') //遵循构建包文档 ``` 1. 要创建 `/tmp/app-initialized` 文件,请使用 Nitro 插件(例如,在项目根目录下创建 `initServer.ts`): ```ts import fs from "node:fs" import { definePlugin } from "nitro" export default definePlugin((nitroApp) => { if((process.env.NODE_ENV || 'development') != 'development') { fs.openSync('/tmp/app-initialized', 'w') } }) ``` 1. 最后,在项目根目录下创建一个 `Procfile`。它会告知 Heroku 启动 nginx,并使用自定义的 `apprunner.mjs` 启动服务器: ```txt [Procfile] web: bin/start-nginx node apprunner.mjs ``` 1. 附加内容:创建一个 `config/nginx.conf.erb` 文件来定制你的 nginx 配置。默认情况下,`node_middleware` 预设不会生成静态文件处理程序,因此你可以通过向服务器代码块中添加正确的 location 规则,让 nginx 提供静态文件。或者,将 `serveStatic` 设置为 `true`,强制预设生成静态文件处理程序。 --- # IIS > 将 Nitro 应用部署到 IIS。 ## 使用 [IISnode](https://github.com/Azure/iisnode) **预设:** `iis_node` 1. 在您的 Windows Server 上安装最新 LTS 版本的 [Node.js](https://nodejs.org/en/)。 2. 安装 [IISnode](https://github.com/azure/iisnode/releases) 3. 安装 [IIS `URLRewrite` 模块](https://www.iis.net/downloads/microsoft/url-rewrite)。 4. 在 IIS 中,将 `.mjs` 添加为新的 MIME 类型,并将其内容类型设置为 `application/javascript`。 5. 将 `.output` 文件夹的内容部署到 IIS 中的网站。 ## 使用 IIS 处理程序 **预设:** `iis_handler` 您可以直接使用 IIS HttpPlatformHandler。 1. 在您的 Windows Server 上安装最新 LTS 版本的 [Node.js](https://nodejs.org/en/)。 2. 安装 [IIS `HttpPlatformHandler` 模块](https://www.iis.net/downloads/microsoft/httpplatformhandler) 3. 将 `.output` 目录复制到 Windows Server 中,并在 IIS 中创建一个指向该确切目录的网站。 ## IIS 配置选项 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ iis: { // merge a pre-existing web.config file into the Nitro default file mergeConfig: true, // override the default Nitro web.config file altogether overrideConfig: false, }, }); ``` --- # Koyeb > 将 Nitro 应用部署到 Koyeb。 **预设:** `koyeb` :read-more{to="https://www.koyeb.com"} ## 使用控制面板 1. 在 [Koyeb 控制面板](https://app.koyeb.com/)中,点击 **Create App**。 2. 选择 **GitHub** 作为部署方式。 3. 选择包含应用代码的 GitHub **repository** 和 **branch**。 4. 为服务命名。 5. 如果你没有在 `package.json` 文件中添加 `start` 命令,请在 **Build and deployment settings** 下,切换运行命令字段对应的覆盖开关。在 **Run command** 字段中输入: ```bash node .output/server/index.mjs ``` 6. 在 **Advanced**(高级)部分,点击 **Add Variable**(添加变量)并添加一个 `NITRO_PRESET` 变量,设置为 `koyeb`。 7. 命名 App(应用)。 8. 点击 **Deploy**(部署)按钮。 ## 使用 Koyeb CLI 1. 根据你的操作系统,按照说明[安装 Koyeb CLI 客户端](https://www.koyeb.com/docs/cli/installation),或直接从 [GitHub 上的发布页面](https://github.com/koyeb/koyeb-cli/releases)下载。 2. 在 Koyeb 控制面板中访问你所在组织的 [API 设置](https://app.koyeb.com/settings/api),创建 Koyeb API 访问令牌。 3. 输入以下命令,使用 Koyeb CLI 登录你的账户: ```bash koyeb login ``` 在提示时粘贴你的 API 凭据。 4. 使用以下命令从 GitHub 仓库部署 Nitro 应用。请务必将 ``、`` 和 `` 替换为你自己的值: ```bash koyeb app init \ --git github.com// \ --git-branch main \ --git-run-command "node .output/server/index.mjs" \ --ports 3000:http \ --routes /:3000 \ --env PORT=3000 \ --env NITRO_PRESET=koyeb ``` ## 使用 Docker 容器 1. 在项目的根目录创建一个 `.dockerignore` 文件,并添加以下行: ``` Dockerfile .dockerignore node_modules npm-debug.log .nitro .output .git dist README.md ``` 2. 在项目的根目录添加一个 `Dockerfile`: ``` FROM node:20-alpine AS base FROM base AS deps RUN apk add --no-cache libc6-compat WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run build && npm cache clean --force FROM base AS runner WORKDIR /app RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nitro COPY --from=builder /app . USER nitro EXPOSE 3000 ENV PORT 3000 CMD ["npm", "run", "start"] ``` 上述 Dockerfile 提供了运行 Nitro 应用所需的最低要求。你可以根据需求轻松扩展它。 接下来,将 Docker 镜像推送到注册表,例如 [Docker Hub](https://hub.docker.com/) 或 [GitHub Container Registry](https://docs.github.com/en/packages/guides/about-github-container-registry)。 在 Koyeb 控制面板中,使用镜像和标签字段指定要部署的镜像。你也可以使用 [Koyeb CLI](https://www.koyeb.com/docs/build-and-deploy/cli/installation)。有关更多信息,请参阅 Koyeb 的 [Docker 文档](https://www.koyeb.com/docs/build-and-deploy/prebuilt-docker-images)。 --- # Netlify > 将 Nitro 应用部署到 Netlify 函数或边缘。 **预设:** `netlify` :read-more{title="Netlify 函数" to="https://www.netlify.com/platform/core/functions/"} ::note 与此提供商的集成可以通过[零配置](/deploy#zero-config-providers)实现。 :: 通常,将应用部署到 Netlify 不需要任何配置。 Nitro 会自动检测你处于 [Netlify](https://www.netlify.com) 构建环境中,并构建正确版本的服务器。 对于新站点,Netlify 会检测到你正在使用 Nitro,并将发布目录设置为 `dist`,将构建命令设置为 `npm run build`。如果你正在升级现有站点,请检查这些设置,并在需要时更新它们。 要添加自定义重定向,请使用 [`routeRules`](/config#routerules),或将 [`_redirects`](https://docs.netlify.com/routing/redirects/#syntax-for-the-redirects-file) 文件添加到你的 `public` 目录中。 要进行部署,只需像平常为 Netlify 执行的那样推送到你的 git 仓库[即可](https://docs.netlify.com/configure-builds/get-started/)。 ::note 创建新项目时,请确保发布目录设置为 `dist` :: ## Netlify 边缘函数 **预设:** `netlify_edge` Netlify Edge Functions 使用 Deno 和强大的 V8 JavaScript 运行时,让你可以运行全局分布的函数,以实现最快的响应时间。 :read-more{title="Netlify 边缘函数" to="https://docs.netlify.com/edge-functions/overview/"} 使用此预设时,Nitro 会直接在边缘运行服务器,使其距离用户更近。 ::note 创建新项目时,请确保发布目录设置为 `dist` :: ## Netlify 静态 **预设:** `netlify_static` 使用 `netlify_static` 预设预渲染你的应用,并将其作为完全静态站点(无函数)部署到 Netlify。 ## 自定义部署配置 你可以在 `nitro.config` 中使用 `netlify.config` 键提供额外的部署配置。它遵循 [Netlify Frameworks API](https://docs.netlify.com/build/frameworks/frameworks-api/) 的 `config.json` 格式,并会与内置的自动生成配置合并。 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ netlify: { config: { // Netlify Image CDN remote images images: { remote_images: ["https://example.com/.*"], }, // Custom headers headers: [{ for: "/static/*", values: { "cache-control": "public, max-age=31536000" } }], // Custom redirects redirects: [{ from: "/old", to: "/new", status: 301 }], }, }, }); ``` 支持的键: - `edge_functions`:其他[边缘函数声明](https://docs.netlify.com/edge-functions/declarations/)。 - `functions`:[函数配置](https://docs.netlify.com/build/frameworks/frameworks-api/#functions),可以是全局配置,也可以按函数模式配置(例如 `included_files`)。 - `headers`:自定义[标头规则](https://docs.netlify.com/routing/headers/)。 - `images`:[Netlify Image CDN](https://docs.netlify.com/image-cdn/create-integration/) 配置(例如 `remote_images`)。 - `redirects`:自定义[重定向规则](https://docs.netlify.com/routing/redirects/)。 ::note 顶层的 `netlify.images` 选项已弃用。请改用 `netlify.config.images` :: --- # Render.com > 将 Nitro 应用部署到 Render.com。 **预设:** `render_com` :read-more{title="render.com" to="https://render.com"} ## 设置应用 1. [创建新的 Web 服务](https://dashboard.render.com/select-repo?type=web) 并选择包含您代码的仓库。 2. 确保已选择 'Node' 环境。 3. 将启动命令更新为 `node .output/server/index.mjs` 4. 点击 'Advanced' 并添加一个环境变量,将 `NITRO_PRESET` 设置为 `render_com`。您可能还需要添加一个 `NODE_VERSION` 环境变量,设置为 `20` 以确保构建成功 ([文档](https://render.com/docs/node-version))。 5. 点击 'Create Web Service'。 ## 基础设施即代码 (IaC) 1. 在仓库根目录创建一个名为 `render.yaml` 的文件,内容如下。此文件遵循 Render 的[基础设施即代码](https://render.com/docs/infrastructure-as-code)规范。 ```yaml [render.yaml] services: - type: web name: env: node branch: main startCommand: node .output/server/index.mjs buildCommand: npx nypm install && npm run build envVars: - key: NITRO_PRESET value: render_com ``` 1. [创建新的 Blueprint 实例](https://dashboard.render.com/select-repo?type=blueprint) 并选择包含 `render.yaml` 文件的仓库。 您应该就可以开始了! --- # Stormkit > 将 Nitro 应用部署到 Stormkit。 **预置配置:** `stormkit` :read-more{title="Stormkit" to="https://www.stormkit.io"} ::note 可以通过[零配置](/deploy#zero-config-providers)与 [Stormkit](https://www.stormkit.io/) 集成。 :: ## 设置 按照以下步骤在 Stormkit 上[创建新应用](https://app.stormkit.io/apps/new)。 ![在 Stormkit 上创建新应用](/images/stormkit-new-app.png) ## 部署 默认情况下,当你将更改推送到主分支时,Stormkit 会自动部署你的应用。要触发手动部署(例如,进行首次部署),请点击 `Deploy now`。 ![使用“立即部署”触发手动部署](/images/stormkit-deploy.png) --- # Upsun > 将 Nitro 应用部署到 Upsun **Preset:** `upsun` :read-more{to="https://upsun.com"} ::note 旧的 `platform_sh` preset 名称仍然有效,并会解析为 `upsun` :: ## 项目创建 首先,在 [Upsun](https://upsun.com) 上创建一个新项目: ```bash upsun project:create ``` 或者关联一个现有项目: ```bash upsun project:set ``` ## Upsun 配置 然后在仓库中创建 `.upsun/config.yaml` 文件: ```yaml [.upsun/config.yaml] applications: nitro: type: nodejs:24 dependencies: nodejs: "pnpm": "*" source: root: "/" hooks: build: | set -eux pnpm install NITRO_PRESET=upsun pnpm build web: commands: start: "node .output/server/index.mjs" locations: "/": root: "dist/client" passthru: true allow: false mounts: ".data": source: storage source_path: data routes: # Primary domain "https://{default}/": type: upstream upstream: "nitro:http" cache: enabled: true cookies: ["*"] default_ttl: 300 headers: ["Accept", "Accept-Language"] ``` :read-more{title="所有可用配置属性的完整列表" to="https://docs.upsun.com/get-started/here/configure/nodejs.html"} :read-more{title="Nitro 存储层" to="/docs/storage"} ## 部署 准备就绪后,使用以下命令部署 `main` 分支: ```bash git push upsun main ``` 或者,如果已安装 Upsun CLI 客户端: ```bash upsun deploy ``` 随后,Upsun 流水线会构建并部署项目: ```bash Building application 'nitro' (runtime type: nodejs:24) Executing build hook... W: + pnpm install W: + pnpm build [info] [nitro] Building Nitro Server (preset: `upsun`) [success] [nitro] Nitro Server built Redeploying environment main Environment routes https://example.org/ is served by application `nitro` ``` 如需任何支持,请联系 [Discord 上的 Upsun 团队](https://discord.com/invite/upsun)。 --- # Vercel > 将 Nitro 应用部署到 Vercel。 **预设:** `vercel` :read-more{title="Vercel 框架支持" to="https://vercel.com/docs/frameworks"} ::note 与此提供商的集成支持[零配置](/deploy#zero-config-providers)。 :: ## 入门指南 部署到 Vercel 具有以下功能以及其他特性: - [预览部署](https://vercel.com/docs/deployments/environments) - [流式计算](https://vercel.com/docs/fluid-compute) - [可观测性](https://vercel.com/docs/observability) - [Vercel 防火墙](https://vercel.com/docs/vercel-firewall) 在 [Vercel 文档](https://vercel.com/docs)中了解更多信息。 ### 通过 Git 部署 Vercel 支持 Nitro 零配置部署。[立即将 Nitro 部署到 Vercel](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fvercel%2Ftree%2Fmain%2Fexamples%2Fnitro)。 ## API 路由 Nitro 顶层的 `/api` 目录与 Vercel 不兼容。请改用 `routes/api/` 目录。 ## Bun 运行时 :read-more{title="Vercel" to="https://vercel.com/docs/functions/runtimes/bun"} 你可以通过在 `nitro.config` 中使用 `vercel.functions` 键指定运行时,从而使用 [Bun](https://bun.com) 替代 Node.js: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ vercel: { functions: { runtime: "bun1.x" } } }) ``` 或者,如果你在 `vercel.json` 中指定了 `bunVersion` 属性,Nitro 也会自动检测 Bun: ```json [vercel.json] { "$schema": "https://openapi.vercel.sh/vercel.json", "bunVersion": "1.x" } ``` ## 每条路由的函数配置 使用 `vercel.functionRules` 为特定路由覆盖[无服务器函数设置](https://vercel.com/docs/build-output-api/primitives#serverless-function-configuration)。每个键都是一个路由模式,其值是一个部分函数配置对象,会与基础的 `vercel.functions` 配置合并。 ::note 路由配置中的数组属性(例如 `regions`)会替换基础配置中的数组,而不是与其合并。 :: 当某些路由需要不同的资源限制、区域或功能(如 [Vercel Queues 触发器](https://vercel.com/docs/queues))时,这非常有用。 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ vercel: { functionRules: { "/api/heavy-computation": { maxDuration: 800, memory: 4096, }, "/api/regional": { regions: ["lhr1", "cdg1"], }, "/api/queues/process-order": { experimentalTriggers: [{ type: "queue/v2beta", topic: "orders" }], }, }, }, }); ``` 路由模式支持通过 [rou3](https://github.com/h3js/rou3) 匹配通配符(例如,`/api/slow/**` 匹配 `/api/slow/` 下的所有路由)。 ## 代理路由规则 Nitro 通过在构建时生成 [CDN 级别的重写规则](https://vercel.com/docs/rewrites),自动优化 Vercel 上的 `proxy` 路由规则。这意味着匹配的请求将在边缘进行代理,而无需调用无服务器函数,从而降低延迟和成本。 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ routeRules: { // Proxied at CDN level, no function invocation "/api/**": { proxy: "https://api.example.com/**", }, }, }); ``` ### 何时应用 CDN 重写 当以下**所有**条件都为真时,代理规则会被卸载到 Vercel CDN 重写: - 目标是**外部 URL**(以 `http://` 或 `https://` 开头)。 - 规则上没有设置高级 `ProxyOptions`。 ### 回退到运行时代理 当代理规则使用了以下任何 `ProxyOptions` 时,Nitro 会将其保留为由无服务器函数处理的运行时代理: - `headers`:对发往上游的请求使用自定义请求头 - `forwardHeaders` / `filterHeaders`:请求头过滤 - `fetchOptions`:自定义 fetch 选项 - `cookieDomainRewrite` / `cookiePathRewrite`:Cookie 操作 - `onResponse`:响应回调 ::note 通过 `headers` 选项在路由规则上定义的响应头仍然会应用到 CDN 级别的重写。只有请求级别的 `ProxyOptions.headers`(发送到上游的)才需要运行时代理。 :: ## 定时任务(Cron 作业) :read-more{title="Vercel 定时任务" to="https://vercel.com/docs/cron-jobs"} Nitro 会在构建时自动将你的 [`scheduledTasks`](/docs/tasks#scheduled-tasks) 配置转换为 [Vercel Cron Jobs](https://vercel.com/docs/cron-jobs)。在 Nitro 配置中定义计划并进行部署。无需手动配置 `vercel.json` 中的 Cron。 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ experimental: { tasks: true }, scheduledTasks: { // 每小时运行 `cms:update` '0 * * * *': ['cms:update'], // 每天午夜运行 `db:cleanup` '0 0 * * *': ['db:cleanup'] } }) ``` ### 安全化 Cron 作业端点 :read-more{title="保护 Cron 作业" to="https://vercel.com/docs/cron-jobs/manage-cron-jobs#securing-cron-jobs"} 要防止未经授权访问 Cron 处理程序,请在 Vercel 项目设置中设置 `CRON_SECRET` 环境变量。设置 `CRON_SECRET` 后,Nitro 会验证每次 Cron 调用中的 `Authorization` 请求头,并对不匹配的请求返回 `401`。 ::warning 默认情况下不会设置 `CRON_SECRET`。与 [Vercel 自身的行为](https://vercel.com/docs/cron-jobs/manage-cron-jobs#securing-cron-jobs)一致,当缺少此变量时,Cron 端点(`/_vercel/cron`,可通过 [`vercel.cronHandlerRoute`](#other-preset-options) 配置)不会执行身份验证。任何知道该路由的人都可以通过 `x-vercel-cron-schedule` 请求头选择一个计划,并按需运行为其注册的任务。在 Vercel 上使用 `scheduledTasks` 时,请始终设置 `CRON_SECRET`。 :: ## 队列 :read-more{title="Vercel 队列" to="https://vercel.com/docs/queues"} Nitro 集成了 [Vercel Queues](https://vercel.com/docs/queues),可用于异步处理消息。你可以在 Nitro 配置中定义队列主题,并通过 `vercel:queue` 运行时钩子处理传入消息。 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ // 独立模式下,Nitro 会扫描 `serverDir` 中的路由和插件 serverDir: "./server", vercel: { queues: { triggers: [ // 仅需 `topic` { topic: "notifications" }, { topic: "orders", retryAfterSeconds: 60, initialDelaySeconds: 5 }, ], }, }, }); ``` ### 处理消息 在 [Nitro 插件](/docs/plugins)中使用 `vercel:queue` 钩子来处理传入的队列消息: ```ts [server/plugins/queues.ts] import { definePlugin } from "nitro"; export default definePlugin((nitroApp) => { nitroApp.hooks.hook("vercel:queue", ({ message, metadata, send }) => { console.log(`[${metadata.topicName}] Message ${metadata.messageId}:`, message); }); }); ``` ### 从队列消息中运行任务 你可以使用队列消息来触发 [Nitro 任务](/docs/tasks): ```ts [server/plugins/queues.ts] import { definePlugin } from "nitro"; import { runTask } from "nitro/task"; export default definePlugin((nitroApp) => { nitroApp.hooks.hook("vercel:queue", async ({ message, metadata }) => { if (metadata.topicName === "orders") { await runTask("orders:fulfill", { payload: message }); } }); }); ``` ### 发送消息 直接使用 `@vercel/queue` 包向指定主题发送消息: ```ts [server/routes/api/orders.post.ts] import { defineHandler } from "nitro"; import { send } from "@vercel/queue"; export default defineHandler(async (event) => { const order = await event.req.json(); const { messageId } = await send("orders", order); return { messageId }; }); ``` ### 本地开发 队列可在 `nitro dev` 中运行:`send()` 会将消息直接发送到你的 `vercel:queue` 钩子,因此你无需部署即可进行迭代。先使用 `vercel link` 和 `vercel env pull` 拉取你的 Vercel 环境,以便 SDK 进行身份验证。 如果你的钩子抛出异常,消息会在本地重试。设置了 `retryAfterSeconds` 时,重试会遵循每个触发器中的该配置。 ## 自定义构建输出配置 你可以在 `nitro.config` 中使用 `vercel` 键下的 `vercel.config` 提供额外的[构建输出配置](https://vercel.com/docs/build-output-api/v3)。它会与内置的自动生成配置合并。 ## 公共资源缓存 不会回退的公共资源目录(任何非根 `baseURL` 的默认行为)会由 Vercel CDN 直接从文件系统提供,并使用根据目录 `maxAge` 生成的 `Cache-Control` 请求头。没有显式设置 `maxAge` 的目录会缓存一年,这是 Vercel 特有的默认值,为保持向后兼容而保留。 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ publicAssets: [ { baseURL: "build", dir: "public/build", maxAge: 3600, }, ], }) ``` 在此类基础路径下,如果请求不匹配任何文件,将返回带有 `Cache-Control: no-store` 的 `404`,而不会到达服务器函数。这与 Nitro 运行时的行为一致:在非回退基础路径下找不到资源时同样会返回 `404`,并可避免动态内容在资源 URL 下被提供,继而按照 `max-age` 的生命周期进行缓存。 设置 `maxAge: 0` 可选择退出一年的默认缓存时间。此时不会为该基础路径生成 `Cache-Control` 请求头: ```ts [nitro.config.ts] export default defineConfig({ publicAssets: [ { baseURL: "build", dir: "public/build", maxAge: 0, }, ], }) ``` 针对该基础路径的 `cache-control` 路由规则优先级高于上述两项,因此可以使用自定义请求头提供目录内容: ```ts [nitro.config.ts] export default defineConfig({ routeRules: { "/build/**": { headers: { "cache-control": "no-cache" } }, }, }) ``` ::note 回退目录不受影响,包括顶层的 `public/` 目录,该目录默认使用 `fallthrough: true`。其中缺失的文件仍会到达你的应用处理程序。 :: :read-more{to="/docs/assets"} ## 不可变静态文件 ::important 此功能目前仅在 Nitro v3 的 [nightly 发布频道](/docs/nightly)中可用。 :: 客户端构建资源(例如 JS 和 CSS 分块)可以作为**不可变静态文件**输出。这些文件会从保留的 `/_vercel/immutable/` 路径提供,因此可以跨部署共享,即使更新后的部署不再引用它们也能继续解析,从而改善跨部署缓存。 此功能需要选择启用。可以通过 `vercel.immutableStaticFiles` 选项启用,也可以设置 `NITRO_VERCEL_IMMUTABLE_STATIC_FILES_ENABLED` 环境变量。 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ vercel: { immutableStaticFiles: true } }) ``` 启用后,Nitro 会在 `/_vercel/immutable/` 下输出基于内容寻址的构建资源。 ::note 系统环境变量 [`VERCEL_HASH_SALT`](https://vercel.com/docs/environment-variables/system-environment-variables) 会计入生成的资源路径,从而提供轮换这些路径的方式。 :: ::warning 不可变静态文件必须从保留的 `/_vercel/immutable/` 路径提供,因此在使用非根 `baseURL` 时不受支持。在这种情况下,Nitro 会跳过不可变输出,并在构建期间发出警告。 :: ::note 此功能与 **Nitro + Vite** 集成开箱即用:Nitro 的 Vercel 预设会设置 `buildAssetsDir` 配置,Vite 插件会自动将其应用为客户端和服务器渲染构建的 `assetsDir`,因此所有生成的资源 URL 都会指向 `/_vercel/immutable/` 下。 客户端构建设置和框架必须自行应用 `nitro.options.buildAssetsDir`。将其用作客户端打包器的资源输出目录(基础路径),以便生成的资源 URL 输出到该路径下。否则,资源仍会输出到默认位置,不可变清单将无法匹配。 :: ## 其他预设选项 Nitro 配置中的 `vercel` 键下提供了其他选项: - `entryFormat`:Vercel Functions 的处理程序格式。`"web"`(默认)或 `"node"`。`node` 格式支持与 Node.js 特定 API 的兼容性(例如 `req.runtime.node`)。 - `regions`:边缘函数的[区域](https://vercel.com/docs/concepts/functions/edge-functions#edge-function-regions)列表。 - `skewProtection`:设置为 `false` 以禁用 Nitro 的 [skew protection](https://vercel.com/docs/skew-protection) 集成(当 Vercel 控制面板中启用 skew protection 时,默认启用)。 - `cronHandlerRoute`:与 `scheduledTasks` 一起使用的 Vercel Cron 处理程序端点的路由路径(默认值:`"/_vercel/cron"`)。 ## 按需增量静态再生成(ISR) 按需重新验证允许你随时清除 ISR 路由的缓存,而无需等待后台重新验证所使用的时间间隔。 ::warning 不要在同一路由上同时使用 `isr` 和 `prerender` 路由规则。预渲染页面会在构建时作为静态文件写入,Vercel 会在到达 ISR 函数之前从文件系统提供这些页面,因此 `isr` 永远不会生效。需要在部署后重新生成时,请单独使用 `isr`。 :: 要按需重新验证页面: 1. 创建一个环境变量来存储重新验证密钥 - 你可以使用命令 `openssl rand -base64 32` 或[生成密钥](https://generate-secret.vercel.app/32)来生成随机值。 2. 更新你的配置: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ vercel: { config: { bypassToken: process.env.VERCEL_BYPASS_TOKEN } } }) ``` 3. 要按需重新验证 Prerender Function 的路径,请向该路径发起 GET 或 HEAD 请求,并在请求中添加 `x-prerender-revalidate: ` 请求头。当使用此请求头访问 Prerender Function 端点时,缓存会被重新验证,对该函数的下一次请求应返回新鲜响应。 ### 通过路由规则进行细粒度 ISR 配置 默认情况下,查询参数会影响缓存键,但不会传递给路由处理程序,除非另有指定。 你可以向 `isr` 路由规则传递选项对象来配置缓存行为: - `expiration`:缓存资源在通过调用无服务器函数重新生成之前的过期时间(以秒为单位)。将值设置为 `false`(或使用 `isr: true` 路由规则)表示永不过期。 - `group`:资源的组编号。具有相同组编号的预渲染资源会同时重新验证。 - `allowQuery`:独立缓存的查询字符串参数名称列表。 - 如果为空数组,则不会将查询值纳入缓存因素。 - 如果为 `undefined`,则每个不同的查询值都会独立缓存。 - 对于通配符 `/**` 路由规则,始终会添加 `url`。 - `passQuery`:为 `true` 时,查询字符串会出现在传递给所调用函数的 `request` 参数中。`allowQuery` 过滤器仍然适用。 - `exposeErrBody`:为 `true` 时,无论状态码为何都会公开响应正文,包括错误状态码(默认值:`false`)。 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ routeRules: { "/products/**": { isr: { allowQuery: ["q"], passQuery: true, exposeErrBody: true }, }, }, }); ``` --- # Zeabur > 将 Nitro 应用部署到 [Zeabur](https://zeabur.com)。 **预设:** `zeabur` :read-more{title="Zeabur" to="https://zeabur.com"} ::note 使用[零配置](/deploy#zero-config-providers)即可与此提供商集成。 :: ## 使用 Git 部署 1. 将你的代码推送到 git 仓库(目前仅支持 GitHub)。 2. 将你的[项目导入](https://zeabur.com/docs/get-started) Zeabur。 3. Zeabur 检测到你正在使用 Nitro,并为你的部署启用正确的设置。 4. 你的应用已部署完成! --- # Zephyr Cloud > 将 Nitro 应用部署到 [Zephyr Cloud](https://zephyr-cloud.io)。 **预设:** `zephyr` :read-more{title="Zephyr Cloud 文档" to="https://docs.zephyr-cloud.io"} 对于大多数 Zephyr 特定主题,例如 BYOC、云集成、环境和 CI/CD 认证,请参阅 [Zephyr Cloud 文档](https://docs.zephyr-cloud.io)。 ::note Zephyr 与大多数 Nitro 部署提供商有所不同。它不是直接面向单一托管供应商,而是作为部署控制平面,运行在 Zephyr 管理的基础设施或你自己的云集成之上。 :: ## BYOC 模式 Zephyr 支持 BYOC(自带云)模式。在 Zephyr 的架构中,控制平面由 Zephyr 管理,而数据平面(工作进程和存储)则运行在你的云账户中。 这使你可以在使用任何受支持的 Zephyr 云集成的服务的同时,保持 Zephyr 的部署工作流程。有关当前支持的提供商列表,请参阅 [Zephyr BYOC 文档](https://docs.zephyr-cloud.io/features/byoc)。 ## 使用 Nitro CLI 部署 使用 Nitro 的部署命令,一键完成构建并上传应用到 Zephyr: ```bash npx nitro deploy --preset zephyr ``` Nitro 将使用 `zephyr-agent` 上传生成的输出。如果缺少 `zephyr-agent`,Nitro 将提示在本地安装,并会在 CI 环境中自动安装。 ## 构建时部署 对于 Zephyr,推荐的工作流程是在 `nitro build` 期间进行部署,并将构建视为主要部署步骤。 如果你的 CI 流水线已经运行 `nitro build`,请在构建步骤期间启用部署: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ preset: "zephyr", zephyr: { deployOnBuild: true, }, }); ``` 这样你正常的构建命令就足够了: :pm-run{script="build"} 构建完成后,Nitro 会将生成的输出上传到 Zephyr,部署到边缘节点,并打印部署 URL: ```txt ◐ Building [Nitro] (preset: zephyr, compatibility: YYYY-MM-DD) ... ZEPHYR Uploaded local snapshot in 110ms ZEPHYR Deployed to Zephyr's edge in 700ms. ZEPHYR ZEPHYR https://my-app.zephyrcloud.app ``` ## CI 认证 Zephyr 要求非交互式部署使用 API 令牌。下面的示例使用更简单的个人令牌设置,将 `ZE_SECRET_TOKEN` 与 `zephyr.deployOnBuild` 配合使用。 ```yaml [.github/workflows/deploy.yml] name: Deploy with Zephyr on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest env: ZE_SECRET_TOKEN: ${{ secrets.ZEPHYR_AUTH_TOKEN }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: npm run build ``` 对于更高级的 CI/CD 设置,Zephyr 还记录了使用 `ZE_SERVER_TOKEN` 的组织级服务器令牌认证。请参阅 [Zephyr CI/CD 服务器令牌文档](https://docs.zephyr-cloud.io/features/ci-cd-server-token)。 ## 选项 ### `zephyr.deployOnBuild` 在使用 `zephyr` 预设时,在 `nitro build` 期间部署到 Zephyr。 - 默认值:`false` --- # Zerops > 将 Nitro 应用部署到 [Zerops](https://zerops.io)。 **预设:** `zerops` :read-more{title="Zerops.io" to="https://zerops.io"} Zerops 支持通过位于项目根目录中的简单配置文件来部署静态和服务端渲染应用。 ## 项目设置 项目和服务可以通过 [Zerops 控制台](https://app.zerops.io) 点击 **Import a project** 来添加,或者使用 `zerops-project-import.yaml` 导入。 ::code-group ```yml [zerops-project-import.yaml (node.js)] project: name: nitro-app services: - hostname: app type: nodejs@22 enableSubdomainAccess: true ``` ```yml [zerops-project-import.yaml (static)] project: name: nitro-app services: - hostname: app type: static enableSubdomainAccess: true ``` :: 然后在项目根目录下创建一个 `zerops.yaml` 配置: ::code-group ```yml [zerops.yaml (node.js)] zerops: - setup: app build: base: nodejs@22 envVariables: NITRO_PRESET: zerops buildCommands: - pnpm i - pnpm run build deployFiles: - .output - package.json - node_modules run: base: nodejs@22 ports: - port: 3000 httpSupport: true start: node .output/server/index.mjs ``` ```yml [zerops.yaml (static)] zerops: - setup: app build: base: nodejs@22 envVariables: NITRO_PRESET: zerops_static buildCommands: - pnpm i - pnpm build deployFiles: - .zerops/output/static/~ run: base: static ``` :: 现在,你可以[使用 Zerops CLI 触发构建和部署流水线](#build-and-deploy),或者在服务详情中将应用服务连接到你的 [GitHub](https://docs.zerops.io/references/github-integration/) / [GitLab](https://docs.zerops.io/references/gitlab-integration) 仓库。 ## 构建和部署 在 Zerops 应用中打开[设置 > 访问令牌管理](https://app.zerops.io/settings/token-management)并生成一个新的访问令牌。 使用以下命令和访问令牌登录: :pm-x{command="@zerops/zcli login "} 导航到你应用的根目录(即 `zerops.yaml` 所在位置),并运行以下命令以触发部署: :pm-x{command="@zerops/zcli push"} 通过将服务连接到你的 [GitHub](https://docs.zerops.io/references/github-integration/) / [GitLab](https://docs.zerops.io/references/gitlab-integration) 仓库,你的代码可以在每次提交或新标签时自动部署。此连接可以在服务详情中进行设置。 --- # 配置 :read-more{to="/docs/configuration"} ::note 本页中的所有示例都假设在 `nitro.config.ts` 中导入了以下内容: ```ts import { defineConfig } from "nitro"; ``` :: ## 常规 预设、日志记录和运行时配置的核心选项。 ### `preset` 用于**生产环境**构建的部署预设。也可以通过 `NITRO_PRESET` 环境变量设置。 开发模式的预设始终为 `nitro_dev`。生产环境的默认预设为 `node_server`,它会构建一个独立的 Node.js 服务器。 如果未设置 `preset` 选项,且 Nitro 正在已知环境中运行,则会自动检测预设。 ```ts export default defineConfig({ preset: "cloudflare_pages", // 部署到 Cloudflare Pages }); ``` ### `defaultPreset` 自定义在未设置 `preset` 且未自动检测到任何已知托管提供商时使用的**回退**预设。 默认情况下,Nitro 会回退到基于运行时的预设(在这些运行时上运行时,使用 `node`,以及 `deno` 或 `bun`)。显式指定的 `preset`、`NITRO_PRESET` 环境变量,以及自动检测到的提供商(Vercel、Netlify、Cloudflare Pages 等)都优先于 `defaultPreset`。 它接受一个预设名称或内联预设定义。 ```ts export default defineConfig({ defaultPreset: "node_cluster", // 默认使用 node_cluster 而不是 node_server }); ``` ### `debug` - 默认值:`false`{lang=ts}(当设置了 `DEBUG` 环境变量时为 `true`{lang=ts}) 启用调试模式以输出详细日志和额外的开发信息。 ```ts export default defineConfig({ debug: true, }); ``` ### `logLevel` - 默认值:`3`{lang=ts}(检测到测试环境时为 `1`{lang=ts}) 日志详细程度级别。更多信息请参见 [consola](https://github.com/unjs/consola?tab=readme-ov-file#log-level)。 ```ts export default defineConfig({ logLevel: 4, // 详细日志 }); ``` ### `runtimeConfig` - 默认值:`{ app: {}, nitro: {} }`{lang=ts} 服务器运行时配置。Nitro 会将 `app.baseURL`(来自 [`baseURL`](#baseurl) 选项)注入 `app` 命名空间。 **注意:** `app` 和 `nitro` 命名空间保留供内部使用。 ```ts export default defineConfig({ runtimeConfig: { apiSecret: "default-secret", // 使用 NITRO_API_SECRET 覆盖 }, }); ``` ### `compatibilityDate` - 未提供时的默认行为:`"latest"`{lang=ts} 部署提供商会引入 Nitro 预设可以利用的新功能,但其中一些功能需要显式启用。请将此项设置为你已测试过的最新日期,格式为 `YYYY-MM-DD`,以利用最新的预设功能。 ```ts export default defineConfig({ compatibilityDate: "2025-01-01", }); ``` ### `static` - 默认值:`false`{lang=ts} 启用静态站点生成模式。 ```ts export default defineConfig({ static: true, // 预渲染所有路由 }); ``` ## 功能特性 启用和配置 Nitro 的内置功能,例如存储、任务和资源。 ### `features` - 默认值:`{}`{lang=ts} 启用内置功能。 #### `runtimeHooks` - 默认值:自动检测(如果至少有一个 nitro 插件则启用) 启用请求和响应的运行时钩子。 #### `websocket` - 默认值:`false`{lang=ts} 启用 WebSocket 支持。 ```ts export default defineConfig({ features: { runtimeHooks: true, websocket: true, // 启用 WebSocket 支持 }, }); ``` :read-more{to="/docs/websocket"} ### `experimental` - 默认值:`{}`{lang=ts} 启用实验性功能。 #### `openAPI` - 默认值:`false`{lang=ts} 启用 `/_scalar`、`/_swagger` 和 `/_openapi.json` 端点。 ::note 建议优先使用顶级的 [`openAPI`](#openAPI) 选项进行配置。 :: #### `typescriptBundlerResolution` 启用 TypeScript 打包器模块解析。参见 [TypeScript#51669](https://github.com/microsoft/TypeScript/pull/51669)。 ::note 此标志目前不起作用:`nitro/tsconfig` 和生成的 tsconfig 始终使用打包器模块解析。 :: #### `asyncContext` 为 `useRequest()` 启用原生异步上下文支持。 #### `sourcemapMinify` 设置为 `false` 可禁用实验性的 source map 压缩(默认在启用 `sourcemap` 时开启)。 #### `envExpansion` 允许在运行时配置中展开环境变量。参见[配置指南](/docs/configuration#env-expansion)和 [#2043](https://github.com/nitrojs/nitro/pull/2043)。 #### `database` 启用实验性数据库支持。参见[数据库](/docs/database)。 #### `tasks` 启用实验性任务支持。参见[任务](/docs/tasks)。 #### `tracingLogger` 使用内置的无依赖遥测接收器,将 Nitro 跟踪通道的 span 记录到控制台;该接收器会记录每个已完成的 span(h3、srvx、unstorage,……)。需要启用 [`tracingChannel`](#tracingchannel)。 ```ts export default defineConfig({ experimental: { typescriptBundlerResolution: true, asyncContext: true, envExpansion: true, database: true, tasks: true, }, }); ``` ### `openAPI` 顶级 OpenAPI 配置。 你可以传递一个对象来修改你的 OpenAPI 规范: ```js openAPI: { meta: { title: '我的超棒项目', description: '这可能会成为下一个重大项目。', version: '1.0' } } ``` 默认情况下,这些路由在生产环境中是禁用的。要启用它们,请使用 `production` 键。 `"runtime"` 允许中间件使用,而 `"prerender"` 是最有效的,因为 JSON 响应是恒定的。 ```js openAPI: { // 重要:如有必要,确保保护 OpenAPI 路由! production: "runtime", // 或 "prerender" } ``` 要自定义 Scalar 集成,[传入配置对象](https://github.com/scalar/scalar),例如: ```js openAPI: { ui: { scalar: { theme: 'purple' } } } ``` 要自定义 Swagger UI,请传入任意 [Swagger UI 配置选项](https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/): ```js openAPI: { ui: { swagger: { persistAuthorization: true, deepLinking: true, docExpansion: 'none', filter: true, } } } ``` 要自定义端点: ```js openAPI: { route: "/_docs/openapi.json", ui: { scalar: { route: "/_docs/scalar" }, swagger: { route: "/_docs/swagger" } } } ``` :read-more{to="/docs/openapi"} ### `future` - 默认值:`{}`{lang=ts} 等待在主要版本中发布的新功能,以避免破坏性变更。 #### `nativeSWR` 对 Netlify 和 Vercel 预设使用内置的 SWR 功能(使用缓存层和存储),而不是回退到 ISR 行为。 ```ts export default defineConfig({ future: { nativeSWR: true, }, }); ``` ### `kv` - 默认值:`{}`{lang=ts} KV 存储配置。 ```ts export default defineConfig({ kv: { redis: { driver: "redis", url: "redis://localhost:6379", }, }, }); ``` 使用 `$development` 键覆盖开发期间的挂载配置: ```ts export default defineConfig({ $development: { kv: { redis: { driver: "fs", base: "./data/redis", // 在开发环境中使用文件系统 }, }, }, }); ``` `$development` 仅适用于开发服务器。使用 `$prerender` 可在预渲染时覆盖挂载配置。具有不同 `driver` 的挂载会替换整个挂载;具有相同 `driver` 的挂载则会对其选项进行深度合并。 ::note 以前名为 `storage`,现仍可作为已弃用的别名使用。已弃用的 `devStorage` 选项由 `$development`(以及 `$prerender`)中的 `kv` 替代。 :: :read-more{to="/docs/storage"} ### `database` 数据库连接配置。需要 `experimental.database: true`。 ```ts export default defineConfig({ database: { default: { connector: "sqlite", options: { name: "db" }, }, }, }); ``` :read-more{to="/docs/database"} ### `devDatabase` 开发模式的数据库连接配置覆盖。 ```ts export default defineConfig({ devDatabase: { default: { connector: "sqlite", options: { name: "db-dev" }, // 独立的开发数据库 }, }, }); ``` ### `renderer` - 类型:`false`{lang=ts} | `{ handler?: string, static?: boolean, template?: string }`{lang=ts} 指向主渲染入口(文件应将事件处理器作为默认导出)。 ```ts export default defineConfig({ renderer: { handler: "~/renderer", // 渲染处理器的路径 }, }); ``` :read-more{to="/docs/renderer"} ### `ssrRoutes` 应进行服务器端渲染的路由。 ```ts export default defineConfig({ ssrRoutes: ["/app/**"], }); ``` ### `serveStatic` - 类型:`boolean`{lang=ts} | `'node'`{lang=ts} | `'inline'`{lang=ts} - 默认值:取决于所用的部署预设。 在生产环境中提供 `public/` 资源服务。 **注意:** 强烈建议让你的边缘 CDN(Nginx、Apache、Cloud)直接提供 `.output/public/` 目录服务,以启用压缩和更高级别的缓存。 ```ts export default defineConfig({ serveStatic: "node", // 使用 Node.js 提供静态资源服务 }); ``` ### `noPublicDir` - 默认值:`false`{lang=ts} 如果启用,将禁用 `.output/public` 目录的创建。跳过复制 `public/` 目录,同时禁用预渲染。 ```ts export default defineConfig({ noPublicDir: true, // 跳过 public 目录输出 }); ``` ### `publicAssets` 在开发中提供、在生产中打包的公共资源目录。 如果检测到 `public/` 目录,它将被默认添加,但你也可以自己添加更多! 可以使用 `maxAge` 选项为资源设置 Cache-Control 标头: ```ts export default defineConfig({ publicAssets: [ { baseURL: "images", dir: "public/images", maxAge: 60 * 60 * 24 * 7, // 7 天 }, ], }); ``` 上述配置会在 `public/images/` 文件夹下的资源中生成以下响应头: `cache-control: public, max-age=604800, immutable` `dir` 选项是你的文件在文件系统中的位置;`baseURL` 选项是它们在提供服务/打包时可访问的文件夹路径。 :read-more{to="/docs/assets"} ### `compressPublicAssets` - 类型:`boolean`{lang=ts} | `{ gzip?: boolean, brotli?: boolean, zstd?: boolean }`{lang=ts} - 默认值:`false`{lang=ts} 如果启用,Nitro 将为大于 1024 字节的公共资源和预渲染路由生成预压缩版本(gzip、brotli 和/或 zstd)到公共目录中。使用默认压缩级别。使用此选项,你可以在不使用 CDN 的情况下支持零开销资源压缩。 ```ts export default defineConfig({ compressPublicAssets: { gzip: true, brotli: true, // 启用 gzip 和 brotli 预压缩 }, }); ``` ### `serverAssets` 服务器逻辑中可访问并在生产中打包的资源。 ```ts export default defineConfig({ serverAssets: [ { baseName: "templates", dir: "./templates", // 将 templates/ 作为服务器资源打包 }, ], }); ``` :read-more{to="/docs/assets#server-assets"} ### `modules` - 默认值:`[]`{lang=ts} Nitro 模块数组。模块可以是字符串(路径)、带有 `setup` 函数的模块对象,或函数。 ```ts export default defineConfig({ modules: [ "./modules/my-module.ts", (nitro) => { nitro.hooks.hook("compiled", () => { /* ... */ }); }, ], }); ``` ### `plugins` - 默认值:`[]`{lang=ts} nitro 插件路径数组。它们会在首次初始化时按顺序执行。 注意,Nitro 会自动注册 `plugins/` 目录中的插件。 ```ts export default defineConfig({ plugins: [ "~/plugins/my-plugin.ts", ], }); ``` :read-more{to="/docs/plugins"} ### `tasks` - 默认值:`{}`{lang=ts} 任务定义。每个键都是一个任务名称,可选配置 `handler` 路径和 `description`。 从 `/tasks/` 扫描到的任务只需在此处提供 `description`。`handler` 会按原样导入,不会根据项目根目录解析,因此必须是绝对路径或包名称。 ```ts import { fileURLToPath } from "node:url"; export default defineConfig({ tasks: { "db:migrate": { description: "运行数据库迁移", }, "db:seed": { handler: fileURLToPath(new URL("scripts/seed.ts", import.meta.url)), description: "为数据库填充数据", }, }, }); ``` :read-more{to="/docs/tasks"} ### `scheduledTasks` - 默认值:`{}`{lang=ts} cron 表达式到任务名称的映射。 ```ts export default defineConfig({ scheduledTasks: { "0 * * * *": "cleanup:temp", "*/5 * * * *": ["health:check", "metrics:collect"], }, }); ``` :read-more{to="/docs/tasks"} ### `virtual` - 默认值:`{}`{lang=ts} 从动态虚拟导入名称到其内容或返回内容的(异步)函数的映射。 ```ts export default defineConfig({ virtual: { "#config": `export default { version: "1.0.0" }`, }, }); ``` ### `ignore` - 默认值:`[]`{lang=ts} 扫描目录时要忽略的全局模式数组。 ```ts export default defineConfig({ ignore: [ "routes/_legacy/**", // 跳过旧版路由处理器 ], }); ``` ### `wasm` - 类型:`false`{lang=ts} | `UnwasmPluginOptions`{lang=ts} - 默认值:`{}`{lang=ts} WASM 支持配置。有关选项请参见 [unwasm](https://github.com/unjs/unwasm)。 ```ts export default defineConfig({ wasm: {}, // 启用 WASM 导入支持 }); ``` ## 开发 仅影响开发服务器(`nitro dev`)的选项。 ### `devServer` - 默认值:`{ watch: [] }`{lang=ts} 开发服务器选项。你可以使用 `watch` 来让开发服务器在指定路径中的任何文件发生变化时重新加载。 支持 `port`、`hostname`、`watch` 和 `runner` 选项。 ```ts export default defineConfig({ devServer: { port: 3001, watch: ["./server/plugins"], }, }); ``` ### `watchOptions` 开发模式的监听选项。更多信息请查看 [chokidar](https://github.com/paulmillr/chokidar)。 ```ts export default defineConfig({ watchOptions: { ignored: ["**/node_modules/**", "**/dist/**"], }, }); ``` ### `devProxy` 开发服务器的代理配置。 你可以使用此选项来覆盖开发服务器路由并代理转发请求。 ```ts export default defineConfig({ devProxy: { "/proxy/test": "http://localhost:3001", "/proxy/example": { target: "https://example.com", changeOrigin: true }, }, }); ``` 查看 [httpxy](https://github.com/unjs/httpxy) 了解所有可用的目标选项。 ## 日志 ### `logging` - 默认值:`{ compressedSizes: true, buildSuccess: true }`{lang=ts} 控制构建日志行为。将 `compressedSizes` 设置为 `false` 可跳过报告压缩后的包大小。将 `buildSuccess` 设置为 `false` 可隐藏构建成功消息。 ```ts export default defineConfig({ logging: { compressedSizes: false, // 跳过压缩大小报告 buildSuccess: false, }, }); ``` ## 路由 用于定义路由、处理程序和路由级行为的选项。 ### `baseURL` - 默认值:`"/"`{lang=ts}(如果提供,则使用 `NITRO_APP_BASE_URL` 环境变量) 服务器的主基础 URL。 ```ts export default defineConfig({ baseURL: "/app/", // 在 /app/ 前缀下提供应用 }); ``` ### `apiBaseURL` - 默认值:`"/api"`{lang=ts} 更改默认的 API 基础 URL 前缀。 ```ts export default defineConfig({ apiBaseURL: "/server/api", // /server/api/ 下的 API 路由 }); ``` ### `serverEntry` - 类型:`false`{lang=ts} | `string`{lang=ts} | `{ handler: string, format?: EventHandlerFormat }`{lang=ts} 自定义服务器入口点配置。设置为 `false` 可禁用默认服务器入口。 ```ts export default defineConfig({ serverEntry: "./server.ts", }); ``` :read-more{to="/docs/server-entry"} ### `handlers` 服务器处理程序和路由。 如果服务器目录内存在 `routes/`、`api/` 或 `middleware/` 目录,它们将自动添加到 handlers 数组中。 ```ts export default defineConfig({ handlers: [ { route: "/health", handler: "./handlers/health.ts" }, { route: "/admin/**", handler: "./handlers/admin.ts", method: "get" }, ], }); ``` :read-more{to="/docs/routing"} ### `devHandlers` 以程序化实例而非文件路径形式提供的处理程序。与常规的 [`handlers`](#handlers) 不同,后者由打包器导入和转换,而 `devHandlers` 接受直接传入的处理程序函数。 **注意:** 它们**仅在开发模式下可用**,**不会包含在生产构建中**。 ```ts export default defineConfig({ devHandlers: [ { route: "/__dev", handler: defineHandler(() => "仅开发环境路由") }, ], }); ``` ### `routes` - 默认值:`{}`{lang=ts} 内联路由定义。从路由模式到处理程序路径或处理程序选项的映射。 ```ts export default defineConfig({ routes: { "/hello": "./routes/hello.ts", "/greet": { handler: "./routes/greet.ts", method: "post" }, }, }); ``` ### `errorHandler` - 类型:`string`{lang=ts} | `string[]`{lang=ts} 自定义运行时错误处理程序的路径。替换 nitro 内置的错误页面。 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ errorHandler: "~/error", }); ``` ```ts [error.ts] import { defineErrorHandler } from "nitro"; export default defineErrorHandler((error, event) => { return new Response('[自定义错误处理程序] ' + error.stack, { headers: { 'Content-Type': 'text/plain' } }); }); ``` ### `routeRules` **🧪 实验性功能!** 路由选项。它是从路由模式(遵循 [rou3](https://github.com/h3js/rou3))到路由选项的映射。 当设置了 `cache` 选项时,匹配该模式的处理程序将自动用 `defineCachedHandler` 包装。 查看[缓存 API](/docs/cache)了解所有可用的缓存选项。 ::note `swr: true|number` 是 `cache: { swr: true, maxAge: number }` 的快捷方式 :: ```js routeRules: { '/blog/**': { swr: true }, '/feed/**': { swr: 600 }, '/docs/**': { static: true }, '/dashboard/**': { cache: { /* cache options*/ } }, '/assets/**': { headers: { 'cache-control': 's-maxage=0' } }, '/api/v1/**': { cors: true, headers: { 'access-control-allow-methods': 'GET' } }, '/old-page': { redirect: '/new-page' }, // 使用状态码 307(临时重定向) '/old-page2': { redirect: { to:'/new-page2', statusCode: 301 } }, '/old-page/**': { redirect: '/new-page/**' }, '/proxy/example': { proxy: 'https://example.com' }, '/proxy/**': { proxy: '/api/**' }, } ``` :read-more{to="/docs/routing#route-rules"} ### `prerender` 预渲染选项。指定的任何路由都会在构建期间被获取,并作为静态资源复制到 `.output/public` 目录。 默认值: ```ts { autoSubfolderIndex: true, concurrency: 1, interval: 0, failOnError: false, crawlLinks: false, ignore: [], routes: [], retry: 3, retryDelay: 500 } ``` 以 `ignore` 中列出的前缀开头,或匹配正则表达式或函数的任何路由(字符串)都会被忽略。 如果将 `crawlLinks` 选项设置为 `true`,nitro 默认从 `/` 开始(或从 `routes` 数组中的所有路由开始),从 HTML 页面中提取 `` 标签,并对这些页面也进行预渲染。 你可以将 `failOnError` 选项设置为 `true`,以便在 Nitro 无法预渲染某个路由时停止 CI。 `interval` 和 `concurrency` 选项允许你控制预渲染速度,这有助于在调用外部 API 时避免触发速率限制。 `autoSubfolderIndex` 选项控制如何在 `.output/public` 目录中生成文件: ```bash # autoSubfolderIndex: true(默认) /about -> .output/public/about/index.html # autoSubfolderIndex: false /about -> .output/public/about.html ``` 当你的托管提供商无法提供关于尾部斜杠的选项时,此选项很有用。 预渲染器将尝试以 500ms 的延迟渲染页面 3 次。使用 `retry` 和 `retryDelay` 来更改此行为。 将 `ignoreUnprefixedPublicAssets` 设置为 `true`,可跳过没有基础 URL 前缀的公共资源的预渲染。 ## 目录 自定义 Nitro 查找源文件和写入输出文件的位置。 ### `workspaceDir` 项目工作空间根目录。 未设置 `workspaceDir` 选项时,会自动检测工作空间目录(例如 pnpm 工作空间)。 ```ts export default defineConfig({ workspaceDir: "../", // monorepo 根目录 }); ``` ### `rootDir` 项目主目录。 ```ts export default defineConfig({ rootDir: "./src/server", }); ``` ### `serverDir` - 类型:`boolean`{lang=ts} | `"./"`{lang=ts} | `"./server"`{lang=ts} | `string`{lang=ts} - 默认值:`false`{lang=ts} 用于扫描 `api/`、`routes/`、`plugins/`、`utils/`、`middleware/`、`modules/` 和 `tasks/` 文件夹的服务器目录。 设置为 `false` 时,禁用自动目录扫描。设置为 `"./"` 以使用根目录,或设置为 `"./server"` 以使用 `server/` 子目录。 ```ts export default defineConfig({ serverDir: "./server", // 扫描 `server/` 子目录 }); ``` ### `scanDirs` - 默认值:`[]`{lang=ts}(为空时会扫描服务器源目录) 要扫描并自动注册文件的目录列表,例如 API 路由。 ```ts export default defineConfig({ scanDirs: ["./modules/auth/api", "./modules/billing/api"], }); ``` ### `apiDir` - 默认值:`"api"`{lang=ts} 定义用于扫描 API 路由处理程序的不同目录。 ```ts export default defineConfig({ apiDir: "endpoints", // 扫描 `endpoints/` 而不是 `api/` }); ``` ### `routesDir` - 默认值:`"routes"`{lang=ts} 定义用于扫描路由处理程序的不同目录。 ```ts export default defineConfig({ routesDir: "pages", // 扫描 `pages/` 而不是 `routes/` }); ``` ### `buildDir` - 默认值:`"node_modules/.nitro"`{lang=ts} Nitro 用于生成构建相关文件的临时工作目录。 ```ts export default defineConfig({ buildDir: ".nitro", // 在项目根目录中使用 `.nitro/` }); ``` ### `output` - 默认值:`{ dir: '.output', serverDir: '.output/server', publicDir: '.output/public' }`{lang=ts} 生产包的输出目录。 ```ts export default defineConfig({ output: { dir: "dist", serverDir: "dist/server", publicDir: "dist/public", }, }); ``` ## 构建 打包工具选择和构建输出调整。 ### `builder` - 类型:`"rollup"`{lang=ts} | `"rolldown"`{lang=ts} | `"vite"`{lang=ts} - 默认值:`undefined`{lang=ts}(自动检测) 指定用于构建的打包工具。 ```ts export default defineConfig({ builder: "vite", }); ``` ### `rollupConfig` 额外的 rollup 配置。 ```ts export default defineConfig({ rollupConfig: { output: { manualChunks: { vendor: ["lodash-es"] } }, }, }); ``` ### `rolldownConfig` 额外的 rolldown 配置。 ```ts export default defineConfig({ rolldownConfig: { output: { banner: "/* 使用 nitro 构建 */" }, }, }); ``` ### `vite` `vite` 打包工具和 `nitro/vite` 插件的选项。 - `path`:要使用的 `vite` 包,可以是其目录或入口文件的路径或 `file://` URL。默认情况下,会从项目根目录解析 `vite`。以编程方式运行 Vite 的框架应传入自己的 `vite`(例如 `import.meta.resolve("vite")`),以便开发模块运行器与正在运行的实例保持一致。 ```ts export default defineConfig({ vite: { path: import.meta.resolve("vite"), }, }); ``` ### `entry` 打包工具的入口点。 ```ts export default defineConfig({ entry: "./server/entry.ts", // 自定义入口文件 }); ``` ### `unenv` 用于环境兼容性的 [unenv](https://github.com/unjs/unenv/) 预设。 ```ts export default defineConfig({ unenv: { alias: { "my-module": "my-module/web" }, }, }); ``` ### `alias` 模块解析的路径别名。 ```ts export default defineConfig({ alias: { "~utils": "./src/utils", "#shared": "./shared", }, }); ``` ### `minify` - 默认值:`false`{lang=ts} 压缩打包文件。 某些预设(尤其是 worker 运行时)默认启用压缩。在调试生产环境、worker 或边缘构建时,将 `minify: false` 设置为 `false`,以便保持堆栈跟踪易于阅读。 ```ts export default defineConfig({ minify: true, // 压缩生产包 }); ``` ```ts export default defineConfig({ minify: false, // 更方便地调试 worker/边缘程序包 }); ``` ### `inlineDynamicImports` - 默认值:`false`{lang=ts} 将所有代码打包到单个文件中,而不是为每个路由创建单独的块。 当为 `false` 时,每个路由处理程序都成为一个按需加载的独立块。当为 `true` 时,所有内容都打包在一起。某些预设默认启用此选项。 ```ts export default defineConfig({ inlineDynamicImports: true, // 单个输出文件 }); ``` ### `sourcemap` - 默认值:`false`{lang=ts} 启用 source map 生成。查看[选项](https://rollupjs.org/configuration-options/#output-sourcemap)。 ```ts export default defineConfig({ sourcemap: true, // 生成 `.map` 文件 }); ``` ### `node` - 默认值:`true`{lang=ts} 指定构建是否用于 Node.js。如果设置为 `false`,nitro 会尝试使用 [unenv](https://github.com/unjs/unenv) 模拟 Node.js 依赖并调整其行为。 ```ts export default defineConfig({ node: false, // 面向非 Node.js 运行时 }); ``` ### `replace` 构建时的字符串替换。 ```ts export default defineConfig({ replace: { "process.env.APP_VERSION": JSON.stringify("1.0.0"), }, }); ``` ### `commonJS` 指定 rollup CommonJS 插件的额外配置。 ```ts export default defineConfig({ commonJS: { requireReturnsDefault: "auto", }, }); ``` ### `exportConditions` 模块解析的自定义导出条件。 ```ts export default defineConfig({ exportConditions: ["worker", "production"], }); ``` ### `noExternals` - 默认值:`false`{lang=ts} 禁止将包外部化。外部化的包会在运行时导入,而不是打包进去,因此它们无法使用你的 `alias` 配置。 在生产构建中,Nitro 已经会打包几乎所有内容:只有原生包或其他无法打包的包,以及 [`traceDeps`](#tracedeps) 中列出的包会保留为外部依赖。在开发环境中,为了提高速度,`node_modules` 中的依赖会保留为外部依赖;此选项在这种情况下最为有用。 将其设为 `true`,可打包所有内容,包括通常保留为外部依赖的包: ```ts export default defineConfig({ noExternals: true, // 打包所有依赖项 }); ``` #### 内联指定的包 传入数组,只内联部分模块。`RegExp` 条目按原样使用;字符串条目会经过转义,并按路径子字符串匹配。 每个模式都会与书写形式的导入说明符(例如 `@scope/my-lib`)以及解析后的绝对文件路径(例如 `/…/node_modules/@scope/my-lib/dist/index.mjs`)进行匹配。任一匹配成功都会使模块保持打包状态,因此通常只需提供包名称即可: ```ts export default defineConfig({ alias: { "@shared": "./shared", }, noExternals: ["@scope/my-lib"], }); ``` 如果需要更精确的匹配,例如在 monorepo 中内联一个包,同时避免匹配名称相似的其他包,请使用路径模式: ```ts export default defineConfig({ noExternals: [/packages[\\/]my-lib/], }); ``` 在此类模式中使用 `[\\/]` 而不是 `/`,以便也能匹配 Windows 路径。 ::note 在 Nitro 2 中,此选项名为 `externals.inline`。 :: ### `traceDeps` - 默认值:`[]`{lang=ts} 要跟踪并包含在构建输出中的额外依赖项。 支持特殊前缀: - `!pkg`:从追踪中排除内置包。 - `pkg*`:完整追踪,复制包的所有文件,而不仅是追踪到的文件。 ```ts export default defineConfig({ traceDeps: [ "sharp", "better-sqlite3", "my-pkg*", // 完整追踪(复制所有包文件) "!unwanted-pkg", // 从追踪中排除 ], }); ``` ### `traceOpts` 传递给 [nf3](https://github.com/unjs/nf3) 的依赖追踪高级选项。 ```ts export default defineConfig({ traceOpts: { // 传递给 @vercel/nft 的文件追踪选项 nft: { /* ... */ }, // 追踪时的模块路径别名 traceAlias: { "old-pkg": "new-pkg" }, // 复制时保留或设置文件权限(true 或类似 0o755 的八进制值) chmod: true, // 复制前转换已追踪文件 transform: [ { filter: (id) => id.endsWith(".js"), handler: (code) => code }, ], // 接入追踪生命周期 hooks: { tracedPackages(pkgs) { console.log("已追踪的包:", Object.keys(pkgs)); }, }, }, }); ``` ### `oxc` rolldown 构建的 OXC 选项。包含 `minify` 和 `transform` 子选项。 ```ts export default defineConfig({ oxc: { minify: { compress: true, mangle: true }, }, }); ``` ## 高级配置 底层选项。大多数项目无需更改这些选项。 ### `dev` - 开发环境中的默认值:`true`{lang=ts};生产环境中的默认值:`false`{lang=ts} ::warning 这是高级配置。如果配置错误,可能会出现问题。 :: ```ts export default defineConfig({ dev: true, // 强制使用开发模式行为 }); ``` ### `typescript` - 默认值:`{}`{lang=ts} TypeScript 选项。`tsConfig` 是打包工具用于 JSX 选项和路径别名的配置,默认为项目的 `tsconfig.json`。 ```ts export default defineConfig({ typescript: { tsConfig: { compilerOptions: { jsx: "react-jsx", }, }, }, }); ``` ### `hooks` ::warning 这是高级配置。如果配置错误,可能会出现问题。 :: Nitro 钩子。更多信息请参见 [hookable](https://github.com/unjs/hookable)。 ```ts export default defineConfig({ hooks: { compiled(nitro) { console.log("构建编译成功!"); // 构建编译成功! }, }, }); ``` :read-more{to="/docs/lifecycle"} ### `commands` ::warning 这是高级配置。如果配置错误,可能会出现问题。 :: 预览和部署命令提示通常由部署预设填充。 `deploy` 可以是 shell 命令(`./` 路径会相对于输出目录解析),也可以是接收 Nitro 实例的函数,供以编程方式部署的预设使用。两者都可以通过 [`nitro deploy`](/docs/cli#nitro-deploy) 和 [`deploy`](/docs/utils#nitrobuilder) builder API 运行。 ```ts export default defineConfig({ commands: { preview: "node ./server/index.mjs", deploy: "npx wrangler deploy ./server/index.mjs", }, }); ``` ### `devErrorHandler` ::warning 这是高级配置。如果配置错误,可能会出现问题。 :: 用于处理开发错误的自定义错误处理函数。 ```ts export default defineConfig({ devErrorHandler: (error, event) => { return new Response(`开发错误:${error.message}`, { status: 500 }); }, }); ``` ### `tracingChannel` **🧪 实验性功能!** - 类型:`boolean`{lang=ts} | `{ srvx?: boolean, h3?: boolean, unstorage?: boolean }`{lang=ts} - 默认值:`false`{lang=ts} `tracingChannel` 选项为请求处理启用 [`TracingChannel`](https://nodejs.org/api/diagnostics_channel.html#class-tracingchannel) 插桩。它已针对 Node.js 配置,也适用于 Bun、Deno 和开发预设,以及任何公开 `node:diagnostics_channel` 的运行时。已发布的 `srvx.request` 和 `h3.request` 通道在这些运行时中仍然可用,可供 APM 工具和自定义订阅程序使用。 传入 `true` 可同时启用两个通道;也可以传入对象分别控制每个通道是否启用。 ```ts export default defineConfig({ tracingChannel: true, }); ``` ### `framework` - 默认值:`{ name: "nitro", version: "" }`{lang=ts} 框架信息。由预设和构建信息使用。通常由更高级的框架设置(例如 Nuxt)。 ```ts export default defineConfig({ framework: { name: "my-framework", version: "2.0.0" }, }); ``` ### `manifest` 构建清单选项。使用 `deploymentId` 可在构建清单中包含自定义部署标识符(某些预设会自动从平台环境变量中填充此项)。 ```ts export default defineConfig({ manifest: { deploymentId: process.env.DEPLOY_ID }, }); ``` ## 预设选项 使用匹配的[部署预设](/deploy)时应用的提供商专属选项。 ### `awsAmplify` AWS Amplify 预设的选项。主要字段包括 `catchAllStaticFallback`、`imageOptimization`(`path`、`cacheControl`)、`imageSettings` 和 `runtime`(`nodejs20.x` | `nodejs22.x` | `nodejs24.x`)。参见[预设文档](/deploy/providers/aws-amplify)。 ```ts export default defineConfig({ awsAmplify: { runtime: "nodejs22.x", imageOptimization: { path: "/_image", cacheControl: "public, max-age=3600" }, }, }); ``` ### `awsLambda` AWS Lambda 预设的选项。将 `streaming` 设置为 `true` 可启用响应流式传输。参见[预设文档](/deploy/providers/aws)。 ```ts export default defineConfig({ awsLambda: { streaming: true, }, }); ``` ### `azure` Azure Static Web Apps 预设的选项。使用 `config` 自定义生成的 `staticwebapp.config.json`(例如 `platform.apiRuntime`、`navigationFallback`)。参见[预设文档](/deploy/providers/azure)。 ```ts export default defineConfig({ azure: { config: { platform: { apiRuntime: "node:20" } }, }, }); ``` ### `firebase` Firebase Functions 预设的选项。参见[预设文档](/deploy/providers/firebase#options)。 ```ts export default defineConfig({ firebase: { gen: 2, // 使用 Cloud Functions 第二代 region: "us-central1", }, }); ``` ### `iis` IIS 预设的选项。使用 `mergeConfig` 将生成的 `web.config` 与现有配置合并,或使用 `overrideConfig` 完全替换该配置。参见[预设文档](/deploy/providers/iis)。 ```ts export default defineConfig({ iis: { mergeConfig: true, }, }); ``` ### `netlify` Netlify 预设的选项。使用 `config` 提供 Netlify [部署配置](https://docs.netlify.com/build/frameworks/frameworks-api/)(`headers`、`redirects`、`functions`、`edge_functions`、`images`,……)。参见[预设文档](/deploy/providers/netlify)。 ```ts export default defineConfig({ netlify: { config: { redirects: [{ from: "/old", to: "/new", status: 301 }], }, }, }); ``` ### `vercel` Vercel 预设的选项。参见[预设文档](/deploy/providers/vercel)。 ```ts export default defineConfig({ vercel: { config: { runtime: "nodejs20.x" }, }, }); ``` ### `cloudflare` Cloudflare 预设的选项。参见[预设文档](/deploy/providers/cloudflare)。 ```ts export default defineConfig({ cloudflare: { wrangler: { compatibility_date: "2025-01-01" }, }, }); ``` ### `zephyr` Zephyr 预设的选项。参见[预设文档](/deploy/providers/zephyr#options)。 --- # 示例 > 探索 Nitro 示例,学习如何构建全栈应用程序 --- # API 路由 > 基于文件的 API 路由,支持 HTTP 方法和动态参数。 ::code-tree{defaultValue="api/hello.ts" expandAll} ```html [index.html] API 路由

API 路由:

``` ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: "./", }); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "nitro dev", "build": "nitro build" }, "devDependencies": { "nitro": "latest" } } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` ```ts [api/hello.ts] import { defineHandler } from "nitro"; export default defineHandler(() => "Nitro is amazing!"); ``` ```ts [api/test.get.ts] import { defineHandler } from "nitro"; export default defineHandler(() => "Test get handler"); ``` ```ts [api/test.post.ts] import { defineHandler } from "nitro"; export default defineHandler(async (event) => { const body = await event.req.json(); return { message: "Test post handler", body, }; }); ``` ```ts [api/hello/[name].ts] import { defineHandler } from "nitro"; export default defineHandler((event) => `Hello (param: ${event.context.params!.name})!`); ``` :: Nitro 支持在 `api/` 或 `routes/` 目录中进行基于文件的路由。每个文件都基于其路径成为一个 API 端点。 ## 基础路由 在 `api/` 目录中创建一个文件以定义路由。文件路径即成为 URL 路径: ```ts [api/hello.ts] import { defineHandler } from "nitro"; export default defineHandler(() => "Nitro is amazing!"); ``` 这将创建一个 `GET /api/hello` 端点。 ## 动态路由 使用方括号 `[param]` 表示动态 URL 段。通过 `event.context.params` 访问参数: ```ts [api/hello/[name].ts] import { defineHandler } from "nitro"; export default defineHandler((event) => `Hello (param: ${event.context.params!.name})!`); ``` 这将创建一个 `GET /api/hello/:name` 端点(例如 `/api/hello/world`)。 ## HTTP 方法 为文件添加 HTTP 方法后缀(`.get.ts`、`.post.ts`、`.put.ts`、`.delete.ts` 等): ### GET 处理器 ```ts [api/test.get.ts] import { defineHandler } from "nitro"; export default defineHandler(() => "Test get handler"); ``` ### POST 处理器 ```ts [api/test.post.ts] import { defineHandler } from "nitro"; export default defineHandler(async (event) => { const body = await event.req.json(); return { message: "Test post handler", body, }; }); ``` ## 了解更多 - [路由](/docs/routing) --- # 缓存处理器 > 缓存路由响应,支持可配置的绕过逻辑。 ::code-tree{defaultValue="server.ts" expandAll} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({}); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "nitro dev", "build": "nitro build" }, "devDependencies": { "nitro": "latest" } } ``` ```ts [server.ts] import { html } from "nitro"; import { defineCachedHandler } from "nitro/cache"; export default defineCachedHandler( async () => { await new Promise((resolve) => setTimeout(resolve, 500)); return html` 响应生成于 ${new Date().toISOString()}(耗时 500ms)
(跳过缓存) `; }, { shouldBypassCache: ({ req }) => req.url.includes("skipCache=true") } ); ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` :: 本示例展示如何缓存一个耗时操作(500 毫秒延迟),以及如何通过查询参数条件性地绕过缓存。首次请求时,处理器执行并缓存结果。后续请求会立即返回缓存的响应,直到缓存过期或被绕过。 ## 工作原理 ```ts [server.ts] import { html } from "nitro"; import { defineCachedHandler } from "nitro/cache"; export default defineCachedHandler( async () => { await new Promise((resolve) => setTimeout(resolve, 500)); return html` 响应生成于 ${new Date().toISOString()}(耗时 500ms)
(跳过缓存) `; }, { shouldBypassCache: ({ req }) => req.url.includes("skipCache=true") } ); ``` 该处理器模拟一个耗时操作,延迟 500 毫秒。由于 `defineCachedHandler` 对其进行了包装,首次执行后响应会被缓存。`shouldBypassCache` 选项会检查 URL 中是否包含 `?skipCache=true`,当存在时,缓存被跳过,处理器将重新运行。 ## 了解更多 - [缓存](/docs/cache) - [存储](/docs/storage)。 --- # 自定义错误处理器 > 使用全局错误处理器来自定义错误响应。 ::code-tree{defaultValue="error.ts" expandAll} ```ts [error.ts] import { defineErrorHandler } from "nitro"; export default defineErrorHandler((error, _event) => { return new Response(`Custom Error Handler: ${error.message}`, { status: 500, headers: { "Content-Type": "text/plain" }, }); }); ``` ```ts [nitro.config.ts] import { defineConfig } from "nitro"; // 从 "./error" 导入 errorHandler export default defineConfig({ errorHandler: "./error.ts", // devErrorHandler: errorHandler }); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "nitro dev", "build": "nitro build" }, "devDependencies": { "nitro": "latest" } } ``` ```ts [server.ts] import { defineHandler, HTTPError } from "nitro"; export default defineHandler(() => { throw new HTTPError("Example Error!", { status: 500 }); }); ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` :: 此示例展示了如何拦截所有错误并返回自定义响应格式。当任何路由抛出错误时,Nitro 会调用你的错误处理器,而不是返回默认的错误页面。 ## 错误处理器 在项目根目录创建 `error.ts` 文件来定义全局错误处理器: ```ts [error.ts] import { defineErrorHandler } from "nitro"; export default defineErrorHandler((error, _event) => { return new Response(`Custom Error Handler: ${error.message}`, { status: 500, headers: { "Content-Type": "text/plain" }, }); }); ``` 处理器接收抛出的错误和 H3 事件对象。你可以使用事件来访问请求详情,如请求头、Cookie 或 URL 路径,以便为每个路由自定义响应。 ## 触发错误 主处理器抛出一个错误来演示自定义错误处理器: ```ts [server.ts] import { defineHandler, HTTPError } from "nitro"; export default defineHandler(() => { throw new HTTPError("Example Error!", { status: 500 }); }); ``` 当你访问页面时,你会看到「Custom Error Handler: Example Error!」而不是通用错误页面,因为错误处理器拦截了抛出的错误。 ## 了解更多 - [服务器入口](/docs/server-entry)。 --- # 数据库 > 使用 SQL 模板字面量的内置数据库支持。 ::code-tree{defaultValue="server.ts" expandAll} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ experimental: { database: true, tasks: true, }, database: { default: { connector: "sqlite" }, }, }); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "nitro dev", "build": "nitro build" }, "devDependencies": { "nitro": "latest" } } ``` ```ts [server.ts] import { defineHandler } from "nitro"; import { useDatabase } from "nitro/database"; export default defineHandler(async () => { const db = useDatabase(); // 创建用户表 await db.sql`DROP TABLE IF EXISTS users`; await db.sql`CREATE TABLE IF NOT EXISTS users ("id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT)`; // 添加新用户 const userId = String(Math.round(Math.random() * 10_000)); await db.sql`INSERT INTO users VALUES (${userId}, 'John', 'Doe', '')`; // 查询用户 const { rows } = await db.sql`SELECT * FROM users WHERE id = ${userId}`; return { rows, }; }); ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` ```ts [tasks/db/migrate.ts] import { defineTask } from "nitro/task"; import { useDatabase } from "nitro/database"; export default defineTask({ meta: { description: "运行数据库迁移", }, async run() { const db = useDatabase(); console.log("正在运行数据库迁移..."); // 创建用户表 await db.sql`DROP TABLE IF EXISTS users`; await db.sql`CREATE TABLE IF NOT EXISTS users ("id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT)`; return { result: "数据库迁移完成!", }; }, }); ``` :: Nitro 提供了一个使用 SQL 模板字面量进行安全、参数化查询的内置数据库层。本示例创建了用户表,插入一条记录,并查询返回。 ## 查询数据库 ```ts [server.ts] import { defineHandler } from "nitro"; import { useDatabase } from "nitro/database"; export default defineHandler(async () => { const db = useDatabase(); // 创建用户表 await db.sql`DROP TABLE IF EXISTS users`; await db.sql`CREATE TABLE IF NOT EXISTS users ("id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT)`; // 添加新用户 const userId = String(Math.round(Math.random() * 10_000)); await db.sql`INSERT INTO users VALUES (${userId}, 'John', 'Doe', '')`; // 查询用户 const { rows } = await db.sql`SELECT * FROM users WHERE id = ${userId}`; return { rows, }; }); ``` 使用 `useDatabase()` 获取数据库实例。可以使用 `db.sql` 查询数据库,像 `${userId}` 这样的变量会自动转义以防止 SQL 注入。 ## 使用 Tasks 运行迁移 Nitro 任务允许你在请求处理程序之外运行操作。对于数据库迁移,在 `tasks/` 中创建任务文件并通过 CLI 运行。这样可以将架构更改与应用程序代码分离。 ```ts [tasks/db/migrate.ts] import { defineTask } from "nitro/task"; import { useDatabase } from "nitro/database"; export default defineTask({ meta: { description: "运行数据库迁移", }, async run() { const db = useDatabase(); console.log("正在运行数据库迁移..."); // 创建用户表 await db.sql`DROP TABLE IF EXISTS users`; await db.sql`CREATE TABLE IF NOT EXISTS users ("id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT)`; return { result: "数据库迁移完成!", }; }, }); ``` ## 了解更多 - [数据库](/docs/database) - [任务](/docs/tasks)。 --- # Elysia > 使用 server entry 将 Elysia 与 Nitro 集成。 ::code-tree{defaultValue="server.ts" expandAll} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({}); ``` ```json [package.json] { "type": "module", "scripts": { "build": "nitro build", "dev": "nitro dev" }, "devDependencies": { "elysia": "^1.4.28", "nitro": "latest" } } ``` ```ts [server.ts] import { Elysia } from "elysia"; const app = new Elysia(); app.get("/", () => "Hello, Elysia with Nitro!"); export default app.compile(); ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` :: ## Server Entry(服务器入口) ```ts [server.ts] import { Elysia } from "elysia"; const app = new Elysia(); app.get("/", () => "Hello, Elysia with Nitro!"); export default app.compile(); ``` Nitro 会自动检测项目根目录中的 `server.ts`,并将其用作服务器入口。Elysia 应用处理所有传入请求,让你完全掌控路由和中间件。 在导出前调用 `app.compile()`,以针对生产环境优化路由。 ## 了解更多 - [Server Entry(服务器入口)](/docs/server-entry) - [Elysia 文档](https://elysiajs.com/) --- # Express > 使用服务端入口将 Express 与 Nitro 集成。 ::code-tree{defaultValue="server.node.ts" expandAll} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({}); ``` ```json [package.json] { "type": "module", "scripts": { "build": "nitro build", "dev": "nitro dev" }, "devDependencies": { "@types/express": "^5.0.6", "express": "^5.2.1", "nitro": "latest" } } ``` ```ts [server.node.ts] import Express from "express"; const app = Express(); app.use("/", (_req, res) => { res.send("Hello from Express with Nitro!"); }); export default app; ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` :: ## 服务端入口 ```ts [server.node.ts] import Express from "express"; const app = Express(); app.use("/", (_req, res) => { res.send("Hello from Express with Nitro!"); }); export default app; ``` Nitro 会在你的项目根目录自动检测 `server.node.ts` 并将其用作服务端入口。Express 应用会处理所有传入的请求,让你完全掌控路由和中间件。 ::note `.node.ts` 后缀表明此入口是 Node.js 专有的,无法在其他运行时(如 Cloudflare Workers 或 Deno)中工作。 :: ## 了解更多 - [服务端入口](/docs/server-entry) - [Express 文档](https://expressjs.com/)。 --- # Fastify > 使用服务端入口将 Fastify 与 Nitro 集成。 ::code-tree{defaultValue="server.node.ts" expandAll} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({}); ``` ```json [package.json] { "type": "module", "scripts": { "build": "nitro build", "dev": "nitro dev" }, "devDependencies": { "fastify": "^5.8.4", "nitro": "latest" } } ``` ```ts [server.node.ts] import Fastify from "fastify"; const app = Fastify(); app.get("/", () => "Hello, Fastify with Nitro!"); await app.ready(); export default app.routing; ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` :: ## 服务端入口 ```ts [server.node.ts] import Fastify from "fastify"; const app = Fastify(); app.get("/", () => "Hello, Fastify with Nitro!"); await app.ready(); export default app.routing; ``` Nitro 会自动检测项目根目录下的 `server.node.ts` 并将其用作服务端入口。 在导出前调用 `await app.ready()` 以初始化所有已注册的插件。导出 `app.routing`(而非 `app`)以向 Nitro 提供请求处理器函数。 ::note `.node.ts` 后缀表明此入口是 Node.js 专用的,无法在其他运行时(如 Cloudflare Workers 或 Deno)中运行。 :: ## 了解更多 - [服务端入口](/docs/server-entry) - [Fastify 文档](https://fastify.dev/)。 --- # 你好世界 > 使用 Web 标准 fetch 处理程序的极简 Nitro 服务器。 ::code-tree{defaultValue="server.ts" expandAll} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({}); ``` ```json [package.json] { "type": "module", "scripts": { "build": "nitro build", "dev": "nitro dev", "preview": "node .output/server/index.mjs" }, "devDependencies": { "nitro": "latest" } } ``` ```ts [server.ts] export default { fetch(req: Request) { return new Response("Nitro Works!"); }, }; ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` :: 最简单的 Nitro 服务器。导出一个带有 `fetch` 方法的对象,该方法接收一个标准的 `Request` 并返回一个 `Response`。没有框架,没有抽象,只有 Web 平台。 ## 服务器入口 ```ts [server.ts] export default { fetch(req: Request) { return new Response("Nitro Works!"); }, }; ``` `fetch` 方法遵循与 Service Workers 和 Cloudflare Workers 相同的签名。这种模式适用于所有部署目标,因为它使用了 Web 标准。 将 Nitro 插件添加到 Vite,它会处理其余工作:开发服务器、热重载和生产构建。 ## 了解更多 - [服务器入口](/docs/server-entry) - [配置](/docs/configuration) --- # Hono > 使用服务端入口将 Hono 与 Nitro 集成。 ::code-tree{defaultValue="server.ts" expandAll} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({}); ``` ```json [package.json] { "type": "module", "scripts": { "build": "nitro build", "dev": "nitro dev" }, "devDependencies": { "hono": "^4.12.9", "nitro": "latest" } } ``` ```ts [server.ts] import { Hono } from "hono"; const app = new Hono(); app.get("/", (c) => { return c.text("你好,Hono 与 Nitro!"); }); export default app; ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` :: ## 服务端入口 ```ts [server.ts] import { Hono } from "hono"; const app = new Hono(); app.get("/", (c) => { return c.text("你好,Hono 与 Nitro!"); }); export default app; ``` Nitro 会自动检测项目根目录中的 `server.ts` 并将其用作服务端入口。Hono 应用处理所有传入请求,让你完全掌控路由和中间件。 Hono 具有跨运行时兼容性,因此该服务端入口可在所有 Nitro 部署目标上运行,包括 Node.js、Deno、Bun 和 Cloudflare Workers。 ## 了解更多 - [服务端入口](/docs/server-entry) - [Hono 文档](https://hono.dev/)。 --- # 导入别名 > 用于更简洁模块路径的自定义导入别名。 ::code-tree{defaultValue="server/routes/index.ts" expandAll} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: true, }); ``` ```json [package.json] { "type": "module", "imports": { "#server/*": "./server/*" }, "scripts": { "build": "nitro build", "dev": "nitro dev", "preview": "node .output/server/index.mjs" }, "devDependencies": { "nitro": "latest" } } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig", "compilerOptions": { "paths": { "~server/*": ["./server/*"] } } } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()], resolve: { tsconfigPaths: true } }); ``` ```ts [server/routes/index.ts] import { sum } from "~server/utils/math.ts"; import { rand } from "#server/utils/math.ts"; export default () => { const [a, b] = [rand(1, 10), rand(1, 10)]; const result = sum(a, b); return `The sum of ${a} + ${b} = ${result}`; }; ``` ```ts [server/utils/math.ts] export function rand(min: number, max: number): number { return Math.floor(Math.random() * (max - min + 1)) + min; } export function sum(a: number, b: number): number { return a + b; } ``` :: 像 `~` 和 `#` 这样的导入别名让你可以使用更短的路径引用模块,而不是使用相对导入。 ## 使用别名导入 ```ts [server/routes/index.ts] import { sum } from "~server/utils/math.ts"; import { rand } from "#server/utils/math.ts"; export default () => { const [a, b] = [rand(1, 10), rand(1, 10)]; const result = sum(a, b); return `The sum of ${a} + ${b} = ${result}`; }; ``` 该路由使用 `~server/` 导入 `sum` 函数,使用 `#server/` 导入 `rand`。两者都解析到同一个 `server/utils/math.ts` 文件。处理器生成两个随机数并返回它们的和。 ## 配置 别名可以在 `package.json` 的 imports 字段或 `nitro.config.ts` 中配置。 ## 了解更多 - [配置](/docs/configuration)。 --- # 中间件 > 用于认证、日志记录和请求修改的请求中间件。 ::code-tree{defaultValue="server/middleware/auth.ts" expandAll} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: true, }); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "nitro dev", "build": "nitro build" }, "devDependencies": { "nitro": "latest" } } ``` ```ts [server.ts] import { defineHandler } from "nitro"; export default defineHandler((event) => ({ auth: event.context.auth, })); ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` ```ts [server/middleware/auth.ts] import { defineMiddleware } from "nitro"; export default defineMiddleware((event) => { event.context.auth = { name: "User " + Math.round(Math.random() * 100) }; }); ``` :: 中间件函数在每个请求的路由处理器之前运行。它们可以修改请求、添加上下文或返回提前响应。 ## 定义中间件 在 `server/middleware/` 中创建文件。它们按字母顺序运行: ```ts [server/middleware/auth.ts] import { defineMiddleware } from "nitro"; export default defineMiddleware((event) => { event.context.auth = { name: "User " + Math.round(Math.random() * 100) }; }); ``` 中间件可以: - 将数据添加到 `event.context` 以供处理器使用 - 提前返回响应以短路请求 - 修改请求头或其他属性 ## 在处理器中访问上下文 在中间件中添加到 `event.context` 的数据在所有后续处理器中都可用: ```ts [server.ts] import { defineHandler } from "nitro"; export default defineHandler((event) => ({ auth: event.context.auth, })); ``` ## 了解更多 - [路由](/docs/routing) --- # Mono JSX > 在 Nitro 中使用 mono-jsx 进行服务端 JSX 渲染。 ::code-tree{defaultValue="server.tsx" expandAll} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({}); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "nitro dev", "build": "nitro build" }, "devDependencies": { "mono-jsx": "latest", "nitro": "latest" } } ``` ```tsx [server.tsx] export default () => (

Nitro + mono-jsx 正常工作!

); ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig", "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "mono-jsx" } } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` :: ## 服务端入口 ```tsx [server.tsx] export default () => (

Nitro + mono-jsx 正常工作!

); ``` Nitro 自动检测 `server.tsx` 并使用 mono-jsx 将 JSX 转换为 HTML。导出一个返回 JSX 的函数,Nitro 会将渲染后的 HTML 作为响应发送。 ## 了解更多 - [渲染器](/docs/renderer) - [mono-jsx](https://github.com/aspect-dev/mono-jsx)。 --- # Nano JSX > 在 Nitro 中使用 nano-jsx 进行服务端 JSX 渲染。 ::code-tree{defaultValue="server.tsx" expandAll} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({}); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "nitro dev", "build": "nitro build" }, "devDependencies": { "nano-jsx": "^0.2.1", "nitro": "latest" } } ``` ```tsx [server.tsx] import { defineHandler, html } from "nitro"; import { renderSSR } from "nano-jsx"; export default defineHandler(() => { return html(renderSSR(() =>

Nitro + nano-jsx works!

)); }); ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig", "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "nano-jsx/esm" } } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` :: ## 服务端入口 ```tsx [server.tsx] import { defineHandler, html } from "nitro"; import { renderSSR } from "nano-jsx"; export default defineHandler(() => { return html(renderSSR(() =>

Nitro + nano-jsx works!

)); }); ``` Nitro 自动检测 `server.tsx` 并将其用作服务端入口。使用 nano-jsx 的 `renderSSR` 将 JSX 转换为 HTML 字符串。来自 H3 的 `html` 辅助函数设置正确的内容类型响应头。 ## 了解更多 - [渲染器](/docs/renderer) - [nano-jsx](https://nanojsx.io/)。 --- # 插件 > 使用自定义插件扩展 Nitro,用于钩子和生命周期事件。 ::code-tree{defaultValue="server/plugins/test.ts" expandAll} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: true, }); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "nitro dev", "build": "nitro build" }, "devDependencies": { "nitro": "latest" } } ``` ```ts [server.ts] import { defineHandler } from "nitro"; export default defineHandler(() => "

Hello Nitro!

"); ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` ```ts [server/plugins/test.ts] import { definePlugin } from "nitro"; import { useNitroHooks } from "nitro/app"; export default definePlugin(() => { const hooks = useNitroHooks(); hooks.hook("response", (res) => { res.headers.set("content-type", "html; charset=utf-8"); }); }); ``` :: 插件让你能够接入 Nitro 的运行时生命周期。本示例展示了一个插件,它在每个响应上修改 `Content-Type` 标头。在 `server/plugins/` 中创建文件,它们将在启动时自动加载。 ## 定义插件 ```ts [server/plugins/test.ts] import { definePlugin } from "nitro"; import { useNitroHooks } from "nitro/app"; export default definePlugin(() => { const hooks = useNitroHooks(); hooks.hook("response", (res) => { res.headers.set("content-type", "html; charset=utf-8"); }); }); ``` 该插件使用 `useNitroHooks()` 访问钩子系统,然后注册一个 `response` 钩子,该钩子在每次请求后运行,并接收传出的 `Response`。这里它将内容类型设置为 HTML,但你也可以记录请求、添加安全标头,或以任何方式修改响应。 ## 主处理器 ```ts [server.ts] import { defineHandler } from "nitro"; export default defineHandler(() => "

Hello Nitro!

"); ``` 该处理器返回 HTML 而不设置内容类型。插件会自动将正确的 `Content-Type: html; charset=utf-8` 标头添加到响应中。 ## 了解更多 - [插件](/docs/plugins) - [生命周期](/docs/lifecycle) --- # 自定义渲染器 > 在 Nitro 中构建一个带有服务端数据获取功能的自定义 HTML 渲染器。 ::code-tree{defaultValue="renderer.ts" expandAll} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: "./", renderer: { handler: "./renderer" }, }); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "nitro dev", "build": "nitro build" }, "devDependencies": { "nitro": "latest" } } ``` ```ts [renderer.ts] import { fetch } from "nitro"; export default async function renderer({ url }: { req: Request; url: URL }) { const apiRes = await fetch("/api/hello").then((res) => res.text()); return new Response( /* html */ ` 自定义渲染器

来自自定义渲染器的问候!

当前路径:${url.pathname}

API 说:${apiRes}

`, { headers: { "content-type": "text/html; charset=utf-8" } } ); } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` ```ts [api/hello.ts] import { defineHandler } from "nitro"; export default defineHandler(() => "Nitro 令人惊叹!"); ``` :: 创建一个自定义渲染器,该渲染器使用来自 API 路由的数据生成 HTML 响应。使用 Nitro 内部的 `fetch` 来调用路由,无需网络开销。 ## 渲染器 ```ts [renderer.ts] import { fetch } from "nitro"; export default async function renderer({ url }: { req: Request; url: URL }) { const apiRes = await fetch("/api/hello").then((res) => res.text()); return new Response( /* html */ ` 自定义渲染器

来自自定义渲染器的问候!

当前路径:${url.pathname}

API 说:${apiRes}

`, { headers: { "content-type": "text/html; charset=utf-8" } } ); } ``` 在 `nitro.config.ts` 中使用 `renderer: { handler: "./renderer" }` 配置渲染器,Nitro 会将其用于所有非 API 路由。渲染器函数接收请求 URL 并返回一个 `Response`。(或者,如果项目根目录中存在 `index.html`,Nitro 会自动将其检测为渲染器模板。) 使用 `nitro` 中的 `fetch` 来调用 API 路由,无需网络开销——这些请求在进程内完成。 ## API 路由 ```ts [api/hello.ts] import { defineHandler } from "nitro"; export default defineHandler(() => "Nitro 令人惊叹!"); ``` 在 `api/` 目录中定义 API 路由。当渲染器调用 `fetch("/api/hello")` 时,该处理程序会运行并返回其响应。 ## 了解更多 - [渲染器](/docs/renderer) --- # 运行时配置 > 支持环境感知的配置,可在运行时访问。 ::code-tree{defaultValue="nitro.config.ts" expandAll} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: "./", runtimeConfig: { apiKey: "", }, }); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "nitro dev", "build": "nitro build" }, "devDependencies": { "nitro": "latest" } } ``` ```ts [server.ts] import { defineHandler } from "nitro"; import { useRuntimeConfig } from "nitro/runtime-config"; export default defineHandler((event) => { const runtimeConfig = useRuntimeConfig(); return { runtimeConfig }; }); ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` :: 运行时配置允许你定义可在运行时被环境变量覆盖的配置值。 ## 定义配置模式 在 `nitro.config.ts` 中声明带有默认值的运行时配置: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: "./", runtimeConfig: { apiKey: "", }, }); ``` ## 在运行时访问 使用 `useRuntimeConfig` 在你的处理程序中访问配置值: ```ts [server.ts] import { defineHandler } from "nitro"; import { useRuntimeConfig } from "nitro/runtime-config"; export default defineHandler((event) => { const runtimeConfig = useRuntimeConfig(); return { runtimeConfig }; }); ``` ## 环境变量 通过带有 `NITRO_` 前缀的环境变量覆盖配置值: ```sh [.env] # 切勿提交敏感数据。这仅用于演示目的。 NITRO_API_KEY=secret-api-key ``` ## 了解更多 - [配置](/docs/configuration)。 --- # 服务端 Fetch > 内部服务器到服务器的请求,无网络开销。 ::code-tree{defaultValue="routes/index.ts" expandAll} ```ts [nitro.config.ts] import { defineConfig, serverFetch } from "nitro"; export default defineConfig({ serverDir: "./", hooks: { "dev:start": async () => { const res = await serverFetch("/hello"); const text = await res.text(); console.log("Fetched /hello in nitro module:", res.status, text); }, }, }); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "nitro dev", "build": "nitro build" }, "devDependencies": { "nitro": "latest" } } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` ```ts [routes/hello.ts] import { defineHandler } from "nitro"; export default defineHandler(() => "Hello!"); ``` ```ts [routes/index.ts] import { defineHandler } from "nitro"; import { fetch } from "nitro"; export default defineHandler(() => fetch("/hello")); ``` :: 当你需要一个路由调用另一个路由时,请使用 Nitro 的 `fetch` 函数,而非全局 fetch。它发起进程内的内部请求,避免网络往返。请求永远不会离开服务器。 ## 主路由 ```ts [routes/index.ts] import { defineHandler } from "nitro"; import { fetch } from "nitro"; export default defineHandler(() => fetch("/hello")); ``` 索引路由从 `nitro` 导入 `fetch`(而非全局 fetch)并调用 `/hello` 路由。此请求在内部处理,无需经过网络协议栈。 ## 内部 API 路由 ```ts [routes/hello.ts] import { defineHandler } from "nitro"; export default defineHandler(() => "Hello!"); ``` 一个返回 "Hello!" 的简单路由。当索引路由调用 `fetch("/hello")` 时,此处理函数运行并直接返回其响应。 ## 了解更多 - [路由](/docs/routing)。 --- # Shiki > 在 Nitro 中使用 Shiki 进行服务端语法高亮。 ::code-tree{defaultValue="api/highlight.ts" expandAll} ```html [index.html] Hello World 代码片段
JavaScript
{{{ hl(`console.log("💚 Simple is beautiful!");`) }}}
``` ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: "./", }); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "vite dev", "build": "vite build" }, "devDependencies": { "nitro": "latest", "shiki": "latest" } } ``` ```css [styles.css] html, body { height: 100%; margin: 0; } body { display: flex; align-items: center; justify-content: center; background: #f6f8fa; font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif; } .card { text-align: left; background: #0b1220; color: #e6edf3; padding: 1rem; border-radius: 8px; box-shadow: 0 8px 24px rgba(2, 6, 23, 0.2); max-width: 90%; width: 520px; } .label { font-size: 12px; color: #9aa7b2; margin-bottom: 8px; } pre { margin: 0; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, "Courier New", monospace; font-size: 14px; background: transparent; white-space: pre; overflow: auto; } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()], }); ``` ```ts [api/highlight.ts] import { createHighlighterCore } from "shiki/core"; import { createOnigurumaEngine } from "shiki/engine/oniguruma"; const highlighter = await createHighlighterCore({ engine: createOnigurumaEngine(import("shiki/wasm")), themes: [await import("shiki/themes/vitesse-dark.mjs")], langs: [await import("shiki/langs/ts.mjs")], }); export default async ({ req }: { req: Request }) => { const code = await req.text(); const html = await highlighter.codeToHtml(code, { lang: "ts", theme: "vitesse-dark", }); return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-8" }, }); }; ``` :: 使用 Shiki 配合 TextMate 语法进行语法高亮。本示例使用 Nitro 的服务端脚本功能在服务端高亮代码,该功能会在发送响应前在 HTML 文件内部运行 JavaScript。 ## API 路由 ```ts [api/highlight.ts] import { createHighlighterCore } from "shiki/core"; import { createOnigurumaEngine } from "shiki/engine/oniguruma"; const highlighter = await createHighlighterCore({ engine: createOnigurumaEngine(import("shiki/wasm")), themes: [await import("shiki/themes/vitesse-dark.mjs")], langs: [await import("shiki/langs/ts.mjs")], }); export default async ({ req }: { req: Request }) => { const code = await req.text(); const html = await highlighter.codeToHtml(code, { lang: "ts", theme: "vitesse-dark", }); return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-8" }, }); }; ``` 创建一个使用 Vitesse Dark 主题和 TypeScript 语言支持的 Shiki 高亮器。当 API 收到 POST 请求时,它会从请求体中读取代码并返回高亮后的 HTML。 ## 服务端渲染 ```html [index.html] Hello World 代码片段
JavaScript
{{{ hl(`console.log("💚 Simple is beautiful!");`) }}}
``` ` ``` ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: "./", }); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "vite dev", "build": "vite build", "preview": "node .output/server/index.mjs" }, "devDependencies": { "nitro": "latest", "takumi-js": "^2.1.1", "vite": "latest" } } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()], }); ``` ```ts [routes/og.png.ts] import { defineHandler } from "nitro"; import { container, text } from "takumi-js/helpers"; import ImageResponse from "takumi-js/response"; export default defineHandler(async ({ url }) => { const title = url.searchParams.get("title") ?? "Takumi + Nitro"; const description = url.searchParams.get("description") ?? "Render OG images from a Nitro route."; const start = performance.now(); const response = new ImageResponse( container({ style: { width: "100%", height: "100%", display: "flex", flexDirection: "column", justifyContent: "center", padding: "64px", backgroundImage: "linear-gradient(to bottom right, #fff1f2, #fecdd3)", }, children: [ text(title, { fontSize: 72, fontWeight: 700, color: "#111827" }), text(description, { fontSize: 42, fontWeight: 500, color: "#4b5563" }), ], }), { width: 1200, height: 630 } ); await response.ready; response.headers.set("Server-Timing", `render;dur=${(performance.now() - start).toFixed(1)}`); return response; }); ``` ```css [src/styles.css] :root { color-scheme: light dark; --bg-from: #fff1f2; --bg-to: #ffe4e6; --card-bg: rgba(255, 255, 255, 0.72); --card-border: rgba(255, 255, 255, 0.7); --card-shadow: 0 24px 60px -20px rgba(190, 24, 93, 0.35); --fg: #111827; --fg-muted: #6b7280; --field-bg: rgba(255, 255, 255, 0.65); --field-border: rgba(17, 24, 39, 0.12); --field-border-focus: #be185d; --surface: rgba(17, 24, 39, 0.05); --surface-hover: rgba(17, 24, 39, 0.1); --accent: #be185d; --accent-hover: #9d174d; --ring: rgba(190, 24, 93, 0.25); } @media (prefers-color-scheme: dark) { :root { --bg-from: #1c1117; --bg-to: #0b0a0f; --card-bg: rgba(30, 27, 34, 0.6); --card-border: rgba(255, 255, 255, 0.08); --card-shadow: 0 24px 60px -20px rgba(0, 0, 0, 0.7); --fg: #f4f4f5; --fg-muted: #a1a1aa; --field-bg: rgba(255, 255, 255, 0.04); --field-border: rgba(255, 255, 255, 0.12); --field-border-focus: #f472b6; --surface: rgba(255, 255, 255, 0.06); --surface-hover: rgba(255, 255, 255, 0.12); --accent: #ec4899; --accent-hover: #f472b6; --ring: rgba(236, 72, 153, 0.35); } } * { box-sizing: border-box; } body { margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; color: var(--fg); background: radial-gradient(1200px 600px at 100% -10%, var(--bg-from), transparent 60%), linear-gradient(to bottom right, var(--bg-from), var(--bg-to)); background-attachment: fixed; } .card { width: 100%; max-width: 680px; background: var(--card-bg); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border: 1px solid var(--card-border); border-radius: 24px; padding: 32px; box-shadow: var(--card-shadow); } .header-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; } .eyebrow { display: inline-flex; align-items: center; gap: 6px; margin: 0 0 12px; padding: 4px 10px; border-radius: 999px; background: var(--surface); color: var(--fg-muted); font-size: 12px; font-weight: 600; letter-spacing: 0.02em; text-transform: uppercase; } .eyebrow::before { content: ""; width: 6px; height: 6px; border-radius: 999px; background: var(--accent); box-shadow: 0 0 0 3px var(--ring); } h1 { margin: 0; font-size: 30px; line-height: 1.15; letter-spacing: -0.02em; } p.lead { margin: 8px 0 24px; color: var(--fg-muted); line-height: 1.55; } .icon-link { display: inline-flex; flex-shrink: 0; align-items: center; justify-content: center; width: 38px; height: 38px; border-radius: 12px; background: var(--surface); color: var(--fg-muted); transition: background 0.15s ease, color 0.15s ease, transform 0.15s ease; } .icon-link:hover { background: var(--surface-hover); color: var(--fg); transform: translateY(-1px); } .preview-wrap { position: relative; } img.preview { display: block; width: 100%; height: auto; aspect-ratio: 1200 / 630; background: var(--surface); border-radius: 14px; border: 1px solid var(--field-border); } .timing-badge { position: absolute; right: 12px; bottom: 12px; display: inline-flex; align-items: center; gap: 6px; padding: 5px 12px; border-radius: 999px; background: rgba(17, 24, 39, 0.78); color: #fff; font-size: 12px; font-weight: 500; font-variant-numeric: tabular-nums; line-height: 1.4; backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); transition: opacity 0.2s ease; } .timing-badge::before { content: ""; width: 6px; height: 6px; border-radius: 999px; background: #4ade80; } .timing-badge[data-pending] { opacity: 0.85; } .timing-badge[data-pending]::before { background: #fbbf24; animation: pulse 1s ease-in-out infinite; } @keyframes pulse { 50% { opacity: 0.3; } } .controls { display: flex; flex-wrap: wrap; gap: 12px; margin: 24px 0 4px; } .field { flex: 1 1 220px; display: flex; flex-direction: column; gap: 6px; } .field label { font-size: 12px; font-weight: 600; color: var(--fg-muted); } .field input { width: 100%; padding: 11px 13px; border-radius: 12px; border: 1px solid var(--field-border); background: var(--field-bg); color: var(--fg); font-size: 14px; transition: border-color 0.15s ease, box-shadow 0.15s ease; } .field input:focus { outline: none; border-color: var(--field-border-focus); box-shadow: 0 0 0 3px var(--ring); } .field input::placeholder { color: var(--fg-muted); opacity: 0.6; } code { font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; font-size: 0.9em; background: var(--surface); padding: 2px 6px; border-radius: 6px; } ``` :: 使用 [Takumi](https://takumi.kane.tw) 通过 Nitro 路由生成动态 [Open Graph](https://ogp.me/) 图片。`index.html` 页面通过其 `og:image` meta 标签引用生成的图片,并实时预览该图片。 ## 服务端路由 使用 Takumi [辅助函数](https://takumi.kane.tw/docs/helpers)构建节点树——无需设置 JSX。Nitro 处理程序可以返回一个 `Response`,因此可以直接返回 `ImageResponse`。该处理程序会等待 `response.ready`,并添加 `Server-Timing` 标头,以便调用方了解渲染耗时: ```ts [routes/og.png.ts] import { defineHandler } from "nitro"; import { container, text } from "takumi-js/helpers"; import ImageResponse from "takumi-js/response"; export default defineHandler(async ({ url }) => { const title = url.searchParams.get("title") ?? "Takumi + Nitro"; const description = url.searchParams.get("description") ?? "Render OG images from a Nitro route."; const start = performance.now(); const response = new ImageResponse( container({ style: { width: "100%", height: "100%", display: "flex", flexDirection: "column", justifyContent: "center", padding: "64px", backgroundImage: "linear-gradient(to bottom right, #fff1f2, #fecdd3)", }, children: [ text(title, { fontSize: 72, fontWeight: 700, color: "#111827" }), text(description, { fontSize: 42, fontWeight: 500, color: "#4b5563" }), ], }), { width: 1200, height: 630 }, ); await response.ready; response.headers.set("Server-Timing", `render;dur=${(performance.now() - start).toFixed(1)}`); return response; }); ``` ## 引用图像 `index.html` 将其 Open Graph 标签指向该路由,以便爬虫获取最新渲染的预览: ```html [index.html] ``` ## 请求端点 访问 `/og.png?title=Hello&description=From%20Nitro`,即可使用自定义文本渲染图像。响应包含一个报告渲染耗时的 `Server-Timing` 标头;演示页面会在你输入标题/描述字段时重新获取图像,并在预览右下角叠加显示“正在生成…”/“N ms”徽章。标题旁的链接图标始终指向当前预览所使用的原始端点。 `index.html` 的 head 会预加载初始图像,使浏览器能够在解析页面的同时开始获取图像;`` 则通过宽度/高度属性和 CSS 保留其 `1200x630` 的宽高比,从而避免图像加载期间发生布局偏移。 Takumi 会根据部署目标选择渲染后端:Node 预设使用原生绑定,边缘预设使用 WebAssembly。无需进行任何配置。 ## 了解更多 - [Takumi](https://takumi.kane.tw)。 --- # 追踪 > 使用诊断通道和内置追踪日志记录请求生命周期 ::code-tree{defaultValue="nitro.config.ts" expandAll} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: true, // Instrument the h3, srvx and unstorage tracing channels. tracingChannel: true, experimental: { // Log every completed span to the console (built-in, dependency-free sink). tracingLogger: true, }, }); ``` ```json [package.json] { "type": "module", "scripts": { "build": "nitro build", "dev": "nitro dev", "preview": "node .output/server/index.mjs" }, "devDependencies": { "nitro": "latest" } } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [server/middleware/timing.ts] import { defineMiddleware } from "nitro"; // A trivial middleware — each request produces its own `middleware` span. export default defineMiddleware((event) => { event.context.requestedAt = Date.now(); }); ``` ```ts [server/routes/index.ts] import { defineHandler } from "nitro"; import { useKV } from "nitro/kv"; // Reads and writes storage so the request emits `unstorage.*` spans (CLIENT) // alongside the `srvx.request`, `middleware` and route (`h3.request`) spans. export default defineHandler(async () => { const storage = useKV(); const hits = ((await storage.getItem("hits")) ?? 0) + 1; await storage.setItem("hits", hits); return { message: "Hello from the Nitro tracing demo", hits }; }); ``` ```ts [server/routes/users/[id].ts] import { defineHandler } from "nitro"; // A dynamic route — spans are named by the matched route template // (`GET /users/:id`), not the concrete path, to keep cardinality low. export default defineHandler((event) => ({ user: { id: event.context.params!.id }, })); ``` :: Nitro 可以通过 Node [诊断通道](https://nodejs.org/api/diagnostics_channel.html)记录其请求生命周期,无需 OpenTelemetry SDK。此示例会开启插桩并启用内置控制台日志记录器,将每个 h3、srvx 和 unstorage span 按请求分组为时间线(瀑布图)。 ## 启用追踪 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ // Instrument the h3, srvx and unstorage tracing channels. tracingChannel: true, experimental: { // Log every completed span to the console (built-in, dependency-free sink). tracingLogger: true, }, }); ``` `tracingChannel: true` 会连接生产者(传入 `{ h3, srvx, unstorage }` 可仅追踪其中一部分)。`experimental.tracingLogger` 会添加一个内置接收器,通过 `console.log` 记录每个已完成的 span,是一种无需依赖的供应商导出器替代方案,适用于本地开发。 ## 试用 ```sh npm run dev # then, in another terminal: curl http://localhost:3000/ curl http://localhost:3000/users/42 ``` 每个请求都会在控制台输出其 span 时间线——包括中间件、匹配的路由以及每个存储操作,并根据它们的执行时间和耗时确定位置与长度: ``` ▶ GET / 4.10ms (4 spans) middleware GET / █······················· 0.12ms h3.handler_type=middleware http.route=/ GET / ·███████████████████····· 2.49ms h3.handler_type=route http.route=/ getItem ···██···················· 0.18ms db.operation=getItem db.system=memory unstorage.keys_count=1 setItem ·····██·················· 0.16ms db.operation=setItem db.system=memory unstorage.keys_count=1 ``` 标题行(`▶`)表示请求本身——包括其方法、路径和总耗时;下面的各行则是请求中运行的 span。请注意,动态路由使用其匹配的模板命名——`GET /users/:id`,而不是 `/users/42`——根据 OpenTelemetry HTTP 约定保持 span 名称的低基数。 请求边界来自 Nitro 的 `request`/`response` 运行时钩子,因此在 `vite dev` 和生产构建中,分组方式完全一致。Span 通过异步上下文按请求分组,因此并发请求的时间线仍彼此独立;失败的 span 会以 `✖` 标记,并显示其错误消息。 ## 追踪内容 | 通道 | Span | 发出方 | | --- | --- | --- | | `h3.request` | 每个匹配的路由和中间件 | `server/routes/*`、`server/middleware/*` | | `unstorage.*` | 每个存储操作(`getItem`、`setItem`、…) | `server/routes/index.ts` 中的 `useKV()` | | `srvx.request` | 整个请求及响应状态(生产服务器) | srvx 服务器层 | --- # 虚拟路由 > 使用 Nitro 的虚拟模块系统以编程方式定义路由。 ::code-tree{defaultValue="nitro.config.ts" expandAll} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ routes: { "/": "#virtual-route", }, virtual: { "#virtual-route": () => /* js */ `export default () => new Response("Hello from virtual entry!")`, }, }); ``` ```json [package.json] { "type": "module", "scripts": { "build": "nitro build", "dev": "nitro dev", "preview": "node .output/server/index.mjs" }, "devDependencies": { "nitro": "latest" } } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` :: 虚拟路由让你可以在配置中将处理器定义为字符串,而无需创建单独的文件。这在动态生成路由、构建插件或保持简单路由内联时非常有用。 ## 配置 ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ routes: { "/": "#virtual-route", }, virtual: { "#virtual-route": () => /* js */ `export default () => new Response("Hello from virtual entry!")`, }, }); ``` `routes` 选项将 URL 路径映射到虚拟模块标识符(以 `#` 为前缀)。`virtual` 选项将模块内容定义为字符串或返回字符串的函数。在构建时,Nitro 会将这些虚拟模块解析为实际的处理器。 此项目中没有路由文件。整个处理器都在配置中内联定义,Nitro 在构建时生成路由。 ## 了解更多 - [路由](/docs/routing) - [配置](/docs/configuration) --- # Vite Nitro 插件 > 将 Nitro 作为 Vite 插件使用,以进行程序化配置。 ::code-tree{defaultValue="vite.config.mjs" expandAll} ```json [package.json] { "type": "module", "scripts": { "build": "vite build", "preview": "vite preview", "dev": "vite dev" }, "devDependencies": { "nitro": "latest", "vite": "latest" } } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```js [vite.config.mjs] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [ nitro(), { name: "my-nitro-plugin", nitro: { setup: (nitro) => { nitro.options.routes["/"] = "#virtual-by-plugin"; nitro.options.virtual["#virtual-by-plugin"] = `export default () => new Response("Hello from virtual entry!")`; }, }, }, ], }); ``` :: 无需使用单独的 `nitro.config.ts` 文件,您可以直接在 Vite 配置中配置 Nitro。这使您可以访问 Nitro 的 setup 钩子,从而以编程方式注册路由和虚拟模块。 ## Vite 配置 ```js [vite.config.mjs] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [ nitro(), { name: "my-nitro-plugin", nitro: { setup: (nitro) => { nitro.options.routes["/"] = "#virtual-by-plugin"; nitro.options.virtual["#virtual-by-plugin"] = `export default () => new Response("Hello from virtual entry!")`; }, }, }, ], }); ``` 该配置添加了两个插件:`nitro()` 插件和一个使用 `nitro.setup` 钩子的自定义插件。在 setup 函数内部,您可以访问 Nitro 的选项对象。此示例在 `/` 处注册了一个虚拟路由,该路由映射到虚拟模块 `#virtual-by-plugin`,然后内联定义了该模块。 ## 了解更多 - [配置](/docs/configuration)。 --- # Vite RSC > React Server Components with Vite and Nitro. ::code-tree{defaultValue="app/root.tsx" expandAll} ```json [package.json] { "name": "@vitejs/plugin-rsc-examples-starter", "version": "0.0.0", "private": true, "license": "MIT", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "react": "^19.2.4", "react-dom": "^19.2.4" }, "devDependencies": { "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", "@vitejs/plugin-rsc": "^0.5.21", "nitro": "latest", "rsc-html-stream": "^0.0.7", "vite": "latest" } } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig", "compilerOptions": { "lib": ["ESNext", "DOM", "DOM.Iterable"], "types": ["vite/client", "@vitejs/plugin-rsc/types"], "jsx": "react-jsx" } } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; import rsc from "@vitejs/plugin-rsc"; import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [ nitro(), rsc({ serverHandler: false, entries: { ssr: "./app/framework/entry.ssr.tsx", rsc: "./app/framework/entry.rsc.tsx", }, }), react(), ], environments: { client: { build: { rollupOptions: { input: { index: "./app/framework/entry.browser.tsx" }, }, }, }, }, }); ``` ```tsx [app/action.tsx] "use server"; let serverCounter = 0; export async function getServerCounter() { return serverCounter; } export async function updateServerCounter(change: number) { serverCounter += change; } ``` ```tsx [app/client.tsx] "use client"; import React from "react"; export function ClientCounter() { const [count, setCount] = React.useState(0); return ; } ``` ```css [app/index.css] :root { font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; color-scheme: light dark; color: rgba(255, 255, 255, 0.87); background-color: #242424; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } a { font-weight: 500; color: #646cff; text-decoration: inherit; } a:hover { color: #535bf2; } body { margin: 0; display: flex; place-items: center; min-width: 320px; min-height: 100vh; } h1 { font-size: 3.2em; line-height: 1.1; } button { border-radius: 8px; border: 1px solid transparent; padding: 0.6em 1.2em; font-size: 1em; font-weight: 500; font-family: inherit; background-color: #1a1a1a; cursor: pointer; transition: border-color 0.25s; } button:hover { border-color: #646cff; } button:focus, button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } @media (prefers-color-scheme: light) { :root { color: #213547; background-color: #ffffff; } a:hover { color: #747bff; } button { background-color: #f9f9f9; } } #root { max-width: 1280px; margin: 0 auto; padding: 2rem; text-align: center; } .logo { height: 6em; padding: 1.5em; will-change: filter; transition: filter 300ms; } .logo:hover { filter: drop-shadow(0 0 2em #646cffaa); } .logo.react:hover { filter: drop-shadow(0 0 2em #61dafbaa); } @keyframes logo-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @media (prefers-reduced-motion: no-preference) { a:nth-of-type(2) .logo { animation: logo-spin infinite 20s linear; } } .card { padding: 1rem; } .read-the-docs { color: #888; text-align: left; } ``` ```tsx [app/root.tsx] import "./index.css"; // css import is automatically injected in exported server components import viteLogo from "./assets/vite.svg"; import { getServerCounter, updateServerCounter } from "./action.tsx"; import reactLogo from "./assets/react.svg"; import nitroLogo from "./assets/nitro.svg"; import { ClientCounter } from "./client.tsx"; export function Root(props: { url: URL }) { return ( {/* eslint-disable-next-line unicorn/text-encoding-identifier-case */} Nitro + Vite + RSC ); } function App(props: { url: URL }) { return (

Vite + RSC + Nitro

Request URL: {props.url?.href}
  • Edit src/client.tsx to test client HMR.
  • Edit src/root.tsx to test server HMR.
  • Visit{" "} _.rsc {" "} to view RSC stream payload.
  • Visit{" "} ?__nojs {" "} to test server action without js enabled.
); } ``` ```text [app/assets/nitro.svg] ``` ```text [app/assets/react.svg] ``` ```text [app/assets/vite.svg] ``` ```tsx [app/framework/entry.browser.tsx] import { createFromReadableStream, createFromFetch, setServerCallback, createTemporaryReferenceSet, encodeReply, } from "@vitejs/plugin-rsc/browser"; import React from "react"; import { createRoot, hydrateRoot } from "react-dom/client"; import { rscStream } from "rsc-html-stream/client"; import { GlobalErrorBoundary } from "./error-boundary"; import type { RscPayload } from "./entry.rsc"; import { createRscRenderRequest } from "./request"; async function main() { // Stash `setPayload` function to trigger re-rendering // from outside of `BrowserRoot` component (e.g. server function call, navigation, hmr) let setPayload: (v: RscPayload) => void; // Deserialize RSC stream back to React VDOM for CSR const initialPayload = await createFromReadableStream( // Initial RSC stream is injected in SSR stream as rscStream ); // Browser root component to (re-)render RSC payload as state function BrowserRoot() { const [payload, setPayload_] = React.useState(initialPayload); React.useEffect(() => { setPayload = (v) => React.startTransition(() => setPayload_(v)); }, [setPayload_]); // Re-fetch/render on client side navigation React.useEffect(() => { return listenNavigation(() => fetchRscPayload()); }, []); return payload.root; } // Re-fetch RSC and trigger re-rendering async function fetchRscPayload() { const renderRequest = createRscRenderRequest(globalThis.location.href); const payload = await createFromFetch(fetch(renderRequest)); setPayload(payload); } // Register a handler which will be internally called by React // on server function request after hydration. setServerCallback(async (id, args) => { const temporaryReferences = createTemporaryReferenceSet(); const renderRequest = createRscRenderRequest(globalThis.location.href, { id, body: await encodeReply(args, { temporaryReferences }), }); const payload = await createFromFetch(fetch(renderRequest), { temporaryReferences, }); setPayload(payload); const { ok, data } = payload.returnValue!; if (!ok) throw data; return data; }); // Hydration const browserRoot = ( ); if ("__NO_HYDRATE" in globalThis) { createRoot(document).render(browserRoot); } else { hydrateRoot(document, browserRoot, { formState: initialPayload.formState, }); } // Implement server HMR by triggering re-fetch/render of RSC upon server code change if (import.meta.hot) { import.meta.hot.on("rsc:update", () => { fetchRscPayload(); }); } } // A little helper to setup events interception for client side navigation function listenNavigation(onNavigation: () => void) { globalThis.addEventListener("popstate", onNavigation); const oldPushState = globalThis.history.pushState; globalThis.history.pushState = function (...args) { const res = oldPushState.apply(this, args); onNavigation(); return res; }; const oldReplaceState = globalThis.history.replaceState; globalThis.history.replaceState = function (...args) { const res = oldReplaceState.apply(this, args); onNavigation(); return res; }; function onClick(e: MouseEvent) { const link = (e.target as Element).closest("a"); if ( link && link instanceof HTMLAnchorElement && link.href && (!link.target || link.target === "_self") && link.origin === location.origin && !link.hasAttribute("download") && e.button === 0 && // left clicks only !e.metaKey && // open in new tab (mac) !e.ctrlKey && // open in new tab (windows) !e.altKey && // download !e.shiftKey && !e.defaultPrevented ) { e.preventDefault(); history.pushState(null, "", link.href); } } document.addEventListener("click", onClick); return () => { document.removeEventListener("click", onClick); globalThis.removeEventListener("popstate", onNavigation); globalThis.history.pushState = oldPushState; globalThis.history.replaceState = oldReplaceState; }; } // eslint-disable-next-line unicorn/prefer-top-level-await main(); ``` ```tsx [app/framework/entry.rsc.tsx] import { renderToReadableStream, createTemporaryReferenceSet, decodeReply, loadServerAction, decodeAction, decodeFormState, } from "@vitejs/plugin-rsc/rsc"; import type { ReactFormState } from "react-dom/client"; import { Root } from "../root.tsx"; import { parseRenderRequest } from "./request.tsx"; // The schema of payload which is serialized into RSC stream on rsc environment // and deserialized on ssr/client environments. export type RscPayload = { // this demo renders/serializes/deserializes entire root html element // but this mechanism can be changed to render/fetch different parts of components // based on your own route conventions. root: React.ReactNode; // Server action return value of non-progressive enhancement case returnValue?: { ok: boolean; data: unknown }; // Server action form state (e.g. useActionState) of progressive enhancement case formState?: ReactFormState; }; // The plugin by default assumes `rsc` entry having default export of request handler. // however, how server entries are executed can be customized by registering own server handler. export default async function handler(request: Request): Promise { // Differentiate RSC, SSR, action, etc. const renderRequest = parseRenderRequest(request); request = renderRequest.request; // Handle server function request let returnValue: RscPayload["returnValue"] | undefined; let formState: ReactFormState | undefined; let temporaryReferences: unknown | undefined; let actionStatus: number | undefined; if (renderRequest.isAction === true) { if (renderRequest.actionId) { // Action is called via `ReactClient.setServerCallback`. const contentType = request.headers.get("content-type"); const body = contentType?.startsWith("multipart/form-data") ? await request.formData() : await request.text(); temporaryReferences = createTemporaryReferenceSet(); const args = await decodeReply(body, { temporaryReferences }); const action = await loadServerAction(renderRequest.actionId); try { // eslint-disable-next-line prefer-spread const data = await action.apply(null, args); returnValue = { ok: true, data }; } catch (error_) { returnValue = { ok: false, data: error_ }; actionStatus = 500; } } else { // Otherwise server function is called via `
` // before hydration (e.g. when JavaScript is disabled). // aka progressive enhancement. const formData = await request.formData(); const decodedAction = await decodeAction(formData); try { const result = await decodedAction(); formState = await decodeFormState(result, formData); } catch { // there's no single general obvious way to surface this error, // so explicitly return classic 500 response. return new Response("Internal Server Error: server action failed", { status: 500, }); } } } // Serialization from React VDOM tree to RSC stream. // We render RSC stream after handling server function request // so that new render reflects updated state from server function call // to achieve single round trip to mutate and fetch from server. const rscPayload: RscPayload = { root: , formState, returnValue, }; const rscOptions = { temporaryReferences }; const rscStream = renderToReadableStream(rscPayload, rscOptions); // Respond RSC stream without HTML rendering as decided by `RenderRequest` if (renderRequest.isRsc) { return new Response(rscStream, { status: actionStatus, headers: { "content-type": "text/x-component;charset=utf-8", }, }); } // Delegate to SSR environment for HTML rendering. // The plugin provides `loadModule` helper to allow loading SSR environment entry module // in RSC environment. however this can be customized by implementing own runtime communication // e.g. `@cloudflare/vite-plugin`'s service binding. const ssrEntryModule = await import.meta.viteRsc.loadModule( "ssr", "index" ); const ssrResult = await ssrEntryModule.renderHTML(rscStream, { formState, // Allow quick simulation of JavaScript disabled browser debugNoJS: renderRequest.url.searchParams.has("__nojs"), }); // Respond HTML return new Response(ssrResult.stream, { status: ssrResult.status, headers: { "Content-Type": "text/html", }, }); } if (import.meta.hot) { import.meta.hot.accept(); } ``` ```tsx [app/framework/entry.ssr.tsx] import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr"; import React from "react"; import type { ReactFormState } from "react-dom/client"; import { renderToReadableStream } from "react-dom/server.edge"; import { injectRSCPayload } from "rsc-html-stream/server"; import type { RscPayload } from "./entry.rsc"; export default { fetch: async (request: Request) => { const rscEntryModule = await import.meta.viteRsc.loadModule( "rsc", "index" ); return rscEntryModule.default(request); }, }; export async function renderHTML( rscStream: ReadableStream, options: { formState?: ReactFormState; nonce?: string; debugNoJS?: boolean; } ): Promise<{ stream: ReadableStream; status?: number }> { // Duplicate one RSC stream into two. // - one for SSR (ReactClient.createFromReadableStream below) // - another for browser hydration payload by injecting . const [rscStream1, rscStream2] = rscStream.tee(); // Deserialize RSC stream back to React VDOM let payload: Promise | undefined; function SsrRoot() { // Deserialization needs to be kicked off inside ReactDOMServer context // for ReactDOMServer preinit/preloading to work payload ??= createFromReadableStream(rscStream1); return React.use(payload).root; } // Render HTML (traditional SSR) const bootstrapScriptContent = await import.meta.viteRsc.loadBootstrapScriptContent("index"); let htmlStream: ReadableStream; let status: number | undefined; try { htmlStream = await renderToReadableStream(, { bootstrapScriptContent: options?.debugNoJS ? undefined : bootstrapScriptContent, nonce: options?.nonce, formState: options?.formState, }); } catch { // fallback to render an empty shell and run pure CSR on browser, // which can replay server component error and trigger error boundary. status = 500; htmlStream = await renderToReadableStream( , { bootstrapScriptContent: `self.__NO_HYDRATE=1;` + (options?.debugNoJS ? "" : bootstrapScriptContent), nonce: options?.nonce, } ); } let responseStream: ReadableStream = htmlStream; if (!options?.debugNoJS) { // Initial RSC stream is injected in HTML stream as // using utility made by devongovett https://github.com/devongovett/rsc-html-stream responseStream = responseStream.pipeThrough( injectRSCPayload(rscStream2, { nonce: options?.nonce, }) ); } return { stream: responseStream, status }; } ``` ```tsx [app/framework/error-boundary.tsx] "use client"; import React from "react"; // Minimal ErrorBoundary example to handle errors globally on browser export function GlobalErrorBoundary(props: { children?: React.ReactNode }) { return {props.children}; } // https://github.com/vercel/next.js/blob/33f8428f7066bf8b2ec61f025427ceb2a54c4bdf/packages/next/src/client/components/error-boundary.tsx // https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary class ErrorBoundary extends React.Component<{ children?: React.ReactNode; errorComponent: React.FC<{ error: Error; reset: () => void; }>; }> { override state: { error?: Error } = {}; static getDerivedStateFromError(error: Error) { return { error }; } reset = () => { this.setState({ error: null }); }; override render() { const error = this.state.error; if (error) { return ; } return this.props.children; } } // https://github.com/vercel/next.js/blob/677c9b372faef680d17e9ba224743f44e1107661/packages/next/src/build/webpack/loaders/next-app-loader.ts#L73 // https://github.com/vercel/next.js/blob/677c9b372faef680d17e9ba224743f44e1107661/packages/next/src/client/components/error-boundary.tsx#L145 function DefaultGlobalErrorPage(props: { error: Error; reset: () => void }) { return ( Unexpected Error

Caught an unexpected error

          Error:{" "}
          {import.meta.env.DEV && "message" in props.error ? props.error.message : "(Unknown)"}
        
); } ``` ```tsx [app/framework/request.tsx] // Framework conventions (arbitrary choices for this demo): // - Use `_.rsc` URL suffix to differentiate RSC requests from SSR requests // - Use `x-rsc-action` header to pass server action ID const URL_POSTFIX = "_.rsc"; const HEADER_ACTION_ID = "x-rsc-action"; // Parsed request information used to route between RSC/SSR rendering and action handling. // Created by parseRenderRequest() from incoming HTTP requests. type RenderRequest = { isRsc: boolean; // true if request should return RSC payload (via _.rsc suffix) isAction: boolean; // true if this is a server action call (POST request) actionId?: string; // server action ID from x-rsc-action header request: Request; // normalized Request with _.rsc suffix removed from URL url: URL; // normalized URL with _.rsc suffix removed }; export function createRscRenderRequest( urlString: string, action?: { id: string; body: BodyInit } ): Request { const url = new URL(urlString); url.pathname += URL_POSTFIX; const headers = new Headers(); if (action) { headers.set(HEADER_ACTION_ID, action.id); } return new Request(url.toString(), { method: action ? "POST" : "GET", headers, body: action?.body, }); } export function parseRenderRequest(request: Request): RenderRequest { const url = new URL(request.url); const isAction = request.method === "POST"; if (url.pathname.endsWith(URL_POSTFIX)) { url.pathname = url.pathname.slice(0, -URL_POSTFIX.length); const actionId = request.headers.get(HEADER_ACTION_ID) || undefined; if (request.method === "POST" && !actionId) { throw new Error("Missing action id header for RSC action request"); } return { isRsc: true, isAction, actionId, request: new Request(url, request), url, }; } else { return { isRsc: false, isAction, request, url, }; } } ``` :: This example demonstrates React Server Components (RSC) using Vite's experimental RSC plugin with Nitro. It includes server components, client components, server actions, and streaming SSR. ## Overview 1. **SSR Entry** handles incoming requests and renders React components to HTML 2. **Root Component** defines the page structure as a server component 3. **Client Components** use the `"use client"` directive for interactive parts ## 1. SSR Entry ```tsx [app/framework/entry.ssr.tsx] import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr"; import React from "react"; import type { ReactFormState } from "react-dom/client"; import { renderToReadableStream } from "react-dom/server.edge"; import { injectRSCPayload } from "rsc-html-stream/server"; import type { RscPayload } from "./entry.rsc"; export default { fetch: async (request: Request) => { const rscEntryModule = await import.meta.viteRsc.loadModule( "rsc", "index" ); return rscEntryModule.default(request); }, }; export async function renderHTML( rscStream: ReadableStream, options: { formState?: ReactFormState; nonce?: string; debugNoJS?: boolean; } ): Promise<{ stream: ReadableStream; status?: number }> { // Duplicate one RSC stream into two. // - one for SSR (ReactClient.createFromReadableStream below) // - another for browser hydration payload by injecting . const [rscStream1, rscStream2] = rscStream.tee(); // Deserialize RSC stream back to React VDOM let payload: Promise | undefined; function SsrRoot() { // Deserialization needs to be kicked off inside ReactDOMServer context // for ReactDOMServer preinit/preloading to work payload ??= createFromReadableStream(rscStream1); return React.use(payload).root; } // Render HTML (traditional SSR) const bootstrapScriptContent = await import.meta.viteRsc.loadBootstrapScriptContent("index"); let htmlStream: ReadableStream; let status: number | undefined; try { htmlStream = await renderToReadableStream(, { bootstrapScriptContent: options?.debugNoJS ? undefined : bootstrapScriptContent, nonce: options?.nonce, formState: options?.formState, }); } catch { // fallback to render an empty shell and run pure CSR on browser, // which can replay server component error and trigger error boundary. status = 500; htmlStream = await renderToReadableStream( , { bootstrapScriptContent: `self.__NO_HYDRATE=1;` + (options?.debugNoJS ? "" : bootstrapScriptContent), nonce: options?.nonce, } ); } let responseStream: ReadableStream = htmlStream; if (!options?.debugNoJS) { // Initial RSC stream is injected in HTML stream as // using utility made by devongovett https://github.com/devongovett/rsc-html-stream responseStream = responseStream.pipeThrough( injectRSCPayload(rscStream2, { nonce: options?.nonce, }) ); } return { stream: responseStream, status }; } ``` The SSR entry handles the rendering pipeline. It loads the RSC entry module, duplicates the RSC stream (one for SSR, one for hydration), deserializes the stream back to React VDOM, and renders it to HTML. The RSC payload is injected into the HTML for client hydration. ## 2. Root Server Component ```tsx [app/root.tsx] import "./index.css"; // css import is automatically injected in exported server components import viteLogo from "./assets/vite.svg"; import { getServerCounter, updateServerCounter } from "./action.tsx"; import reactLogo from "./assets/react.svg"; import nitroLogo from "./assets/nitro.svg"; import { ClientCounter } from "./client.tsx"; export function Root(props: { url: URL }) { return ( {/* eslint-disable-next-line unicorn/text-encoding-identifier-case */} Nitro + Vite + RSC ); } function App(props: { url: URL }) { return (

Vite + RSC + Nitro

Request URL: {props.url?.href}
  • Edit src/client.tsx to test client HMR.
  • Edit src/root.tsx to test server HMR.
  • Visit{" "} _.rsc {" "} to view RSC stream payload.
  • Visit{" "} ?__nojs {" "} to test server action without js enabled.
); } ``` Server components run only on the server. They can import CSS directly, use server-side data, and call server actions. The `ClientCounter` component is imported but runs on the client because it has the `"use client"` directive. ## 3. Client Component ```tsx [app/client.tsx] "use client"; import React from "react"; export function ClientCounter() { const [count, setCount] = React.useState(0); return ; } ``` The `"use client"` directive marks this as a client component. It hydrates on the browser and handles interactive state. Server components can import and render client components, but client components cannot import server components. ## Learn More - [React Server Components](https://react.dev/reference/rsc/server-components) --- # Vite SSR HTML > 使用原生 HTML、Vite 和 Nitro 进行服务端渲染。 ::code-tree{defaultValue="app/entry-server.ts" expandAll} ```html [index.html] Nitro 名言
由 Vite 和 Nitro v3 提供支持。
``` ```json [package.json] { "type": "module", "scripts": { "build": "vite build", "dev": "vite dev", "preview": "vite preview" }, "devDependencies": { "@tailwindcss/vite": "^4.2.2", "nitro": "latest", "tailwindcss": "^4.2.2", "vite": "latest" } } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; import tailwindcss from "@tailwindcss/vite"; export default defineConfig({ plugins: [ nitro({ serverDir: "./", }), tailwindcss(), ], }); ``` ```ts [app/entry-server.ts] import { fetch } from "nitro"; export default { async fetch() { const quote = (await fetch("/quote").then((res) => res.json())) as { text: string; }; return tokenizedStream(quote.text, 50); }, }; function tokenizedStream(text: string, delay: number): ReadableStream { const tokens = text.split(" "); return new ReadableStream({ start(controller) { let index = 0; function push() { if (index < tokens.length) { const word = tokens[index++] + (index < tokens.length ? " " : ""); controller.enqueue(new TextEncoder().encode(word)); setTimeout(push, delay); } else { controller.close(); } } push(); }, }); } ``` ```ts [routes/quote.ts] const QUOTES_URL = "https://github.com/JamesFT/Database-Quotes-JSON/raw/refs/heads/master/quotes.json"; let _quotes: Promise | undefined; function getQuotes() { return (_quotes ??= fetch(QUOTES_URL).then((res) => res.json())) as Promise< { quoteText: string; quoteAuthor: string }[] >; } export default async function quotesHandler() { const quotes = await getQuotes(); const randomQuote = quotes[Math.floor(Math.random() * quotes.length)]; return Response.json({ text: randomQuote.quoteText, author: randomQuote.quoteAuthor, }); } ``` :: 此示例使用服务器端数据渲染 HTML 模板,并逐词流式传输响应。它演示了如何在不使用框架的情况下使用 Nitro 的 Vite SSR 集成。 ## 概述 1. **添加 Nitro Vite 插件**以启用 SSR 2. **创建 HTML 模板**,包含 `` 注释,用于标记服务器内容的插入位置 3. **创建服务器入口**,获取数据并返回流 4. **添加 API 路由**,用于服务器端数据 ## 工作原理 `index.html` 文件包含 `` 注释,用于标记服务器渲染内容的插入位置。Nitro 会使用服务器入口的输出来替换此注释。 服务器入口导出一个包含 `fetch` 方法的对象。它使用 Nitro 的内部 fetch 调用 `/quote` API 路由,然后返回一个 `ReadableStream`,以逐词发送名言文本,每个词之间延迟 50 毫秒。 名言路由从 GitHub 获取包含名言的 JSON 文件,缓存结果,并返回随机名言。服务器入口调用此路由来获取页面内容。 ## 了解更多 - [Renderer](/docs/renderer) - [Server Entry](/docs/server-entry) --- # 使用 Preact 进行 SSR > 在 Nitro 中使用 Vite 和 Preact 进行服务端渲染。 ::code-tree{defaultValue="src/entry-server.tsx" expandAll} ```json [package.json] { "type": "module", "scripts": { "build": "vite build", "preview": "vite preview", "dev": "vite dev" }, "devDependencies": { "@preact/preset-vite": "^2.10.5", "@tailwindcss/vite": "^4.2.2", "nitro": "latest", "preact": "^10.29.0", "preact-render-to-string": "^6.6.7", "tailwindcss": "^4.2.2", "vite": "latest" } } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig", "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact" } } ``` ```js [vite.config.mjs] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; import preact from "@preact/preset-vite"; export default defineConfig({ plugins: [nitro(), preact()], environments: { client: { build: { rollupOptions: { input: "./src/entry-client.tsx", }, }, }, }, }); ``` ```tsx [src/app.tsx] import { useState } from "preact/hooks"; export function App() { const [count, setCount] = useState(0); return ; } ``` ```tsx [src/entry-client.tsx] import { hydrate } from "preact"; import { App } from "./app.tsx"; function main() { hydrate(, document.querySelector("#app")!); } main(); ``` ```tsx [src/entry-server.tsx] import "./styles.css"; import { renderToReadableStream } from "preact-render-to-string/stream"; import { App } from "./app.jsx"; import clientAssets from "./entry-client?assets=client"; import serverAssets from "./entry-server?assets=ssr"; export default { async fetch(request: Request) { const url = new URL(request.url); const htmlStream = renderToReadableStream(); return new Response(htmlStream, { headers: { "Content-Type": "text/html;charset=utf-8" }, }); }, }; function Root(props: { url: URL }) { const assets = clientAssets.merge(serverAssets); return ( {assets.css.map((attr: any) => ( ))} {assets.js.map((attr: any) => ( ))} ``` ```json [package.json] { "type": "module", "scripts": { "build": "vite build", "dev": "vite dev", "preview": "vite preview" }, "devDependencies": { "@tanstack/react-router": "^1.168.8", "@tanstack/react-router-devtools": "^1.166.11", "@tanstack/router-plugin": "^1.167.9", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", "nitro": "latest", "react": "^19.2.4", "react-dom": "^19.2.4", "vite": "latest" } } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig", "compilerOptions": { "baseUrl": ".", "jsx": "react-jsx", "paths": { "@/*": ["./src/*"] } } } ``` ```js [vite.config.mjs] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; import react from "@vitejs/plugin-react"; import { tanstackRouter } from "@tanstack/router-plugin/vite"; export default defineConfig({ plugins: [tanstackRouter({ target: "react", autoCodeSplitting: true }), react(), nitro()], }); ``` ```tsx [src/main.tsx] import { StrictMode } from "react"; import ReactDOM from "react-dom/client"; import { RouterProvider, createRouter } from "@tanstack/react-router"; // 导入生成的路由树 import { routeTree } from "./routeTree.gen.ts"; // 创建新的路由实例 const router = createRouter({ routeTree }); // 注册路由实例以确保类型安全 declare module "@tanstack/react-router" { interface Register { router: typeof router; } } // 渲染应用 const rootElement = document.querySelector("#root")!; if (!rootElement.innerHTML) { const root = ReactDOM.createRoot(rootElement); root.render( ); } ``` ```ts [src/routeTree.gen.ts] /* eslint-disable */ // @ts-nocheck // noinspection JSUnusedGlobalSymbols // 此文件由 TanStack Router 自动生成。 // 你不应修改此文件,因为它会被覆盖。 // 此外,你还应将此文件排除在代码检查器(linter)和/或格式化工具之外,以防止其被检查或修改。 import { Route as rootRouteImport } from './routes/__root' import { Route as IndexRouteImport } from './routes/index' const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', getParentRoute: () => rootRouteImport, } as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: '/' fileRoutesByTo: FileRoutesByTo to: '/' id: '__root__' | '/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute } declare module '@tanstack/react-router' { interface FileRoutesByPath { '/': { id: '/' path: '/' fullPath: '/' preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } } } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) ._addFileTypes() ``` ```css [src/assets/main.css] :root { font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; color-scheme: light dark; color: rgba(255, 255, 255, 0.87); background-color: #242424; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } a { font-weight: 500; color: #ff2056; text-decoration: inherit; } a:hover { color: #ff637e; } body { margin: 0; display: flex; flex-direction: column; place-items: center; justify-content: center; min-width: 320px; min-height: 100vh; } h1 { font-size: 3.2em; line-height: 1.1; } #app { max-width: 1280px; margin: 0 auto; padding: 2rem; text-align: center; } .logo { height: 6em; padding: 1.5em; will-change: filter; transition: filter 300ms; transition: transform 300ms; } .logo:hover { transform: scale(1.1); } .card { padding: 2em; } .read-the-docs { color: #888; } button { border-radius: 8px; border: 1px solid transparent; padding: 0.6em 1.2em; font-size: 1em; font-weight: 500; font-family: inherit; background-color: #1a1a1a; cursor: pointer; transition: border-color 0.25s; } button:hover { border-color: #646cff; } button:focus, button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } @media (prefers-color-scheme: light) { :root { color: #213547; background-color: #ffffff; } a:hover { color: #747bff; } button { background-color: #f9f9f9; } } ``` ```tsx [src/routes/__root.tsx] import { createRootRoute, Link, Outlet } from "@tanstack/react-router"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; const RootLayout = () => ( <>
Home

); export const Route = createRootRoute({ component: RootLayout }); ``` ```tsx [src/routes/index.tsx] import { createFileRoute } from "@tanstack/react-router"; export const Route = createFileRoute("/")({ loader: async () => { const r = await fetch("/api/hello"); return r.json(); }, component: Index, }); function Index() { const r = Route.useLoaderData(); return (

{JSON.stringify(r)}

); } ``` :: 使用 React、Vite 和 Nitro 配置 TanStack Router。此配置提供基于文件的路由、类型安全的导航以及自动代码分割。 ## 概述 1. 将 Nitro Vite 插件添加到你的 Vite 配置中 2. 创建包含应用入口的 HTML 模板 3. 创建初始化路由的主入口文件 4. 使用基于文件的路由定义路由 ## 1. 配置 Vite 将 Nitro、React 和 TanStack Router 插件添加到你的 Vite 配置中: ```js [vite.config.mjs] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; import react from "@vitejs/plugin-react"; import { tanstackRouter } from "@tanstack/router-plugin/vite"; export default defineConfig({ plugins: [tanstackRouter({ target: "react", autoCodeSplitting: true }), react(), nitro()], }); ``` `tanstackRouter` 插件会根据你的 `routes/` 目录结构生成路由树。启用 `autoCodeSplitting` 可将路由自动分割成独立的代码块。在数组中将 TanStack Router 插件放在 React 插件之前。 ## 2. 创建 HTML 模板 创建一个作为应用外壳的 HTML 文件: ```html [index.html] Nitro + TanStack Router + React
``` ## 3. 创建应用入口 创建初始化 TanStack Router 的主入口文件: ```tsx [src/main.tsx] import { StrictMode } from "react"; import ReactDOM from "react-dom/client"; import { RouterProvider, createRouter } from "@tanstack/react-router"; // 导入生成的路由树 import { routeTree } from "./routeTree.gen.ts"; // 创建新的路由实例 const router = createRouter({ routeTree }); // 注册路由实例以确保类型安全 declare module "@tanstack/react-router" { interface Register { router: typeof router; } } // 渲染应用 const rootElement = document.querySelector("#root")!; if (!rootElement.innerHTML) { const root = ReactDOM.createRoot(rootElement); root.render( ); } ``` `routeTree.gen.ts` 文件是根据你的 `routes/` 目录结构自动生成的。`Register` 接口声明为路由路径和参数提供完整的类型推断。`!rootElement.innerHTML` 检查可防止在热模块替换(HMR)期间重新渲染。 ## 4. 创建根路由 根路由(`__root.tsx`)定义了应用的布局: ```tsx [src/routes/__root.tsx] import { createRootRoute, Link, Outlet } from "@tanstack/react-router"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; const RootLayout = () => ( <>
Home

); export const Route = createRootRoute({ component: RootLayout }); ``` 使用 `Link` 进行类型安全的导航,并支持激活状态样式。`Outlet` 组件用于渲染子路由。包含 `TanStackRouterDevtools` 以使用开发工具(在生产环境中会自动移除)。 ## 5. 创建页面路由 页面路由使用 `createFileRoute` 创建,并可包含加载器(loader): ```tsx [src/routes/index.tsx] import { createFileRoute } from "@tanstack/react-router"; export const Route = createFileRoute("/")({ loader: async () => { const r = await fetch("/api/hello"); return r.json(); }, component: Index, }); function Index() { const r = Route.useLoaderData(); return (

{JSON.stringify(r)}

); } ``` 使用 `loader` 函数在渲染前获取数据——数据可通过 `Route.useLoaderData()` 获取。文件路径决定 URL 路径:`routes/index.tsx` 映射到 `/`,`routes/about.tsx` 映射到 `/about`,`routes/users/$id.tsx` 映射到 `/users/:id`。 ## 了解更多 - [TanStack Router 文档](https://tanstack.com/router) - [渲染器](/docs/renderer) --- # 使用 TanStack Start 进行 SSR > 在 Nitro 中使用 Vite 配合 TanStack Start 实现全栈 React。 ::code-tree{defaultValue="server.ts" expandAll} ```json [package.json] { "type": "module", "scripts": { "build": "vite build", "dev": "vite dev", "start": "node .output/server/index.mjs" }, "dependencies": { "@tanstack/react-router": "^1.168.8", "@tanstack/react-router-devtools": "^1.166.11", "@tanstack/react-start": "^1.167.13", "nitro": "latest", "react": "^19.2.4", "react-dom": "^19.2.4", "tailwind-merge": "^3.5.0", "zod": "^4.3.6" }, "devDependencies": { "@tailwindcss/vite": "^4.2.2", "@types/node": "latest", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", "tailwindcss": "^4.2.2", "typescript": "^6.0.2", "vite": "latest" } } ``` ```ts [server.ts] import handler, { createServerEntry } from "@tanstack/react-start/server-entry"; export default createServerEntry({ fetch(request) { return handler.fetch(request); }, }); ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig", "compilerOptions": { "baseUrl": ".", "jsx": "react-jsx", "paths": { "~/*": ["./src/*"] } } } ``` ```js [vite.config.mjs] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; import { tanstackStart } from "@tanstack/react-start/plugin/vite"; import viteReact from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; export default defineConfig({ plugins: [tanstackStart(), viteReact(), tailwindcss(), nitro()], resolve: { tsconfigPaths: true }, environments: { ssr: { build: { rollupOptions: { input: "./server.ts" } } }, }, }); ``` ```tsx [src/router.tsx] import { createRouter } from "@tanstack/react-router"; import { routeTree } from "./routeTree.gen.ts"; export function getRouter() { const router = createRouter({ routeTree, defaultPreload: "intent", defaultErrorComponent: () =>
内部服务器错误
, defaultNotFoundComponent: () =>
未找到
, scrollRestoration: true, }); return router; } ``` ```ts [src/routeTree.gen.ts] /* eslint-disable */ // @ts-nocheck // noinspection JSUnusedGlobalSymbols // 此文件由 TanStack Router 自动生成。 // 你不应对这个文件进行任何更改,因为它会被覆盖。 // 此外,你还应该将此文件从你的 linter 和/或 formatter 中排除,以防止被检查或修改。 import { Route as rootRouteImport } from './routes/__root' import { Route as IndexRouteImport } from './routes/index' import { Route as ApiTestRouteImport } from './routes/api/test' const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', getParentRoute: () => rootRouteImport, } as any) const ApiTestRoute = ApiTestRouteImport.update({ id: '/api/test', path: '/api/test', getParentRoute: () => rootRouteImport, } as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute '/api/test': typeof ApiTestRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/api/test': typeof ApiTestRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/api/test': typeof ApiTestRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: '/' | '/api/test' fileRoutesByTo: FileRoutesByTo to: '/' | '/api/test' id: '__root__' | '/' | '/api/test' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute ApiTestRoute: typeof ApiTestRoute } declare module '@tanstack/react-router' { interface FileRoutesByPath { '/': { id: '/' path: '/' fullPath: '/' preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } '/api/test': { id: '/api/test' path: '/api/test' fullPath: '/api/test' preLoaderRoute: typeof ApiTestRouteImport parentRoute: typeof rootRouteImport } } } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, ApiTestRoute: ApiTestRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) ._addFileTypes() import type { getRouter } from './router.tsx' import type { createStart } from '@tanstack/react-start' declare module '@tanstack/react-start' { interface Register { ssr: true router: Awaited> } } ``` ```tsx [src/routes/__root.tsx] /// import { HeadContent, Link, Scripts, createRootRoute } from "@tanstack/react-router"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; import * as React from "react"; import appCss from "~/styles/app.css?url"; export const Route = createRootRoute({ head: () => ({ meta: [ { charSet: "utf8" }, { name: "viewport", content: "width=device-width, initial-scale=1" }, ], links: [{ rel: "stylesheet", href: appCss }], scripts: [{ src: "/customScript.js", type: "text/javascript" }], }), errorComponent: () =>

500: 内部服务器错误

, notFoundComponent: () =>

404: 页面未找到

, shellComponent: RootDocument, }); function RootDocument({ children }: { children: React.ReactNode }) { return (
首页 {" "} 404

{children} ); } ``` ```tsx [src/routes/index.tsx] import { createFileRoute } from "@tanstack/react-router"; export const Route = createFileRoute("/")({ component: Home }); function Home() { return (

欢迎回家!

/api/test
); } ``` ```css [src/styles/app.css] @import "tailwindcss"; @layer base { *, ::after, ::before, ::backdrop, ::file-selector-button { border-color: var(--color-gray-200, currentcolor); } } @layer base { html { color-scheme: light dark; } * { @apply border-gray-200 dark:border-gray-800; } html, body { @apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200; } .using-mouse * { outline: none !important; } } ``` :: 使用 Nitro 设置 TanStack Start,获得包含服务端渲染、基于文件的路由和集成 API 路由的全栈 React 框架体验。 ## 概述 1. 将 Nitro Vite 插件添加到你的 Vite 配置中 2. 使用 TanStack Start 的服务端处理程序创建服务端入口 3. 使用默认组件配置路由 4. 使用基于文件的路由定义路由和 API 端点 ## 1. 配置 Vite 将 Nitro、React、TanStack Start 和 Tailwind 插件添加到你的 Vite 配置中: ```js [vite.config.mjs] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; import { tanstackStart } from "@tanstack/react-start/plugin/vite"; import viteReact from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; export default defineConfig({ plugins: [tanstackStart(), viteReact(), tailwindcss(), nitro()], resolve: { tsconfigPaths: true }, environments: { ssr: { build: { rollupOptions: { input: "./server.ts" } } }, }, }); ``` `tanstackStart()` 插件提供完整的 SSR 集成,并自动处理客户端入口。`environments.ssr` 选项指向服务端入口文件。 ## 2. 创建服务端入口 创建一个使用 TanStack Start 处理程序的服务端入口: ```ts [server.ts] import handler, { createServerEntry } from "@tanstack/react-start/server-entry"; export default createServerEntry({ fetch(request) { return handler.fetch(request); }, }); ``` TanStack Start 自动处理 SSR。`createServerEntry` 包装器与 Nitro 的服务端入口格式集成,`handler.fetch` 处理所有传入的请求。 ## 3. 配置路由 创建一个包含默认错误和未找到组件的路由工厂函数: ```tsx [src/router.tsx] import { createRouter } from "@tanstack/react-router"; import { routeTree } from "./routeTree.gen.ts"; export function getRouter() { const router = createRouter({ routeTree, defaultPreload: "intent", defaultErrorComponent: () =>
内部服务器错误
, defaultNotFoundComponent: () =>
未找到
, scrollRestoration: true, }); return router; } ``` 路由工厂配置了预加载行为、滚动恢复以及默认的错误/未找到组件。 ## 4. 创建根路由 根路由定义了你的 HTML 外壳,包含 head 管理和脚本: ```tsx [src/routes/__root.tsx] /// import { HeadContent, Link, Scripts, createRootRoute } from "@tanstack/react-router"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; import * as React from "react"; import appCss from "~/styles/app.css?url"; export const Route = createRootRoute({ head: () => ({ meta: [ { charSet: "utf8" }, { name: "viewport", content: "width=device-width, initial-scale=1" }, ], links: [{ rel: "stylesheet", href: appCss }], scripts: [{ src: "/customScript.js", type: "text/javascript" }], }), errorComponent: () =>

500: 内部服务器错误

, notFoundComponent: () =>

404: 页面未找到

, shellComponent: RootDocument, }); function RootDocument({ children }: { children: React.ReactNode }) { return (
首页 {" "} 404

{children} ); } ``` 在 `head()` 函数中定义 meta 标签、样式表和脚本。`shellComponent` 提供了包裹所有页面的 HTML 文档外壳。使用 `HeadContent` 渲染 head 配置,使用 `Scripts` 注入用于水合的客户端 JavaScript。 ## 5. 创建页面路由 页面路由定义了你的应用页面: ```tsx [src/routes/index.tsx] import { createFileRoute } from "@tanstack/react-router"; export const Route = createFileRoute("/")({ component: Home }); function Home() { return (

欢迎回家!

/api/test
); } ``` ## API 路由 TanStack Start 支持 API 路由与页面路由并存。在 `src/routes/api/` 中创建文件以定义 Nitro 自动提供服务的端点。 ## 了解更多 - [TanStack Start 文档](https://tanstack.com/start) - [服务器入口](/docs/server-entry) --- # 基于 Vue Router 的 SSR > 在 Nitro 中使用 Vite 和 Vue Router 进行服务端渲染。 ::code-tree{defaultValue="app/entry-server.ts" expandAll} ```json [package.json] { "type": "module", "scripts": { "build": "vite build", "dev": "vite dev", "preview": "vite preview" }, "devDependencies": { "@vitejs/plugin-vue": "^6.0.5", "nitro": "latest", "unhead": "^2.1.12", "vite": "latest", "vite-plugin-devtools-json": "^1.0.0", "vue": "^3.5.31", "vue-router": "^5.0.4" } } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```js [vite.config.mjs] import vue from "@vitejs/plugin-vue"; import { defineConfig } from "vite"; import devtoolsJson from "vite-plugin-devtools-json"; import { nitro } from "nitro/vite"; export default defineConfig((_env) => ({ plugins: [patchVueExclude(vue(), /\?assets/), devtoolsJson(), nitro()], environments: { client: { build: { rollupOptions: { input: "./app/entry-client.ts" } } }, ssr: { build: { rollupOptions: { input: "./app/entry-server.ts" } } }, nitro: { build: { rollupOptions: { treeshake: { moduleSideEffects: () => false } } } }, }, })); // 变通方案 https://github.com/vitejs/vite-plugin-vue/issues/677 function patchVueExclude(plugin, exclude) { const original = plugin.transform.handler; plugin.transform.handler = function (...args) { if (exclude.test(args[1])) return; return original.call(this, ...args); }; return plugin; } ``` ```vue [app/app.vue] ``` ```ts [app/entry-client.ts] import { createSSRApp } from "vue"; import { RouterView, createRouter, createWebHistory } from "vue-router"; import { routes } from "./routes.ts"; async function main() { const app = createSSRApp(RouterView); const router = createRouter({ history: createWebHistory(), routes }); app.use(router); await router.isReady(); app.mount("#root"); } // eslint-disable-next-line unicorn/更倾向于顶层-await main(); ``` ```ts [app/entry-server.ts] import { createSSRApp } from "vue"; import { renderToString } from "vue/server-renderer"; import { RouterView, createMemoryHistory, createRouter } from "vue-router"; import { createHead, transformHtmlTemplate } from "unhead/server"; import { routes } from "./routes.ts"; import clientAssets from "./entry-client.ts?assets=client"; async function handler(request: Request): Promise { const app = createSSRApp(RouterView); const router = createRouter({ history: createMemoryHistory(), routes }); app.use(router); const url = new URL(request.url); const href = url.href.slice(url.origin.length); await router.push(href); await router.isReady(); const assets = clientAssets.merge( ...(await Promise.all( router.currentRoute.value.matched .map((to) => to.meta.assets) .filter(Boolean) .map((fn) => (fn as any)().then((m: any) => m.default)) )) ); const head = createHead(); head.push({ link: [ ...assets.css.map((attrs: any) => ({ rel: "stylesheet", ...attrs })), ...assets.js.map((attrs: any) => ({ rel: "modulepreload", ...attrs })), ], script: [{ type: "module", src: clientAssets.entry }], }); const renderedApp = await renderToString(app); const html = await transformHtmlTemplate(head, htmlTemplate(renderedApp)); return new Response(html, { headers: { "Content-Type": "text/html;charset=utf-8" }, }); } function htmlTemplate(body: string): string { return /* html */ ` Vue Router 自定义框架
${body}
`; } export default { fetch: handler, }; ``` ```ts [app/routes.ts] import type { RouteRecordRaw } from "vue-router"; export const routes: RouteRecordRaw[] = [ { path: "/", name: "app", component: () => import("./app.vue"), meta: { assets: () => import("./app.vue?assets"), }, children: [ { path: "/", name: "home", component: () => import("./pages/index.vue"), meta: { assets: () => import("./pages/index.vue?assets"), }, }, { path: "/about", name: "about", component: () => import("./pages/about.vue"), meta: { assets: () => import("./pages/about.vue?assets"), }, }, { path: "/:catchAll(.*)", name: "not-found", component: () => import("./pages/not-found.vue"), meta: { assets: () => import("./pages/not-found.vue?assets"), }, }, ], }, ]; ``` ```ts [app/shims.d.ts] declare module "*.vue" { import type { DefineComponent } from "vue"; const component: DefineComponent<{}, {}, any>; export default component; } ``` ```css [app/styles.css] * { box-sizing: border-box; } body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f5f5f5; color: #333; } main { max-width: 800px; margin: 0 auto; padding: 2rem; } h1 { font-size: 2.5rem; margin-bottom: 0.5rem; } .card { background: white; border-radius: 8px; padding: 2rem; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); margin: 2rem 0; } button { background: rgb(83, 91, 242); color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; font-size: 1rem; cursor: pointer; } button:hover { background: #535bf2; } .subtitle { color: #666; font-size: 1.1rem; margin-bottom: 2rem; } ``` ```vue [app/pages/about.vue] ``` ```vue [app/pages/index.vue] ``` ```vue [app/pages/not-found.vue] ``` :: 使用 Vue、Vue Router、Vite 和 Nitro 设置服务端渲染(SSR)。该配置支持按路由代码分割、使用 unhead 进行 head 管理以及客户端水合。 ## 概述 1. 将 Nitro Vite 插件添加到 Vite 配置中 2. 定义带有懒加载组件的路由 3. 创建支持路由渲染的服务端入口 4. 创建客户端入口,用于水合并接管路由 5. 创建页面组件 ## 1. 配置 Vite 将 Nitro 和 Vue 插件添加到 Vite 配置中。定义 `client` 和 `ssr` 两个环境: ```js [vite.config.mjs] import vue from "@vitejs/plugin-vue"; import { defineConfig } from "vite"; import devtoolsJson from "vite-plugin-devtools-json"; import { nitro } from "nitro/vite"; export default defineConfig((_env) => ({ plugins: [patchVueExclude(vue(), /\?assets/), devtoolsJson(), nitro()], environments: { client: { build: { rollupOptions: { input: "./app/entry-client.ts" } } }, ssr: { build: { rollupOptions: { input: "./app/entry-server.ts" } } }, }, })); // 变通方案 https://github.com/vitejs/vite-plugin-vue/issues/677 function patchVueExclude(plugin, exclude) { const original = plugin.transform.handler; plugin.transform.handler = function (...args) { if (exclude.test(args[1])) return; return original.call(this, ...args); }; return plugin; } ``` `patchVueExclude` 辅助函数可防止 Vue 插件处理资源导入(带有 `?assets` 查询参数的文件)。 ## 2. 定义路由 创建带有懒加载组件和资源元数据的路由定义: ```ts [app/routes.ts] import type { RouteRecordRaw } from "vue-router"; export const routes: RouteRecordRaw[] = [ { path: "/", name: "app", component: () => import("./app.vue"), meta: { assets: () => import("./app.vue?assets"), }, children: [ { path: "/", name: "home", component: () => import("./pages/index.vue"), meta: { assets: () => import("./pages/index.vue?assets"), }, }, { path: "/about", name: "about", component: () => import("./pages/about.vue"), meta: { assets: () => import("./pages/about.vue?assets"), }, }, { path: "/:catchAll(.*)", name: "not-found", component: () => import("./pages/not-found.vue"), meta: { assets: () => import("./pages/not-found.vue?assets"), }, }, ], }, ]; ``` 使用动态导入实现懒加载组件,以启用代码分割。`meta.assets` 函数加载路由特定的 CSS 和 JS 代码块。在根布局组件下定义子路由以实现嵌套路由。 ## 3. 创建服务端入口 服务端入口用于渲染 Vue 应用,并支持路由和 head 管理: ```ts [app/entry-server.ts] import { createSSRApp } from "vue"; import { renderToString } from "vue/server-renderer"; import { RouterView, createMemoryHistory, createRouter } from "vue-router"; import { createHead, transformHtmlTemplate } from "unhead/server"; import { routes } from "./routes.ts"; import clientAssets from "./entry-client.ts?assets=client"; async function handler(request: Request): Promise { const app = createSSRApp(RouterView); const router = createRouter({ history: createMemoryHistory(), routes }); app.use(router); const url = new URL(request.url); const href = url.href.slice(url.origin.length); await router.push(href); await router.isReady(); const assets = clientAssets.merge( ...(await Promise.all( router.currentRoute.value.matched .map((to) => to.meta.assets) .filter(Boolean) .map((fn) => (fn as any)().then((m: any) => m.default)) )) ); const head = createHead(); head.push({ link: [ ...assets.css.map((attrs: any) => ({ rel: "stylesheet", ...attrs })), ...assets.js.map((attrs: any) => ({ rel: "modulepreload", ...attrs })), ], script: [{ type: "module", src: clientAssets.entry }], }); const renderedApp = await renderToString(app); const html = await transformHtmlTemplate(head, htmlTemplate(renderedApp)); return new Response(html, { headers: { "Content-Type": "text/html;charset=utf-8" }, }); } function htmlTemplate(body: string): string { return /* html */ ` Vue Router 自定义框架
${body}
`; } export default { fetch: handler, }; ``` 服务端使用 `createMemoryHistory()`,因为没有浏览器地址栏——路由会在渲染前导航到请求的 URL。资源会根据匹配的路由动态加载,确保仅包含当前页面所需的 CSS 和 JS。`unhead` 库负责管理 `` 元素,并通过 `transformHtmlTemplate` 注入样式表和脚本。 ## 4. 创建客户端入口 客户端入口用于对服务端渲染的 HTML 进行注水并接管路由: ```ts [app/entry-client.ts] import { createSSRApp } from "vue"; import { RouterView, createRouter, createWebHistory } from "vue-router"; import { routes } from "./routes.ts"; async function main() { const app = createSSRApp(RouterView); const router = createRouter({ history: createWebHistory(), routes }); app.use(router); await router.isReady(); app.mount("#root"); } // eslint-disable-next-line unicorn/prefer-top-level-await main(); ``` 客户端入口使用 `createWebHistory()` 创建 Vue 应用,以实现基于浏览器的路由。路由就绪后,它会挂载到 `#root` 元素并对服务端渲染的 HTML 进行注水。 ## 5. 创建根组件 根组件提供导航并渲染子路由: ```vue [app/app.vue] ``` ## 了解更多 - [Vue Router 文档](https://router.vuejs.org/) - [Unhead 文档](https://unhead.unjs.io/) - [渲染器](/docs/renderer) - [服务端入口](/docs/server-entry) --- # Vite + tRPC > 在 Nitro 中使用 Vite 和 tRPC 实现端到端类型安全的 API。 ::code-tree{defaultValue="server/trpc.ts" expandAll} ```html [index.html] tRPC 计数器
计数器
``` ```json [package.json] { "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "devDependencies": { "@trpc/client": "^11.16.0", "@trpc/server": "^11.16.0", "nitro": "latest", "vite": "latest", "zod": "^4.3.6" } } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig", "compilerOptions": {} } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [ nitro({ routes: { "/trpc/**": "./server/trpc.ts", }, }), ], }); ``` ```ts [server/trpc.ts] import { initTRPC } from "@trpc/server"; import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; let counter = 0; const t = initTRPC.create(); export const appRouter = t.router({ get: t.procedure.query(() => { return { value: counter }; }), inc: t.procedure.mutation(() => { counter++; return { value: counter }; }), }); export type AppRouter = typeof appRouter; export default { async fetch(request: Request): Promise { return fetchRequestHandler({ endpoint: "/trpc", req: request, router: appRouter, }); }, }; ``` :: 使用 Vite 和 Nitro 设置 tRPC,实现无需代码生成的端到端类型安全 API。本示例构建了一个计数器,使用服务端渲染获取初始值,并支持客户端更新。 ## 概述 1. 使用 Nitro 插件配置 Vite 并路由 tRPC 请求 2. 创建带有过程的 tRPC 路由 3. 创建具有服务端渲染和客户端交互功能的 HTML 页面 ## 1. 配置 Vite 添加 Nitro 插件并配置 `/trpc/**` 路由指向你的 tRPC 处理器: ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [ nitro({ routes: { "/trpc/**": "./server/trpc.ts", }, }), ], }); ``` `routes` 选项将 URL 模式映射到处理器文件。所有发往 `/trpc/*` 的请求都由 tRPC 路由器处理。 ## 2. 创建 tRPC 路由器 使用过程定义你的 tRPC 路由器,并将其导出为 fetch 处理器: ```ts [server/trpc.ts] import { initTRPC } from "@trpc/server"; import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; let counter = 0; const t = initTRPC.create(); export const appRouter = t.router({ get: t.procedure.query(() => { return { value: counter }; }), inc: t.procedure.mutation(() => { counter++; return { value: counter }; }), }); export type AppRouter = typeof appRouter; export default { async fetch(request: Request): Promise { return fetchRequestHandler({ endpoint: "/trpc", req: request, router: appRouter, }); }, }; ``` 使用 `t.procedure.query()` 定义读取操作的过程,使用 `t.procedure.mutation()` 定义写入操作的过程。导出 `AppRouter` 类型,以便客户端获得完整的类型推断。默认导出使用 tRPC 的 fetch 适配器来处理传入请求。 ## 3. 创建 HTML 页面 创建具有服务端渲染和客户端交互功能的 HTML 页面: ```html [index.html] tRPC 计数器
计数器
``` `

{{ message.user }}

Avatar

{{ message.text }}

{{ message.date }}

` ``` ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: "./", renderer: { static: true }, features: { websocket: true }, }); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "nitro dev", "build": "nitro build" }, "devDependencies": { "nitro": "latest" } } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` ```ts [routes/_ws.ts] import { defineWebSocketHandler } from "nitro"; export default defineWebSocketHandler({ open(peer) { peer.send({ user: "server", message: `Welcome ${peer}!` }); peer.publish("chat", { user: "server", message: `${peer} joined!` }); peer.subscribe("chat"); }, message(peer, message) { if (message.text().includes("ping")) { peer.send({ user: "server", message: "pong" }); } else { const msg = { user: peer.toString(), message: message.toString(), }; peer.send(msg); // 回显 peer.publish("chat", msg); } }, close(peer) { peer.publish("chat", { user: "server", message: `${peer} left!` }); }, }); ``` :: 这个示例使用 WebSocket 实现了一个简单的聊天室。客户端连接、发送消息,并实时接收来自其他用户的消息。服务器使用发布/订阅频道向所有连接的客户端广播消息。 ## WebSocket 处理器 使用 `defineWebSocketHandler` 创建 WebSocket 路由。 ```ts [routes/_ws.ts] import { defineWebSocketHandler } from "nitro"; export default defineWebSocketHandler({ open(peer) { peer.send({ user: "server", message: `Welcome ${peer}!` }); peer.publish("chat", { user: "server", message: `${peer} joined!` }); peer.subscribe("chat"); }, message(peer, message) { if (message.text().includes("ping")) { peer.send({ user: "server", message: "pong" }); } else { const msg = { user: peer.toString(), message: message.toString(), }; peer.send(msg); // 回显 peer.publish("chat", msg); } }, close(peer) { peer.publish("chat", { user: "server", message: `${peer} left!` }); }, }); ``` `defineWebSocketHandler()` 暴露了不同的钩子,用于集成 WebSocket 生命周期的不同部分。 ## 了解更多 - [路由](/docs/routing) - [crossws 文档](https://crossws.h3.dev/guide/hooks) --- # 博客 Nitro 博客文章。 --- # Nitro v3 测试版已发布! > Nitro v3 现已作为公开测试版发布:这是一次从底层重新演进的服务器框架,围绕 Web 标准、Rolldown、Vite v8 以及随处部署的承诺构建。 ## 发展历程 Nitro 最初是作为 [Nuxt 3](https://nuxt.com) 的服务器引擎而诞生的,旨在解决一个特定问题:与部署环境无关的服务器。随着时间推移,Nitro 逐渐超越了 Nuxt 的范畴,成为许多元框架的基础,以及构建独立服务器的工具集。 在 Nitro v3 中,我们借此机会重新思考了基础架构:更精简的 API、Web 标准、一流的 [Rolldown](https://rolldown.rs/) 和 [Vite v8](https://vite.dev/) 集成,以及为开发者和 AI 助手提供的更好体验(稍后会详细介绍!) 自从我们在首届 [Vite Conf](https://viteconf.amsterdam/) 上悄悄发布 v3 [alpha.0](https://github.com/nitrojs/nitro/releases/tag/v3.0.1-alpha.0)(2025 年 10 月 11 日)以来,Nitro v3 已被众多用户采用(每周下载量约 [28 万](https://npmtrends.com/nitro-vs-nitro-nightly)!),并通过出色的贡献和反馈不断完善。包括 [Tanstack Start](https://tanstack.com/start/latest/docs/framework/react/guide/hosting#nitro)、[Vercel Workflows](https://useworkflow.dev/docs/getting-started),以及 [T3Chat](https://t3.chat/) 等生产环境应用。 衷心感谢 VoidZero(Vite 和 Rolldown)、Nuxt([v5 即将发布!](#nuxt-v5))和 TanStack Start 团队,以及每一位帮助 Nitro v3 达到这一里程碑的贡献者。❤️ ## 为什么要构建服务器? 我们不会将原始源文件直接发送到浏览器。我们使用构建工具是因为它们能解决实际问题:**HMR** 提供即时反馈,**代码分割**确保只加载路由所需的代码,**Tree Shaking** 消除死代码,**压缩**减小体积。Webpack 和后来的 [Vite](https://vite.dev/) 等工具将前端开发从痛苦变得高效。 但前端应用并非孤立存在,它们需要 API、数据库、认证、实时数据。它们需要服务器。 随着无服务器和边缘计算的兴起,服务器端如今面临着前端多年前解决过的相同限制。**冷启动**意味着启动的每一毫秒都很重要。**内存限制**十分严格,臃肿的依赖可能会让你超出限制。**包体积**会直接影响部署速度和启动时间。而且你的代码需要在任何地方运行:Node.js、Deno、Bun、Cloudflare Workers、Vercel 等。然而,大多数服务器框架仍然提供未经优化、未打包的代码,假定应用会在一个长期运行的进程中运行,而这些问题在这种场景下都不重要。 Nitro 将构建工具的理念带到后端。你期望从前端工具获得的一流开发体验:用于快速迭代的 HMR,以及由 Rolldown 驱动的优化构建,带有 Tree Shaking 的生产输出,性能尽可能接近裸机。**一套代码,任何运行时,任何平台。** ## ⚡ 一流的 Vite 集成 Nitro 现在拥有原生的 [Vite](https://vite.dev) 插件,用于构建全栈应用。 ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()], }); ``` 在你的 Vite 应用中添加 `nitro()` 即可获得: - **API 路由**:通过文件系统路由实现 - **服务端渲染**:与前端构建集成 - **生产服务器**:单次 `vite build` 即可生成经过优化的 `.output/` 文件夹,其中包含前端和后端,可直接部署 这意味着你可以为任何 Vite 项目添加完整的后端。查看使用 [React](/examples/vite-ssr-react)、[Vue](/examples/vite-ssr-vue-router) 和 [Solid.js](/examples/vite-ssr-solid) 的[示例](/examples)。 ## 🚀 默认高性能,零膨胀 Nitro 在构建时编译你的路由。运行时没有路由器;每个路由按需加载。只有处理特定请求所需的代码才会被加载和执行。 使用 `standard` 预设构建的极简服务器包小于 `10kB`,可以使用 [srvx](https://srvx.h3.dev/) 以接近原生的速度提供服务,并包含 [H3](https://h3.dev/) 的所有优秀特性。 我们还显著减少了依赖数量,从 [321 个依赖](https://npmgraph.js.org/?q=nitropack) 减少到 [不足 20 个](https://npmgraph.js.org/?q=nitro-nightly)。 ## 🖌️ 新身份:`nitro` Nitro v3 使用新的 NPM 包发布:[`nitro`](https://npmx.dev/package/nitro),替代了旧版的 `nitropack`。 所有导入现在使用简洁的 `nitro/*` 子路径: ```ts import { defineNitroConfig } from "nitro/config"; import { defineHandler } from "nitro"; import { useStorage } from "nitro/storage"; import { useDatabase } from "nitro/database"; ``` 不再有深层的 `nitropack/runtime/*` 路径,此外,你可以在构建器外部导入 nitro 子路径,这对单元测试很有用。 ## 🔧 自带框架 Nitro v3 对你的 HTTP 层没有硬性规定。你可以使用内置的文件系统路由,或者通过 `server.ts` 入口文件完全掌控,并携带你喜欢的任何框架: ```ts [server.ts] import { Hono } from "hono"; const app = new Hono(); app.get("/", (c) => c.text("Hello from Hono!")); export default app; ``` ## 🌐 H3 (v2) 与 Web 标准 Nitro v3 升级至 [H3 v2](https://h3.dev),后者围绕 Web 标准原语进行了全面重写:[`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)、[`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)、[`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) 和 [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL)。 结果是更简洁、更可移植的服务器代码: ```ts [routes/hello.ts] import { defineHandler } from "nitro"; export default defineHandler((event) => { const ua = event.req.headers.get("user-agent"); return { message: "Hello Nitro v3!", ua }; }); ``` 读取请求体使用原生 API: ```ts [routes/submit.ts] import { defineHandler } from "nitro"; export default defineHandler(async (event) => { const body = await event.req.json(); return { received: body }; }); ``` 没有包装器,没有为平台已提供的内容添加抽象。如果你懂 Web API,你就懂 H3 v2。 任何支持 Web 标准的内容都能与 Nitro 配合使用:[Elysia](https://elysiajs.com/)、[h3](https://h3.dev)、[Hono](https://hono.dev)。 ## 🗄️ 内置原语 Nitro 提供强大但小巧且**完全可选**的通用服务器原语,可在每个运行时中工作。 ::note 当不使用时,不会向服务器包添加任何额外内容。你仍然可以与 Nitro 的内置原语一起使用原生平台原语。 我们还在为开发环境提供平台特定原语的一流模拟,详情请参见 [env-runner](https://github.com/unjs/env-runner) 和 [nitrojs/nitro#4088](https://github.com/nitrojs/nitro/pull/4088)。 :: ### 存储 与运行时无关的键值层,提供 20 多种驱动:FS、Redis、S3、Cloudflare KV、Vercel Blob 以及[更多](https://unstorage.unjs.io/drivers)。将驱动附加到命名空间,并且无需更改应用代码即可进行切换。 ```ts import { useStorage } from "nitro/storage"; const storage = useStorage(); await storage.setItem("user:1", { name: "Nitro" }); ``` :read-more{to="/docs/storage"} ### 缓存 缓存服务器路由和函数,由存储层提供支持。开箱即支持 stale-while-revalidate、TTL 和自定义缓存键。 ```ts import { defineCachedHandler } from "nitro/cache"; export default defineCachedHandler((event) => { return "I am cached for an hour"; }, { maxAge: 60 * 60 }); ``` :read-more{to="/docs/cache"} ### 数据库 内置 SQL 数据库,开发环境默认使用 SQLite,可以使用相同的 API 连接到 Postgres、MySQL 等[更多](https://db0.unjs.io/connectors)。 ```ts import { useDatabase } from "nitro/database"; const db = useDatabase(); const users = await db.sql`SELECT * FROM users`; ``` :read-more{to="/docs/database"} ## 🌍 随处部署 将你的服务器构建到优化的 `.output/` 文件夹,兼容: - **运行时**:Node.js、Bun、Deno - **平台**:Cloudflare Workers、Netlify、Vercel、AWS Lambda、Azure、Firebase、Deno Deploy 等 无需配置。Nitro 会自动检测你的部署目标。无需更改任何一行代码,即可利用 ISR、SWR 和边缘渲染等平台特性。 ## 🎨 服务端渲染 使用你喜欢的模板引擎渲染 HTML,或直接在服务器上使用 React、Vue 或 Svelte 等组件库。通过客户端注水实现完全同构渲染。 Nitro 提供基础和渐进式方案:从 API 路由开始,在需要时添加渲染,并按照自己的节奏扩展到完整的 SSR。 :read-more{to="/docs/renderer"} ## 🟢 Nuxt v5 Nitro v3 将为 [Nuxt](https://nuxt.com) 的下一个主要版本提供动力。 [Nuxt v5](https://nuxt.com/blog/roadmap-v4) 将搭载 Nitro v3 和 H3 v2 作为核心,将 Web 标准请求处理、Rolldown 驱动的构建和 Vite 环境 API 带到 Nuxt 生态系统。 如果你是 Nuxt 用户,你可以通过熟悉 Nitro v3 的新 API 来提前准备,这些 API 将直接延续到 Nuxt 5,你可以[关注进展](https://github.com/nuxt/nuxt/discussions/34504)了解 Nuxt 中采用 Nitro v3 的情况。 ## 🏁 开始使用 ### 创建新项目 :pm-x{command="create-nitro-app"} 查看[快速入门指南](/docs/quick-start)获取完整的分步教程。 ## 🔄 从 v2 迁移 Nitro v3 引入了有意的破坏性变更,以建立更简洁的基础。以下是关键变更: - `nitropack` → `nitro`(包重命名) - `nitropack/runtime/*` → `nitro/*`(简洁的子路径导入) - `eventHandler` → `defineHandler`(H3 v2) - `createError` → `HTTPError`(H3 v2) - Web 标准的 `event.req` 请求头和请求体 API - Node.js 最低版本:**20** - 预设重命名和整合(例如,`cloudflare` → `cloudflare_module`) 完整列表请参见[迁移指南](/docs/migration)。 --- 感谢多年来为 Nitro 做出贡献的每一个人。我们迫不及待想看到你用新版 Nitro 构建的作品!❤️ - [GitHub](https://github.com/nitrojs/nitro):问题与讨论 - [Discord](https://discord.nitro.build):与社区聊天