news 2026/9/14 14:31:55

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TanStack Router 自定义搜索参数序列化指南:用 parseSearchWith 与 stringifySearchWith 替换默认 JSON 编解码

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/router

TanStack Router 默认使用JSON.stringify/JSON.parse自动解析和序列化 URL 中的搜索参数(Search Params),但这一行为并不适合所有场景——你需要 base64 编码以保证跨浏览器与 URL 分享解析器(unfurlers)的兼容性,或希望引入query-stringJSURL2Zipson等专用压缩/解析库。本文基于 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),最终得到:

?page=1&sort=asc&filters=%7B%22author%22%3A%22tanner%22%2C%22min_words%22%3A800%7D

可以看到,filters这个嵌套对象被整体编码成了一段%7B...%7D(即{"author":"tanner","min_words":800})的百分号转义串,而pagesort这类原始值则保持可读。

源码视角:默认实现如何工作

在仓库中,默认行为定义于 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):

  1. 若搜索串以?开头则先剥掉;
  2. 调用 qss.ts 中的decode函数(基于URLSearchParams重写,负责解转义)将字符串解成键值对象;
  3. 对每个字符串值尝试用你提供的parser(默认JSON.parse)解析,解析失败则静默保留原字符串。

值得注意的是,源码里有一个性能与健壮性并重的守卫:

// JSON 值只可能以空白、"、[、{、数字、- 或 fa/nu/tr 开头。 // 误报会安全地落入 JSON.parse。 const jsonStart = /^(?:\s|["[{\d-]|fa|nu|tr)/

只有当解析器恰好是JSON.parse时,才会先用jsonStart正则预判:像filter=foopath=/products这类不可能构成合法 JSON 的值会被直接跳过,避免无谓的JSON.parse开销。这一行为由测试 searchParams.test.ts#L113-L133 中的parseSpy断言验证——对?empty=&filter=foo&tab=specs&sort=newest这类输入,JSON.parse一次都不会被调用。而当你传入自定义解析器(非JSON.parse)时,该守卫不生效,每个字符串值都会交给你的解析器处理,这给了你完全的自由度,也意味着你需要保证解析器对任意输入都是安全的。

stringifySearchWith(searchParams.ts#L67-L100)则对称地工作:

  • 对象值直接交给你的stringify函数;
  • 若提供了可选的parser参数,它会先尝试parser(val)——如果字符串本身可被解析(例如字符串"123"),就重新stringify一次,从而保证“序列化后反序列化能拿回原对象”的对称性;
  • 其余原始值原样返回,最终由qss.tsencode(基于URLSearchParams)完成转义与拼接。

这个对称性在 测试用例 中被逐条验证,例如{ foo: 123 }序列化为?foo=123,而字符串{ foo: '123' }会被额外包上引号编码为?foo=%22123%22,防止往返后类型被错误地还原成数字;重复键(如?foo=1&foo=2)会被解析为数组{ foo: [1, 2] }

在路由器中的实际调用点

parseSearchstringifySearch并非只影响展示,它们嵌入在路由器的 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: Record<string, any>) => string,可选,用于生成链接时序列化搜索参数,默认defaultStringifySearch
  • parseSearch:类型为(search: string) => Record<string, any>,可选,用于解析当前 location 时反序列化搜索参数,默认defaultParseSearch

幂等性:自定义序列化必须满足的底线

自定义序列化时最重要的一条原则是:反序列化必须能拿回与序列化前完全一致的对象。一旦序列化与反序列化过程不对称,就会丢失信息——例如使用一个不支持嵌套对象的库时,嵌套对象可能在往返后直接丢失或变形。这也是为什么下文每个方案都给出配套的parseSearchstringifySearch两侧实现。

为此,官方提供两个内置辅助函数来简化工作:

  • 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,函数体完全相同。此时,开篇的搜索对象会变为:

?page=1&sort=asc&filters=eyJhdXRob3IiOiJ0YW5uZXIiLCJtaW5fd29yZHMiOjgwMH0%3D

filters值从百分号转义串变成了 base64(解码后即 JSON 字符串{"author":"tanner","min_words":800}),URL 可读性虽下降,但对链接分享工具更友好。

警告:如果直接把用户输入序列化成 Base64,可能与 URL 本身的解码过程发生冲突(collision),导致 URL 解析错误或值被误解。为避免这个问题,务必使用下文“安全二进制编解码”中的encodeToBinary/decodeFromBinary,而不是裸用btoa/atob

方案二:query-string 库

query-string(sindresorhus 出品)是解析/序列化查询串的流行选择,可以按需求定制序列化格式(如嵌套对象的展开方式、数组编码风格等):

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 }), ), })

