H3 官方实战示例完全指南:Cookie、Session、静态资源、流式响应与数据校验
2026/9/17 12:49:39 网站建设 项目流程

H3 官方实战示例完全指南:Cookie、Session、静态资源、流式响应与数据校验

【免费下载链接】h3⚡️ Minimal H(TTP) framework built for high performance and portability项目地址: https://gitcode.com/GitHub_Trending/h31/h3

本文围绕 H3 官方文档 docs/4.examples 中的六类实战示例展开:Cookie 处理、HTTPQUERY方法、会话(Session)、静态资源服务、流式响应与数据校验。读完本文,你将掌握 H3 事件处理器中最常用的六大场景的完整写法、底层原理与可运行的完整代码,并了解如何结合 examples/ 目录中的可运行示例快速上手。

引言:官方示例能带给你什么

H3 是一个为高性能与可移植性而生的极简 HTTP 框架,其官方文档在 docs/4.examples/0.index.md 中汇总了一组“常见用法示例”:

  • Cookies —— 用 Cookie 在客户端存储数据
  • HTTP QUERY Method —— 在请求体中携带查询条件的安全只读请求
  • Session —— 用 Session 记住用户
  • Static Assets —— 服务 HTML、图片、CSS、JavaScript 等静态资源
  • Streaming Response —— 向客户端流式发送数据
  • Validation —— 在数据处理前校验数据的形状与合法性

这六个主题恰好覆盖了 Web 服务开发中最常见、最刚需的能力。本文将以这些文档为主体,逐一向你讲解每个示例的完整代码、配置参数、适用场景,并结合仓库中的源码实现(如 src/utils/cookie.ts、src/utils/session.ts、src/utils/static.ts)与可运行示例(examples/cookies.mjs、examples/query.mjs 等)做纵深剖析。

需要说明的是,H3 官方仓库在 examples/ 目录下提供了更多可直接运行的文件(如 examples/auth.mjs、examples/router.mjs、examples/websocket.mjs 等),文档中用::read-more组件指向了这些目录,你可按需查看。

一、处理 Cookie

在 H3 中处理 Cookie 非常直观,共有三个工具函数(见 docs/4.examples/handle-cookie.md 与源码 src/utils/cookie.ts):

  • setCookie:把 Cookie 附加到响应上;
  • getCookie:从请求中读取 Cookie;
  • deleteCookie:从响应中清除 Cookie。

1.1 设置 Cookie

在事件处理器中调用setCookie即可:

import { setCookie } from "h3"; app.use(async (event) => { setCookie(event, "name", "value", { maxAge: 60 * 60 * 24 * 7 }); return ""; });

setCookie的第四参是配置项,对应 Set-Cookie 响应头 的各种 Cookie 标志(flag),均是可选的:

