资讯动态

TanStack Router 自定义搜索参数序列化指南:用 parseSearchWith 与 stringifySearchWith 替换默认 JSON 编解码

发布时间:2026/9/14 14:31:58 来源:尧图企业网站定制
TanStack Router 自定义搜索参数序列化指南用 parseSearchWith 与 stringifySearchWith 替换默认 JSON 编解码【免费下载链接】router A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/routerTanStack Router 默认使用JSON.stringify/JSON.parse自动解析和序列化 URL 中的搜索参数Search Params但这一行为并不适合所有场景——你需要 base64 编码以保证跨浏览器与 URL 分享解析器unfurlers的兼容性或希望引入query-string、JSURL2、Zipson等专用压缩/解析库。本文基于 custom-search-param-serialization.md 官方指南展开完整覆盖默认行为、幂等性原则、四种自定义序列化方案与安全二进制编解码工具函数并结合 router-core 源码 与 测试用例 深入剖析其底层调用链帮助你在 React、Solid以及同样导出这两个辅助函数的 Vue项目中安全地定制 URL 搜索参数格式。默认序列化行为JSON 编解码 URL 转义路由器创建后每一个包含搜索参数的链接生成都要经过“序列化stringify”这一步而每次 URL 变化时当前 location 的搜索字符串又要经过“反序列化parse”还原为对象。默认实现即// React import { createRouter, parseSearchWith, stringifySearchWith, } from tanstack/react-router const router createRouter({ // ... parseSearch: parseSearchWith(JSON.parse), stringifySearch: stringifySearchWith(JSON.stringify), })// Solid import { createRouter, parseSearchWith, stringifySearchWith, } from tanstack/solid-router const router createRouter({ // ... parseSearch: parseSearchWith(JSON.parse), stringifySearch: stringifySearchWith(JSON.stringify), })也就是说给定如下搜索对象const search { page: 1, sort: asc, filters: { author: tanner, min_words: 800 }, }默认配置下它会先被JSON.stringify序列化再经过 URL 转义escaping最终得到?page1sortascfilters%7B%22author%22%3A%22tanner%22%2C%22min_words%22%3A800%7D可以看到filters这个嵌套对象被整体编码成了一段%7B...%7D即{author:tanner,min_words:800}的百分号转义串而page、sort这类原始值则保持可读。源码视角默认实现如何工作在仓库中默认行为定义于 searchParams.ts/** 默认 parseSearch剥掉开头的 ? 并尝试对值做 JSON.parse。 */ export const defaultParseSearch parseSearchWith(JSON.parse) /** 默认 stringifySearch复杂值使用 JSON.stringify 序列化。 */ export const defaultStringifySearch stringifySearchWith( JSON.stringify, JSON.parse, )parseSearchWith的核心逻辑searchParams.ts#L26-L53若搜索串以?开头则先剥掉调用 qss.ts 中的decode函数基于URLSearchParams重写负责解转义将字符串解成键值对象对每个字符串值尝试用你提供的parser默认JSON.parse解析解析失败则静默保留原字符串。值得注意的是源码里有一个性能与健壮性并重的守卫// JSON 值只可能以空白、、[、{、数字、- 或 fa/nu/tr 开头。 // 误报会安全地落入 JSON.parse。 const jsonStart /^(?:\s|[[{\d-]|fa|nu|tr)/只有当解析器恰好是JSON.parse时才会先用jsonStart正则预判像filterfoo、path/products这类不可能构成合法 JSON 的值会被直接跳过避免无谓的JSON.parse开销。这一行为由测试 searchParams.test.ts#L113-L133 中的parseSpy断言验证——对?emptyfilterfootabspecssortnewest这类输入JSON.parse一次都不会被调用。而当你传入自定义解析器非JSON.parse时该守卫不生效每个字符串值都会交给你的解析器处理这给了你完全的自由度也意味着你需要保证解析器对任意输入都是安全的。stringifySearchWithsearchParams.ts#L67-L100则对称地工作对象值直接交给你的stringify函数若提供了可选的parser参数它会先尝试parser(val)——如果字符串本身可被解析例如字符串123就重新stringify一次从而保证“序列化后反序列化能拿回原对象”的对称性其余原始值原样返回最终由qss.ts的encode基于URLSearchParams完成转义与拼接。这个对称性在 测试用例 中被逐条验证例如{ foo: 123 }序列化为?foo123而字符串{ foo: 123 }会被额外包上引号编码为?foo%22123%22防止往返后类型被错误地还原成数字重复键如?foo1foo2会被解析为数组{ foo: [1, 2] }。在路由器中的实际调用点parseSearch与stringifySearch并非只影响展示它们嵌入在路由器的 URL 处理主链路上。从 router.ts 源码结构看至少有以下几处调用点buildLocation/buildHref中生成链接时先parseSearch(search)再stringifySearch(parsedSearch)保证 href 与 URL 规范化后的 canonical 形式一致见 router.ts#L1448-L1455启用rewrite重写规则时对重写后的 URL 同样执行 parse→stringify 往返见 router.ts#L1473-L1479导航计算下一个位置时将合并后的搜索对象经stringifySearch落到新的 URL 上见 router.ts#L2071-L2075。同时router.ts 的 createRouter 中可以看到这两个选项的默认值注入stringifySearch: options.stringifySearch ?? defaultStringifySearch, parseSearch: options.parseSearch ?? defaultParseSearch,这与 API 文档 RouterOptionsType.md 中的描述一致stringifySearch类型为(search: Recordstring, any) string可选用于生成链接时序列化搜索参数默认defaultStringifySearchparseSearch类型为(search: string) Recordstring, any可选用于解析当前 location 时反序列化搜索参数默认defaultParseSearch。幂等性自定义序列化必须满足的底线自定义序列化时最重要的一条原则是反序列化必须能拿回与序列化前完全一致的对象。一旦序列化与反序列化过程不对称就会丢失信息——例如使用一个不支持嵌套对象的库时嵌套对象可能在往返后直接丢失或变形。这也是为什么下文每个方案都给出配套的parseSearch与stringifySearch两侧实现。为此官方提供两个内置辅助函数来简化工作parseSearchWith(parser)接收一个“字符串 → 值”的解析器返回符合Router选项要求的parseSearch函数你无需自己处理?前缀剥离与 URL 解转义stringifySearchWith(stringify, parser?)接收一个“值 → 字符串”的序列化器可选第二个参数用于对称性检测返回stringifySearch函数你无需自己处理 URL 转义。这两个函数从 router-core 入口 统一导出并被 react-router、solid-router 与 vue-router 各自再导出因此三种框架下的用法完全一致。方案一Base64 编码对 URL 分享卡片unfurlers、跨浏览器兼容性要求高的场景常见做法是把搜索参数做 base64 编码import { createRouter, parseSearchWith, stringifySearchWith, } from tanstack/react-router const router createRouter({ parseSearch: parseSearchWith((value) JSON.parse(decodeFromBinary(value))), stringifySearch: stringifySearchWith((value) encodeToBinary(JSON.stringify(value)), ), }) function decodeFromBinary(str: string): string { return decodeURIComponent( Array.prototype.map .call(atob(str), function (c) { return % (00 c.charCodeAt(0).toString(16)).slice(-2) }) .join(), ) } function encodeToBinary(str: string): string { return btoa( encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) { return String.fromCharCode(parseInt(p1, 16)) }), ) }Solid 版本只需将导入源换成tanstack/solid-router函数体完全相同。此时开篇的搜索对象会变为?page1sortascfilterseyJhdXRob3IiOiJ0YW5uZXIiLCJtaW5fd29yZHMiOjgwMH0%3Dfilters值从百分号转义串变成了 base64解码后即 JSON 字符串{author:tanner,min_words:800}URL 可读性虽下降但对链接分享工具更友好。警告如果直接把用户输入序列化成 Base64可能与 URL 本身的解码过程发生冲突collision导致 URL 解析错误或值被误解。为避免这个问题务必使用下文“安全二进制编解码”中的encodeToBinary/decodeFromBinary而不是裸用btoa/atob。方案二query-string 库query-stringsindresorhus 出品是解析/序列化查询串的流行选择可以按需求定制序列化格式如嵌套对象的展开方式、数组编码风格等import { createRouter } from tanstack/react-router import qs from query-string const router createRouter({ // ... stringifySearch: stringifySearchWith((value) qs.stringify(value, { // ...options }), ), parseSearch: parseSearchWith((value) qs.parse(value, { // ...options }), ), })同样的搜索对象在此配置下会输出?page1sortascfiltersauthor%3Dtanner%26min_words%3D800与默认行为整个对象 JSON 化不同query-string默认把嵌套对象展开为filters[author]tannerfilters[min_words]800这类键路径形式示例中filters值本身是被JSON.stringify后的字符串再经 URL 编码。你可以通过{ ...options }传入库的选项调整具体风格但需要自行验证qs.parse(qs.stringify(x))的往返幂等性——这是上文强调的底线。方案三JSURL2 压缩库JSURL2是一个非标准但可读性良好的 URL 压缩方案能在压缩体积的同时保留一定可读性import { createRouter, parseSearchWith, stringifySearchWith, } from tanstack/react-router import { parse, stringify } from jsurl2 const router createRouter({ // ... parseSearch: parseSearchWith(parse), stringifySearch: stringifySearchWith(stringify), })此时搜索对象会变成?page1sortascfilters(author~tanner~min*_words~800)~嵌套对象被压缩成(author~tanner~min*_words~800)这样的紧凑语法。由于jsurl2的stringify/parse本身就是互逆的一对直接作为两个辅助函数的入参即可满足幂等性要求。方案四Zipson JSON 压缩库Zipson是一个兼顾运行时性能与压缩率的 JSON 压缩库。它的stringify输出仍可能需要转义/解转义与 base64 编解码因此同样要搭配安全二进制工具函数import { createRouter, parseSearchWith, stringifySearchWith, } from tanstack/react-router import { stringify, parse } from zipson const router createRouter({ parseSearch: parseSearchWith((value) parse(decodeFromBinary(value))), stringifySearch: stringifySearchWith((value) encodeToBinary(stringify(value)), ), }) function decodeFromBinary(str: string): string { return decodeURIComponent( Array.prototype.map .call(atob(str), function (c) { return % (00 c.charCodeAt(0).toString(16)).slice(-2) }) .join(), ) } function encodeToBinary(str: string): string { return btoa( encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) { return String.fromCharCode(parseInt(p1, 16)) }), ) }此时搜索对象会变为?page1sortascfiltersJTdCJUMyJUE4YXV0aG9yJUMyJUE4JUMyJUE4dGFubmVyJUMyJUE4JUMyJUE4bWluX3dvcmRzJUMyJUE4JUMyJUEyQ3UlN0Q%3D这是四种方案中 URL 最短、但可读性最低的形态适合对体积敏感、又希望保留 JSON 数据完整性的场景。安全二进制编解码为什么不能裸用 atob/btoa浏览器中的atob和btoa对非 UTF-8 字符例如中文、emoji 等多字节字符并不保证行为正确——直接btoa(雪)会抛异常。官方指南因此推荐以下两个工具函数配合使用字符串 → 二进制字符串编码export function encodeToBinary(str: string): string { return btoa( encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) { return String.fromCharCode(parseInt(p1, 16)) }), ) }原理先用encodeURIComponent把每个非 ASCII 字符变成%XX形式再把百分号替换回原始字节最后交给btoa只处理纯 Latin-1 字符保证不抛错。二进制字符串 → 字符串解码export function decodeFromBinary(str: string): string { return decodeURIComponent( Array.prototype.map .call(atob(str), function (c) { return % (00 c.charCodeAt(0).toString(16)).slice(-2) }) .join(), ) }原理atob还原出字节序列后把每个字节重新编码为%XX最后由decodeURIComponent统一还原为正确的 Unicode 字符串。这一对函数是上文 Base64 与 Zipson 两个方案代码中decodeFromBinary/encodeToBinary的来源也是你自定义任何“先压缩、再 base64”方案时的推荐基座。选型建议与实践要点结合四个方案与源码行为可以总结出以下实践要点往返幂等是硬约束无论选哪种方案都要保证parse(stringify(search))能拿回原对象尤其是嵌套对象与“长得像 JSON 的字符串”123、true、{}这类边界值。仓库测试 searchParams.test.ts#L14-L47 中“isomorphism”系列用例就是针对默认实现的这一约束引入第三方库时建议用同样的方法自测只换一侧是不够的parseSearch与stringifySearch必须成对替换为同一套编解码协议否则路由器在生成 hrefstringify 侧与解析 locationparse 侧之间会出现协议错配非 JSON 解析器会收到所有字符串源码中jsonStart守卫仅对JSON.parse生效见 searchParams.test.ts#L144-L154 的验证自定义解析器必须对任意字符串保持安全失败时静默返回原值或抛错由框架兜底——框架侧try/catch只保证不中断不会帮你修正语义URL 会被人手工编辑测试中的“alien deserialization”系列searchParams.test.ts#L240-L254验证了反序列化端要能优雅处理“不可能由序列化器产生”的输入比如用户手改的?foo{}。选择可读性更强的格式如 query-string、JSURL2时这类手工编辑场景的容错会更自然压缩换可读性的取舍query-string 保持扁平可读、JSURL2 紧凑可读、Base64/Zipson 最短但不可读。选择时优先考虑目标场景——是程序间传递选压缩、还是用户可见与手工分享选可读。参考路径指南原文docs/router/guide/custom-search-param-serialization.mdparseSearch/stringifySearch选项定义docs/router/api/router/RouterOptionsType.md核心实现packages/router-core/src/searchParams.ts、packages/router-core/src/qss.ts路由器调用链packages/router-core/src/router.ts单元测试packages/router-core/tests/searchParams.test.ts【免费下载链接】router A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价