mcpField
S
spa-routes

spa-routes

github.com/lobehub开发83.6k204 次安装今天更新
代码仓库

关于 spa-routes

用于在 LobeHub 代码库中组织 SPA 路由和功能的开发技能,指导路由文件归属、Web/Electron 共享路由、懒加载骨架屏及平台差异处理。

主要功能

  1. 01分离路由片段与功能实现
  2. 02定义共享桌面路由
  3. 03为懒加载路由注册骨架屏
  4. 04处理 Web 与 Electron 变体
  5. 05指导渐进式路由迁移

适用场景

  1. 01添加 SPA 路由
  2. 02将业务逻辑移出路由文件
  3. 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 路由

  1. 选择路由组

    • (main)/ – 桌面端主应用
    • (mobile)/ – 移动端
    • (desktop)/ – Electron 专用
    • onboarding/、share/ – 特殊流程
  2. 只在 src/routes/ 下创建片段文件

    • 例如 src/routes/(main)/my-feature/_layout/index.tsx 和 src/routes/(main)/my-feature/index.tsx(以及可选的 [id]/index.tsx)。
  3. 在 src/features/ 中实现布局和页面内容

    • 创建或复用一个领域(例如 src/features/MyFeature/)。
    • 在其中放置布局(侧边栏、页眉、主体)和页面 UI;通过该功能模块的 index 导出。
  4. 保持路由文件精简

    • 布局:使用 export { default } from '@/features/MyFeature/MyLayout' 导出,或组合少量功能组件与 <Outlet />。
    • 页面:从 @/features/MyFeature(或特定子路径)导入并渲染;路由文件中不写业务逻辑。
  5. 在正确的定义层注册路由

    • **Web/Electron 共享路由:**在 desktopRouter.shared.tsx 中使用 dynamicElement / dynamicLayout 添加一次该路由段。将其 preloadId 放在此处,作为共享懒加载器定义的一部分。
    • **仅限 Web 或仅限 Electron 的路由:**将其添加到对应的精简型 desktopRouter.config.tsx 适配器中。明确区分平台专属差异,并尽量保持精简。
    • **仅限移动端的流程:**使用 mobileRouter.config.tsx;移动端不使用共享桌面路由树。
  6. 注册路由骨架(每个懒加载路由都必须注册)

    • 主区域内的每个懒加载路由都会在其代码块加载时渲染 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.tssrc/features/Settings/features/componentMap.desktop.ts设置标签页 → 组件映射表。Web 使用动态 import();桌面端使用同步导入。componentMap.sync.test.ts 会确保键名一致。
src/routes/(main)/agent/index.tsxsrc/routes/(main)/agent/index.desktop.tsx页面入口。桌面端变体会完整覆盖 Web 页面(例如,增加额外的弹窗防护)。
src/routes/(main)/group/index.tsxsrc/routes/(main)/group/index.desktop.tsx与 agent 采用相同模式。

规则:

  1. 编辑 src/routes/ 下的任何 .ts/.tsx 文件后,使用 glob 在同一目录中查找 <filename>.desktop.{ts,tsx} 同级文件。如果存在,请在同一提交中对其应用等效更改。
  2. 添加新的 SettingsTab 时,需在 componentMap.ts(使用 dynamic(...))和 componentMap.desktop.ts(使用同步 import)中都注册。否则,componentMap.sync.test.ts 将导致构建失败。
  3. 新增仅限桌面端、用于完整覆盖页面的实现时,优先在单个基础文件中编写平台感知代码,而不是引入新的 .desktop.tsx 变体——只有当两棵树确实存在差异(例如,store 接线或弹窗防护不同)时,才添加新变体。
  4. 删除时,需同时删除两个文件。

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 时:

  1. 优先在 src/features/<Domain>/ 中添加新代码,并从 routes 导入。
  2. 对于较大的重构,将现有仅供 route 使用的逻辑移至合适的 feature,然后精简 route 文件(从 features 重新导出或组合)。
  3. 移动文件时使用 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。

评论

支持 Markdown