用好 TanStack Start 的路由层
摘要
写 TanStack Start 项目时,路由层能做的事情远比你想的多。这篇文章以博客的真实代码为例,聊聊怎么用好 beforeLoad、loader 和 head,把权限、校验和 SEO 收拢到正确的地方。
在《我的博客项目架构 - Tanstack Start与依赖注入实战》里,我们聊了 TanStack Start 里的中间件和依赖注入。这篇想换个角度,专门聊聊在这个博客项目里,我是怎么理解和使用路由层的。
刚接触这类全栈框架时,最常见的做法就是把路由文件当成一个“页面壳子”:随便拉点数据,然后一股脑全丢给 React 组件去渲染。
但实际跑下来,我发现一旦把所有逻辑都往组件里塞,页面代码会变得非常臃肿。其实,路由层是一个极佳的前置防线,它天然适合处理这些事:
权限守卫(能不能看)
搜索参数校验(参数对不对)
首屏数据预取(尽早发起请求)
SEO 元信息(标题和 Meta 标签)
404 和 Loading 边界(异常怎么兜底)
把这些逻辑提上来,你的 React 组件就能干干净净地只负责渲染。
文章详情页的核心逻辑
文章也算是整个博客最核心的入口了。看看我现在的路由配置是怎么写的:
在这个文件里,还没有碰到真正的页面组件,路由层其实已经把“验参数、拉主数据、预取边缘数据、处理 404、生成 SEO 信息”这五件事全包办了。
拦截脏数据:validateSearch
你看文章页里的搜索参数其实很简单,主要用来定位评论:
为什么要在路由里校验,而不是等进了 React 组件再用 useSearchParams 自己去 parse 呢?
因为组件不该去处理 URL 里的脏数据。在路由层校验,哪怕参数乱填,在进入页面前就能被规整或者拦截掉。而且好处是,下游的 loader 和组件拿到的,永远是类型安全的参数。
列表页也是同样的操作,而且还能通过 loaderDeps 明确告诉系统:只有这些参数变了,才需要重新跑一次拉取逻辑。
权限守卫:beforeLoad
说到门卫,后台管理页面是最典型的场景:
在这个博客里,我对 beforeLoad 的态度非常克制:它只用来决定你此时此刻能不能进这条路由。
千万别为了图省事,把页面该展示的业务数据也塞进 beforeLoad 去查。查权限归 beforeLoad,查数据归 loader,这两者一旦混在一起,后面的重构会非常痛苦。
如果是诸如 OAuth 回调或者登录判断,更是天然顺手:直接在 beforeLoad 里检查 Session,如果没有,带上 location.href 原路跳回登录页,组件里连一行判断逻辑都不用写。说起这个,在给后台接入 OAuth 时,我还遇到了 Antigravity MCP 的 Auth 状态坑,有兴趣的可以顺道避个雷。
数据加载与预热:loader
很多时候,你的页面不止依赖一份数据。文章详情页除了加载文章本身,可能还需要配图、作者信息、相关推荐。
如果把所有的请求都一个大 Promise.all 框起来 await,只要有一个接口慢,首屏就得一直白板。这里路由层的轻量编排能力就体现出来了:
我们只 await 最核心的内容,至于底部的相关推荐,直接扔一个 prefetchQuery 到后台默默拉取。等用户滚动到底部组件 mount 时,缓存里已经有数据了,完美做到无缝切换。关于这种缓存思路的极致榨取,前两天写的 发布后缓存系统如何联动 也有异曲同工之妙。
SEO 原生支持:head
以前写单页应用,总喜欢在页面最外层套个 <Helmet> 或者 <Head> 组件,把标题和摘要拼进去。
但这很不直观。文章的标题、描述、Canonical 链接,甚至是用作结构化数据的 JSON-LD,它们本身就是由刚才 loader 拿回来的数据决定的。
既然如此,直接在路由配置的 head 属性里返回不是更顺畅吗?
这样最大的好处是:SSR 的时候,HTML 头部信息天然就是完整的,并且不会出现客户端渲染时默认标题闪烁的尴尬情况。这正是 《上手Tanstack Start框架,舒适的开发体验》 里提到的 Data-Only SSR 模式的威力。
异常与骨架屏:notFound 和 pendingComponent
你看 loader 代码里有一句:
这意味着我们绝不让一个“空的文章对象”流到底下的 React 组件里去。组件不需要写 if(!post) return <div>找不到文章</div> 这种恶心的分支逻辑。交给路由系统,它会自动渲染全局的 404 组件,而且 HTTP 状态码也能顺理成章地变成准确的 404。
如果是网络稍微慢一点呢?路由级别的 pendingComponent 就能接管:
不过说实话,整页 Skeleton 比较适合大页面的切换。如果你只是局部某个小按钮、小区域要更新,那还是把 Loading 状态交给具体组件内部去维护更自然。有两个颗粒度的设计,我们在 《全栈开发的错误处理》 也有提到类似的关注点分离。
总结
经过一番折腾,我现在觉得 TanStack Start 最舒服的体位就是:业务逻辑继续留在 Service 层,而把“进入页面”的一系列把关动作全交接给路由层完成。
路由层干的活儿虽然杂,但很关键。它帮你挡住了乱填的 URL 参数,掐断了未授权的访问,预热了该有的缓存,还顺手把 SEO 安排明白了。
当你把这些“杂活”都从 UI 代码里剥离出去后,你会发现底下的 React 组件其实非常纯粹:只需要专注做数据展现、渲染骨架、绑定事件。这正是全栈开发里最不容易重构到吐血的安全感来源。
