路由
添加路由
你可以使用 H3.on、H3.[method] 或 H3.all 向 H3 实例 注册路由处理器。
示例: 注册一个路由,用于匹配 /hello 端点的 HTTP GET 请求。
- 使用
H3.[method]app.get("/hello", () => "Hello world!"); - 使用
H3.onapp.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 9110,HEAD 请求会自动匹配对应的 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:
requireContentType(event, acceptedTypes)— 断言请求的Content-Type(400/415/422)。appendAcceptQuery(event, mediaTypes)— 通过Accept-Query响应标头声明接受的查询格式。
QUERY 的处理方式与 GET 相同(通过 handleCacheHeaders 返回 304 响应);而 proxy 会将 QUERY连同其正文一起转发。与 GET 不同,QUERY不在 CORS 安全列表中,因此浏览器会发送预检请求——如果向 handleCors 传入显式的 methods 允许列表,请包含 "QUERY"。路由模式
路由模式是一个路径名,而不是 URL:其形式与 event.url.pathname 相同,并加上 rou3 语法。H3.on、H3.[method]、H3.all、H3.use(route, ...)、H3.mount 和 removeRoute 会以完全相同的方式对其进行规范化,因此,使用与路由相同字符串注册的中间件始终会守护该路由。
规范化规则:
- 缺少开头的
/时会自动添加("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
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 } });