Skip to content

Component API Guide

sheepluo edited this page Jan 8, 2024 · 3 revisions

流程

组件进入开发阶段前,API 必须经过评审,评审会组织内容请参考 一个组件的诞生 - 准备阶段

多框架融合

TDesign 是一个比较大的体系,包含多种前端技术栈,大家彼此互相碰撞,又互相兼容。

作为同一个体系,API 需要尽可能保持一致,但是各个框架又有自身约定俗成的一些特性。比如事件名称,React 一般使用 on 开头的小驼峰命名(如:onVisibleChange),而 Vue 一般又使用不带 on 的中划线命名(如:visible-change)。这就冲突了,API 如何保持一致呢?经过一番讨论,我们决定:React 继续保持原有命名风格;Vue 侧在 emit 事件时也继续保持原有风格,但是 Vue 需要同时支持和 React 一样的 Props 事件参数。代码如下,

<!-- For React -->
<Checkbox onChange={onChange} />

<!-- For Vue -->
<checkbox @change={onChange} />

<!-- For Vue, This won't work in JSX, only works in template -->
<checkbox onChange={onChange} />

如此,既满足了框架各自的特性,又保证了统一性。

除了这个,还有很多其他的 API 规范。了解这些规范,有助于大家更加清楚地理解和使用 TDesign,节约很多查阅文档的时间。

TNode

有一种业务需求,是希望在子组件里面嵌入自定义内容,比如按钮(Button),按钮的内容需要完全自定义。这类需求,React 里面使用 ReactNode 实现,一般命名为 children;Vue 里面使用插槽或 props 实现,一般命名为默认插槽 default;Angular 使用 TemplateRef 实现,一般命名为 content;小程序 Miniprogram 一般使用默认插槽。每一个框架的实现方式和通用命名规范都不一样,这该怎么统一呢?

经过 PMC 例会讨论,我们决定使用一种新的类型 TNode 来统一规范和描述自定义节点类型,

/**
 * TNode For React
 * TNode = ReactNode
 * TNode<params> = (params) => ReactNode
 */
type TNode<T = undefined> = T extends {} ? ((props: T) => ReactNode) : ReactNode;

/** TNode For Vue2(props) */
type TNode<T = any> = (h: CreateElement, props?: T) => TNodeReturnValue;

/** TNode For Vue3(props) */
TNode = (h: typeof import('vue').h, props?: T) => import('vue').VNodeChild

/** TNode For Vue(slot) */
<template>
 <div><slot name='content' /></div>
</template>

/** TNode For Angular(TemplateRef) */
<div>
 <ng-content></ng-content>
</div>

/** TNode For Miniprogram(Slot) */
<view><slot name='text' /></view>

/** TNode For Miniprogram(Props) */
<view>{{ text }}</view>

组件子节点统一使用 content 进行描述,各框架也同时继续保持原有特性。

  • React 同时支持 children 和 content
  • Vue 同时支持使用插槽 content/default 渲染节点,也支持同名的 props 属性 content/default 渲染节点,以减少自定义节点的 API 记忆
  • Angular 仅需支持 content
  • Miniprogram 需支持插槽 content,也需支持同名的 props 属性(如果组件只有一个插槽则为默认插槽) 示例代码如下:
/** TNode: React Props (children) */
<TButton>按钮</TButton>

/** TNode: React Props (content) */
<TButton content='按钮'>
<TButton content={<div><TIcon />按钮</div>}>

/** TNode: Vue Slot */
<t-button>按钮</t-button>
<t-button><template #content>按钮</template></t-button>

