-
Notifications
You must be signed in to change notification settings - Fork 270
Component API Guide
组件进入开发阶段前,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,节约很多查阅文档的时间。
有一种业务需求,是希望在子组件里面嵌入自定义内容,比如按钮(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 |
访问 TDesign 官网