路由

每个请求会匹配到一个(最具体的)路由处理器。

添加路由

你可以使用 H3.onH3.[method]H3.allH3 实例 注册路由处理器

路由器由 🌳 Rou3 提供支持,这是一款超快且体积小巧的路由匹配引擎。

示例: 注册一个路由,用于匹配 /hello 端点的 HTTP GET 请求。

  • 使用 H3.[method]
    app.get("/hello", () => "Hello world!");
    
  • 使用 H3.on
    app.on("GET", "/hello", () => "Hello world!");
    

你可以为同一路由注册多个不同方法的事件处理器:

app
  .get("/hello", () => "GET Hello world!")
  .post("/hello", () => "POST Hello world!")
  .all("/hello", () => "Any other method!");

你也可以使用 H3.all 方法注册一个允许接受任意 HTTP 方法的路由:

app.all("/hello", (event) => `This is a ${event.req.method} request!`);

HEAD 请求

根据 RFC 9110HEAD 请求会自动匹配对应的 GET 路由并运行其处理器,但会省略响应正文(仅发送标头和状态)。无需注册单独的 HEAD 处理器:

app.get("/hello", () => "Hello world!");

// HEAD /hello → 200,具有与 GET 相同的标头,但正文为空

当你想要覆盖此行为时,可以注册显式的 HEAD 处理器——例如,跳过正文计算:

app.head("/hello", (event) => {
  event.res.headers.set("content-length", "12");
  return null;
});

显式的 head() 路由始终优先于自动的 GET 回退。

HTTP QUERY 方法

H3 将 HTTP QUERY 方法(RFC 10008) 作为一等方法提供支持。QUERY 类似于 GET —— 安全、幂等且可缓存 —— 但携带请求正文(包含 Content-Type),弥补了长期存在的“带正文的 GET”空白。对于无法将筛选条件放入 URL 的复杂读取操作,它非常理想。

使用 app.query()(或 app.on("QUERY", …))注册 QUERY 处理器,并像往常一样读取请求正文:

import { readBody } from "h3";

app.query("/search", async (event) => {
  const criteria = await readBody(event); // 读取 query 正文
  return runSearch(criteria);
});

由于 QUERY 携带攻击者可控的正文,因此正文大小限制POST 一样适用。

以下两个工具函数有助于实现该 RFC:

对于条件缓存QUERY 的处理方式与 GET 相同(通过 handleCacheHeaders 返回 304 响应);而 proxy 会将 QUERY连同其正文一起转发。与 GET 不同,QUERY不在 CORS 安全列表中,因此浏览器会发送预检请求——如果向 handleCors 传入显式的 methods 允许列表,请包含 "QUERY"
请参阅 HTTP QUERY 方法示例,其中提供了一个可运行的 /books 资源,用于验证 Content-Type 并声明可缓存的 GET 替代方案。

路由模式

路由模式是一个路径名,而不是 URL:其形式与 event.url.pathname 相同,并加上 rou3 语法。H3.onH3.[method]H3.allH3.use(route, ...)H3.mountremoveRoute 会以完全相同的方式对其进行规范化,因此,使用与路由相同字符串注册的中间件始终会守护该路由。

规范化规则:

  • 缺少开头的 / 时会自动添加("hello"/hello)。
  • URL 会被拒绝app.get("http://example.com/admin") 会抛出异常)。权限机构部分绝不会被静默丢弃://admin 会注册为包含两个段的路径 //admin,而不是 /
  • ... 段会按照 URL 解析器解析请求路径的方式进行解析(/admin/../admin/admin)。
  • 请求路径名始终携带百分号编码的字符会被编码:空格、非 ASCII 字符、控制字符、"#<>`。因此,app.get("/café") 会注册为 /caf%C3%A9——这正是浏览器实际发送的形式。
  • 不必要的转义会被解码为请求路径名规范化后的字面量(/%40handle/@handle,请参阅安全工具)。

带有 rou3 语义的字符会完全按原样保留,包括转义符 \(在请求路径名中永远无效)。如果要匹配字面意义上的 ?{}^,请将其写成百分号编码形式:

app.get("/u/:id?", () => "optional param"); // rou3 syntax, kept as written
app.get("/x%3Fy", () => "literal ?"); // matches the path a client sends for /x?y
非 ASCII 文本可以与动态语法自由混用——app.get("/café/:id") 会注册为 /caf%C3%A9/:id,并匹配 /café/42——但有两个例外,这两个例外都源于编码后的形式进入 rou3 自身的语法。参数名称必须是 ASCII([\w-]):/:naïve 会变成 /:na%C3%AFve,rou3 会将其解析为名为 na 的参数,后面跟着字面量 %C3%AFve。而在 (...) 组中,只有字面文本和交替项会在编码后保留((café|thé) 有效);字符类则无效,因为 [é] 会变成 [%C3%A9]——请改为使用编码后的交替项 (?:%C3%A9)

动态路由

你可以通过 : 前缀定义动态路由参数:

// [GET] /hello/Bob => "Hello, Bob!"
app.get("/hello/:name", (event) => {
  return `Hello, ${event.context.params.name}!`;
});

你也可以使用 * 表示未命名的可选参数:

app.get("/hello/*", (event) => `Hello!`);

通配符路由

添加 /hello/:name 路由将匹配 /hello/world/hello/123,但不会匹配 /hello/foo/bar。 当你需要匹配多级子路由时,可以使用 ** 前缀:

app.get("/hello/**", (event) => `Hello ${event.context.params._}!`);

这将匹配 /hello/hello/world/hello/123/hello/world/123 等路径。

参数 _ 会以单个字符串的形式存储完整的通配符内容。

路由元数据

您可以在注册路由时定义可选的路由元数据,任何中间件都可以访问。

import { H3 } from "h3";

const app = new H3();

app.use((event) => {
  console.log(event.context.matchedRoute?.meta); // { auth: true }
});

app.get("/", (event) => "Hi!", { meta: { auth: true } });
在使用 defineHandler 对象语法定义路由时,也可以添加路由元信息。