Phoenix 前端开发规范实战:React 组件、Relay 数据流与可访问性的工程化指南
2026/9/23 11:48:31 网站建设 项目流程
  • 可观测性
  • AI 评测
  • LLMOps
  • AI 应用
  • 人工智能

【免费下载链接】phoenix

AI Observability & Evaluation

项目地址:https://gitcode.com/gh_mirrors/phoenix13/phoenix
点击查看免费下载

Phoenix 是一款 AI 可观测性与评估平台,其前端位于 js/app 目录,由 React 19、TypeScript、Relay 与 React Aria 构建。本文以仓库内置的 phoenix-frontend 技能文档 为骨架,结合其五份参考文档(组件模式、Relay 数据获取、无障碍、测试标识、SVG Logo 资源)与真实源码实现,系统梳理 Phoenix 前端开发的核心规范:如何组织组件分层、如何选择 Drawer 与 Modal、如何正确管理 Relay 查询缓存、如何命名data-testid、如何维护路由可发现性等。读完本文,你将掌握一套可直接套用的 Phoenix 前端工程化打法,并理解每一条规范背后的源码依据。

一、规范总览与工作起点

phoenix-frontend 技能文档开篇即给出开发前的强制动作:在动工之前,先探索js/app/src/components/js/app/package.json,理解已有的组件模式、依赖包与约定,再遵循规则编码。这一原则贯穿全文:Phoenix 前端不是从零写起的绿地项目,而是高度约定化的既有代码库,任何新功能都应当先"对齐存量",再"增量实现"。

SKILL.md 提供了一张参考文件索引表,按任务类型选择阅读对象:

参考文件适用场景
references/components.md创建、组合或重构组件
references/relay.md使用 Relay 进行数据获取
references/accessibility.md任何交互元素、表单、浮层或语义化标记
references/test-ids.md为 E2E 测试新增或修改data-testid属性
references/resize-svg-logo-assets.md新增或更新 provider/integration 品牌 Logo 图标

此外还有两条贯穿始终的全局要求:

  • 视觉变更后必须验证:任何界面改动都要借助浏览器工具确认 UI 渲染正确;修改共享组件时,必须检查其在全应用中的使用点。
  • 路由元数据同步:新增、删除、重命名或实质性改变页面内容时,若希望智能助手(PXI)能引导用户跳转到该目的地,需要更新 js/app/src/Routes.tsx 中路由的handle.agentRoute元数据——保持精简且面向搜索:label为人类可读的页面名,description为凝练的页面用途说明,并包含用户在寻找该页面时可能对 PXI 说出的口语化关键词。如果内容改动使既有路由的自然语言可发现性发生变化,应在同一次改动中同步调整description

二、组件分层:Core 原语与 Domain 组合

