Dify UI(dify-ui)公共 API 编写规范:子路径导出、命名约定与泛型契约

发布时间:2026/9/7 18:35:35
Dify UI(dify-ui)公共 API 编写规范:子路径导出、命名约定与泛型契约 Dify UI(dify-ui)公共 API 编写规范:子路径导出、命名约定与泛型契约【免费下载链接】difyBuild Agentic workflows, RAG pipelines, with rich AI model and tool support on one collaborative workspace. Deploy on cloud, VPC, or self-hosted, so teams move from prototype to production without rebuilding the stack.项目地址: https://gitcode.com/GitHub_Trending/di/dify本文以 Dify 仓库中 dify-ui 公共 API 编写指南 为核心,系统讲解langgenius/dify-ui包在什么算公共 API这一边界问题上的全部规则:每个src/primitive/index.tsx如何充当显式的公共 API 边界、package.json#exports子路径与组件命名的对应关系、泛型契约如何端到端保留,以及如何用本地类型测试保护这些契约。读完本文,你可以在该 UI 基元包中正确新增或修改一个组件,使它的导出面、类型与测试完全符合包的既有约定。包背景:dify-ui 是什么包 README 对langgenius/dify-ui的定义是:面向 Dify 产品的独立 UI 基元、设计令牌、CSS-first 的 Tailwind 样式,以及cn()工具。绝大多数交互型基元是围绕 Base UI(Headless 组件库)编写的薄而有主张的包装器;由 Dify 自研的基元则使用语义化 HTML、cva、cn与 Dify 设计令牌。有两个前提决定了公共 API这个概念在此包中格外重要:包是 workspace 私有的(private: true),但 README 明确声明其公共子路径被当作稳定的包边界对待;包故意不提供根 barrel(根入口桶导出),消费者只能通过公共子路径导入,例如:import { Button } from langgenius/dify-ui/button import { Dialog, DialogContent, DialogTrigger } from langgenius/dify-ui/dialog import { Field, FieldLabel } from langgenius/dify-ui/field import { Input } from langgenius/dify-ui/input import { cn } from langgenius/dify-ui/cn import langgenius/dify-ui/styles.css因此,每个子路径导出了什么、以什么名字导出,直接决定了下游web等应用包的可用面。AGENTS.md 进一步划定了包边界:不得从应用包导入,不得依赖路由、i18n、应用状态、schema、数据获取或业务 API;并约定了契约归属——导入、导出、命名、公共类型、泛型与 anatomy 的归属人正是本文档(Public API authoring)。一、index.tsx即显式的公共 API 边界编写指南的第一条规则:每个src/primitive/index.tsx都是一个显式的公共 API 边界。具体要求:实现细节保持模块私有(module-local);完整的公共面通过文件底部的独立清单式导出发布,即分别使用export { ... }(值导出)与export type { ... }(类型导出)两个清单;禁止散落的内联导出与清单式导出混用,禁止通配符导出(export *)。Checkbox 基元 是该规则的完整样板。它的文件主体定义了CheckboxRoot、CheckboxIndicator、Checkbox、CheckboxSkeleton四个组件,但文件顶部没有任何export关键字;全部公共面集中在文件末尾:export { Checkbox, CheckboxIndicator, CheckboxRoot, CheckboxSkeleton } export type { CheckboxIndicatorProps, CheckboxProps, CheckboxRootProps, CheckboxSkeletonProps }这样做的好处是:任何人在 15 秒内就能看全这个子路径对外暴露了什么,新增/删除导出只发生在文件底部的两处清单,不会有人无意间把一个内部辅助函数导出出去。二、子路径与命名规则每个公共基元都要有配套的 exports 子路径package.json 中的exports字段是子路径的单一事实来源,每个基元一条,types与import条件同时指向src/primitive/index.tsx:exports: { ./styles.css: ./src/styles/styles.css, ./cn: { types: ./src/cn.ts, import: ./src/cn.ts }, ./button: { types: ./src/button/index.tsx, import: ./src/button/index.tsx }, ./checkbox: { types: ./src/checkbox/index.tsx, import: ./src/checkbox/index.tsx } // ……dialog、drawer、select、tabs、toast 等约 40 个子路径,均遵循同一结构 }指南要求:包内组件之间使用相对导入互相引用,消费者只通过公共子路径导入。新增一个基元时,必须先在这里登记一个同名字段,否则即使源码写好了,子路径也无法被包外解析。规范化边界不用Root后缀命名规则是:规范边界(即组件本身)使用基元原名的组件 同名的Props类型:Select配SelectProps、Drawer配DrawerProps;只有当同一个子路径同时导出底层 anatomy(原子部件)和高层便利组件时,才保留Root后缀,典型如 Checkbox 子路径:CheckboxRoot(可自由组装指示器的底层部件)与Checkbox(自带CheckboxIndicator的便利组合)并存;每个运行时组件都必须有一个准确且可导入的、与组件同名的 props 类型。上游 Base UI 部件若未被改动,直接用直接别名(type XProps BaseXNS.Root.Props);Dify 自研组合组件则在 Dify UI 边界处定义自己的组合 props,而不是照抄上游的形状;当某一个 prop 会改变相关 prop 的合法形状时(如受控/非受控状态、单选/多选),应使用**可辨识联合(discriminated union)**表达。SegmentedControl的类型测试(segmented-control 测试)正好演示了这种值形状受 prop 约束的契约:// ts-expect-error segmented controls require either value or defaultValue SegmentedControl aria-labelMissing value SegmentedControlItem valueoneOne/SegmentedControlItem /SegmentedControl缺少value/defaultValue的写法在类型层面就是非法的。className包装的统一写法当 Dify 的包装器消费上游的className时,标准写法是先Omit掉上游的className(Base UI 的 state-callback 形式),再暴露className?: string,并用cn()合并默认样式类与调用方传入的类。Tabs 子路径 是精简而标准的例子:type TabsListProps OmitBaseTabsNS.List.Props, className { className?: string } function TabsList({ className, ...props }: TabsListProps) { return BaseTabs.List className{cn(flex gap-4, className)} {...props} / }注意其中Tabs本身是未改动的直接别名——指南要求未改动的 Base UI 部件使用直接别名:type TabsProps BaseTabsNS.Root.Props const Tabs BaseTabs.Root三、端到端的泛型契约指南Generic contracts一节的核心主张:泛型关系必须端到端保留,覆盖 picker 的Value与Multiple、表单值、radio/slider 的值,以及 overlay 的 payload 与 handle 等。具体规则:不要用any或写死的string抹掉调用方拥有的类型;unknown只作为被独立消费的、其 value JSX 无法从父级推断类型的 anatomy的安全默认值;不要为 root 单独添加一个泛型,当可独立渲染的 anatomy 可能产生 root 推断类型之外的值时尤其如此——应保持上游契约,直到整个组件家族都能强制同一种值类型;不要宣传一套完整 anatomy 无法强制的类型关系。指南点名了Tabs:它有意跟随 Base UI 的非泛型 root,因为其当前 tab 值类型是any | null;在整套 anatomy 能强制统一值类型之前,不要为它虚构泛型关系。这也解释了 tabs/index.tsx 中Tabs只是一个纯别名、不引入任何泛型参数的事实;上游 anatomy 的各部件若各自拥有不同的语义、交互或定位行为,应原样保留;只有当包确实新增了一份共享契约时才创建 Dify 自研的便利组件,不要为了缩短调用点而隐藏基元部件。仓库中泛型契约的真实验证在本地测试里。SegmentedControl 的类型示例 展示了调用方如何显式声明值类型,并让泛型穿透到 Item:SegmentedControlnumber value{10} onValueChange{() {}} aria-labelPage size SegmentedControlItemnumber value{10}10/SegmentedControlItem SegmentedControlItemnumber value{20}20/SegmentedControlItem /SegmentedControlradio-group 的类型测试 则验证了值的形状约束:boolean radio 项不接受string值(用ts-expect-error boolean radio items should not accept string values守护)。四、把公共面保持在最小Keep the public surface small一节的判断标准很明确:一个类型不会仅仅因为 Base UI 命名了它、或某次实现曾经导出过它就是公共的;除了与组件匹配的 props 之外,只在该类型与某个公共工厂配对、或真实消费者必须独立命名它时才导出;以下类别默认私有:state、事件细节与原因(reasons)、actions、受控状态辅助函数、context 值、render 辅助函数、样式辅助函数,以及上游的透传别名。原因是:公共 props 本身已经为内联 render 与事件回调提供了上下文类型,单独导出这些类型只会放大维护面;当包装器通过cn()消费className时,省略上游的 state-callback 形式,改为暴露className?: string——公共类型必须描述包装器真正实现了的行为,而不是它透传的、但实际未生效的上游能力。对照 checkbox 子路径 可以完整检验这条标准:导出的 4 个组件 4 个同名 Props 类型一一对应,没有任何 Base UI 内部的 state/handler/context 类型泄漏到子路径;而OmitBaseCheckboxNS.Root.Props, className的写法正是省略上游 className 形态、暴露className?: string的落地。五、用证据守护契约:Evidence 部分指南最后一节Evidence规定了守护上述契约的工作方式:使用本地公共子路径类型测试,保护泛型推断、必需的 prop 关联关系,以及故意设计成会报错的非法用法;在修改任何源自上游的契约之前,先阅读当前官方 Base UI 文档与已安装的类型声明(base-ui/react),而不是凭记忆改动。落地形态在包内随处可见:各基元的__tests__/index.spec.tsx中混入了纯类型断言组件(如SegmentedControlTypeExamples、radio-group 的ts-expect-error用例),它们不参与运行时断言,只让tsc在类型检查阶段验证契约。配套的维护手段是 package.json 中的脚本:命令作用type-check(即tsc)全量类型检查,类型测试用例在此被验证test(即vp test --project unit)运行单元测试项目storybook/test:storybook本地启动 Storybook / 运行 Storybook 视觉与 a11y 测试项目也就是说,对 dify-ui 的一次改动是否守约,最终由三把锁共同把关:类型检查(泛型与 props 关联)、单元与浏览器测试(运行时行为)、Storybook 项目(文档化基线)。AGENTS.md 也提醒:只有当组件拥有其类型、stories 与上游文档都无法表达的实质性 Dify 专属契约时,才需要为它单独建立本地 README,不要为了完备而创建。六、修改 dify-ui 公共 API 前检查清单综合 authoring 文档 与仓库现状,新增或修改一个基元的公共面时,按序确认:package.jsonexports中是否有(或新增)与该子路径完全对应的条目,且types/import均指向src/primitive/index.tsx;文件底部是否只存在两个清单式导出(export { ... }与export type { ... }),无内联散乱导出、无export *;组件名是否去掉了多余的Root后缀,同名XxxProps是否可导入且准确;Root是否只用于anatomy 便利组件共存的子路径(如CheckboxRoot/Checkbox);受控/非受控、单值/多值等形状分叉是否用了可辨识联合;调用方拥有的泛型(Value、Multiple 等)是否端到端保留,没有被any/string抹掉;root-only 泛型是否被克制;被导出的每个类型是否都与公共工厂配对或被真实消费者独立命名;state、事件细节、context 值等是否保持私有;className包装是否统一为Omit上游, className { className?: string }cn()合并;是否有(或更新)了本地类型测试,包括用ts-expect-error守护的故意错误用法;动手改上游契约前,是否已核对官方 Base UI 文档与已安装的base-ui/react类型声明。这套规则的本质,是把公共 API 边界从一个隐含约定提升为可检查、可测试、可评审的显式契约:子路径列表即目录,文件底部清单即边界,类型测试即证据。【免费下载链接】difyBuild Agentic workflows, RAG pipelines, with rich AI model and tool support on one collaborative workspace. Deploy on cloud, VPC, or self-hosted, so teams move from prototype to production without rebuilding the stack.项目地址: https://gitcode.com/GitHub_Trending/di/dify创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考