Gadgets Workshop 前端工程规范:基于 Kumo 与 TanStack Router 的 React SPA 架构与编码约定
2026/9/24 23:45:11 网站建设 项目流程
  • 人工智能
  • AI 应用
  • AI Agent
  • Agent 沙箱
  • AI 安全治理

【免费下载链接】cloudflare-os

Agent workspace built on Cloudflare Workers for creating documents, building apps, and running agents with your company’s context and systems.

项目地址:https://gitcode.com/gh_mirrors/cl/cloudflare-os
点击查看免费下载

导读:本文以@gadgets/workshop-frontend包的开发规范文档为核心,系统讲解 Workshop 单页应用的目录架构、页面/路由边界、共享代码归属、Kumo 语义化样式体系、React 编码约定与测试策略,并结合仓库源码逐条印证每一项约定的落地方式。读者读完后,既能按规范组织新代码,也能理解该前端在 Cloudflare Workers 生态中如何通过 WebSocket RPC 与后端通信、如何用 Kumo 语义 token 保持全站视觉一致。

一、包定位与技术栈

packages/workshop-frontend是 Gadgets Workshop 的纯客户端单页应用(SPA),完全运行在浏览器中,通过基于 WebSocket 的 RPC 与packages/workshop-backend通信。从 package.json 可以看到其技术栈组合:

  • React 19react/react-dom19.x)作为视图层基础;
  • TanStack Router@tanstack/react-router)负责路由,配合@tanstack/router-plugin做自动代码分割;
  • Kumo@cloudflare/kumo)提供组件库与语义设计 token;
  • Tailwind CSS 4tailwindcss+@tailwindcss/vite)负责布局与结构样式;
  • capnweb实现浏览器端 RPC(newWebSocketRpcSession);
  • Phosphor Icons@phosphor-icons/react)提供图标;
  • CodeMirror 系列包支撑聊天编辑器与代码编辑器场景;
  • Vite + Vitest + jsdom作为构建与测试工具链。

包名@gadgets/workshop-frontend归属于该仓库的公共 workspace;其构建通过仓库级工具vp(Vite+)驱动(见 scripts/package.json 与 README.md)。

二、目录架构:先按产品归属,再按实现类型

规范的核心原则是:新代码按产品归属(product ownership)优先组织,实现类型(implementation type)其次。目标目录形态:

src/ routes/ TanStack route declarations and route-level wiring pages/ Substantial route-level screens that compose one or more features features/ Product features and their owned UI, hooks, and logic components/ UI and application primitives shared across unrelated features hooks/ Hooks shared across unrelated features utils/ Feature-independent utilities

需要特别说明的是:现有代码树先于该约定存在。例如当前src/根目录下仍散落着AdminPage.tsxConnections.tsxSettingsPage.tsx等历史遗留页面级文件,并没有独立的pages/目录。规范明确:约定适用于新代码与大规模重构中的代码,不应顺带迁移无关文件——避免为迎合规范而做无收益的大面积搬动。

2.1 Feature 目录:扁平起步,按职责细分

Feature 目录拥有产品行为,可包含属于该行为的组件、hooks、测试与工具。规范给出的理想形态:

features/chat/composer/ ChatComposer.tsx ComposerAddMenu.tsx ComposerAddMenu.test.tsx useComposerDraft.ts composerTokens.ts

即:一开始保持扁平,文件名本身已传达类型(useXxx是 hook、Xxx.test.tsx是测试、PascalCase.tsx是组件),不要仅仅为了按实现类型分类而创建components/hooks/helpers/tests/子目录;测试通常与被测对象同目录放置。

只有当某个连贯的内部子系统积累到多个文件、或扁平目录难以扫描时,才引入以其职责命名的子目录,例如draft/tokens/attachments/slash-commands/,并避免通用的helpers/桶式目录。

这一约定在仓库中有真实落点:src/features/chat/composer/内部正是按职责划分出attachments/draft/(内含composerDraft.tsuseComposerDraft.ts)、inline-items/slash-commands/四个子系统目录,其余组件(ChatComposer.tsxComposerAddMenu.tsxComposerModelSelector.tsx等)平铺在 composer 目录下,测试(如ComposerAddMenu.test.tsxuseComposerDraft.test.tsx)与被测文件同置。