components.md 定义了 Phoenix 组件架构的两层结构,这是理解整个前端组织方式的第一把钥匙:

  • Core(components/core/:纯展示性原语。严禁包含数据获取或业务逻辑,只负责视觉与交互基础能力。
  • Domain(其余所有目录):数据密集型组件,由 Core 原语组合而成。

两条硬性规定随之而来:新功能必须优先复用既有 Core 原语,而不是重新造低级 UI;布局时必须复用已有的 flex/view 布局原语以保证间距与对齐一致,禁止随手写临时布局包装器。从源码结构看,components/core/下已经沉淀了包括 overlay(Drawer/Modal)、表单控件、布局原语等一整套基础件,Domain 层通过组合它们完成具体业务页面。

文件组织约定

新增组件时应先探索几个既有组件,对齐已经成型的文件结构:组件本体、样式、类型定义、barrel 导出(index.ts)各归其位。共享常量(校验正则、固定字符串集合等)必须收敛到 js/app/src/constants 目录下的聚焦模块中,并通过index.ts统一再导出,以@phoenix/constants路径引用——禁止从组件或表单文件中导出共享常量

Storybook 要求

新 Core 组件必须附带最小化的 Storybook stories,覆盖主要变体与状态。既有约定位于 js/app/stories 目录,新增 story 时先参考该目录的写法保持一致。

三、React 19 时代的组件编写规则

Phoenix 前端已启用React Compiler,这直接改变了一大批传统 React 编码习惯,是本节所有规则的根因。

1. 不要手动 memoization

React Compiler 会自动处理记忆化,因此禁止使用useMemouseCallbackReact.memo。这包括传给子组件的回调 props——直接内联定义,不要用useCallback包裹。编译器会在编译期自动完成依赖分析与缓存生成,手写 memoization 反而可能干扰编译器的优化决策并增加噪音。

2. 条件 className 统一走 classNames

构建条件 className 字符串必须使用@phoenix/utils/classNames(它是clsx的再导出),禁止手写重复 base class 的三元表达式或模板字符串。规范写法是:base class 传字符串,开关条件传对象:

className={classNames("attachment-info", { "attachment-info--with-detail": detail, })}

3. React 19 的 ref 即普通 prop

React 19 将ref视为普通 prop,因此不要使用forwardRef。直接在 props 类型中声明ref?: Ref<ElementType>,并像普通 prop 一样解构使用。这一约定与 React 19 的官方演进方向一致,也让组件签名更扁平、更易阅读。

4. 回调 props 以"事件"命名

回调 props 必须按事件命名(onProjectCreatedonDismiss),而不是按父组件的实现命名(如refetchProjects)。目的让调用点读起来像自然语言:

// Good —— 调用点读起来像一句英文 <NewProjectButton onProjectCreated={() => refetchProjects()} /> // Avoid —— 泄露了父组件内部实现 <NewProjectButton refetchProjects={() => refetchProjects()} />

5. 规避 useEffect,拥抱声明式

优先使用声明式 React 与 React Aria 模式,避免命令式useEffect——只有不存在声明式替代方案时才允许使用。两条具体的替代路径:

  • 防抖搜索/过滤输入:组合共享的DebouncedSearch字段,而不是手写useEffect+setTimeout
  • 高开销的上下文驱动更新(树过滤、列表过滤、全局展开/折叠):保持 transition 策略与状态所有者一致——通过 context 暴露动作,由 provider 在底层状态更新外包一层startTransition,消费者直接调用这些动作即可,无需关心 transition 细节。

四、覆盖层体系:Drawer 与 Modal 的选择

Phoenix 提供两个用途截然不同的覆盖层组件,选错会直接破坏交互模型。这一节的决策依据在 components.md 中有完整论述,源码实现分别位于 Drawer.tsx 与 Modal.tsx。

Drawer:非模态右侧面板

Drawer 用于列表-详情(list-detail)流程:用户选中一行、查看其详情,同时列表保持可见、可交互。从 Drawer.tsx 的源码可以看出其设计特征:

  • 不渲染 backdrop,点击可穿透到页面背后;
  • 通过拖拽手柄调整宽度,宽度以视口百分比持久化到localStorage(源码中定义了DRAWER_DEFAULT_SIZEDRAWER_DEFAULT_MIN_SIZEDRAWER_HARD_MIN_SIZE_PX等常量,还支持键盘以 5% 步进调整);
  • 通过 Escape 或折叠箭头按钮关闭;
  • 路由驱动:打开 Drawer 意味着导航到嵌套路由(如/sessions/:sessionId);
  • 对应的表格行必须高亮data-selected,让用户始终清楚正在查看哪一行。

适用场景:traces、spans、sessions 这类"瞥一眼详情、随即返回列表"的浏览型操作。

Modal:模态浮层

Modal 是要求焦点集中的模态覆盖层,通过ModalOverlay阻断与背后页面的交互,有两个变体:

  • variant="default"——居中对话框 + backdrop,用于确认、表单和需要用户全神贯注的聚焦工作流;
  • variant="slideover"——全高右侧面板 + backdrop,用于内容比居中对话框更宽、但仍需模态焦点的场景(如复杂的创建表单,不应与背后页面竞争注意力)。

决策速查表

信号选择
用户在浏览列表并检查条目Drawer
用户必须完成某动作才能继续Modal(default)
模态内容需要全高面板布局Modal(slideover)

五、分层列表-详情模式(layered list-detail)

对于"点击行打开详情视图"的表格,Phoenix 强制采用分层列表-详情模式,其完整实现路径如下:

  1. 行点击导航到嵌套路由(如/traces/:traceId),详情视图通过<Outlet />与表格并列渲染;
  2. 选中行高亮:在<tr>上设置data-selected={isSelected},其中isSelecteduseParams取 URL 参数与row.original.id比较;既有样式selectableTableCSS会自动为tr[data-selected="true"]加高亮;
  3. Drawer 打开承载详情内容,表格在其后保持可见、可滚动;
  4. 关闭 Drawer即导航回父路由,同时清除选中状态。

这个模式的价值在于持续定向(orientation):用户永远看得到自己选中了哪一行,且无需先关闭当前详情就能直接点击另一行切换查看。从仓库的useParams用法与selectableTableCSS样式钩子看,该模式已在多个表格页面(traces、sessions 等)落地为通用范式。

六、Relay 数据获取规范:缓存保留与所有权

relay.md 是一份风险导向极强的数据层规范,核心围绕"数据会不会被静默逐出缓存"展开。

1. 声明式 hooks 有缓存保留保证,fetchQuery 没有

usePreloadedQueryuseLazyLoadQuery这类声明式 hooks,其查询与拉取的数据会在组件挂载期间保留在 Relay store 缓存中,因此可以安全地用于水合(hydrate)页面渲染数据。

fetchQuery没有这种保留保证——在足够的后续请求(如分页触发的请求)之后,数据可能被逐出 Relay store。这意味着fetchQuery水合页面上渲染的数据是危险的:组件仍挂载时数据可能已静默消失。

2. 查询 ref 的所有权与销毁

loadQuery返回的查询 ref 会被保留,直到被 dispose:

  • 组件自己负责加载时,用useQueryLoader——它自动处理 ref 的保留与销毁;
  • 路由 loader 或其他外部持有者直接把loadQueryref 交给组件时,组件在不再拥有它时必须负责 dispose。

3. useOwnedPreloadedQuery:loader 持有的 ref 专用钩子

Phoenix 为最常见的路由 loader 模式提供了专用 hook:js/app/src/hooks/useOwnedPreloadedQuery.ts。从源码看,其实现非常精巧:它把外部传入的 query ref 交给useQueryLoader(query, queryRef)初始化,从而让 Relay 在 ref 被替换或组件卸载时自动 dispose,再通过usePreloadedQuery读取数据,并用invariant保证 ref 必存在。

适用条件:当前组件拥有外部创建的 query ref 的生命周期,典型场景是useLoaderData()返回loadQuery的结果。禁止在以下情况使用:

  • query ref 已由useQueryLoader管理;
  • ref 是共享的、销毁权归另一个组件;
  • ref 经 context 或 props 传给多个读者,无明确的单一所有者语义。

4. 五条铁律

  1. 优先声明式 hooks:用usePreloadedQuery/useLazyLoadQuery获取要渲染到页面的数据;
  2. 页面渲染数据禁用 fetchQuery:不要用它水合挂载组件依赖的渲染数据;
  3. fetchQuery 的有限安全用途:仅当结果被立即消费、不留在 store 中用于渲染时才可接受(如为 redirect 或一次性动作取数);
  4. 组件自持的 ref 用 useQueryLoader
  5. loader 持有的 ref 用 useOwnedPreloadedQuery,把销毁当作所有权决策——过早销毁共享 ref,会让仍挂载的读者在后续遭遇缺失数据或 GC 相关崩溃。

5. 单实体查询走 node(id:),禁止过度拉取

需要按 id 取单个对象时(如懒加载 tooltip、详情 popover),应通过根字段node(id: $id)配合具体类型的内联 fragment 直接获取——不要拉取整个集合再在客户端.find()。按列表取单行是浪费一次往返且扩展性差的反模式。

如果目标类型尚未暴露到node接口,规范要求在后端将其做成Node(GQL 类型声明id: NodeID[int]/ 实现Node,字段按 id 惰性解析,并在src/phoenix/server/api/queries.pyQuery.node中为type_name增加return X(id=node_id)分支),而不是用集合查询绕过。

七、无障碍:WCAG 2.1 AA 与语义化基线

accessibility.md 篇幅不长,但规定了两条不可妥协的底线:

  1. 语义化元素强制

    • 交互动作必须用 button,禁止可点击的 div;
    • 列表必须<ul>/<ol>+<li>,即使是非项目符号布局(键值行、菜单、标签列表)也不能退化成 div 堆叠。重置浏览器默认样式时用list-style: none; margin: 0; padding: 0;,再在语义列表之上施加 flex/grid 布局。
  2. WCAG 2.1 AA 基线:文本对比度 4.5:1、键盘可操作性、可见焦点指示、表单输入必须有 label。

这条规范的价值在于它把"语义正确"与"视觉还原"解耦——先保证结构语义,再通过样式覆盖视觉,避免视觉稿驱动出不可访问的 DOM。设计系统层面的错误展示、布局、对话框、tokens 规则由 phoenix-design 技能(.agents/skills/phoenix-design/)单独承载,组件开发与设计规范各司其职。

八、data-testid 命名与状态分离

test-ids.md 定义了 E2E 测试标识的完整命名体系。data-testid是当 role/label/text 选择器不够稳定或不够具体时的逃生通道,加了就必须遵守以下规则:

命名规则

  • kebab-case,纯 ASCII
  • 不缩写,完整拼出元素角色:buttonmenu-itemlinktabdialoginputoption——绝不写btnmi
  • 具体且限定范围:用功能/页面/组件名做前缀保证全局唯一。create-dataset-button优于create-buttondataset-form-submit-button优于submit-button
  • 以元素角色结尾,模式为<scope>-<subject>-<role>
    • create-dataset-button
    • playground-run-button
    • dataset-form-submit-button
    • run-dataset-experiment-via-sdk-menu-item
    • llm-evaluator-form-submit-button
  • 表单主提交控件统一为<form-name>-submit-button,不用随模式变化的动词(同一元素避免create-.../update-...反复横跳)。

状态进>// ❌ testid 随状态变化,编辑模式下干脆消失 <Button>page.getByTestId("llm-evaluator-form-submit-button"); // 永远解析成功 page.locator('[data-testid="llm-evaluator-form-submit-button"][data-mode="create"]');

常用的状态属性包括data-modecreate|edit)、data-stateopen|closed|loading)、data-selected。若既有组件已自带data-state等能表达状态的属性,优先复用而不是另造新名。

