TanStack Router 搜索参数校验适配器实战指南:Zod / Valibot / ArkType 深度解析
【免费下载链接】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
本指南以 search-validator-adapters 示例 为核心,系统讲解 TanStack Router 中validateSearch的搜索参数校验机制,以及如何通过官方适配器包将 Zod、Valibot、ArkType 三种主流 schema 校验库无缝接入路由,获得端到端的类型安全 URL 状态管理。读完本文,你将掌握三类适配器的接入姿势、fallback兜底模式的底层实现原理,以及校验与 React Query 数据预取的组合用法,可直接迁移到自己的项目中。
示例概览:一个页面、三套校验方案
examples/react/search-validator-adapters是一个基于 Vite + React + TypeScript 的最小演示应用:页面提供一个搜索框,用户输入关键字后,URL 中的?search=xxx会被实时同步,并驱动一个基于 React Query 的用户列表查询。同一套交互,分别用 Zod、Valibot、ArkType 三套 schema 校验方案实现了三条等价路由:
| 路由 | 校验库 | 适配器包 | 核心文件 |
|---|---|---|---|
/users/zod/ | Zod | @tanstack/zod-adapter | zod.index.tsx |
/users/valibot/ | Valibot | @tanstack/valibot-adapter | valibot.index.tsx |
/users/arktype/ | ArkType | @tanstack/arktype-adapter | arktype.index.tsx |
三个页面的导航入口统一由 Header.tsx 中的三个Link提供,方便直接在浏览器中对比三种校验库的写法差异。
快速启动:安装、开发与构建
该示例与仓库内其他示例一样使用 pnpm 管理依赖,脚本定义在 package.json:
# 安装依赖 pnpm install # 启动开发服务器(vite --port 3000) pnpm dev # 生产构建 + 全量类型检查(NODE_OPTIONS=--max-old-space-size=4096 tsc --noEmit) pnpm build # 运行单元测试与类型测试(vitest,typecheck 已启用) pnpm test:unit其中build脚本在vite build之后还会以tsc --noEmit做一次全量类型检查,这正是该示例强调"类型安全"的体现——如果搜索参数的推断类型有误,构建阶段就会直接报错。测试运行器由 vite.config.ts 配置,测试目录为./tests,环境为jsdom,并开启了typecheck以同时执行.test-d.tsx类型测试。
核心机制:validateSearch与 URL 状态管理
在 TanStack Router 中,URL 的 query string 并不是"字符串",而是被建模为强类型的路由状态。每条路由都可以通过validateSearch选项声明一个校验器,路由器会在每次导航、前进后退时对?search=...进行解析和校验,把字符串化的 URL 参数还原为类型安全的对象,这就是示例 README 中提到的 "URL state management"。
validateSearch的完整定义记录在 RouteOptionsType.md 中,其底层契约是一个ValidatorAdapter接口:
types:声明校验器的输入类型(URL 中解析出的原始形态)与输出类型(校验后组件的使用形态);parse:接收 URL 解析出的原始值,执行 schema 解析并返回结果。
@tanstack/zod-adapter、@tanstack/valibot-adapter、@tanstack/arktype-adapter三个包分别把各自生态的 schema 包装成这个接口,因此无论底层用哪个校验库,路由层代码都能以统一方式工作。示例的应用入口 main.tsx 通过createRouter装配路由树,并开启scrollRestoration: true,让搜索参数变化时页面滚动位置也能正确恢复。
从底层实现看,校验发生在类型层面与运行时层面两个维度:类型层面,路由器基于ValidatorAdapter的types推断出Route.useSearch()、navigate({ search })、Link的searchprop 等全部 API 的入参/出参类型;运行时层面,parse负责真正把?search=...字符串解析并校验成对象。二者缺一不可——这也就是示例中同名路由要分别放在zod、valibot、arktype三个目录下的原因:每条路由的搜索参数类型彼此独立、互不干扰。
Zod 适配器:zodValidator与fallback兜底
Zod 路由的完整实现见 zod.index.tsx:
const fallbackString = fallback as unknown as ( schema: z.ZodString, fallback: string, ) => z.ZodType<string, z.ZodTypeDef, string> export const Route = createFileRoute('/users/zod/')({ validateSearch: zodValidator( z.object({ search: fallbackString(z.string(), '').default(''), }), ), ... })关键点是zodValidator与fallback的组合。查看 zod-adapter 源码,zodValidator接受一个 Zod schema(或{ schema, input, output }选项对象),并完成两件事:
- 从
schema._input/schema._output提取类型,按input/output选项组装成ValidatorAdapter的types。默认input: 'input'、output: 'output',即 URL 侧接收 Zod 的输入类型、组件侧使用输出类型;若设置{ input: 'output' }则可让 URL 侧直接使用输出类型。 parse直接调用schema.parse(input),把 Zod 的解析能力桥接到路由层。
fallback是官方提供的一个"宽松输入 + 严格兜底"工具,其实现(zod-adapter/src/index.ts)是一个管道类型:
return z.custom<TSchema['_input']>().pipe(schema.catch(fallback))含义是:先用z.custom接受任意输入(保证 URL 中参数缺失、类型错误都不会在解析阶段抛错),再通过schema.catch(fallback)在解析失败时回落到兜底值。配合.default(''),即使 URL 里完全没有search参数,最终得到的也是空字符串,而不会出现undefined导致的渲染崩溃。
由于该管道返回的是ZodPipeline类型,示例中通过fallbackString这个类型断言将其收窄为普通的ZodType<string>,让z.object({ search: ... })的类型推断保持整洁——这是实践中值得借鉴的细节。
Valibot 适配器:极简的 schema 直传
Valibot 路由见 valibot.index.tsx,写法比 Zod 更简洁——无需任何包装函数,schema 对象直接作为validateSearch:
export const Route = createFileRoute('/users/valibot/')({ validateSearch: v.object({ search: v.fallback(v.optional(v.string(), ''), ''), }), ... })这里v.fallback(v.optional(v.string(), ''), '')与 Zod 版的fallback语义完全对应:v.optional(v.string(), '')让参数缺省时取空串,外层v.fallback再兜底非法输入。Valibot 的 schema 之所以能直接使用,是因为@tanstack/valibot-adapter提供了valibotValidator包装器(valibot-adapter/src/index.ts):
export const valibotValidator = <TOptions extends GenericSchema>( options: TOptions, ): ValibotValidatorAdapter<TOptions> => { return { types: { input: null, output: null }, parse: (input) => parse(options, input), } }其parse内部调用 Valibot 的parse(options, input),类型层面则借助InferInput/InferOutput从GenericSchema提取输入与输出类型。值得注意的是 Valibot 是模块化设计的库——示例中只引入了object、fallback、optional、string这几个用到的校验器,打包体积天然可控,这也正是 README 强调"多种校验器适配器"的实用价值之一。
ArkType 适配器:类型优先的原生 schema
ArkType 路由见 arktype.index.tsx,它是三种方案中"类型表达"最直接的一个:
const search = type({ search: 'string = ""', }) export const Route = createFileRoute('/users/arktype/')({ validateSearch: search, ... })ArkType 用字符串 DSL 描述 schema——'string = ""'一行就同时声明了"字段类型为 string"与"默认值为空串",schema 对象直接作为validateSearch传入。这是因为 ArkType 的type对象在结构上恰好满足路由器的适配器契约:inferIn/infer对应输入/输出类型,assert承担解析校验。官方同时也提供了显式的arkTypeValidator包装(arktype-adapter/src/index.ts),它把inferIn/infer提取为types、以assert作为parse,两种方式效果等价,显式包装在需要强约束类型边界时更推荐。
类型安全如何落地:从Link到useSearch
搜索参数的类型安全不仅作用于路由内部,还会反向传播到所有导航入口。示例在 tests/arktype.test-d.tsx 中用类型级测试锁死了这一行为:
expectTypeOf(Link<typeof router, string, '/users/arktype'>) .parameter(0) .toHaveProperty('search') .exclude<boolean | ((...args: ReadonlyArray<any>) => any)>() .toEqualTypeOf<{ search?: string } | undefined>() expectTypeOf(ArkTypeRoute.useSearch()).toEqualTypeOf<{ search: string }>()测试断言了三个关键事实:
- 指向
/users/arktype/的Link组件,其searchprop 的类型被精确推断为{ search?: string }; - 路由的
useSearch()返回{ search: string }——由于 schema 声明了默认值,输出类型中search不再是可选; - 由于
@tanstack/react-router的类型注册(见 main.tsx 中的declare module),这些类型推断对整个应用全局生效。
这意味着:如果某个组件误传了{ search: number },或者把search写成serach,TypeScript 会在编辑器和pnpm build的类型检查阶段直接报错——URL 参数的拼写错误从运行时错误提前到了编译期。tests目录下还配套了 arktype.test.tsx、valibot.test-d.tsx、valibot.test.tsx 等运行时与类型双重测试,vite.config.ts中的typecheck: { enabled: true }确保两类测试在同一命令下执行。
与 React Query 集成:search 参数驱动的数据预取
示例真正的"实战价值"在于把校验后的搜索参数与数据层串联起来。三条路由的loader部分结构一致,以 Zod 版为例:
loaderDeps: (opt) => ({ search: opt.search }), loader: (opt) => { opt.context.queryClient.ensureQueryData( usersQueryOptions(opt.deps.search.search ?? ''), ) },配合 Users.tsx 中定义的 query options:
export const usersQueryOptions = (search: string) => queryOptions({ queryKey: ['users', search], queryFn: async () => searchUsers(search), })这条链路完整展示了 TanStack Router 的"搜索参数即状态"哲学:
loaderDeps:声明 loader 依赖的搜索参数子集。?search变化时,loader 会带着新的opt.deps.search重新执行;loader+ensureQueryData:在路由渲染前就把用户列表查询结果预取进 QueryClient 缓存,组件侧再用useSuspenseQuery同步读取,配合React.Suspense实现"零闪烁"的加载体验;queryKey: ['users', search]:搜索关键字成为查询键的一部分,不同关键字天然对应不同缓存条目,输入防抖、切换回退都不必额外处理缓存失效;- 路由上下文:
queryClient通过 __root.tsx 的createRootRouteWithContext<Context>()注入,并在 main.tsx 创建时挂载到createRouter的context中,loader 中经opt.context.queryClient访问。
交互侧,Search.tsx 的onChange调用navigate({ search: { search }, replace: true })——replace: true避免每次输入都产生新的历史记录,保证浏览器前进/后退键的体验符合直觉。三条路由在 Header.tsx 中切换时,各自的搜索参数状态独立保存,你可以直接在浏览器中验证"切走再切回,搜索词仍然保留"这一 URL 状态管理的核心体验。
小结
通过这一个示例,你可以一次性对照掌握三种主流 schema 校验库在 TanStack Router 中的接入方式:
- Zod:
zodValidator(z.object({...}))显式包装,fallback管道实现"任意输入 + 失败兜底",适配器包还支持input/output类型方向的精细控制(源码见 zod-adapter/src/index.ts); - Valibot:schema 即插即用,模块化引入按需打包,
valibotValidator内部以parse(options, input)桥接(源码见 valibot-adapter/src/index.ts); - ArkType:字符串 DSL 一行声明类型与默认值,schema 对象天然满足适配器契约,也提供显式
arkTypeValidator包装(源码见 arktype-adapter/src/index.ts)。
三者共享同一套路由器契约ValidatorAdapter(types+parse),因此接入成本低、可替换性强;而搜索参数的类型会从validateSearch一路传播到Link、useSearch、navigate和 loader 依赖,最终由 React Query 驱动数据预取,构成一个完整的"强类型 URL 状态 + 服务端数据"闭环。若想深入学习validateSearch的完整选项语义,可继续阅读 RouteOptionsType.md;参考其他文件路由示例(如 basic-file-based)还能看到该校验机制在真实路由树中的更多组合方式。
【免费下载链接】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),仅供参考