组织代码的依据是这些文件因何而一起变更(organize by the reason files change together),而 Feature 化组织并不取代一个组件一个文件的模型

2.2 组件文件的提取标准

当组件满足以下任一属性时,应单独创建组件文件:

  • 拥有有意义的 state、effects 或交互行为;
  • 可独立测试或可复用;
  • 代表一个明确的 UI 职责;
  • 保持内联会让父组件难以理解;
  • 已积累出遮蔽父组件主流程的支持类型或逻辑。

反之,仅在父组件中使用、且易于理解的小型无状态渲染辅助应保持私有、留在父文件内;当它发展出自己的行为或测试时,再提取到父组件旁边。

命名规则:组件用PascalCase文件名,hooks 与非组件模块用camelCase文件名;优先直接导入,不要仅为缩短路径而添加 barrel 文件(index.ts 聚合导出)。

三、页面与路由:职责边界与迁移路径

src/routes/下的文件定义路由,应只关注路由关注点:参数(parameters)、搜索校验(search validation)、loaders、导航,以及组合路由页面。

页面(page)是组合边界而非 feature:一个页面可以组合多个 feature,一个 feature 可以出现在多个页面上。小型页面可以直接留在路由文件里;体量较大的页面实现应移到src/pages/<page>/,在那里组合 feature 组件——仅对该页面有意义的组件与 hooks 也留在页面目录中,而不是放进全局components/hooks/。把页面支撑代码移出src/routes/的另一个好处是:避开 TanStack Router 的文件扫描,避免被误当作路由。

src/pages/下每个路由级屏幕组件必须以Page.tsx后缀命名,如HomePage.tsxWorkspacePage.tsx;支撑组件则用描述自身职责的名字,如HomeTaskSuggestions.tsx

两条重要边界:

  1. 不要把产品行为搬进页面,仅仅因为页面当前消费了它。具有独立产品含义、或会被多个页面使用的行为属于src/features/<feature>/。不确定时先默认放在页面,当归属拓宽时再提升(promote)。
  2. src/routeTree.gen.ts是生成文件,禁止手动编辑——它在构建时由@tanstack/router-plugin重新生成。实际路由注册见 router.tsx:createRouter({ routeTree, scrollRestoration: true, defaultPreload: 'intent', defaultPreloadStaleTime: 0 })routeTree.gen导入路由树。

文档给出的 Home 页例子恰是"先于规则"的历史代码:components/AppShell/HomeTaskSuggestions.tsxcomponents/MeshBackground.tsx唯一的线上消费者是routes/index.tsx,它们本质是 Home 专属的组合与展示层,而非共享的 AppShell 原语,因此目标组织应是:

pages/home/ HomePage.tsx HomeTaskSuggestions.tsx MeshBackground.tsx

若未来任务建议(task suggestions)成为被其他页面使用的独立工作流,再将其提升为features/task-suggestions/——不要仅凭推测性复用一开始就建 feature

四、共享代码:components/ 与 hooks/ 不是默认目的地

src/components/src/hooks/共享应用基础设施,而非默认放置地。代码应先放在拥有其行为的 feature 下。复用本身不构成"通用化"的理由,规范给出了明确的升级阶梯:

  • 若一个 feature 的多个部分使用某物,移到它们最近的共同 feature 目录
  • 若另一个 feature 消费了仍由原 feature 拥有的概念,保持从原 feature 导出
  • 只有当互不相关的 feature使用同一个与 feature 无关的抽象、且其 API 不再依赖原始 feature 时,才提升到全局components/hooks/
  • 不要因为看起来相似就合并组件,避免抹掉重要领域行为的通用 prop 重载式抽象。

只把代码移动到其归属要求的高度。例如:被 composer 与聊天历史共享的 skill pill 属于features/chat/,而通用的应用图标按钮属于components/

五、组件 API 与组合设计

规范的 API 设计原则强调让非法状态不可表达

5.1 成对属性用对象或可辨识联合

只在一起才有效的 props 应表达为一个对象或 discriminated union,禁止允许部分配置:

// 避免:调用方可以给 count 却不给任何关闭方式 count?: number; onDismiss?: () => void; // 优先:该能力要么完整存在,要么完全缺失 notice?: { count: number; onDismiss: () => void };