/** TNode: Vue Props */
<t-button default={(() => <div><TIcon />按钮</div>}></t-button>
<t-button content={(() => <div><TIcon />按钮</div>}></t-button>

/** TNode: Angular TemplateRef */
<t-button>按钮</t-button>

/** TNode: Miniprogram Slot */
<view>
  <t-button>按钮</t-button>
</view>
<t-button content='按钮'></t-button>
``` js


## 属性和插槽同名
只要看到 TNode,在 Vue 和 Miniprogram 中就表示 API 属性和插槽同时都支持,且名称相同。



## 属性透传
组件并非独立存在,也会存在组件之间互相依赖。比如:Select 组件依赖 Popup 组件,TreeSelect 组件依赖 Tree 组件。对于这些存在依赖关系的组件,父组件需要默认支持透传子组件的全部 API ,以便业务侧可以自由控制更多内容。即:

Select 组件必须包含 PopupProps ,用于透传 Popup 组件全部属性 API。

TreeSelect 组件必须包含 TreeProps,用于透传 Tree 组件全部属性 API。

Calendar 组件则是包含了 ButtonProps/RadioGroupProps/SelectProps 等多个属性 API 透传。

## 事件规范
事件命名规范:onXxxChange / onXxxClick / onXxxKeydown / onXxxKeyup,示例:onVisibleChange / onConfirmClick / onKeydown / onEscKeydown / onEnter
Vue Emit 事件使用中划线命名(不带 on),如:page-size-change
Vue Props 事件回调使用小驼峰命名(带 on),如:onPageSizeChange
React 事件使用小驼峰命名(带 on),如:onPageSizeChange
Miniprogram 事件名称使用中划线命名(不带 on),如:page-sizer-change
Angular 事件名称使用驼峰命名(不带 on),如:cancelClik

示例代码

``` js
<div :onClick="onClick">For Vue Props</div>

<div @click="onClick">For Vue Emit Event</div>

<div onClick={onClick}>For React Props</div>

<div (cancelClick)="onCancelClick()">For Angular Event</div>

<div bindtap={onTag}>For Miniprogram Emit Event</div>

参数的无序性

不知大家是否有见过 (h, row, rowIndex, col, colIndex, xxx) 这样的参数类型,参数是有顺序的。使用这样的参数开发,需要牢记每个参数的顺序。当你只需要使用 colIndex 的时候,也必须把前几个参数一起写上,而这时,某些 lint 规则可能就开始报错了,有点难受。按照顺序排列的参数,发现顺序不合适的时候,想改也不是很方便改,改了会出现 breaking change。

为此,TDesign 的参数设置会尽可能保证无序性,组件涉及到的事件参数和函数参数一般只会存在一个(Object),特殊情况可以允许两个。比如为支持 Vue 框架的语法糖,change 相关的事件参数第一个值必须 value,如此,像 MouseEvent 这类额外的环境参数只能放在第二个位置。

比如:Anchor 组件 的 click 事件参数为 (link: { href: string; title: string; e: MouseEvent }),只有一个参数 link,其中 href / title / e 均为 link 的属性。

Popup 组件的 visibleChange 事件参数为 (visible: boolean, context: PopupVisibleChangeContext),有两个参数 visible 和 context。

单一原则

一个 API 只做一件事,避免 API 发生变化时影响到多个事件。一件事物也尽量使用一个 API 表示,以保证互斥性。

什么是互斥性呢?比如:variant = 'base/outline/dashed/text',如果 varaint = outline,那么variant 就永远不可能等于 dashed,outline 和 dashed 无法同时共存于一个 API,天然互斥。

业内某知名组件库 DatePicker 组件 API 设计如下

字段 描述 类型
placeholder 非范围选择时的占位内容 string
start-placeholder 范围选择时开始日期的占位内容 string
end-placeholder 范围选择时结束日期的占位内容 string

一个占位符使用了 3 个 API 控制,业务侧需要记住 3 个 API 并理解 3 个 API 的关系,才能正确使用。如果 placeholder 和 start-placeholder 同时出现,势必会有一个是无效的。为了减少这样的情况,在 TDesign 中,我们会处理成一个 API,只要是占位符,全部统一叫 placeholder,数据类型为:string | [string, string]。示例:['开始日期', '结束日期'] 或者 '请选择日期'。

再看业内某库的一对 API 设计:

字段 描述 类型
plain 是否朴素按钮 boolean
round 是否圆角按钮 boolean
circle 是否圆形按钮 boolean

如果 plain/round/circle 同时存在,又是 round,又是 circle,组件应该以哪个为准呢?

round 和 circle 本是互斥的两个特性被分成两个了 API,失去了一个 API 的天然互斥性。

元素定位层级

和层级相关的组件都会有一个预设的 zIndex 值,我们称为默认值,写在 CSS 样式中。这个默认值是我们经过深思熟虑得出的,考虑了绝大多数的业务场景。所以在一般的业务使用场景中,并不需要关心这个值。

如果在复杂的业务场景中,我们还提供了一个 zIndex 值建议范围,以做参考。如下表:

组件 建议 z-index 范围 组件默认 zIndex
Drawer 1500 ~ 1600 1500
Toast 2000 ~ 2500 2000
Dialog 对话框 2500 ~ 2600 2500
Loading 组件 3500 ~ 3600 3500
Message 消息 5000 ~ 5100 5000
Popup 弹框气泡 5500 ~ 5600 5500
Notification 消息通知 6000 ~ 6100 6000

zIndex 规范:https://github.com/Tencent/tdesign/wiki/%E7%BB%84%E4%BB%B6-z-index

由于业务需求纷繁复杂,预设好的 zIndex 值并不能满足所有的业务需求。我们来看两个场景:

场景一:点击 Dropdown 组件(基于 Popup 实现)中的某一项后显示确认弹框 Dialog。此时,期望 Dialog 层级 比 Popup 高。

场景二:在 Dialog 弹框里面有个表单 Select 组件(基于 Popup 实现),此时又需要 Dialog 层级比 Popup 低

两个场景的 zIndex 值高低出现了不同,无论选择哪一种都不能完全满足需求。怎么办呢?

针对这种复杂的情况,我们提供的处理方式是:给这些层级组件全部都加上 zIndex API,以便用户在特定场景中自行决定元素的层级关系。如:

其他命名规范

  • 方法类 API 使用动词,其他类型使用名词
  • Vue 和 React 属性均使用小驼峰命名,如:disabled / destroyOnClose
  • API 命名须尽可能避开 HTML 原生属性,避免属性冲突,如:button 的 type 属性
  • 是否显示某个元素,使用 show 开头(而非 is)如:showOverlay
  • 不同组件的相同行为命名尽可能保持一致,示例如下:
描述 名称 类型/事件参数
选中值,唯一标识 value -
占位符 placeholder String
描述组件不同风格 theme String
尺寸,可选值:small/medium/large size String
是否可见 visible Boolean
禁用状态 disabled Boolean
只读状态 readonly Boolean
是否多选 multiple Boolean
布局 layout String
有边框(默认无边框组件) bordered Boolean
无边框(默认有边框组件) borderless Boolean
元素呈现位置,示例:center 或 top placement String
关闭按钮 closeBtn String/Boolean/TNode
确认按钮 confirmBtn String/Object/TNode
取消按钮 cancelBtn String/Object/tNode
偏移量,示例:[-100, '10em'] offset
空数据元素,默认值 '暂无数据' empty String/TNode
是否可搜索过滤 filterable Boolean
是否禁用组件 disabled Boolean
是否允许清空 clearable Boolean
是否处于加载中 loading Boolean
对象属性别名设置,示例:{ label: 'text', value: 'id' },主要应用于 Tree/Select/Table/Cascader 等组件 keys Object
是否允许多选 multiple Boolean
前置图标 prefixIcon TNode
后置图标 suffixicon TNode
底部 footer -
内容 body -
头部 header -
挂载元素,应用于 Dialog/Drawer/Message 等 attach String/Function
防止滚动穿透 preventScrollThrough Boolean
单元格,应用于 Table/Calendar 等组件 cell -
格式化数据 format String / Function