放置位置与使用优先级

data-testid(及配套data-*)必须放在元素的第一个 prop,便于一致性与快速扫描。使用优先级上不要第一时间就上 testid,依次尝试:role 选择器(getByRole)→ label 选择器(getByLabel)→ 文本/占位符选择器;仅当这些方案有歧义、不稳定或不存在时才加data-testid——典型场景是纯图标按钮、重复出现的行操作、可见文本会变化的元素。

九、URL 状态可还原性

SKILL.md 的最后一个硬性要求:显著视图状态必须能从 URL 重建。用户能够选择的 tab、子视图或详情状态,只要需要扛住刷新、分享或相邻记录分页,就必须编码进路由参数(route params)或搜索参数(search params),并在导航过程中保留相关 URL 状态。

这条规则与前面的"路由驱动 Drawer"一脉相承:Phoenix 的深链能力(trace/session 详情、tab 选择、过滤条件)全部依赖 URL 承载状态,既保证了刷新后的还原,也让 PXI 等智能助手可以生成可直达的链接。

十、SVG Logo 资源:从原始 SVG 到 TSX 组件的确定性流程

resize-svg-logo-assets.md 描述了为 Phoenix 前端新增 provider/integration 品牌 Logo 的完整管线,配套脚本为 scale-svg.py。

