完全指南:动态段、可选参数、Splat 通配与类型安全导航)
TanStack Router 路径参数Path Params完全指南动态段、可选参数、Splat 通配与类型安全导航【免费下载链接】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导读路径参数Path Params是 TanStack Router 将 URL 中的动态片段捕获为具名变量的核心机制以路径中的$前缀声明覆盖动态段、可选段、前缀/后缀模式、Splat 全捕获等多种形态。本文以仓库中 path-params/SKILL.md 为骨架结合 path.ts、new-process-route-tree.ts 等底层源码系统讲解参数声明语法、useParams的类型安全读取、导航时的params传参方式、编码规则与常见错误帮助你写出在 React / Solid / Vue 三大框架下完全类型安全、可维护的路由参数代码。CRITICAL永远不要把参数直接插值进to字符串必须使用paramsprop——这是 Agent 与开发者处理路径参数时最高频的错误。CRITICAL参数类型完全由框架推断永远不要手动为useParams()的返回值添加类型注解。动态段Dynamic Segments以$前缀修饰的路径段会捕获从当前位置到下一个/之间的所有文本并在组件、loader、beforeLoad 中以具名变量的形式出现。// src/routes/posts.$postId.tsx import { createFileRoute } from tanstack/react-router export const Route createFileRoute(/posts/$postId)({ loader: async ({ params }) { // params.postId 是 string —— 完全推断不要手动注解 return fetchPost(params.postId) }, component: PostComponent, }) function PostComponent() { const { postId } Route.useParams() const data Route.useLoaderData() return ( h1 Post {postId}: {data.title} /h1 ) }多个动态段可以跨路径层级同时存在例如/teams/$teamId/members/$memberId// src/routes/teams.$teamId.members.$memberId.tsx export const Route createFileRoute(/teams/$teamId/members/$memberId)({ component: MemberComponent, }) function MemberComponent() { const { teamId, memberId } Route.useParams() return ( div Team {teamId}, Member {memberId} /div ) }源码视角段如何被解析与匹配从源码结构看路由路径在初始化时会被解析成一棵 segment 前缀树trie。new-process-route-tree.ts 中的parseSegment定义了四种段类型段类型常量值含义示例SEGMENT_TYPE_PATHNAME0静态路径段postsSEGMENT_TYPE_PARAM1动态参数段$postId、post-{$postId}SEGMENT_TYPE_WILDCARD2通配捕获段$、{$}SEGMENT_TYPE_OPTIONAL_PARAM3可选参数段{-$category}parseSegment对每个段先做快速路径检查不含$即静态段再依次识别裸$通配、$param动态段最后才尝试解析花括号内的{-$...}可选参数与{$...}/{prefix{$x}suffix}前缀后缀模式。每个节点还会记录prefix前缀文本与suffix后缀文本的位置供匹配与参数提取使用。匹配时 extractParams 会根据节点的前缀/后缀长度从 URL 段中切出参数原始值并通过decodeURIComponent解码后写入rawParams。Splat / Catch-All 路由以裸$结尾的路由会捕获其后剩余的全部内容包括/捕获值通过_splat键读取。注意 TanStack Router 使用$而非 React Router 风格的*作为通配符。// src/routes/files.$.tsx import { createFileRoute } from tanstack/react-router export const Route createFileRoute(/files/$)({ component: FileViewer, }) function FileViewer() { const { _splat } Route.useParams() // URL: /files/documents/report.pdf → _splat documents/report.pdf return divFile path: {_splat}/div }源码视角_splat的编码特殊性在 path.ts 的encodeParam中_splat有专门的处理分支它不会像普通参数那样对整个值做一次encodeURIComponent那样会把/编码为%2F而是按/切分后对每个段分别编码再拼接从而保证文件路径类场景中多级目录结构得以保留。代码中还保留了对旧语法*的兼容写入usedParams[*] splat并标注了 TODO: Deprecate *说明*键将在 v2 中移除。在匹配侧extractParams 同样会把通配捕获同时写入rawParams[*]与rawParams._splat。仓库测试 router.test.tsx 验证了/files/$这类路由捕获_splat tanner并支持这类需要编码的 Unicode 字符。可选参数Optional Params可选参数使用{-$paramName}语法。该段可以存在也可以不存在不存在时值为undefined。// src/routes/posts.{-$category}.tsx import { createFileRoute } from tanstack/react-router export const Route createFileRoute(/posts/{-$category})({ component: PostsComponent, }) function PostsComponent() { const { category } Route.useParams() // URL: /posts → category is undefined // URL: /posts/tech → category is tech return div{category ? Posts in ${category} : All Posts}/div }多个可选参数可以连续出现匹配任意组合// 匹配/posts、/posts/tech、/posts/tech/hello-world export const Route createFileRoute(/posts/{-$category}/{-$slug})({ component: PostComponent, })源码视角可选段的跳过机制在 new-process-route-tree.ts 中MatchStackFrame使用了一个skipped位掩码来记录哪些可选段被跳过beyond 32 segments we cant track skipped optionals注释明确说明这是为了性能而采用的高效布尔数组替代方案。参数提取时被跳过的可选段会回退partIndex与pathIndex让后续段对齐到正确的 URL 片段只有段内有实际值时才会写入rawParams见 extractParams。i18n 场景可选 locale 前缀可选参数最常见的实战场景是国际化语言前缀。下面的路由同时匹配/about、/en/about、/fr/about// src/routes/{-$locale}/about.tsx import { createFileRoute } from tanstack/react-router export const Route createFileRoute(/{-$locale}/about)({ component: AboutComponent, }) function AboutComponent() { const { locale } Route.useParams() const currentLocale locale || en return h1{currentLocale fr ? À Propos : About Us}/h1 } // 匹配/about、/en/about、/fr/about前缀与后缀模式Prefix and Suffix Patterns当$paramName被花括号{}包裹时可以在同一段内、动态部分的前后附带静态文本。这正是花括号语法唯一合理的两种用途前缀/后缀模式与可选参数。前缀// src/routes/posts/post-{$postId}.tsx import { createFileRoute } from tanstack/react-router export const Route createFileRoute(/posts/post-{$postId})({ component: PostComponent, }) function PostComponent() { const { postId } Route.useParams() // URL: /posts/post-123 → postId 123 return divPost ID: {postId}/div }后缀// src/routes/files/{$fileName}[.]txt.tsx import { createFileRoute } from tanstack/react-router export const Route createFileRoute(/files/{$fileName}.txt)({ component: FileComponent, }) function FileComponent() { const { fileName } Route.useParams() // URL: /files/readme.txt → fileName readme return divFile: {fileName}.txt/div }前缀 后缀组合// URL: /users/user-456.json → userId 456 export const Route createFileRoute(/users/user-{$userId}.json)({ component: UserComponent, }) function UserComponent() { const { userId } Route.useParams() return divUser: {userId}/div }源码视角带前缀/后缀的动态段如何匹配与排序在 parseSegment 中花括号形式会被解析为带prefix/suffix的PARAM段extractParams 提取参数时会先按node.prefix.length切掉前缀、再按node.suffix.length切掉后缀。匹配器对动态兄弟节点按特异性排序规则见 sortDynamic带params.parse的节点优先前缀/后缀更长、更具体的节点优先caseSensitive优先特异性相同时保持声明顺序稳定排序。因此post-{$postId}会比裸$slug更早被尝试匹配。带路径参数的导航Navigating with Path Params对象形式import { Link } from tanstack/react-router function PostLink({ postId }: { postId: string }) { return ( Link to/posts/$postId params{{ postId }} View Post /Link ) }函数形式保留其他参数当需要基于当前参数派生新目标时params可以是接收prev并返回新参数对象的函数从而保留既有参数function PostLink({ postId }: { postId: string }) { return ( Link to/posts/$postId params{(prev) ({ ...prev, postId })} View Post /Link ) }编程式导航import { useNavigate } from tanstack/react-router function GoToPost({ postId }: { postId: string }) { const navigate useNavigate() return ( button onClick{() { navigate({ to: /posts/$postId, params: { postId } }) }} Go to Post /button ) }带可选参数的导航可选参数既可以显式传入也可以传undefined将其省略// 包含可选参数 Link to/posts/{-$category} params{{ category: tech }} Tech Posts /Link // 省略可选参数渲染为 /posts Link to/posts/{-$category} params{{ category: undefined }} All Posts /Link源码视角interpolatePath的插值编码流程所有导航最终都会把参数对象插值进路径模板。核心实现位于 path.ts 的interpolatePath服务端isServer走快速路径仅处理$id与裸$通配跳过/间的空段客户端走通用解析路径通过parseSegment逐段识别类型分别处理 PATHNAME / WILDCARD / PARAM / OPTIONAL_PARAM通配段缺失!splat时若存在前缀/后缀则拼接前后缀、否则整段省略可选段值为null/undefined时直接跳过该段见 path.ts所有值经encodeURIComponent编码缺失的必填参数会被记录为isMissingParams。在路由组件之外读取参数useParamsfrom在非路由组件如布局、头部组件中可以用from指定要读取参数的已匹配路由路径import { useParams } from tanstack/react-router function PostHeader() { const { postId } useParams({ from: /posts/$postId }) return h2Post {postId}/h2 }useParamsstrict: false若希望放宽类型约束读取当前匹配到的任意路由的参数可用strict: false。此时返回值是所有可能路由参数的联合类型需要做空值兜底function GenericBreadcrumb() { const params useParams({ strict: false }) // params 是所有可能路由参数的联合类型 return span{params.postId ?? Home}/span }源码视角useParams的实现本质以 React 实现为例useParams.tsx 内部基于useMatch实现select回调中strict: false时读取match.params否则读取match._strictParams并支持select投影与structuralSharing以获得稳定的引用、避免多余重渲染。在 Solid 与 Vue 版本中solid-router/src/useParams.tsx 与 vue-router/src/useParams.tsx 提供同样的 API 语义。类型层面router-core/src/useParams.ts 中的ResolveUseParams在strict: false时解析为AllParamsTRouter[routeTree]全路由参数并集在严格模式下则精确解析为TFrom对应路由的allParams——这正是类型完全推断、勿手动注解的底层原因。Loader 与beforeLoad中的参数参数在数据加载与权限校验阶段同样可用。beforeLoad适合做基于参数的鉴权loader适合基于参数拉取数据export const Route createFileRoute(/posts/$postId)({ beforeLoad: async ({ params }) { // 此处可读取 params.postId const canView await checkPermission(params.postId) if (!canView) throw redirect({ to: /unauthorized }) }, loader: async ({ params }) { return fetchPost(params.postId) }, })允许的字符Allowed Characters与编码默认情况下参数值使用encodeURIComponent编码。若某些 URL 语义字符如邮箱中的、查询分隔符不希望被编码可在创建 router 时通过pathParamsAllowedCharacters放行import { createRouter } from tanstack/react-router const router createRouter({ routeTree, pathParamsAllowedCharacters: [, ], })可放行的完整字符集合为;、:、、、、、$、,。源码视角允许字符如何生效在 router.ts 中pathParamsAllowedCharacters的类型被限定为上述 8 个字符的联合类型router 初始化或update时compileDecodeCharMap 会把每个允许字符的encodeURIComponent编码形式构建成一张字符映射表并编译为一次性的全局正则——用于参数解码时把%40还原为。编码侧仍走encodeURIComponent随后用 decoder 恢复允许字符见encodePathParam。由于该正则只编译一次性能开销可控。仓库测试 router.test.tsx 逐一验证了放行字符后的行为在pathParamsAllowedCharacters: [character]下导航params: { slug: ${character}jane% }路径名中的该字符不会被编码而/、?、\、%、#始终被排除在外。常见错误Common Mistakes错误 1严重跨技能通用把路径参数插值进to字符串直接拼接会破坏类型安全与参数编码特殊字符无法被正确编码/解码// 错误 —— 破坏类型安全与参数编码 Link to{/posts/${postId}}Post/Link // 正确 —— 使用 params prop Link to/posts/$postId params{{ postId }}Post/Link错误 2中等用*而不是$声明 Splat 路由TanStack Router 用$表示通配段捕获值位于_splat键而非*// 错误React Router 等其他框架的写法 // Route path/files/* / // 正确TanStack Router // 文件src/routes/files.$.tsx export const Route createFileRoute(/files/$)({ component: () { const { _splat } Route.useParams() return div{_splat}/div }, })注意*在 v1 中仅为向后兼容而保留将在 v2 中移除请始终使用_splat。源码中的 TODO 注释亦证实了这一点见 new-process-route-tree.ts错误 3中等对基本动态段滥用花括号花括号仅用于前缀/后缀模式与可选参数。基本动态段使用裸$// 错误 —— 基本参数不需要花括号 createFileRoute(/posts/{$postId}) // 正确 —— 基本动态段用裸 $ createFileRoute(/posts/$postId) // 正确 —— 前缀模式用花括号 createFileRoute(/posts/post-{$postId}) // 正确 —— 可选参数用花括号 createFileRoute(/posts/{-$category})错误 4忘记路径参数永远是字符串路径参数一律以字符串解析。需要数值时应在 loader 或组件中自行转换并校验合法性export const Route createFileRoute(/posts/$postId)({ loader: async ({ params }) { const id parseInt(params.postId, 10) if (isNaN(id)) throw notFound() return fetchPost(id) }, })进阶params.parse与params.stringify双向转换如果不想在 loader 中手动parseInt可以在路由上声明params.parse/params.stringify实现参数值在 URL 字符串与运行时类型之间的双向映射。声明后loader 中拿到的params.postId直接就是numberexport const Route createFileRoute(/posts/$postId)({ params: { parse: (raw) ({ postId: parseInt(raw.postId, 10) }), stringify: (parsed) ({ postId: String(parsed.postId) }), }, loader: async ({ params }) { // params.postId 现在是 number return fetchPost(params.postId) }, })源码视角params.parse如何参与匹配在 new-process-route-tree.ts 中params.parse或旧的parseParams会被挂在段树节点上node.parse匹配过程中validateParseParams 会先校验解析结果解析失败则放弃该候选分支并尝试其他更具体的匹配。sortDynamic中带 parse 的节点排在不带 parse 的节点之前正是为了优先尝试更精确的参数约束同时路由级params.priority可进一步控制同层动态段的匹配顺序。小结路径参数是 TanStack Router 类型安全体系的基石$paramName提供动态段、$提供 Splat 全捕获、{-$paramName}提供可选段、{prefix{$x}suffix}提供段内前后缀配合useParams含Route.useParams、from、strict: false与params.parse/stringify可以在 React、Solid、Vue 三个框架中实现端到端推断、零手写类型声明的 URL 数据绑定。核心要点始终是导航一律通过paramsprop 传递参数参数类型交给框架推断需要数值转换时使用params.parse。想深入理解匹配与编码的实现细节可以继续阅读 path.ts、new-process-route-tree.ts 与各框架的 router.test.tsx 测试用例。【免费下载链接】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),仅供参考