5.2 受控值必须配变更回调

受控值(controlled value)必须同时提供 change callback;否则组件应自己拥有该值。不要用 Effect 把受控 prop 复制进本地 state:

// 受控 value: string; onValueChange: (value: string) => void; // 非受控 initialValue?: string;

5.3 回调命名与传值

回调命名为on<Action>,传递领域值(domain values)而非 React setter 或浏览器事件。例如对外暴露onModelChange(modelId),而不是setSelectedModelonChange(event)

这一条在源码中有直接实例:src/features/chat/composer/ComposerModelSelector.tsx的 props 定义即为{ selectedModel: string | null; onModelChange: (modelId: string | null) => void },选中项通过onModelChange(model.id)/onModelChange(null)上报,消费者拿到的是模型 ID 领域值。

5.4 扩展面只在当前需要时添加

children、命名插槽、variants、className、DOM prop 透传、命令式 ref,只有当前调用方确实需要该控制能力时才添加,不要为预期的复用提前加上。

5.5 Context 的使用边界

用 Context 承载应用级的值:认证、主题、toast。实例特定的 feature 数据与动作应通过 props 传递;不要仅仅为了避免穿透一两个组件层级就引入 Context。仓库中RpcContextAuthContextThemeContextServerConfigContextFeatureFlagsContext均属应用级基础设施(见 main.tsx 与 __root.tsx 的 Provider 嵌套)。

5.6 提取时机

当被提取单元拥有完整关注点——如 state 及其转移、一个 Effect 生命周期与清理、一个可访问的交互、或一个可独立测试的纯变换——就提取组件或 hook。若子组件主要只是转发标记(markup),或需要父组件的 refs、setters 与同步回调才能工作,则应保持代码聚合。

六、Kumo 与样式体系:语义 token 优先,杜绝自定义色

样式规范的核心是:默认使用 Kumo 组件与 Kumo 语义设计 token。在创建自定义控件、交互模式或视觉原语之前,先查 Kumo。Workshop 已有的包装器(wrapper)只有在提供了 Kumo 未直接提供的既定应用行为时才可使用。

6.1 硬性禁止项

除非用户明确要求,禁止在 Kumo 之外添加自定义颜色与设计 token,具体包括:

  • 组件样式中出现 Hex、RGB、HSL 或 OKLCH颜色字面量
  • 任意 Tailwind 颜色值(arbitrary color values);
  • 新增应用级颜色变量或 token 族;
  • 为 Kumo 的 surface、border、text、status、focus、interaction token 做feature 本地替换

应使用 Kumo 语义类,例如bg-kumo-basetext-kumo-subtleborder-kumo-line,而不是调色板颜色。全局 Kumo token 主题化是应用级的刻意决策,不得作为日常 feature 工作的一部分引入或更改。现有代码中的遗留自定义 token 与颜色声明不是新代码的先例,不要扩散其使用,只可在明确范围的清理任务中迁移。

源码印证:src/components/AppShell/AppShell.tsx中侧栏布局大量使用bg-kumo-baseborder-kumo-linetext-kumo-defaulthover:bg-kumo-tinttext-kumo-inactive等语义类;CommandPalette.tsx 同样只使用bg-kumo-basetext-kumo-strong等 token 类,未出现任何调色板颜色。根路由 __root.tsx 的加载与错误页也全部基于bg-kumo-base/text-kumo-subtle/bg-kumo-brand等语义类。

6.2 Tailwind 与自定义 CSS 的分工

Tailwind 仍适用于结构:布局、间距、尺寸、定位、响应式行为与排版。自定义 CSS 仅用于 Kumo 与工具类无法表达的技术行为,如编辑器集成或测量型浮层;自定义 CSS不会放宽颜色与 token 规则。

当 Kumo 组件不适用时,先记录具体的行为或可访问性缺口,再添加共享的 Workshop 抽象;不要仅为换肤而包装 Kumo

6.3 主题机制的源码视角