两类目标

目标画布目标文件组件形态
provider24×24js/app/src/components/generative/GenerativeProviderIcon.tsx私有常量组件,接受{ height }prop
integration32×32js/app/src/components/project/IntegrationIcons.tsx命名导出,无 props,固定 32×32

用户未指定目标时必须先询问再动手

七步工作流

Step 1 — 收集 SVG:接受单个文件路径、文件列表、含.svg的目录,或对话中粘贴的原始 SVG 标记(先写入临时文件)。

Step 2 — 确定性缩放:使用辅助脚本,缩放是数学级坐标重写而非包一层 transform,输出与 Figma 缩放结果等价:

# 单文件 uvx --with svgpathtools python .agents/skills/phoenix-frontend/scripts/scale-svg.py <target_size> <input.svg> <output.svg> # 批量 uvx --with svgpathtools python .agents/skills/phoenix-frontend/scripts/scale-svg.py --batch <target_size> <input_dir> <output_dir>

目标尺寸:provider24integration32。从 scale-svg.py 源码看,脚本借助svgpathtools解析 path 数据,按SCALE_X/SCALE_Y/SCALE_UNIFORM分类缩放坐标属性(含 Line/CubicBezier/QuadraticBezier/Arc 各段类型处理),并处理多子路径的不连续 M 命令;数值统一保留最多 4 位小数。

Step 3 — SVG 转 JSX:将输出 SVG 转为 JSX 组件,属性名映射表如下:classclassNameclip-pathclipPathclip-ruleclipRulefill-rulefillRulefill-opacityfillOpacitystop-colorstopColorstop-opacitystopOpacitystroke-widthstrokeWidthstroke-linecapstrokeLinecapstroke-linejoinstrokeLinejoinstroke-dasharraystrokeDasharraystroke-dashoffsetstrokeDashoffsetstroke-opacitystrokeOpacityxmlns:xlink→删除。同时:移除根<svg>xmlns(React 自动添加)、无子元素的标签自闭合(如<path ... />)、viewBox原样保留。

