TanStack Router 搜索参数校验适配器实战指南:Zod / Valibot / ArkType 深度解析
2026/9/15 18:01:25 网站建设 项目流程

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-adapterzod.index.tsx
/users/valibot/Valibot@tanstack/valibot-adaptervalibot.index.tsx
/users/arktype/ArkType@tanstack/arktype-adapterarktype.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,让搜索参数变化时页面滚动位置也能正确恢复。

从底层实现看,校验发生在类型层面与运行时层面两个维度:类型层面,路由器基于ValidatorAdaptertypes推断出Route.useSearch()navigate({ search })Linksearchprop 等全部 API 的入参/出参类型;运行时层面,parse负责真正把?search=...字符串解析并校验成对象。二者缺一不可——这也就是示例中同名路由要分别放在zodvalibotarktype三个目录下的原因:每条路由的搜索参数类型彼此独立、互不干扰。

Zod 适配器:zodValidatorfallback兜底

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(''), }), ), ... })

关键点是zodValidatorfallback的组合。查看 zod-adapter 源码,zodValidator接受一个 Zod schema(或{ schema, input, output }选项对象),并完成两件事:

  1. schema._input/schema._output提取类型,按input/output选项组装成ValidatorAdaptertypes。默认input: 'input'output: 'output',即 URL 侧接收 Zod 的输入类型、组件侧使用输出类型;若设置{ input: 'output' }则可让 URL 侧直接使用输出类型。
  2. 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/InferOutputGenericSchema提取输入与输出类型。值得注意的是 Valibot 是模块化设计的库——示例中只引入了objectfallbackoptionalstring这几个用到的校验器,打包体积天然可控,这也正是 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,两种方式效果等价,显式包装在需要强约束类型边界时更推荐。

类型安全如何落地:从LinkuseSearch

搜索参数的类型安全不仅作用于路由内部,还会反向传播到所有导航入口。示例在 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 }>()

测试断言了三个关键事实:

  1. 指向/users/arktype/Link组件,其searchprop 的类型被精确推断为{ search?: string }
  2. 路由的useSearch()返回{ search: string }——由于 schema 声明了默认值,输出类型中search不再是可选;
  3. 由于@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 创建时挂载到createRoutercontext中,loader 中经opt.context.queryClient访问。

交互侧,Search.tsx 的onChange调用navigate({ search: { search }, replace: true })——replace: true避免每次输入都产生新的历史记录,保证浏览器前进/后退键的体验符合直觉。三条路由在 Header.tsx 中切换时,各自的搜索参数状态独立保存,你可以直接在浏览器中验证"切走再切回,搜索词仍然保留"这一 URL 状态管理的核心体验。

小结

通过这一个示例,你可以一次性对照掌握三种主流 schema 校验库在 TanStack Router 中的接入方式:

  • ZodzodValidator(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)。

三者共享同一套路由器契约ValidatorAdaptertypes+parse),因此接入成本低、可替换性强;而搜索参数的类型会从validateSearch一路传播到LinkuseSearchnavigate和 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询