同样的搜索对象在此配置下会输出:

?page=1&sort=asc&filters=author%3Dtanner%26min_words%3D800

与默认行为(整个对象 JSON 化)不同,query-string默认把嵌套对象展开为filters[author]=tanner&filters[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), })

此时搜索对象会变成:

?page=1&sort=asc&filters=(author~tanner~min*_words~800)~

嵌套对象被压缩成(author~tanner~min*_words~800)这样的紧凑语法。由于jsurl2stringify/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)) }), ) }

此时搜索对象会变为:

?page=1&sort=asc&filters=JTdCJUMyJUE4YXV0aG9yJUMyJUE4JUMyJUE4dGFubmVyJUMyJUE4JUMyJUE4bWluX3dvcmRzJUMyJUE4JUMyJUEyQ3UlN0Q%3D

这是四种方案中 URL 最短、但可读性最低的形态,适合对体积敏感、又希望保留 JSON 数据完整性的场景。

安全二进制编解码:为什么不能裸用 atob/btoa

浏览器中的atobbtoa对非 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”方案时的推荐基座。

选型建议与实践要点

结合四个方案与源码行为,可以总结出以下实践要点:

  1. 往返幂等是硬约束:无论选哪种方案,都要保证parse(stringify(search))能拿回原对象,尤其是嵌套对象与“长得像 JSON 的字符串”('123''true''{}')这类边界值。仓库测试 searchParams.test.ts#L14-L47 中“isomorphism”系列用例就是针对默认实现的这一约束,引入第三方库时建议用同样的方法自测;
  2. 只换一侧是不够的parseSearchstringifySearch必须成对替换为同一套编解码协议,否则路由器在生成 href(stringify 侧)与解析 location(parse 侧)之间会出现协议错配;
  3. 非 JSON 解析器会收到所有字符串:源码中jsonStart守卫仅对JSON.parse生效(见 searchParams.test.ts#L144-L154 的验证),自定义解析器必须对任意字符串保持安全(失败时静默返回原值或抛错由框架兜底——框架侧try/catch只保证不中断,不会帮你修正语义);
  4. URL 会被人手工编辑:测试中的“alien deserialization”系列(searchParams.test.ts#L240-L254)验证了反序列化端要能优雅处理“不可能由序列化器产生”的输入,比如用户手改的?foo={}。选择可读性更强的格式(如 query-string、JSURL2)时,这类手工编辑场景的容错会更自然;
  5. 压缩换可读性的取舍:query-string 保持扁平可读、JSURL2 紧凑可读、Base64/Zipson 最短但不可读。选择时优先考虑目标场景——是程序间传递(选压缩)、还是用户可见与手工分享(选可读)。

参考路径

  • 指南原文:docs/router/guide/custom-search-param-serialization.md
  • parseSearch/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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 14:30:27

Django构建汽车美容行业网站:从项目搭建到业务建模实战

简介&#xff1a;这份源码包面向汽车美容行业线上转型需求&#xff0c;提供基于Django框架的完整网站设计与实现方案&#xff0c;适合Web开发学习者、行业从业者及需要快速搭建预约服务平台的开发者参考。项目涵盖车辆美容预约、服务套餐选择、技师团队展示、在线支付、会员管理…

作者头像 李华
网站建设 2026/9/14 14:29:49

Claude Code 配 TaoToken:GLM Coding Plan 的 Base URL 和 Key 这样改

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 14:29:35

STM32C552高精度ADC电压采集实战指南

1. 项目概述&#xff1a;为什么STM32C552的ADC电压采集值得深挖&#xff1f; 你手头有一块STM32C552开发板&#xff0c;想测个电池电压、传感器输出或者电源轨状态&#xff0c;结果发现——采出来的数值跳得像心电图&#xff0c;滤波加了还是漂&#xff0c;DMA一开数据就错位&a…

作者头像 李华
网站建设 2026/9/14 14:27:02

同一把 TaoToken Key,MarsCode 从 Doubao-1.5-pro 切到 DeepSeek-R1

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 14:26:29

五颗芯片打造工业设备监测与远程IO控制终端,双MCU架构全解析

前阵子做项目选型&#xff0c;手头同时到了一批芯片&#xff1a;TLE7272-2D、GD32F427VGT6、STM32F417ZGT6、MCP4631-503E/ST、GRX350A3BC160。乍一看这就是一张“购物车清单”&#xff0c;但把它们铺到原理图工作区后我发现&#xff0c;这五颗器件刚好能组成一套完整的智能系统…

作者头像 李华