关于 spa-routes
用于在 LobeHub 代码库中组织 SPA 路由和功能的开发技能,指导路由文件归属、Web/Electron 共享路由、懒加载骨架屏及平台差异处理。
主要功能
- 01分离路由片段与功能实现
- 02定义共享桌面路由
- 03为懒加载路由注册骨架屏
- 04处理 Web 与 Electron 变体
- 05指导渐进式路由迁移
适用场景
- 01添加 SPA 路由
- 02将业务逻辑移出路由文件
- 03更新 Web 与 Electron 共享路径
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ lobehub/ lobehub --skill spa-routes安装后在 Claude Code 中输入/spa-routes确认已加载。
详细介绍
SPA 路由与功能指南
SPA 结构:
src/spa/– 入口文件(entry.web.tsx、entry.mobile.tsx、entry.desktop.tsx)和路由器配置(router/)。路由器位于此处,避免与src/routes/混淆。src/routes/– 仅包含页面片段(根目录)。src/features/– 按领域组织的业务逻辑和 UI。
本项目采用 根目录与功能分离 的结构:src/routes/ 仅存放页面片段;业务逻辑和 UI 按领域放在 src/features/ 中。
Agent 约束 — 共享桌面路由器: 通用的 Web/Electron 路径、嵌套结构、元数据、延迟加载器和预加载分组都应放在 src/spa/router/desktopRouter.shared.tsx 中。两个 desktopRouter.config* 文件是精简的平台适配器;只有在运行时确实存在差异时才修改它们。不要在两个适配器中重复定义通用路由。
何时使用此指南
- 添加新的 SPA 路由或路由片段
- 在
src/routes/下定义或重构布局/页面文件 - 将特定于路由的组件或逻辑移至
src/features/ - 判断新组件应放在路由目录还是功能目录中
1. src/routes/(根目录)中应包含什么
每个路由目录应 仅 包含:
| 文件 / 文件夹 | 用途 |
|---|---|
_layout/index.tsx 或 layout.tsx | 此片段的布局:使用 <Outlet /> 包裹,可选外壳(例如侧边栏 + 主区域)。应保持精简:优先从 @/features/* 重新导出或组合组件。 |
index.tsx 或 page.tsx | 此片段的页面入口。只从功能模块导入并渲染,不包含业务逻辑。 |
[param]/index.tsx(例如 [id]、[cronId]) | 动态片段页面。规则相同:保持精简,委托给功能模块。 |
规则: 路由文件只能 导入并组合。不要在 src/routes/ 中新建 features/ 文件夹或放置大型组件。
2. src/features/ 中应包含什么
在此处放置 面向领域 的 UI 和逻辑:
- 布局构建块:侧边栏、页眉、主体面板、抽屉
- 该领域的 Hook 和 Store 使用逻辑
- 特定于领域的表单、列表、模态框等
按 领域 组织(例如 Pages、Home、Agent、PageEditor),而不是按路由路径组织。一个路由可以使用多个功能模块;一个功能模块也可以供多个路由使用。
每个功能模块应:
- 位于
src/features/<FeatureName>/下 - 通过
index.ts或index.tsx导出清晰的公共 API - 必要时,内部导入使用
@/features/<FeatureName>/...
3. 如何添加新的 SPA 路由
-
选择路由组
(main)/– 桌面端主应用(mobile)/– 移动端(desktop)/– Electron 专用onboarding/、share/– 特殊流程
-
只在
src/routes/下创建片段文件- 例如
src/routes/(main)/my-feature/_layout/index.tsx和src/routes/(main)/my-feature/index.tsx(以及可选的[id]/index.tsx)。
- 例如
-
在
src/features/中实现布局和页面内容- 创建或复用一个领域(例如
src/features/MyFeature/)。 - 在其中放置布局(侧边栏、页眉、主体)和页面 UI;通过该功能模块的
index导出。
- 创建或复用一个领域(例如
-
保持路由文件精简
- 布局:使用
export { default } from '@/features/MyFeature/MyLayout'导出,或组合少量功能组件与<Outlet />。 - 页面:从
@/features/MyFeature(或特定子路径)导入并渲染;路由文件中不写业务逻辑。
- 布局:使用
-
在正确的定义层注册路由
- **Web/Electron 共享路由:**在
desktopRouter.shared.tsx中使用dynamicElement/dynamicLayout添加一次该路由段。将其preloadId放在此处,作为共享懒加载器定义的一部分。 - **仅限 Web 或仅限 Electron 的路由:**将其添加到对应的精简型
desktopRouter.config.tsx适配器中。明确区分平台专属差异,并尽量保持精简。 - **仅限移动端的流程:**使用
mobileRouter.config.tsx;移动端不使用共享桌面路由树。
- **Web/Electron 共享路由:**在
-
注册路由骨架(每个懒加载路由都必须注册)
- 主区域内的每个懒加载路由都会在其代码块加载时渲染
RouteSegmentSkeleton(src/components/Skeleton/RouteSegment.tsx)。它只通过handle.meta.Skeleton解析(匹配最深的路由优先,并沿父路由向上查找)。省略Skeleton会渲染空白面板——如果这是有意为之,请使用NoRouteSkeleton。 - 添加或重构路由时,选择合适的骨架:
- 如果存在足够接近的通用形状 → 使用来自
@/components/Skeleton/Surface的Skeleton: createSurfaceSkeleton('list' | 'form' | 'grid' | 'editor' | 'detail')。 - 页面有独特布局(仪表盘、多面板、对话) → 在
src/components/Skeleton/下编写专用组件并注册(参见ResourceHome.tsx、Generation.tsx、Conversation/)。
- 如果存在足够接近的通用形状 → 使用来自
- 放置位置:放在路由的
routeMeta中(功能模块的routeMeta.ts或desktopRouter.shared.tsx中的内联定义)。如果整个子树共用一种形状,可以在父路由/布局路由的handle上注册一次——有自己Skeleton的子路由仍会覆盖它。 - 更改页面布局时,在同一个 PR 中更新已注册的骨架——与页面不再匹配的陈旧骨架属于回归问题。
- 仅包含骨架的父级
handle可安全用于标题:标题/图标解析也会优先从最深层向上查找,并始终以叶子路由的 meta 为准。 - 同一个最深层骨架会在路由链的每个等待边界渲染(布局代码块、页面代码块,然后是页面自身的数据等待)。将页面组件内的加载状态设为已注册的骨架,或设为延续该骨架的主体;如果形状不同,看起来就像第二个无关的骨架。
- 路由骨架始终使用静态导入:回退内容必须在首次渲染时绘制出来,而
lazy()骨架(无论是否预加载)都会在其代码块加载前保持空白。这会让骨架的整个导入图进入入口代码块,因此它只能依赖 UI 原语、其他骨架、常量和import type。不要让状态库、服务、功能组件和来自聚合模块的值导入(@/types/*会重新导出 zod schemas)进入其中;请改为将所需的最小部分移到骨架旁边(参见ResourceHome/Skeleton和Explorer/viewMode.ts)。 - 自定义回退(
fallback:选项、页面内的Suspense或提前return <XSkeleton />)只有通过delayed()/DelayedFallback或RouteLoading注册骨架为可见状态(retainSkeleton),才能保持平滑衔接。以其他方式渲染骨架会导致下一个边界在空白面板上等待其 200ms 延迟。
- 主区域内的每个懒加载路由都会在其代码块加载时渲染
3a. 共享桌面路由定义与平台适配器
| 文件 | 作用 |
|---|---|
desktopRouter.shared.tsx | 公共路径、嵌套结构、元数据、延迟导入和优先级路由预加载分组的唯一事实来源。 |
desktopRouter.config.tsx | 精简的 Web 适配器:在 / 挂载公共内容树,并添加仅限 Web 的路由。 |
desktopRouter.config.desktop.tsx | 精简的 Electron 适配器:注入各标签页的 Home 行为、TabHost 根桩以及仅限 Electron 的引导流程。 |
仅在 desktopRouter.shared.tsx 中添加或删除公共路由。确保 desktopRouter.sync.test.tsx 持续通过,以验证路径行为、延迟加载边界、预加载归属以及有意保留的平台差异。
3b. src/routes/ 中的其他 .desktop.{ts,tsx} 变体
精简的路由适配器并非重复的路由树。其他路由模块仍可能将 <name>.desktop.{ts,tsx} 与基础版 <name>.{ts,tsx} 并列放置;Vite 在 Electron 构建中会用 .desktop 文件替换基础文件。这些成对的模块实现仍存在偏离风险。
目前已知的变体:
| 基础文件(Web) | 桌面文件(Electron) | 用途 |
|---|---|---|
src/features/Settings/features/componentMap.ts | src/features/Settings/features/componentMap.desktop.ts | 设置标签页 → 组件映射表。Web 使用动态 import();桌面端使用同步导入。componentMap.sync.test.ts 会确保键名一致。 |
src/routes/(main)/agent/index.tsx | src/routes/(main)/agent/index.desktop.tsx | 页面入口。桌面端变体会完整覆盖 Web 页面(例如,增加额外的弹窗防护)。 |
src/routes/(main)/group/index.tsx | src/routes/(main)/group/index.desktop.tsx | 与 agent 采用相同模式。 |
规则:
- 编辑
src/routes/下的任何.ts/.tsx文件后,使用 glob 在同一目录中查找<filename>.desktop.{ts,tsx}同级文件。如果存在,请在同一提交中对其应用等效更改。 - 添加新的 SettingsTab 时,需在
componentMap.ts(使用dynamic(...))和componentMap.desktop.ts(使用同步import)中都注册。否则,componentMap.sync.test.ts将导致构建失败。 - 新增仅限桌面端、用于完整覆盖页面的实现时,优先在单个基础文件中编写平台感知代码,而不是引入新的
.desktop.tsx变体——只有当两棵树确实存在差异(例如,store 接线或弹窗防护不同)时,才添加新变体。 - 删除时,需同时删除两个文件。
4. 如何划分文件(route 与 feature)
| 问题 | 放在 src/routes/ | 放在 src/features/ |
|---|---|---|
| 这是 route 的布局包装器或页面入口吗? | 是——_layout/index.tsx、index.tsx、[id]/index.tsx | 否 |
| 它包含业务逻辑或复杂的 UI 吗? | 否 | 是——放在对应的领域下 |
| 它是可复用的布局组件(侧边栏、页眉、正文)吗? | 否 | 是 |
| 它是 hook、store 用法或领域逻辑吗? | 否 | 是 |
| 它是否仅重新导出或组合 feature 组件? | 是 | 否 |
示例
- Route(精简):
src/routes/(main)/page/_layout/index.tsx→export { default } from '@/features/Pages/PageLayout' - Feature(实际实现):
src/features/Pages/PageLayout/→ 侧边栏、DataSync、正文、页眉、样式等 - Route(精简):
src/routes/(main)/page/index.tsx→ 从@/features/Pages和@/features/PageExplorer导入PageTitle、PageExplorerPlaceholder,并使用<PageTitle />和占位组件进行渲染。 - Feature:
页面列表、操作、抽屉和 hooks 位于src/features/Pages/下。
5. 渐进式迁移(现有代码)
现有 route 并未统一遵循这一结构:一些已经是精简的 segment 文件(例如 src/routes/(main)/page/、src/routes/(main)/settings/),另一些仍在 src/routes/ 下包含逻辑或 features/ 文件夹。请检查 route 的当前状态,不要想当然。
处理仍在 src/routes/ 中包含逻辑或 features/ 的旧 route 时:
- 优先在
src/features/<Domain>/中添加新代码,并从 routes 导入。 - 对于较大的重构,将现有仅供 route 使用的逻辑移至合适的 feature,然后精简 route 文件(从 features 重新导出或组合)。
- 移动文件时使用
git mv,以保留历史记录。
6. 参考结构(Phase 1 之后)
Route(精简):
src/routes/(main)/page/
├── _layout/index.tsx → re-export or compose from @/features/Pages/PageLayout
├── index.tsx → import from @/features/Pages, @/features/PageExplorer
└── [id]/index.tsx → import from @/features/Pages, @/features/PageExplorer
Feature(实现):
src/features/Pages/
├── index.ts → export PageLayout, PageTitle
├── PageTitle.tsx
└── PageLayout/
├── index.tsx → Sidebar + Outlet + DataSync
├── DataSync.tsx
├── Sidebar.tsx
├── style.ts
├── Body/ → list, actions, drawer, etc.
└── Header/ → breadcrumb, add button, etc.
Router 配置仍指向 route 路径(例如 @/routes/(main)/page、@/routes/(main)/page/_layout);route 文件随后委托给 features。