从 theme.ts 可以看到该规范背后的主题架构:亮/暗基础调色板通过 Tailwind@themeCSS 变量与[data-mode="dark"]覆盖静态定义在styles.cssapplyThemeMode<html>上设置data-modecolorScheme,使 Kumo 语义 token 与原生控件一致解析;部署方可通过applyAccentColor在运行时以单个 seed 色派生 accent 家族(hover/lighter/selection 用 CSSoklch(from ...)相对色语法推导)。这解释了为什么"新增自定义颜色变量"被严格禁止——主题是应用级决策,feature 层只能消费语义 token。

七、React 编码约定

7.1 组件与 hook 的写法

默认用命名const箭头函数定义组件与 hooks,保证声明在使用之前、不依赖函数声明提升。props 直接类型化,不用React.FC。被memoforwardRef等包裹的组件要保留显式名称(必要时用displayName),保证 React DevTools 与堆栈可读。

7.2 Effect 是逃生舱,不是状态管理工具

Effect 用于与外部系统同步,遵循 React 官方 "You Might Not Need an Effect" 的思路,具体规则:

  • 不要用 Effect 从 props/state 派生渲染数据——渲染期间计算
  • 不要用 Effect 承载用户交互引起的逻辑——在知道发生了什么的事件处理器里执行
  • 不要用 Effect 同步两份 React state——优先单一数据源、派生值、受控组件或提升 state;
  • 当身份变化应重置组件状态时,优先用组件key
  • 外部 store 订阅适合时优先用useSyncExternalStore
  • 发起 fetch 或订阅的 Effect 必须清理过期工作与订阅,组件在开发模式下 Effect 重启/重挂载时仍须正确;
  • 避免用"Effect 链"——更新 state 只为触发另一个 Effect;
  • state 尽量靠近拥有该行为的位置

7.3 hook 与记忆化

在表示连贯行为或被复用时才提取 hook,不要仅为缩短文件而提取。默认不加useMemo/useCallback,只在 identity 或昂贵计算产生实际影响时使用。

7.4 可访问性与 RPC stub 纪律

提取交互式 UI 时必须保留键盘行为、焦点管理与可访问名称。RPC stubs 必须遵守仓库级 AGENTS.md 的处置与 React state 规则——这点在源码中有典型体现:main.tsx 中 RPC 连接中断时用stub[Symbol.dispose]()清理候选连接(disposeQuietly),并在handleBroken中以new RpcPromise<PublicApi>(reconnect())替换当前 stub,保证同一时刻只有一个处置路径。

7.5 连接管理实例

main.tsx的 WebSocket RPC 连接管理还体现了若干约定:初始退避 1s、最大退避 10s、抖动系数 0.85–1.15(0.85 + 0.3 * Math.random(),防惊群)、重连前用ping()探活(20s 超时)、在visibilitychange/online事件时对"疑似僵尸"连接做唤醒探测(WAKE_PROBE_MIN_IDLE_MS = 15000)。这一实现与文档"订阅/连接必须清理、重启后必须正确"的 Effect 纪律一脉相承——连接管理被抽到模块级而非组件 Effect 内,正是为了避免开发模式下 Effect 双跑导致重复建连。

八、注释原则

优先让命名、类型与结构传达意图,不要添加逐行复述代码的注释。注释只适用于:

  • 坑(gotchas)与非显然的不变量;
  • 外部约束;
  • 安全或性能推理;
  • 刻意偏离惯例的决策——解释为什么这个意外选择是必要的,而不是代码中可见的机制。

当注释所描述的约束不再存在时,删除或更新它。

九、测试约定

9.1 测试的放置

单元测试与其主题同目录存放,命名*.test.ts*.test.tsx。仅当场景跨多个模块、没有单一归属时,才使用 feature 级__tests__/目录。仓库中的典型形态:ComposerAddMenu.test.tsxComposerAddMenu.tsx同目录、useComposerDraft.test.tsxuseComposerDraft.ts同目录;而src/components/format/__tests__/messageFormatRefs.test.ts属于跨模块场景示例。

9.2 测试什么、不测试什么

测试应保护回归重要的行为,或验证变更引入的真实逻辑。不要因为 React、JavaScript、TypeScript、Kumo 或某个框架的行为出现在实现里就去测框架本身。应测试:

  • 可观察契约(observable contracts);
  • 产品规则;
  • 状态转移;
  • 可访问性行为;
  • 竞态处理;
  • 失败路径。

