ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Prefect UI v2 路由层开发指南:基于 Tanstack Router 的数据加载、搜索参数与分页实践

Prefect UI v2 路由层开发指南:基于 Tanstack Router 的数据加载、搜索参数与分页实践 Prefect UI v2 路由层开发指南基于 Tanstack Router 的数据加载、搜索参数与分页实践【免费下载链接】prefectPrefect is a workflow orchestration framework for building resilient data pipelines in Python.项目地址: https://gitcode.com/GitHub_Trending/pr/prefect导读本文以 Prefect 开源仓库中 UI v2 前端ui-v2的路由定义目录 ui-v2/src/routes/AGENTS.md 为核心系统讲解该仓库在 Tanstack Router 体系下沉淀的路由开发规范loader 数据加载三段式模式、基于 Zod 4 的搜索参数校验、loaderDeps与 UI 状态隔离以及跨页面持久化分页大小等实战方案。读完本文你将掌握 Prefect UI v2 中每一个列表页/详情页如 Flows、Blocks、Deployments 等路由的实现套路并能将这套模式复用到自己的 Tanstack Router React Query 项目中。背景路由目录与 Tanstack Routerui-v2是 Prefect 的新一代 Web 控制台Vue 之外的 React 实现其前端路由完全建立在 Tanstack Router 之上。路由定义集中存放于 ui-v2/src/routes 目录采用文件即路由的约定顶层路由文件如 dashboard.tsx、login.tsx、settings.tsx子目录对应带参数的详情路由例如 flows/flow.$id.tsx/flows/flow/$id、flow-runs/flow-run.$id.tsx、flow-runs/task-run.$id.tsx编译产物 routeTree.gen.ts 由文件结构自动生成并通过 router.tsx 中的createAppRouter装配。在 router.tsx 中可以看到路由器的全局配置这些配置直接决定了路由加载行为return createRouter({ routeTree, basepath: resolveRouterBasePath(), history, context: { queryClient: routerQueryClient, auth: undefined as unknown as AuthState, // Will be provided by App }, defaultPreload: intent, // Since were using React Query, we dont want loader calls to ever be stale // This will ensure that the loader is always called when the route is preloaded or visited defaultPreloadStaleTime: 0, // Show pending component after 400ms of loading, and keep it visible for at least 400ms defaultPendingMs: 400, defaultPendingMinMs: 400, });几个值得注意的要点RouterContext 注入依赖路由上下文携带queryClientReact Query 客户端和auth认证状态loader 通过context.queryClient访问数据层无需在组件里再创建客户端defaultPreload: intentTanstack Router 默认在链接悬停/即将导航时预加载目标路由配合defaultPreloadStaleTime: 0保证 loader 每次访问都会真正执行避免读到陈旧缓存defaultPendingMs/defaultPendingMinMs均为 400ms加载超过 400ms 才显示 pending 组件且至少展示 400ms避免快速加载时界面闪烁。三条核心准则ui-v2/src/routes/AGENTS.md 为该目录定下了三条铁律任何新路由都应遵守使用createFileRoute定义路由所有路由文件必须以 Tanstack Router 的createFileRoute(/path)方式导出Route配合 routeTree.gen.ts 自动生成的路由树完成注册关键数据在 loader 中 await慢速/非关键数据后台 prefetch区分渲染前必须就绪的数据与可以稍后到达的数据是避免首屏白屏与 Suspense 抖动的基础使用 suspense query 的路由必须开启wrapInSuspense: true让 Tanstack Router 在数据未就绪时展示 pending 组件项目统一使用PrefectLoading而不是让组件自身抛 Suspense。以 flows/flow.$id.tsx 为例路由定义末尾wrapInSuspense: true, pendingComponent: PrefectLoading,wrapInSuspense使整个路由组件被 Suspense 包裹pendingComponent指定加载期间的占位 UI同时该路由还通过errorComponent内部使用RouteErrorState与categorizeError分类错误提供错误兜底。Data Loading Patternloader 的三段式数据加载这是 AGENTS.md 中给出的核心代码骨架也是 ui-v2 所有路由的 loader 标准写法export const Route createFileRoute(/path)({ component: RouteComponent, loader: async ({ params, context: { queryClient } }) { // Non-dependent deferred data (prefetch) void queryClient.prefetchQuery(buildSomeQuery()); // Critical data (await) const criticalData await queryClient.ensureQueryData( buildCriticalQuery(params.id), ); // Dependent deferred data (prefetch based on critical data) void queryClient.prefetchQuery(buildDependentQuery(criticalData.id)); }, wrapInSuspense: true, });这一模式把 loader 内的取数动作严格分成三个阶段顺序不可颠倒阶段手段是否阻塞渲染适用数据1. 无依赖的后台数据void queryClient.prefetchQuery(...)否与关键数据无依赖关系的统计、历史、次要列表2. 关键数据await queryClient.ensureQueryData(...)是返回给 loader页面主体渲染所必需的记录如 flow 详情3. 依赖关键数据的后台数据void queryClient.prefetchQuery(...)否需要基于关键数据的 id 才能发起的查询三个关键 API 的语义区别prefetchQuery将查询写入缓存但不等待结果、不阻塞 loader 返回适合能提前加载就提前加载的次要数据ensureQueryData若缓存中没有数据则发起请求并且await 其完成保证 loader 返回时数据一定就绪适合关键路径void运算符显式声明这些 prefetch Promise 的结果被有意忽略避免未处理的 Promise 告警同时保持代码自文档化。实战剖析/flows/flow/$id路由的 loaderflows/flow.$id.tsx 是这一模式最完整的落地实现其 loader 的执行顺序完全遵循三段式批量无依赖 prefetchflow 详情、分页后的 flow runs、分页后的 deployments、deployments 总数、FlowStatsSummary 相关的历史/计数查询buildFlowRunsHistoryFilter、buildFlowRunsCountFilterForHistory、任务运行累计卡片buildTotalTaskRunsCountFilter、buildCompletedTaskRunsCountFilter、buildFailedTaskRunsCountFilter、buildRunningTaskRunsCountFilter、任务运行历史buildTaskRunsHistoryQuery等全部以void queryClient.prefetchQuery(...)发起关键数据 await最后返回context.queryClient.ensureQueryData(buildFLowDetailsQuery(id))Flow 详情必须在渲染前就绪依赖数据的后台链式加载loader 内还启动了一个异步链——await ensureQueryData(buildPaginateFlowRunsQuery(...))拿到当前页 flow run 列表后再为每个 run id 批量 prefetch 任务运行计数buildGetFlowRunsTaskRunsCountQuery(flowRunIds)确保FlowRunCard渲染时计数已就绪见 flows/flow.$id.tsx。组件内部还有两处值得学习的预取技巧悬停预取分页onPrefetchPage在鼠标悬停分页按钮时先ensureQueryData目标页数据再链式 prefetch 该页每个 run 的任务计数flows/flow.$id.tsxplaceholderData 防抖动flow runs 与 deployments 的列表查询使用useQuerykeepPreviousData而非useSuspenseQuery配合注释prevents the page from suspending when search/filter changes——筛选条件变化时展示旧数据过渡而不是重新触发整页 Suspenseflows/flow.$id.tsx。Search Parameters直接用 Zod 4 校验AGENTS.md 规定搜索参数直接传 Zod schema 给validateSearch不经过手写的中间解析层import { z } from zod; const searchSchema z.object({ redirect: z.string().optional(), }); export const Route createFileRoute(/path)({ validateSearch: searchSchema, component: function RouteComponent() { const { redirect } Route.useSearch(); // ... }, });Zod schema 会同时承担运行时校验与 TypeScript 类型推导通过z.infer得到SearchParams类型见 flows/flow.$id.tsx组件内通过Route.useSearch()读取类型安全地访问redirect等字段。ui-v2 路由中 Zod schema 的典型写法来自 flows/index.tsxconst searchParams z .object({ name: z.string().optional(), page: z.number().int().positive().optional().default(1).catch(1), limit: z.number().int().positive().max(100).optional().catch(undefined), tags: z.array(z.string()).optional(), sort: z .enum([CREATED_DESC, UPDATED_DESC, NAME_ASC, NAME_DESC]) .optional() .default(NAME_ASC), }) .optional() .prefault({});细节说明.catch(...)的容错URL 参数天然是字符串z.number()在解析时会做字符串到数字的转换.catch(1)保证脏数据如负数、非法值时回落为 1 而不是抛错枚举约束sort用z.enum限定合法排序键非法值自动被校验拦下.prefault({})当 URL 上没有任何搜索参数时为所有字段填充默认值避免组件读到undefined。在 flows/flow.$id.tsx 中可以看到更复杂的分组参数设计tab、runs.page、runs.limit、runs.sort、runs.flowRuns.nameLike、runs.flowRuns.state.name、deployments.page、deployments.limit、deployments.nameLike、deployments.tags、deployments.sort等采用命名空间前缀runs.*/deployments.*隔离不同子模块的 UI 状态详情页内切换 tab、翻页、筛选互不干扰。loaderDeps 与 UI-Only Search Params防止整页重挂载问题的本质loaderDeps决定哪些搜索参数的变化会重新触发 loader。而 loader 重跑时路由会重新进入 Suspense、重置所有本地 UI 状态——手风琴展开的折叠面板会收起、打开的对话框会关闭、滚动位置会丢失。因此 AGENTS.md 给出判断标准只把影响loader 取什么数据的参数放进loaderDeps凡是只控制 UI 状态的参数分页页码、激活的 tab、手风琴开关一律排除loaderDeps: ({ search }) { // page and flow drive accordion pagination but dont affect // loader fetches — exclude them to avoid re-suspending the route. const { page, flow, ...rest } search; void page; void flow; return rest; },void page; void flow;是解构掉但明确表示忽略的惯用法防止未使用变量告警。如果遗漏这一步每次点击分页按钮都会重跑 loader已展开的手风琴、未提交的表单输入等临时 UI 状态全部丢失体验劣化非常明显。仓库中的实际取舍从源码看ui-v2 各路由对loaderDeps的处理有两种策略策略一把全部 search 传给 loader详情页。/flows/flow/$id的 loader 确实依赖runs.page、runs.limit、runs.sort、deployments.*等参数来构造分页请求因此loaderDeps: ({ search }) ({ flowRunsDeps: search, }),见 flows/flow.$id.tsx。这类参数变化会触发 loader 重新取数——这是有意为之因为过滤条件变了数据确实要重查。策略二数据层用非 Suspense 查询兜底列表页。/flows/、/blocks/、/deployments/、/variables/、/events/、/runs/等列表页对分页/筛选类参数使用useQuery而非useSuspenseQueryplaceholderData: keepPreviousData配合 React Query 的缓存键设计即便loaderDeps携带了这些参数页面主体也不会因搜索条件变化而整页 Suspense——旧数据原地保留新数据到达后平滑替换见 flows/index.tsx 与 blocks/index.tsx。Pagination with Persisted Page Size持久化分页大小一个容易踩的坑不要用.default(10)在 ui-v2 中需要跨导航记住每页条数的路由Flows、Blocks、Deployments、Variables统一使用usePageSizePreference这个 hook。AGENTS.md 特别强调这类路由的limit搜索参数必须使用.optional().catch(undefined)绝不能写.default(10)。原因在于 use-page-size-preference.ts 的实现useEffect(() { if (hasInitializedRef.current) return; hasInitializedRef.current true; if (urlPageSize undefined) { onInitialize(storedPageSize); } }, [urlPageSize, storedPageSize, onInitialize]);hook 依赖urlPageSize undefined来识别用户首次进入、URL 中无显式 limit的场景此时调用onInitialize把 localStorage 里保存的偏好写回 URL。一旦给limit加了.default(10)URL 永远有值hook 永远收不到undefinedonInitialize永远不会被调用——持久化静默失效。这正是 flows/index.tsx 写成limit: z.number().int().positive().max(100).optional().catch(undefined)的原因。hook 的双向同步机制use-page-size-preference.ts 的完整行为源码注释与实现可互相印证存储键STORAGE_KEY prefect-page-size默认页大小DEFAULT_PAGE_SIZE 10首次挂载时若 URL 无limit写入本地存储的偏好onInitialize用户更改页大小后新的urlPageSize通过第二个useEffect写回 localStorageuse-page-size-preference.ts返回值urlPageSize ?? storedPageSize作为组件实际使用的分页大小URL 有值用 URL 值否则用存储值。配套的单元测试 use-page-size-preference.test.ts 覆盖了恢复存储值到 URLURL 值写回存储等关键路径是验证该行为契约的权威依据。实际路由中的用法见 blocks/index.tsxpage用.default(1).catch(1)而limit保持.optional().catch(undefined)二者职责清晰分离。Best Practices 速查清单汇总 ui-v2/src/routes/AGENTS.md 与仓库源码ui-v2 路由层的最终实践清单如下Prefetch 一律用void显式忽略 Promise既避免告警也向读者表明此数据不阻塞渲染关键数据用ensureQueryData并 await渲染依赖的数据必须在 loader 返回前就绪次要数据用prefetchQuery后台加载不拖慢首屏loader 顺序固定为无依赖 prefetch → await 关键数据 → 依赖数据 prefetch让关键路径最短、依赖关系清晰搜索参数用 Zod 4 schema 校验配合z.infer获得类型loaderDeps只放影响取数的参数纯 UI 参数分页、tab、手风琴排除在外避免重挂载丢状态持久化分页大小的路由limit用.optional().catch(undefined)禁用.default(...)否则usePageSizePreference的首次恢复逻辑失效suspense 查询路由开启wrapInSuspense: true并配置pendingComponent与errorComponent。小结Prefect UI v2 的路由层ui-v2/src/routes通过 AGENTS.md 将 Tanstack Router React Query 的最佳实践固化成了可执行的工程规范loader 三段式取数让关键路径最短Zod 4 让 URL 状态类型安全loaderDeps与keepPreviousData双保险保证筛选/翻页不丢 UI 状态usePageSizePreference则解决了分页大小的跨会话记忆。当你阅读或新增 ui-v2 中任何路由文件从 dashboard.tsx 到 flows/flow.$id.tsx时这套模式就是贯穿始终的施工图纸对于在自己项目中搭建类似管理控制台的开发者这也是一份可直接迁移的高质量参考实现。【免费下载链接】prefectPrefect is a workflow orchestration framework for building resilient data pipelines in Python.项目地址: https://gitcode.com/GitHub_Trending/pr/prefect创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表