Supabase Studio 组件编写规范:目录职责、文件组织模式与代码标准
Supabase Studio(apps/studio)是一个代码量庞大的管理控制台前端,其 components/ 目录下聚集了数千个 React 组件。apps/studio/components/README.md 是面向贡献者的组件编写指南,规定了组件应该放在哪个目录、复杂组件如何按文件拆分,以及新组件的基本代码模板。阅读本文后,你将掌握 Studio 组件的目录职责划分、"单文件 vs 文件夹 + 入口"两种组织模式,并结合真实源码示例理解命名、类型与测试约定,从而在 Studio 中写出符合团队规范的组件。
一、组件目录职责:先决定组件放在哪里
README 开篇给出了三条目录放置规则(见 components/README.md):
| 组件类型 | 放置位置 | 说明 |
|---|---|---|
| 声明页面整体结构和布局的组件 | components/layouts/xxx |
页面级框架 |
| 与某个具体界面(功能页)强耦合的组件 | components/interfaces/xxx |
跟随所属功能模块 |
| 打算跨多个页面复用的组件 | components/ui/xxx |
通用可复用 |
从仓库实际目录结构可以印证这三类划分是真实生效的:
- components/layouts/ 下按功能命名了
AppLayout、DatabaseLayout、AuthLayout、BillingLayout、BranchLayout等页面框架组件; - components/interfaces/ 下有
SQLEditor、Realtime、Storage、EdgeFunctions、SignIn、ProjectCreation等与具体控制台界面一一对应的功能模块; - components/ui/ 下则是
DataTable、CodeEditor、DatePicker、ErrorBoundary、InfiniteList等可跨页面复用的组件。
README 还提到一条历史背景:团队正在把 components/to-be-cleaned/ 中的文件逐步迁移到上述对应目录。查看 components/to-be-cleaned/ 可以看到该目录目前只剩 Table.tsx、ListIcons.tsx、ProductEmptyState.tsx 三个文件——这既说明"渐进式清理"的约定长期在执行,也给出一个实操提示:新代码不应再放进该目录。
另外,components/ 下还存在 grid/(表格网格相关)与 ui-patterns/ 目录。关于后者的定位,apps/studio/AGENTS.md 给出了明确的导入分层约定:'ui' 是原语(primitives),'ui-patterns' 是组合好的模式(如 ConfirmationModal),@ui/* 别名则指向 monorepo 中的 packages/ui/src。也就是说,"跨页面复用"的组件在落地时还有一层粒度区分:基础原语进 ui,复合交互模式进 ui-patterns。
二、组件文件组织:单文件与"文件夹 + 入口"两种模式
README 的第二部分定义了组件内部的文件组织原则(components/README.md):
- 如果组件有与自身强耦合的常量和工具方法,就把它们放在组件旁边,并统一收进一个以
index.tsx作为入口的文件夹; - 否则组件就是一个独立文件;
- 原文给出的目标结构示例:
components/ui
- SampleComponentA
- SampleComponentA.tsx
- SampleComponentA.constants.ts
- SampleComponentA.utils.ts
- SampleComponentA.types.ts
- index.ts
- SampleComponentB.tsx
这一模式在 Studio 源码中有大量真实落地。下面选取两个代表性例子对照说明。
例 1:文件夹 + 入口模式的 HelpPanel
components/ui/HelpPanel/ 目录包含 HelpPanel.tsx(组件本体)、HelpButton.tsx 与 HelpSection.tsx(子组件)、HelpOptionsList.tsx,以及按约定拆出的配套文件:
- HelpPanel.constants.ts:导出
HelpOptionId类型与HELP_OPTION_IDS常量数组,并用as const satisfies readonly HelpOptionId[]保证常量集合与类型定义一致——这是常量文件与 types 联动的典型写法; - HelpPanel.utils.ts:存放纯函数逻辑,例如
getSupportLinkQueryParams根据 project / org / routerRef 推导支持链接的查询参数; - HelpPanel.utils.test.ts:与 utils 同目录的单元测试,覆盖参数优先级(
parent_project_ref优先、其次回退routerRef、最后orgSlug)等各分支行为。
这个例子完整演示了"常量与工具方法紧耦合于组件时,就地拆分并配套单测"的约定。
例 2:index.tsx 入口模式的 DatePicker
components/ui/DatePicker/ 目录包含 index.tsx、TimeSplitInput.tsx 与 DatePicker.types.ts。其中:
- DatePicker.types.ts 定义了
Time({ HH, mm, ss })、TimeType等类型,供TimeSplitInput.tsx与index.tsx共同引用,避免类型散落在组件内部; - index.tsx 作为入口承载主组件
DatePicker:声明DatePickerProps(onChange、to/fromISO 字符串、triggerButtonVariant、hideTime、minDate等),组合ui包的Popover/Calendar原语与本地TimeSplitInput子组件。
再看一个规模更大的 components/ui/DataTable/:DataTable.constants.ts、DataTable.types.ts、DataTable.utils.ts 与 DataTableColumn、DataTableFilters、providers、hooks 等子目录并存,是"文件夹模式"在复杂组件上的自然扩展。
例 3:单文件组件 CopyButton
并非每个组件都需要文件夹。components/ui/CopyButton.tsx 就是一个独立的单文件组件,它仍然遵循了 README 隐含的质量要求:
type CopyButtonWithText = CopyButtonBaseProps & {
text: string
asyncText?: never
}
type CopyButtonWithAsyncText = CopyButtonBaseProps & {
text?: never
asyncText: () => Promise<string> | string
}
export type CopyButtonProps = (CopyButtonWithText | CopyButtonWithAsyncText) &
ComponentProps<typeof Button>
这里用 never 字段构造了互斥的联合类型(传 text 或 asyncText 二选一),并通过 ComponentProps<typeof Button> 透传底层按钮的全部属性。同时它使用具名导出 const CopyButton = forwardRef(...),与 README 模板中"use a named export, not a default export"的要求一致。
三、组件代码模板与命名约定
README 给出了新组件的标准模板(components/README.md):
// Declare the prop types of your component
interface ComponentAProps {
sampleProp: string
}
// Name your component accordingly — use a named export, not a default export
export const ComponentA = ({ sampleProp }: ComponentAProps) => {
return <div>ComponentA: {sampleProp}</div>
}
模板要点有三个:props 类型显式声明、组件与 props 接口一一对应、一律使用具名导出。"不用 default export"在 Studio 中不只是风格偏好,而是有 CI 兜底的硬性约束:AGENTS.md 的"Defaults that differ here"一节说明,ESLint 告警在 CI 中按数量"棘轮"(ratchet)收紧,新增的 any、未解析的 exhaustive-deps 告警乃至 default export 都会让构建失败;本地可用 pnpm --filter studio run lint:ratchet 预检。同理,knip 会在 CI 中拦截死文件与死依赖,意味着新建的组件文件必须被真实引用,否则会被视为 dead file 而挂掉门禁。
在 AGENTS.md 的"Code style"部分还有与组件编写直接相关的补充约定,写新组件时值得对照执行:
- 命名:布尔 prop 与变量读起来像
is/has/can/should,且应直接从已有状态推导,不要把可推导的值镜像进useState;prop 回调命名为onX,组件内部处理函数命名为handleX;自定义 hook 返回对象而非元组; - 获取状态渲染:用顶层 early return 或互斥守卫的扁平
&&链,禁止嵌套三目(AGENTS.md 中给出了 loading / error / empty / success 四态的标准写法); - 组件体量:约 200–300 行时应拆分;重复 JSX 抽成小组件、纯逻辑抽进
.utils.ts函数并配单元测试、有状态的可复用逻辑抽成自定义 hook——这正是 HelpPanel 这类"文件夹模式"组件的由来; - 类型安全:避免
as断言,外部数据用 zod 解析;多状态值用可辨识联合建模而非多个布尔标志位; - 子组件就近放置:sub-component 与父组件同目录,避免 barrel 再导出文件;移动或抽取代码到新的模块时,直接更新所有 import 指向新位置,不要在原文件里留 re-export shim。
最后一条与 README 的"文件夹 + index.tsx 入口"模式需要放在一起理解:README 描述的是新组件文件夹的正门入口(如 DatePicker 的 index.tsx),而 AGENTS.md 反对的是在重构搬迁后于旧位置残留的"兼容垫片"。两者结合的含义是:入口文件可以有且应当是新的、单一的事实来源。
四、实操流程:在 Studio 中落地一个新组件
综合上述规范,在 apps/studio 中新增一个组件的完整决策路径如下:
- 定位目录:页面框架 →
components/layouts/;某功能页私有 →components/interfaces/<功能名>/;跨页面复用 →components/ui/(原语)或ui-patterns(组合模式)。不要放入to-be-cleaned/。 - 选择组织形态:只有组件本体的,写单文件
.tsx(参考 CopyButton.tsx);有耦合常量/工具/类型的,建文件夹,按Xxx.tsx+Xxx.constants.ts+Xxx.utils.ts+Xxx.types.ts+index.tsx组织(参考 HelpPanel 与 DatePicker)。 - 按模板编写:显式 props 接口、具名导出、布尔 prop 用
is/has/can/should前缀、回调 prop 用onX。 - 控制体量与状态:接近 200–300 行或出现多个不相关
useState时拆分;纯逻辑进.utils.ts并写 vitest 单测(Studio 使用 vitest + MSW,组件测试使用tests/lib/中的customRender+addAPIMock)。 - 遵守导入分层与门禁:从
'ui'/'ui-patterns'/'common'引入既有能力,不要重复造 hook 前先检索hooks/、lib/、packages/common;提交前跑pnpm --filter studio run lint:ratchet与 knip 检查,确认无新增 ESLLint 棘轮告警与死文件。
五、小结
apps/studio/components/README.md 虽然篇幅不长,但它把 Studio 前端组件体系的三个核心问题——"放哪、怎么拆、怎么写"——给出了明确答案:目录按页面框架 / 功能界面 / 通用复用来划分职责;文件按"强耦合就文件夹 + 入口,否则单文件"的原则组织;代码上坚持具名导出、显式 props 类型与就近拆分。配合 AGENTS.md 中关于命名、体量、状态管理与 CI 棘轮门禁的补充约定,并参考 HelpPanel、DatePicker、CopyButton 这些真实组件的落地方式,即可在 Supabase Studio 中产出与既有代码库一致、可直接通过质量门禁的组件代码。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00