避免只断言实现细节、琐碎的透传标记、或已由类型与底层平台保证的行为。一个测试应有对本应用有意义的明确失败含义。行为保持不变的移动(behavior-preserving moves)除 import 外应保持测试不变;提取暴露了此前未测的重要逻辑时才补针对性覆盖,不要仅因为文件或组件边界出现了就加测试。

9.3 常用命令

在公开 workspace 根目录执行:

pnpm --filter @gadgets/workshop-frontend test:run pnpm exec tsc -p packages/workshop-frontend/tsconfig.json pnpm exec tsc -p packages/workshop-frontend/tsconfig.vite.json pnpm exec vp run -F @gadgets/workshop-frontend build

推送前在 workspace 状态允许时运行pnpm lint(对应仓库级 lint 门禁lint:checktypes:check)。注意test:run直接走 vitest,适合迭代期使用;vp run走任务缓存。

十、构建与开发工具链(补充)

vite.config.ts 揭示了该包构建的独特设计,与规范中的目录纪律互为表里:

  • build是 Vite+ 任务而非 package.json 脚本,因为任务可以声明env: ['VITE_*']:缓存的vp运行会在干净环境中执行脚本、丢失环境变量,而声明在任务上既能转发VITE_*标志、又能将其纳入指纹(fingerprint),改变取值会触发缓存未命中而非回放旧产物。VITE_CF_ACCESS_MODE会被内联进src/useAuth.tsVITE_FRONTEND_ERROR_REPORTING决定是否生成隐藏 sourcemap,二者都会改变产物语义,因此必须被指纹追踪。
  • build 任务由三段命令组成:tsc(应用类型检查)、tsc -p tsconfig.vite.json(配置文件自身检查)、以及强制NODE_ENV='production'的 vite build——保证无论 shell 环境如何都产出生产包。
  • dist/被从任务 input 中排除({ pattern: '!dist/**', base: 'package' }),因为vp不会缓存"既读又写"的任务;**/.wrangler/**被 workspace 级排除,因为 wrangler 的随机命名临时产物会让任何跑过wrangler dev的兄弟包导致缓存失效。
  • 开发服务器代理/api/client-errors/blueprint-screenshot/api/site-logoVITE_BACKEND_HOST(默认localhost:8787),对应 README.md 中描述的 dev 工作流:pnpm dev(端口 3000)、pnpm exec vp run buildpnpm preview

10.1 构建期认证模式

README 明确该前端支持两种构建期选择的认证模式:默认的密码模式(用户名+密码,/signup注册)与 Cloudflare Access 模式(VITE_CF_ACCESS_MODE=true构建,由后端authenticateFromCfAccess()基于 CF Access 已建立的会话自动认证,同时禁用密码登录与注册页;后端需配置CF_ACCESS_ISSCF_ACCESS_AUD环境变量用于 JWT 校验)。这与 __root.tsx 中CF_ACCESS_MODE分支(未认证时显示 "Authenticating..." 加载态)以及useAuth的实现对应。

结语

workshop-frontend的这份 AGENTS 规范本质上回答了三个问题:代码放哪里(产品归属优先、扁平起步、按职责细分)、组件长什么样(语义 token、领域值回调、非法状态不可表达、Kumo 优先)、如何保证长期健康(Effect 纪律、共享代码的克制提升、只测有意义行为)。它与仓库级 AGENTS.md 一脉相承,后者进一步约定了 RPC stub 处置、capnweb promise 流水线、useState不得直接持有 RpcStub 等跨包规则。对任何要在此 SPA 上新增或重构功能、评审他人前端代码的开发者而言,这份规范配合 AGENTS.md、README.md 与 vite.config.ts 即可完整掌握该包的组织逻辑与构建机制。

  • 人工智能
  • AI 应用
  • AI Agent
  • Agent 沙箱
  • AI 安全治理

【免费下载链接】cloudflare-os

Agent workspace built on Cloudflare Workers for creating documents, building apps, and running agents with your company’s context and systems.

项目地址:https://gitcode.com/gh_mirrors/cl/cloudflare-os
点击查看免费下载

相关推荐

上一篇:快速免费解锁网易云音乐NCM格式解密:ncmdump终极使用指南
下一篇:TypeSpec http-server-csharp Emitter 完整使用指南:命令行、tspconfig 配置与全部选项解析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询