选项作用
maxAge设置 Cookie 的过期时间,单位为秒(上例为 7 天)
expiresDate对象设置 Cookie 的过期时间
path设置 Cookie 的路径
domain设置 Cookie 的域名
secure设置Secure标志(仅 HTTPS 传输)
httpOnly设置HttpOnly标志(禁止脚本访问)
sameSite设置SameSite标志(strict/lax/none

仓库中的可运行示例 examples/cookies.mjs 演示了完整用法:访问/set时写入 Cookie,访问/时读取并回显:

import { H3, serve, getCookie, setCookie } from "h3"; export const app = new H3(); app .get("/", (event) => { const testCookie = getCookie(event, "testCookie"); return `testCookie is ${JSON.stringify(testCookie)} (go to /set to set it)`; }) .get("/set", (event) => { // By default, path is set to `/`. You can use any of the options supported by the Set-Cookie header. setCookie(event, "testCookie", "bar", { httpOnly: true }); return "TestCookie is set. Go back to / to see it!"; }); serve(app);

示例中的注释提到:path默认为/,并且Set-Cookie头支持的所有选项都可以直接传入。运行node examples/cookies.mjs后访问http://localhost:3000即可体验。

1.2 读取 Cookie

读取 Cookie 同样简单:

import { getCookie } from "h3"; app.use(async (event) => { const name = getCookie(event, "name"); // do something... return ""; });

如果 Cookie 存在,getCookie返回其值;否则返回undefined

1.3 删除 Cookie

删除 Cookie 使用deleteCookie

import { deleteCookie } from "h3"; app.use(async (event) => { deleteCookie(event, "name"); return ""; });

从源码看,deleteCookie实际上是setCookie的一个包装:把值设置为""maxAge设置为0,从而让客户端立即使该 Cookie 过期失效。这样即可把 Cookie 从客户端擦除。

二、HTTPQUERY方法:在请求体中携带查询

HTTPQUERY方法(RFC 10008)与GET一样安全(safe)、幂等(idempotent)、可缓存(cacheable),但它的查询条件放在请求中,并带有Content-Type。它是“我想要一个 GET,但查询条件太大或太结构化、放不进 URL”时的标准答案。

H3 将QUERY作为一等公民方法支持:可通过app.query()注册处理器(底层等价于app.on("QUERY", …)),并提供了两个辅助工具函数。关于路由层面的完整说明,可参考 docs/1.guide/1.basics/2.routing.md 中的 “HTTP QUERY Method” 小节。

2.1 注册QUERY处理器

像处理POST一样读取请求体即可:

import { readBody } from "h3"; app.query("/books", async (event) => { const query = await readBody(event, { type: "text" }); return runSearch(query); });

[!NOTE] 由于QUERY携带的是可被攻击者控制的请求体,因此与POST一样会应用请求体大小限制。

2.2 宣告可接受的查询格式

使用appendAcceptQuery告知客户端某个资源支持哪些查询格式。它会设置Accept-Query响应头(一个 Structured Fields List),并且也可以在普通的GET上设置,让客户端在发送QUERY之前先发现可用的格式:

import { appendAcceptQuery } from "h3"; app.get("/books", (event) => { appendAcceptQuery(event, ["application/sql", "application/jsonpath"]); // Accept-Query: application/sql, application/jsonpath return "Send a QUERY request with a SQL or JSONPath body."; });

2.3 校验Content-Type

使用requireContentType强制实施 RFC 的错误语义:它返回匹配到的媒体类型,或在缺失时抛出400、不支持时抛出415、格式错误时抛出422

import { requireContentType, readBody } from "h3"; app.query("/books", async (event) => { const type = requireContentType(event, ["application/sql", "application/jsonpath"]); const query = await readBody(event, { type: "text" }); return runQuery(type, query); });

2.4 提供可缓存的GET替代方案

QUERY的响应无法用 URL 定位,因此浏览器和 CDN 无法缓存它。RFC 10008 建议通过Content-Location响应头把客户端指向一个等价的、可缓存的GET端点:把查询结果存放在稳定的 id 下,客户端即可用普通的、HTTP 可缓存的GET重复该查询:

app.query("/books", async (event) => { const result = runQuery(type, query); const id = queryId(type, query); // stable hash of the query cache.set(id, result); event.res.headers.set("content-location", `/books/${id}`); return result; });

2.5 完整可运行示例

仓库中的 examples/query.mjs 是一个自包含、可直接运行的完整演示:一个/books资源,接受 SQL 风格与 JSONPath 风格的查询,校验Content-Type,并宣告可缓存的GET替代方案;同时还在/上提供了一个小型交互页面。可用node examples/query.mjs本地运行。

它的核心逻辑包括:用 FNV-1a 哈希生成稳定查询 id(queryId),把每次查询结果存入Map缓存;/books/:id端点从缓存取出结果并设置cache-control: public, max-age=60,让浏览器/CDN 可以缓存;/booksQUERY处理器则依次执行appendAcceptQueryrequireContentTypereadBody({ type: "text" })→ 计算 id 并设置Content-Location

你还可以用curl从终端体验:

# 发现可接受的查询格式 curl -i http://localhost:3000/books # -> Accept-Query: application/sql, application/jsonpath # SQL 查询(注意响应中的 Content-Location 头) curl -i -X QUERY http://localhost:3000/books \ -H "Content-Type: application/sql" \ --data "SELECT * FROM books WHERE author = 'Simpson'" # -> 200, Content-Location: /books/<id> # 通过可缓存的 GET 替代方案重新获取同一结果 curl -i http://localhost:3000/books/<id> # -> 200, Cache-Control: public, max-age=60 # JSONPath 查询 curl -X QUERY http://localhost:3000/books \ -H "Content-Type: application/jsonpath" \ --data '$[?(@.year==2015)]' # 不支持的格式 curl -i -X QUERY http://localhost:3000/books -H "Content-Type: text/plain" --data x # -> 415 Unsupported Media Type

[!NOTE] 与GET不同,QUERY不在CORS 的安全列表(safelist)中,浏览器会先发送预检请求(preflight)。如果你给handleCors传了显式的methods白名单,请把"QUERY"加进去。

另外从路由文档可知,QUERY在条件缓存上与GET同等对待(可通过handleCacheHeaders返回304),proxy转发QUERY时也会带上其请求体

三、Session:用会话记住用户

会话(Session)是一种通过 Cookie 记住用户的方式,是认证用户或保存其偏好(如语言、个性化设置)的常见手段。H3 为此提供了丰富的工具函数(见 docs/4.examples/handle-session.md):

  • useSession:初始化一个会话,返回控制它的包装器;
  • getSession:初始化或获取当前用户的会话;
  • updateSession:更新当前会话的数据;
  • clearSession:清除当前会话。

大多数时候,你只需要useSession就足够了,因为它在底层会自动调用其余几个工具。

3.1 初始化一个会话

import { useSession } from "h3"; app.use(async (event) => { const session = await useSession(event, { password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9", }); // do something... });

这会初始化一个会话,并返回一个名为h3的 Cookie(Set-Cookie响应头),内容经过加密。如果请求中带有名为h3的 Cookie 或名为x-h3-session的请求头,会话将以该 Cookie/请求头中的内容初始化。

[!NOTE] 请求头优先于 Cookie。

[!WARNING]password会对每个会话 Cookie 进行密封(seal),它的熵才是真正的安全边界。被盗的会话 Cookie 会以明文携带盐与完整性摘要,因此弱密码或可猜测的密码可以被离线暴力破解——提高 PBKDF2 迭代次数只能减缓破解速度,无法修复低熵密钥的问题。请始终从密码学安全随机源生成密码,例如:

node -e "console.log(require('node:crypto').randomBytes(32).toString('base64'))"

上文示例使用硬编码值是为了可读性;真实应用中,请从环境变量(如process.env.SESSION_PASSWORD)加载至少 32 字符的随机生成的密钥,且切勿提交到版本控制。可猜测的口令(即使超过 32 个字符)也不安全。

3.2 从会话读取数据

数据存放在会话的data属性中;如果没有数据,则为空对象{}。读取仍使用useSession(底层调用getSession):

import { useSession } from "h3"; app.use(async (event) => { const session = await useSession(event, { password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9", }); return session.data; });

3.3 向会话写入数据

写入仍使用useSession(底层调用updateSession)。下面是一个经典的“访问计数器”示例:

import { useSession } from "h3"; app.use(async (event) => { const session = await useSession(event, { password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9", }); const count = (session.data.count || 0) + 1; await session.update({ count: count, }); return count === 0 ? "Hello world!" : `Hello world! You have visited this page ${count} times.`; });

流程是:先从请求中获取会话,若不存在则新建;然后把count属性加一并通过session.update()写回;最后返回访问次数。多次刷新页面即可看到计数递增。

[!NOTE] 如果用curl之类的 CLI 工具测试,由于 CLI 不保存 Cookie,计数不会递增。你需要从响应中取出 Cookie 并在后续请求中回传。

3.4 清除会话

清除会话仍使用useSession(底层调用clearSession):

import { useSession } from "h3"; app.use("/clear", async (event) => { const session = await useSession(event, { password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9", }); await session.clear(); return "Session cleared"; });

H3 会发送一个名为h3的空 Cookie 的Set-Cookie响应头来清除会话。

3.5 配置选项

useSession的第二个参数是配置对象,除password外所有选项都是可选的:

import { useSession } from "h3"; app.use(async (event) => { const session = await useSession(event, { name: "my-session", password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9", cookie: { httpOnly: true, secure: true, sameSite: "strict", }, maxAge: 60 * 60 * 24 * 7, // 7 days }); return session.data; });

值得单独说明的是name选项:它决定了存储会话的 Cookie 名称,默认为h3。H3 还会从一个由name派生的请求头读取会话,该请求头被规范化为小写的x-${name.toLowerCase()}-session,所以默认名称h3对应x-h3-session请求头;即使name写成混合大小写(如MyApp),请求头仍解析为小写的x-myapp-session,而 Cookie 保留原始大小写。这正是前文示例中 Cookie 名为h3的原因。

[!NOTE] 会话 Cookie 默认值为:secure: truehttpOnly: truesameSite: "lax"path: "/"。均可通过cookie选项覆盖。

[!NOTE]secure: true会告诉浏览器仅在 HTTPS 下存储和发送 Cookie。本地开发使用纯 HTTP 时,合规浏览器(尤其是 Safari 和 iOS,以及某些本地域名下的 Chrome)会静默丢弃该 Cookie,导致会话无法持久化。本地开发时请设置cookie: { secure: false }解决。

3.6 过期机制

会话有两套相互独立的过期控制,可以单独使用也可以同时使用:

  • maxAge绝对生命周期,从会话创建时起算,无论用户多活跃都会到期;
  • idleTimeout滑动生命周期,从最后一次请求起算。活跃用户保持登录,闲置用户被登出。
const session = await useSession(event, { password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9", idleTimeout: 60 * 30, // signed out after 30 minutes of inactivity... maxAge: 60 * 60 * 24 * 7, // ...and after 7 days regardless });

设置idleTimeout后,H3 通过重新密封(reseal)会话 Cookie 来前移空闲窗口,并把重新密封的时间戳写入其中。createdAt保持不变,这正是maxAge仍能作为硬性上限的原因。Cookie 的Expires设为两者中先到期的那个。

从源码 src/utils/session.ts 可以印证这些机制:过期判定读取createdAtmaxAge的基准)与lastSeenAtidleTimeout的基准,仅在设置idleTimeout时写入);最终过期时间取createdAt + maxAgelastSeenAt + idleTimeout的较早者。

如果你来自express-sessionkoa-sessionidleTimeout就相当于它们的rolling选项。区别在于它自带独立时长,而不是重新解释maxAge,所以启用它不会牺牲绝对上限。

重密封是会话中最昂贵的操作,因此 H3 不会在每次请求时都做:只有当窗口被用掉超过一半时才重密封,且更新会话也算一次重密封。因此活跃用户永远不会被登出,但记录的最近活动时间可能比真实时间最多落后半个窗口:

// idleTimeout: 60 * 30 // Sign-out happens 15 to 30 minutes after the last request, never later.

如果你需要把该区间较短的一端作为真实上限,可以把idleTimeout减半。相关实现可参见源码中基于SLIDE_THRESHOLDshouldSlide判定(见 src/utils/session.ts)。

[!NOTE] 只有 Cookie 会话才会滑动。通过x-{name}-session请求头传输的会话无法重密封,因此它在密封签发后经过idleTimeout就会过期。

[!IMPORTANT] 因为会话存放在 Cookie 中,只读会话的请求在滑动窗口时也会把它写回。如果这样的请求与写入会话的请求并发,浏览器最后应用哪个响应,哪个就生效,写入可能丢失。而不设置idleTimeout时,只读请求不会设置任何 Cookie,也就不会覆盖并发写入。

[!NOTE] 滑动窗口的请求需要额外付出一次密封,并在响应中携带Set-Cookie头——共享缓存和 CDN 通常拒绝存储这类响应。而只在节流窗口内读取会话的请求不会设置任何 Cookie。

另外,会话 Cookie 同样应用于错误响应,因此抛出异常的请求仍会滑动窗口,并在其中创建的新会话依然会被持久化。

3.7 使用多个会话

由于每个会话存放在各自的name下,你可以对同一个请求运行多个相互独立的会话。它们位于不同的 Cookie 中、互不覆盖,非常适合隔离不相关的关注点,例如长生命周期的认证会话与短生命周期的 flash 消息:

import { useSession } from "h3"; app.use(async (event) => { const auth = await useSession(event, { name: "auth", password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9", }); const flash = await useSession(event, { name: "flash", password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9", }); await flash.update({ message: "Saved!" }); // `auth` and `flash` are backed by different cookies, so they stay separate return { user: auth.data.user, flash: flash.data.message }; });

[!NOTE] 请为每个会话指定不同的name。两个同名的会话共享同一个 Cookie,后写者胜出。

四、静态资源服务

H3 可以服务 HTML、图片、CSS、JavaScript 等静态资源,核心工具是serveStatic(见 docs/4.examples/serve-static-assets.md 与源码 src/utils/static.ts)。

4.1 基本结构

import { H3, serveStatic } from "h3"; const app = new H3(); app.use("/public/**", (event) => { return serveStatic(event, { getContents: (id) => { // TODO }, getMeta: (id) => { // TODO }, }); });

仅凭这段代码还不会服务任何文件——你必须实现getContentsgetMeta两个方法:

  • getContents:读取文件内容。返回一个Promise,解析为文件内容;文件不存在时返回undefined
  • getMeta:获取文件的元数据。返回一个Promise,解析为元数据;文件不存在时返回undefined

两者分离的设计目的是:让 H3 可以在不读取文件内容的情况下响应HEAD请求,并使用Last-Modified头做条件请求。

4.2 结合node:fs的实现

先在public目录中创建一个带简单消息的index.html,然后在浏览器打开http://localhost:3000,你就能看到这条消息。接着实现getContentsgetMeta

import { stat, readFile } from "node:fs/promises"; import { join } from "node:path"; import { H3, serve, serveStatic } from "h3"; const app = new H3(); app.use("/public/**", (event) => { return serveStatic(event, { indexNames: ["/index.html"], getContents: (id) => readFile(join("public", id)), getMeta: async (id) => { const stats = await stat(join("public", id)).catch(() => {}); if (stats?.isFile()) { return { size: stats.size, mtime: stats.mtimeMs, }; } }, }); }); serve(app);

getContents读取文件并返回其内容;getMetafs.stat获取文件元数据,若文件不存在或不是普通文件则返回undefined,否则返回文件大小size与最后修改时间mtime

从源码看,serveStatic的配置还支持etagindexNames、编码协商等选项(src/utils/static.ts):id的解析与event.url.pathname保持一致,默认会按["/index.html"]查找目录索引文件;当getMeta返回了etag时,H3 会把它设置为响应头的etag;随后通过条件请求匹配(isCacheMatch)判断是否返回304 Not Modified

文件大小与最后修改时间用于生成 etag,如果文件自上次请求以来未发生变化,H3 会返回304 Not Modified响应,避免重复发送相同的文件——这对减少带宽消耗很有价值。

五、流式响应(Stream Response)

流式响应允许你在拿到数据的第一时间就发送给客户端,非常适合大文件或长时间运行的响应(见 docs/4.examples/stream-response.md)。

5.1 创建一个流

先使用 Web 标准的ReadableStreamAPI 创建流:

const stream = new ReadableStream();

下面这个示例创建一个start函数:每 100 毫秒发送一个随机数,1000 毫秒后关闭流:

let interval: NodeJS.Timeout; const stream = new ReadableStream({ start(controller) { controller.enqueue("<ul>"); interval = setInterval(() => { controller.enqueue("<li>" + Math.random() + "</li>"); }, 100); setTimeout(() => { clearInterval(interval); controller.close(); }, 1000); }, cancel() { clearInterval(interval); }, });

5.2 发送流

import { H3 } from "h3"; export const app = new H3(); app.use((event) => { // Set to response header to tell to the client that we are sending a stream. event.res.headers.set("Content-Type", "text/html"); event.res.headers.set("Cache-Control", "no-cache"); event.res.headers.set("Transfer-Encoding", "chunked"); let interval: NodeJS.Timeout; const stream = new ReadableStream({ start(controller) { controller.enqueue("<ul>"); interval = setInterval(() => { controller.enqueue("<li>" + Math.random() + "</li>"); }, 100); setTimeout(() => { clearInterval(interval); controller.close(); }, 1000); }, cancel() { clearInterval(interval); }, }); return stream; });

关键点有三处:

  1. 先设置三个响应头告知客户端正在接收流:Content-Type: text/htmlCache-Control: no-cacheTransfer-Encoding: chunked
  2. ReadableStreamstart(controller)通过controller.enqueue(...)逐步推入数据,用controller.close()结束,并在cancel()中清理定时器,避免客户端断开后继续泄漏资源;
  3. 直接把stream作为处理器返回值返回,H3 会将其作为响应体发送。

打开浏览器访问http://localhost:3000,你会看到一个随机数列表每 100 毫秒新增一项。

六、数据校验(Validation)

服务端收到数据后必须先校验——所谓校验,是指接收数据的形状必须符合预期形状。这一点至关重要,因为你无法信任来自未知来源(用户或外部 API)的数据(见 docs/4.examples/validate-data.md)。

[!WARNING] 不要把类型泛型当作校验。给readBody之类的工具提供一个 interface 并不是校验,必须在使用数据之前真正执行校验。

6.1 校验工具一览

H3 提供三个校验工具:

  • getValidatedQuery—— 校验 query;
  • getValidatedRouterParams—— 校验路由参数(params);
  • readValidatedBody—— 校验请求体(body)。

H3 本身不捆绑任何校验库,但支持来自Standard-Schema兼容库的 schema,例如 Zod、Valibot、ArkType 等。若你想使用不兼容 Standard-Schema 的校验库,仍然可以,但必须使用该库自带的解析函数(参见下文“安全解析”一节)。

[!WARNING] H3 是运行时无关的,可在任意运行时中使用;但部分校验库并非兼容所有运行时。

6.2 校验路由参数

getValidatedRouterParams替代getRouterParams,传入 schema 后直接得到校验结果:

import { getValidatedRouterParams } from "h3"; import * as z from "zod"; import * as v from "valibot"; // Example with Zod const contentSchema = z.object({ topic: z.string().min(1), uuid: z.string().uuid(), }); // Example with Valibot const contentSchema = v.object({ topic: v.pipe(v.string(), v.nonEmpty()), uuid: v.pipe(v.string(), v.uuid()), }); app.all( // You must use a router to use params "/content/:topic/:uuid", async (event) => { const params = await getValidatedRouterParams(event, contentSchema); return `You are looking for content with topic "${params.topic}" and uuid "${params.uuid}".`; }, );

发送合法请求/content/posts/123e4567-e89b-12d3-a456-426614174000,得到:

You are looking for content with topic "posts" and uuid "123e4567-e89b-12d3-a456-426614174000".

如果校验失败,H3 会抛出400 Validation Error,错误数据中包含校验错误明细,可在客户端用来向用户展示友好的错误信息。

6.3 校验查询参数

getValidatedQuery替代getQuery。与上例不同的是,这里还能借助校验库转换输入数据——例如把数字的字符串表示转换为真正的数字,这对分页等场景非常有用:

import { getValidatedQuery } from "h3"; import * as z from "zod"; import * as v from "valibot"; // Example with Zod const stringToNumber = z.string().regex(/^\d+$/, "Must be a number string").transform(Number); const paginationSchema = z.object({ page: stringToNumber.optional().default(1), size: stringToNumber.optional().default(10), }); // Example with Valibot const stringToNumber = v.pipe( v.string(), v.regex(/^\d+$/, "Must be a number string"), v.transform(Number), ); const paginationSchema = v.object({ page: v.optional(stringToNumber, 1), size: v.optional(stringToNumber, 10), }); app.use(async (event) => { const query = await getValidatedQuery(event, paginationSchema); return `You are on page ${query.page} with ${query.size} items per page.`; });

发送合法请求/?page=2&size=20,得到:

You are on page 2 with 20 items per page.

校验失败时同样抛出400 Validation Error,错误数据中带有校验错误明细。

6.4 校验请求体

readValidatedBody替代readBody

import { readValidatedBody } from "h3"; import { z } from "zod"; import * as v from "valibot"; // Example with Zod const userSchema = z.object({ name: z.string().min(3).max(20), age: z.number({ coerce: true }).positive().int(), }); // Example with Valibot const userSchema = v.object({ name: v.pipe(v.string(), v.minLength(3), v.maxLength(20)), age: v.pipe(v.number(), v.integer(), v.minValue(0)), }); app.use(async (event) => { const body = await readValidatedBody(event, userSchema); return `Hello ${body.name}! You are ${body.age} years old.`; });

发送一个合法的 JSON POST 请求体:

{ "name": "John", "age": 42 }

得到:

Hello John! You are 42 years old.

非法请求同样返回400 Validation Error及校验错误明细。

6.5 安全解析(Safe Parsing)

默认情况下,直接向三个校验工具传入 schema(作为第二个参数)时,校验失败会抛出400 Validation Error。但在某些场景下你可能想自己处理校验错误——这时需要把校验库的安全解析函数作为第二个参数传入。

回到第一个示例,Zod 的写法如下:

import { getValidatedRouterParams } from "h3"; import { z } from "zod/v4"; const contentSchema = z.object({ topic: z.string().min(1), uuid: z.string().uuid(), }); app.all("/content/:topic/:uuid", async (event) => { const params = await getValidatedRouterParams(event, contentSchema.safeParse); if (!params.success) { // Handle validation errors return `Validation failed:\n${z.prettifyError(params.error)}`; } return `You are looking for content with topic "${params.data.topic}" and uuid "${params.data.uuid}".`; });

Valibot 的写法如下:

import { getValidatedRouterParams } from "h3"; import * as v from "valibot"; const contentSchema = v.object({ topic: v.pipe(v.string(), v.nonEmpty()), uuid: v.pipe(v.string(), v.uuid()), }); app.all("/content/:topic/:uuid", async (event) => { const params = await getValidatedRouterParams(event, v.safeParser(contentSchema)); if (!params.success) { // Handle validation errors return `Validation failed:\n${v.summarize(params.issues)}`; } return `You are looking for content with topic "${params.output.topic}" and uuid "${params.output.uuid}".`; });

两种写法都接收{ success, ... }形式的结果对象:失败时自行处理错误并返回友好的提示,成功时从params.data(Zod)或params.output(Valibot)取出已解析的数据。同理,getValidatedQueryreadValidatedBody也支持传入安全解析函数。

结语:从示例到生产

回顾这六大示例,可以看到 H3 的设计哲学:核心框架保持极简,通用能力以轻量工具函数的形式提供。Cookie 的三个工具函数覆盖了读写删全流程;QUERY方法的一等公民支持让复杂只读查询有了标准答案;Session 在useSession一个入口下隐藏了密封、滑动过期与多会话隔离等复杂机制;serveStatic通过getContents/getMeta的职责分离,天然支持HEAD与条件请求;ReadableStream让流式响应无需任何额外抽象;而基于 Standard-Schema 的校验工具则让你自由选择 Zod、Valibot 等生态。

每个示例在仓库中都有对应的可运行文件与测试佐证。你可以在 examples/ 目录中找到cookies.mjsquery.mjs等完整实现直接运行;相关单元测试(如 test/unit/cookie-validation.test.ts、test/session.test.ts、test/static.test.ts、test/validate.test.ts)则进一步验证了这些能力的行为边界。建议你结合本文逐步运行这些示例,再对照源码理解其底层实现,即可快速把这些实战能力应用到自己的 H3 服务中。

【免费下载链接】h3⚡️ Minimal H(TTP) framework built for high performance and portability项目地址: https://gitcode.com/GitHub_Trending/h31/h3

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询