H3 路由规则代理与自定义
内置规则覆盖不了所有场景:还要把请求转给另一台机器、还要按自己的业务做一条规则、还要在几千条规则下保持匹配速度。这一节讲这三件事——代理、自定义规则处理器、以及让规则跑得更快的两个机制。
代理:也是可选处理器
proxy 和 cache 一样是按需引入的。处理器内部用 proxyRequest,所以单独放在 h3/rules/proxy,不代理的应用不会把它打进产物。规则存在却没注册处理器,创建匹配器时抛错。
import { routeRules } from "h3/rules";
import { proxy } from "h3/rules/proxy";
app.use(
routeRules(
{ "/api/proxy/**": { proxy: "https://example.com/**" } },
{ handlers: { proxy } },
),
);
代理目标的行为与 redirect 目标完全一致:/** 尾部会被替换,原查询串会被转发。所以 3.1 里讲的两条规则在这里同样成立——尾巴不能改变目标主机;请求已在目标上时不会自己转自己。
只留数据、不真正代理
有时框架想把 proxy 当作一份声明,由外层构建工具去实现。传 handlers: { proxy: undefined } 即可:规则照样匹配、合并、挂到上下文,但不产生任何运行时行为。cache 同理。
目标安全:两条必须知道的拒绝条件
代理比重定向更危险——它会把请求送到别处并带回响应。H3 在两条线上设了闸:
第一条:尾巴不能逃出目标基路径。 拼好的路径会与目标自身基路径比对,会逃逸的请求(例如用编码的 ..%2f 做目录穿越)直接 400。
第二条:前缀段数必须稳定。 H3 靠「按段数移除规则模式的前缀」得到尾巴,所以同一模式的每次匹配,前缀都得是同样的段数。下列模式会让前缀段数变化,H3 对这类请求返回 400,而不是转发一个删错前缀的路径:
| 模式 | 结果 | 原因 |
|---|---|---|
/:lang?/old/** |
拒绝 | 可选参数,前缀可能是 1 段或 2 段 |
/x{/a}?/old/** |
拒绝 | 分组跨越了分隔符 |
/:lang/old/** |
正常 | 普通参数,恒定 1 段 |
/x/:id(\d+)/old/** |
正常 | 正则参数,恒定 1 段 |
/blog{-:title}?/old/** |
正常 | 单段内的分组 |
/x/*/old/** |
创建时就拒绝 | * 是 catch-all,一条路由只能有一个 |
需要「一段但内容不定」时,用 :param 或 ([^\x2f]*),不要用 *。
自定义规则处理器
内置规则不够时,自己写一个。处理器定义形状是 { handler, order? }:
handler把命中的规则变成 H3 中间件;order决定执行时机,越小越早,默认0。内置规则占-3到-1与1到3。
处理器收到四样东西:{ options, route, params?, handler? }。options 是合并后的规则值;route 是贡献该值的最具体模式——这份来源信息在 event.context.routeRules 上是拿不到的。
app.use(
routeRules(
{ "/x/**": { shout: "hello" } },
{
handlers: {
// 返回值必须是中间件:(event) => ...
shout: {
handler: (matched) => (event) => {
event.res.headers.set("x-shout", String(matched.options).toUpperCase());
},
},
},
},
),
);
❗ 如果你的自定义处理器限制访问(鉴权门、限流、IP 白名单),必须同时设置
restricting: true。默认值是false——对「授权」是安全的选择,对「限制」却是错的选择,而且不会有任何警告。原因在 3.4 讲false重置时展开。
性能:两个开关
记忆化(默认开)
同一个 method + pathname 永远得到同样的合并结果,所以 routeRules() 默认按 method + pathname 记忆化。重复请求跳过模式查找、路径归一化、合并与中间件构造,热路径退化成一次表查询。
- 默认上限 1024 条,动态路径不会把表撑爆;用
memoize: { max }调整。 - 驱逐用 SIEVE:按插入顺序淘汰,但在淘汰指针上次扫过之后被访问过的条目能活过这一轮。这样一小撮热点路径不会被大量一次性动态路径冲掉——纯 FIFO 会一起淘汰;而命中仍然只是一次查表,不像 LRU 要在读时调整顺序。
memoize: false则每个请求都重新解析,并生成全新的结果对象。
底层匹配器不自动开记忆化,要显式包一层 memoizeRouteRulesMatcher(matcher, opts?);不包,打包器就能把这段代码摇掉。
预合并(preMerge)
设 preMerge: true,每条模式的继承链在匹配器启动时(或构建期由编译器)提前合并好。之后每个请求只解析最具体的那一层,而不是把每层都合并一遍。方法专属规则、通用规则、false 重置、每规则的 params 行为都不变。
但它只对「关系可以事先算清」的规则集成立。像 /a/:x/c 与 /a/b/:y 这种部分重叠(最具体匹配本身有歧义),或带正则参数的路径,无法安全分析:
| 场景 | 结果 |
|---|---|
| 运行时匹配器 | 启动时抛错(宁可启动失败,不带着错误结果跑) |
| 构建期编译器 | 安全降级:打一条警告,改用普通编译,产物依然正确 |
直接使用匹配器
多数应用用 routeRules() 就够。需要在中间件之外解析规则的框架,可以直接用底层导出:
import {
createRouteRulesMatcher,
normalizeRouteRules,
memoizeRouteRulesMatcher,
} from "h3/rules";
import { cache } from "h3/rules/cache";
// 注意:createRouteRulesMatcher 要求「已归一化」的规则
const matcher = memoizeRouteRulesMatcher(
createRouteRulesMatcher(normalizeRouteRules(config), {
baseURL: "/base",
preMerge: true,
handlers: { cache },
}),
);
const { routeRules, matchedRules, routeRuleMiddleware } = matcher("GET", "/blog/post");
三个返回值的分工:
| 返回值 | 内容 |
|---|---|
routeRules |
合并后的值,与挂在 event.context.routeRules 上的是同一份 |
matchedRules |
每项带上来源模式(route)与 rou3 参数(params) |
routeRuleMiddleware |
排好序的中间件链 |
normalizeRouteRules() 负责把 swr 这类简写展开、把字符串与布尔形式归一、把键规范化——routeRules() 内部会自动做,createRouteRulesMatcher() 则期待你已经做完。这条分工是为了让不需要归一化的运行时产物不必打包这段代码。
另外两个导出:mergeMatchedRouteRules() 是纯合并函数(输入匹配层、输出匹配规则);ruleHandlers 是 headers、redirect、cors 的默认处理器注册表。
⚠️ 匹配器 API 收到的方法必须是大写。
routeRules()中间件会替你归一化,但手写的包装器要自己传event.req.method.toUpperCase()——否则一个用小写拼的请求不会命中任何方法限定规则,连带跳过它的鉴权门。
一个容易搞坏的写法
routeRules() 要放在 app.use() 里,不要挂到单条路由上:
// 能跑,但每条请求会把整条规则链跑两遍
app.get("/api/x", handler, { middleware: [routeRules(rules, { handlers: { cache } })] });
这样写时,整条规则链被塞进了 cache 规则要分发的那个 handler 内部,分发会重新进入自己。规则能识别重入并继续走到路由 handler(响应与缓存结果都是对的),但其余每条命中的规则会在两趟里各跑一次。
排查表
| 现象 | 原因 |
|---|---|
| 启动就抛错,说某规则没有处理器 | cache / proxy 忘了在 handlers 里注册 |
| 代理返回 400 | 尾巴会逃出目标基路径,或模式的前缀段数不固定 |
| 自定义鉴权规则被编码路径绕过 | 漏了 restricting: true(见 3.4) |
| 自定义规则没按预期顺序执行 | 默认 order 是 0,短路类必须显式指定(-2 是留给它们的空位) |
开了 preMerge 启动报错 |
规则集里有部分重叠的模式或正则参数,无法预合并 |
| 手写匹配器匹配不到方法限定规则 | 方法没转大写 |
| 日志里每条规则跑两次 | routeRules() 被挂到了路由中间件上 |
延伸阅读
- 匹配与合并的基础规则:见 3.1
- 缓存键、凭据剥离与执行顺序:见 3.2
- 类型扩展、
restricting与构建期编译器:见 3.4 proxyRequest与代理工具函数的细节:见 6.7 H3 代理工具

TRAE-AI编程


更多建议: