设计边界
Decurl 的定位很窄:为 URLSearchParams 提供一套显式的解析与序列化规则,以及执行这些规则的工具。
解析与序列化
URL search params 本质上是一段字符串数据。我们对它做的一切,都是在做解析和序列化。
类似的事情在很多场景都会发生:
- TypeScript 源码本质是文本,会被 TypeScript Compiler 解析成 AST;AST 也可以再被打印成文本。
- JavaScript value 可以序列化为 JSON 字符串,也可以从 JSON 字符串解析回 JavaScript value。
YYYY-MM-DD字符串可以解析成Date对象,Date对象也可以序列化成适合传输的字符串。location.search是 string,可以被解析成URLSearchParams实例;URLSearchParams也可以重新序列化成 search string。
Decurl 针对 URL search params 做的是同一类事情:
decode:把string、string[]或缺失值解析成业务值。encode:把业务值序列化回 URLSearchParams。
Search Fields 是这套规则的静态定义;URLSearchParams codec 是这套规则的执行器。
跨字段不变式
FieldCodec 围绕一个逻辑字段设计。Search Fields 可以组合多个 FieldCodec,但每个字段仍然只根据自己的 raw input 完成 decode。
一个逻辑字段不一定只有一个 raw value。例如:
这里的两个值属于同一个 URL key。multi FieldCodec 会一次得到完整的 string[],因此可以在自己的 decode 中排序、过滤,或者检查两个值能否组成有效范围。legacy alias 也是同一个逻辑字段的候选 key,不属于跨字段关系。
下面的形式则不同:
startTime 和 endTime 是两个独立字段。它们都可以单独完成字符串解析,但“startTime 必须小于 endTime”只有在两个字段组合后才成立。这类规则属于跨字段不变式,FieldCodec 无法也不应该单独处理。
如果允许一个 FieldCodec 在 decode 时读取其他字段,会让字段行为依赖解析顺序和外部上下文,也会让字段复用、局部 patch 和类型推导变得不再明确。因此 Decurl 只保证字段级解析,不提供 schema 级的跨字段 validation 或自动修正。
推荐在 Search Fields decode 完成后,由业务代码处理组合语义:
具体策略由页面决定:它可以展示错误、阻止请求、清除某个字段,或者修正成新的范围。正常交互也应该在写入前控制值的合法性;如果两个字段需要一起变化,可以通过一次 setValues({ startTime, endTime }) 提交,但外部 URL 仍然需要业务层检查。
Decode 不应该默认抛异常
URL search params 是用户可编辑、可分享、可遗留的字符串输入。Decode 遇到预期外字符串时,默认不应该把抛异常作为控制流,而应该返回 null 或 undefined,再交给 defaultValue 或页面 guard 处理。
分页参数是典型例子:
如果 URL 中出现 page=abc.123,页面通常不应该进入 ErrorBoundary。分页参数大多只是辅助状态,decode 失败后回退到默认页码,会比抛异常更符合用户预期。
核心参数则需要另一种处理方式:
如果 id 是页面展示的核心参数,而 URL 中出现 id=abc.12343,页面确实可能无法正常展示。但这也不一定要通过 decode 抛异常来交给 ErrorBoundary。
更推荐在页面中显式 guard:
这种方式更容易为不同页面定制错误说明、返回按钮、刷新按钮或其他辅助操作。
validation 库很适合“校验失败 -> 结构化错误”的表单、API payload 或服务端契约;而 URL search params 更常见的是“无效值当作缺失值”、“缺失值走默认值”、“核心参数缺失由页面 guard 展示专门 UI”。
当然,预期之外的异常仍然可能发生,例如开发者自己的 decode 函数有 bug,或调用了会 throw 的第三方 parser。Decurl 的理念不是“永远不可能 throw”,而是“URL 值不符合预期时,不应该默认用 throw 表达”。
Decode 保持同步
Decurl 的 URLSearchParams codec 目前不支持异步 decode。这也是有意的边界。
URL search params 中更适合存放核心基础参数,而不是异步派生结果。例如页面可能只把 id 放在 URL 中:
而真正的业务实体可能包含更多字段:
如果页面只需要实体中的时间范围,看起来可以把异步请求放进 decode pipeline:
Decurl 不支持这种写法。异步解析通常会引入状态管理问题:loading 如何展示,error 如何处理,同一个 key 在页面多处使用时如何复用请求结果,以及它是否会和系统里已有的 swr、react-query 等数据请求模块重复。
更推荐把 URL 参数 decode 成基础值,再把异步数据交给专门的请求或缓存模块:
字符串的解析和序列化天然是同步流程。把异步请求放进 decode 会让模块边界变得模糊,也会让 loading、error、缓存和重试等 UI 状态难以统一管理。异步状态更适合交给专门的模块处理。
为什么 decode first
写入 URL 往往不难:
真正困难的是读取:
这段代码没有说明:
- 空字符串是否有效。
1e3是否有效。0是否有效。- 参数缺失时默认值是什么。
- 旧 key 是否还需要兼容。
Decurl 把这些规则前置到 Search Fields 中,让 URL 进入业务层之前就被显式处理。
Decurl 不接管什么
Decurl 不尝试接管所有数据规则。它不负责:
- 完整 validation DSL。
- 表单错误树和字段级错误展示。
- 路由匹配和页面生命周期。
- 把所有业务状态都放进 URL。
- 把 URL 当成数据库或全局 store。
- 鼓励把复杂对象都塞进 search params。
如果状态不需要分享、刷新保留或浏览器历史记录,它未必适合进入 URL。Decurl 只希望让那些确实属于 URL search params 的状态,有明确、可推导、可复用的解析与序列化规则。