Step 4 — 颜色规则:纯单色(黑/近黑/深灰)Logo 将 fill 替换为currentColor,使其跟随 Phoenix 主题的明暗模式;有明确品牌色的多色 Logo 保留原色。

Step 5 — 插入 TSX:插入前确认与既有条目不冲突,若有相似 Logo 需先列出并征得用户确认再覆盖。Provider 图标模式:

const NewProviderSVG = ({ height }: { height: number }) => ( <svg viewBox="0 0 24 24" width={height} height={height} xmlns="http://www.w3.org/2000/svg"> {/* scaled paths here */} </svg> );

再在PROVIDER_ICONSrecord 中注册:NEW_PROVIDER: NewProviderSVG。注意key 必须匹配ModelProvider类型中的某个值;若 provider 不存在于该类型中,需向用户说明类型可能需要在上游更新。Integration 图标则作为命名导出写入:

export const NewIntegrationSVG = () => ( <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"> {/* scaled paths here */} </svg> );

Step 6 — 由文件名推导组件名:遵循icon=Name.svg约定时,组件名 = 去掉icon=前缀、去除空格、追加SVG。例如icon=LlamaIndex.svgLlamaIndexSVGicon=Cerebras.svgCerebrasSVGicon=LiveKit Agents.svgLiveKitAgentsSVG

Step 7 — 验证:读取修改后的 TSX 确认组件能与周边代码一起编译;若用户提供了期望图标名列表,逐一确认已添加。

效率与安全红线

  • 逐文件推进,不批量倾倒:缩放一个 → 读输出 → 改 TSX → 再处理下一个;
  • 不让文件内容穿越对话:缩放产物在磁盘上,需要时直接读取;
  • 整体替换组件:更新既有 SVG 组件时整段删除重写,不零敲碎打;
  • 绝不手改 SVG path 数据:所有坐标变更必须走缩放脚本,不猜不近似路径几何;脚本对某 SVG 失败时向用户报告错误而不是手动修补;
  • 未经明确要求不得修改文件中既有图标

十一、规范在仓库中的落地印证

上述规范并非纸上谈兵,均可在仓库中找到对应实现:

  • useOwnedPreloadedQuery:见 js/app/src/hooks/useOwnedPreloadedQuery.ts,其"用useQueryLoader接管外部 ref 以实现自动 dispose"的实现与文档描述完全一致;
  • Drawer 源码:见 js/app/src/components/core/overlay/Drawer.tsx(约 484 行),DRAWER_DEFAULT_*/DRAWER_VISIBLE_GUTTER_PX等常量、键盘 5% 步进调整、localStorage宽度持久化等特性均有据可查;
  • Modal 双变体:见 js/app/src/components/core/overlay/Modal.tsx;
  • Provider 图标注册表:见 js/app/src/components/generative/GenerativeProviderIcon.tsx;
  • 缩放脚本:见 .agents/skills/phoenix-frontend/scripts/scale-svg.py;
  • 路由元数据:见 js/app/src/Routes.tsx 的handle.agentRoute
  • 前端依赖与脚本:见 js/app/package.json(React 19、React Compiler、Relay、React Aria 等依赖与项目脚本均在次声明)。

十二、结语:一套"可解释"的前端规范

Phoenix 前端规范的独特之处在于:每条规则几乎都能追溯到一个具体的风险或一个具体的源码实现。Core/Domain 分层防止了业务逻辑泄漏进展示层;React Compiler 的启用让 memoization 规则从"建议"变为"禁令";Relay 缓存保留语义直接决定了数据获取 hooks 的选择;Drawer/Modal 的决策表把交互模型的选择从个人偏好变成了可判定的工程决策;data-testid的"ID 恒定、状态分离"原则则从根本上解决了 E2E 选择器因状态变化而失效的经典难题。

对于在 js/app 下工作的开发者与 AI 编码助手而言,这套规范既是编码约束,也是决策框架:动手前先读 SKILL.md 与对应的参考文件,按需对照源码,即可保证产出与 Phoenix 现有代码库在组件分层、数据流、无障碍与测试可访问性上保持一致,同时让页面状态可深链、可被智能助手发现。

  • 可观测性
  • AI 评测
  • LLMOps
  • AI 应用
  • 人工智能

【免费下载链接】phoenix

AI Observability & Evaluation

项目地址:https://gitcode.com/gh_mirrors/phoenix13/phoenix
点击查看免费下载

相关推荐

上一篇:3种高效修复MP4视频损坏的方法:untrunc工具完全指南
下一篇:Laravel HTML 生成器教程

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

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

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

立即咨询