
在 Monorepo 中落地类型安全的 TanStack Solid RouterRouter 独立库 Solid Query 的工程实践【免费下载链接】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导读本文以本仓库中的 router-monorepo-solid-query 示例 为蓝本讲解在 pnpm/npm monorepo 工程中如何将 TanStack Solid Router 拆分为纯路由库 数据查询库 UI 特性库 应用壳的多包结构从而解决 monorepo 场景下 TypeScript 类型增强type augmentation失效、跨包链接不类型安全、以及路由与数据层循环依赖这三大痛点。读完本文你将掌握如何在独立包中注册 Router 类型、为何必须由 Router 库统一 re-export 路由 API、以及如何在应用入口用一张路由 → 组件映射表把整棵路由树与组件绑定起来最终获得完全类型安全的 Solid 单页应用。一、问题背景为什么 monorepo 会破坏路由的类型安全TanStack Router包括 Solid 版本实现端到端类型安全的核心机制是TypeScript 模块增强module augmentationrouteTree.gen.ts会把FileRoutesByPath等接口注入tanstack/solid-router的模块声明中同时应用需要声明declare module tanstack/solid-router { interface Register { router: typeof router } }这套机制在单包应用里没有问题。但在 monorepo 中一个典型场景是数据查询库或 UI 特性库内部也要使用Link to...、getRouteApi(/$postId)等 API。如果类型注册只发生在最终 app 包里那么库内代码引用tanstack/solid-router时TypeScript 解析到的是node_modules 里的原始类型声明而不是 app 里增强过的版本于是库里的to/$postId、params等属性退化为宽松的string拼错路径、写错参数、漏写必填字段都不会在编译期报错类型安全的链路在路由库 → 特性库这一段直接断裂。原文档明确指出这一困境如果你直接把这个设置放在最终 app 里库内部的链接将不再类型安全。二、解法总览独立的 Router 库 自下而上的依赖方向示例给出的工程化答案是单独建一个只负责路由、不包含任何业务组件的 Router 库并让依赖方向保持单向流动packages/router 路由树 注册 路由 API re-export ↑ packages/post-query 数据查询选项集合可被 router 与 feature 同时引用 ↑ packages/post-feature UI 组件仅依赖 router 与 post-query ↑ packages/app 应用壳做路由 → 组件的最终绑定这样设计有两个关键收益原文明确强调数据库的 query options 可以同时被 Router 库loader 中和特性库组件中复用而不会产生循环依赖——数据层位于依赖图的最底部只依赖tanstack/solid-query与数据获取逻辑。Router 库 re-export 路由组件Link、Outlet、getRouteApi等特性库一律从 Router 库导入而不是直接 importtanstack/solid-router从而保证这些组件始终与类型增强绑定在一起天然类型安全。下面是该示例在 monorepo 中的结构示意来自示例自带的架构图图中可见四个包之间的依赖箭头app → post-feature → post-query以及app → router、router → post-query依赖方向自底向上单向流动。三、四个包的分工与关键源码3.1packages/router路由树的唯一主人Router 库是整个方案的枢纽包含四类文件路由文件src/routes/、生成的routeTree.gen.ts、router.tsx创建实例与index.ts注册 re-export。创建 Router 实例router.tsximport { createRouter } from tanstack/solid-router import { QueryClient } from tanstack/solid-query import { routeTree } from ./routeTree.gen import type { RouteIds } from tanstack/solid-router export const queryClient new QueryClient() export const router createRouter({ routeTree, context: { queryClient, }, defaultPendingComponent: () ( divLoading form global pending component.../div ), // loader 等待 200ms 即显示 pending 组件而非默认的 1000ms defaultPendingMs: 200, defaultPreload: intent, // 使用 Solid Query 时不希望 loader 结果过期 // 保证每次 preload 或访问路由都会调用 loader defaultPreloadStaleTime: 0, scrollRestoration: true, }) export type RouterType typeof router export type RouterIds RouteIdsRouterType[routeTree]这里值得注意的配置项及其作用context.queryClient把 Solid Query 的QueryClient注入路由上下文后续 loader 通过{ context: { queryClient } }取用避免在路由文件里手动 new 客户端defaultPendingMs: 200缩短 pending 组件出现前的等待阈值让慢 loader 时 UI 反馈更快defaultPreload: intent按鼠标悬停等意图预加载defaultPreloadStaleTime: 0与 React Query 版示例注释一致——使用数据查询库时禁止 loader 结果被视为新鲜确保预加载/访问时始终重新执行 loaderscrollRestoration: true启用滚动位置恢复。类型注册与 API re-exportindex.ts是整套方案的核心import { queryClient, router } from ./router export type { RouterType, RouterIds } from ./router // Register the router instance for type safety declare module tanstack/solid-router { interface Register { router: typeof router } } export { router, queryClient } // 通过 re-export 路由 API强制其他包依赖本包而非直接依赖 tanstack/solid-router // 从而使类型注册对所有下游生效 export { Outlet, Link, useRouteContext, useRouter, RouterProvider, getRouteApi, ErrorComponent, } from tanstack/solid-router export type { ErrorComponentProps } from tanstack/solid-router关键点在于最后一段 re-export特性库里的import { Link, Outlet } from router-solid-mono-solid-query/router走的是经过 module augmentation 的模块实例而declare module tanstack/solid-router只在此包中出现一次。这正好回应了原文档的结论由于 Router 库 re-export 了路由组件在特性库中导入它们就能保证类型安全因为它们与 TypeScript 增强绑定在一起。3.2packages/post-query与路由无关的数据层该包不感知路由只负责定义查询选项与数据获取函数postsQueryOptions.tsxqueryOptions({ queryKey: [posts], queryFn: () fetchPosts() })postQueryOptions.tsx按参数化postId生成queryKey: [posts, { postId }]posts.tsx用redaxios请求jsonplaceholder.typicode.com并定义PostNotFoundError404 时抛出。由于它位于依赖图最底层Router 库可以在 loader 中这样消费它routes/index.tsimport { createFileRoute } from tanstack/solid-router import { postsQueryOptions } from router-solid-mono-solid-query/post-query export const Route createFileRoute(/)({ loader: ({ context: { queryClient } }) { return queryClient.ensureQueryData(postsQueryOptions) }, })参数路由 routes/$postId.ts 同理只是把postId从params中取出传入postQueryOptions(postId)。ensureQueryData保证已有缓存则直接复用否则发起请求避免组件与 loader 双重请求——这正是把 query options 放进独立库的最大收益loader 与组件共享同一份查询定义无循环依赖。根路由 routes/__root.tsx 还展示了类型化的上下文声明import { Link, createRootRouteWithContext } from tanstack/solid-router import type { QueryClient } from tanstack/solid-query export const Route createRootRouteWithContext{ queryClient: QueryClient }()({ notFoundComponent: () { return ( div pThis is the notFoundComponent configured on root route/p Link to/Start Over/Link /div ) }, })createRootRouteWithContext{ queryClient: QueryClient }让context的类型在createFileRoute的 loader 签名里自动可用。3.3packages/post-feature只从 Router 库导入特性库的所有组件统一从router-solid-mono-solid-query/router导入路由 API从post-query导入数据PostList.tsxuseQuery(() postsQueryOptions)渲染列表并用类型安全的Link to/$postId params{{ postId: post.id }}跳转详情同时渲染Outlet /挂载子路由PostIdPage.tsx用getRouteApi(/$postId)获得类型化的useParams再useQuery(() postQueryOptions(postId))拉取详情PostError.tsx详情页错误组件index.ts统一export *。观察PostList.tsx的导入来源就能验证re-export 强制类型安全的设计Link、Outlet来自 router 库postsQueryOptions来自 post-query 库——特性包从不直接触碰tanstack/solid-router。3.4packages/app把路由树与组件绑定的最终拼图应用壳main.tsx做了两件事。其一路由 → 组件映射表const routerMap { /: PostsListComponent, /$postId: PostIdComponent, __root__: RootComponent, } as const satisfies RecordRouterIds, () JSX.Element Object.entries(routerMap).forEach(([path, component]) { const foundRoute router.routesById[path as RouterIds] foundRoute.update({ component }) })RecordRouterIds, ...与satisfies保证映射表的键必须是真实存在的路由 id——如果路由树里没有/$postId这一行就会编译报错。原文档特别指出在这里本可以强制执行懒加载lazy loading但为了简单起见被省略了——注释中也提示可以逐个从特性库导出组件并在 app 层用lazy包装以达成代码分割。其二错误组件映射const errorComponentMap { /: null, /$postId: PostErrorComponent, __root__: null, }遍历时跳过null把PostErrorComponent通过foundRoute.update({ errorComponent })挂到/$postId上。这验证了update()是一个可反复调用的通用接口组件、错误页乃至其他路由属性都可以在应用层按需注入。最后用QueryClientProvider包裹RouterProvider完成挂载main.tsxrender( () ( QueryClientProvider client{queryClient} RouterProvider router{router} / /QueryClientProvider ), rootElement, )根组件 rootComponent.tsx 同样只从 router 库导入Link、Outlet并挂载TanStackRouterDevtools与SolidQueryDevtools便于调试。四、工程配置pnpm workspace 与包依赖示例提供了 pnpm-workspace.yaml.example内容极简packages: - packages/*各包通过workspace:*相互引用例如 app 的 package.json{ name: router-solid-mono-solid-query/app, private: true, type: module, scripts: { dev: vite --port3001, build: vite build tsc --noEmit, preview: vite preview, start: vite }, dependencies: { tanstack/solid-query: ^5.90.9, router-solid-mono-solid-query/post-feature: workspace:*, router-solid-mono-solid-query/router: workspace:*, solid-js: ^1.9.10 } }其中nx.targets.dev.dependsOn: [^build]表示开发模式下先构建上游依赖包保证 router/post-query 等包的最新代码被 app 消费。pnpm install会把pnpm-workspace.yaml.example复制为pnpm-workspace.yaml后生效。五、运行方式原文档给出的运行步骤以仓库根目录为基准cd examples/solid/router-monorepo-solid-query pnpm install # 或 npm install / yarn pnpm dev # 或 npm dev / yarn devpnpm dev会触发vite --port3001启动后访问本地服务即可看到帖子列表与详情页。应用入口 HTML 位于 packages/app/index.html。六、已知限制Stackblitz 上的 IDE 类型反馈原文档记录了一个环境相关的注意点由于 Stackblitz 的限制示例的类型在 IDE 中不会立即被正确推断但只要点击右下角的 fork类型推断就会恢复正常。这是 Stackblitz 云端环境的已知行为与代码本身无关在本地 clone 后使用 VSCode 等编辑器打开时不受影响。七、模式总结与可迁移要点关注点推荐做法本示例中的落点类型注册只做一次独立 Router 库内declare module tanstack/solid-routerrouter/src/index.ts库内必须类型安全所有包从 Router 库导入Link/Outlet/getRouteApi等post-feature/src/PostList.tsx数据与路由解耦query options 放独立数据包router loader 用ensureQueryDatarouter/src/routes/index.ts组件与路由绑定推迟到应用层router.routesById[id].update({ component })映射表app/src/main.tsx后续代码分割在 app 层对组件做 lazy 包装示例为简洁略去见 main.tsx 内注释这套独立 Router 库 单向依赖 应用层绑定的架构同样适用于 React 版 TanStack Router本仓库在 examples/react/router-monorepo-react-query 提供了对等示例可以作为团队在 monorepo 中推行全栈类型安全的通用模板。【免费下载链接】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),仅供参考