公共组件设计
核心目的:在一开始考虑组件的指责范围和分层,能避免后期大的重构和迭代
设计案例
封装前
const Trigger = ({type:string})=>{
...
if(type==='Tooltip'){
return <Trigger>...
}
}
封装后
const Tooltip = ()=>{
...
return <Trigger>...
}
const Popover = ()=>{
...
return <Trigger>...
}
底层 trigger 暴露的属性过多,对用户不友好,且 tootip 和 popver 才用户常用组件
这些组件可以暴露了一个对象属性triggerProps: Partial<TriggerProps>
该对象可以接受所有 Trigger 组件的参数
而聚合这些偏低层的定制属性 ‘
分层的好处有哪些?
- 可复用性:抽象的底层 Trigger 可以用在各种触发弹层场景中,各个组件无需重复开发,同时后续 Trigger 增加的能力各个组件都可以共享到
- 稳定性: Popover 的迭代不会影响到 Tooltip 等组件,可以解耦开独立演进
- 易用性: Popover 和 Tooltip 自身只暴露必要的属性,对于所有组件通用的弹层配置统一到 triggerProps 中,使用方便,用户心智负担低
到具体的业务场景,怎么运用分层
比如需求中遇到一个上传组件 可以将其封装为两层
- 下面一层是纯 UI 交互,不涉及具体的上传能力和 SDK
- 上层基于不同上传渠道和上传方式分出不同的组件,开箱即用,共用相同的下层 UI 组件
父子组件设计
父子组件的设计中,父组件用来管理一组子组件的状态 比如 Tabs 组件用来控制当前展示的选项卡以及改变的回调函数,而 Tabs.TabPane 用来包裹每一项卡片
配置写法/JSX 写法
当每一个 Tab 卡片是复杂的节点时,在配置中书写复杂的对象相对不友好
<Tabs
tabConfig={[{ key: "1", title: "Tab 1", children: "TabContent1" }]}
></Tabs>
- 非复杂场景使用配置的方式对新人上手更友好,约束性更强
- 比 Select 的 Options 只提供 label, value 和 extra 三个属性即可满足大部分场景
- 复杂场景使用 JSX,(如需要 disabled) 灵活性更好
- 两者也可以都兼容
父子组件的实现
父子组件的状态通信基本用 context 实现 公共组件开发不建议引入状态管理库 可以使用 byted hooks 的 createModel,这是用来做状态管理的
代理模式捕获数据
如果用户自定义的控件也需要和封装好的组件(如 Form)进行通信
Form 会给 Form.Item 的唯一子节点传递 onChange 和 value 这两个 props 来管理子节点的值
自定义 控件中只要使用 value 来作为值,使用 onChange 将自己更新的值回调给 Form,能完成数据的通讯
数据的流向:Form->Form.Item->自定义->原生控件->自定义handleChange->Form.Item->Form
form->form.item 通过 context 通信 form.item->用户组件通过回调通信
FormItem 实现原理
使用 React.cloneElement,重包一次子组件, 给子组件传入 value 和 onChange(会覆盖原先定义的 value 和 onChange
对于 value 会强行覆盖,所以用户自己传入的无效,被 form 代理了
对于 handleChange, 不仅要执行用户的 onChange, 也完成内部数据的通信(利用 context 触发 form)
const FormItem = ({ children, field, value, onChange }) => {
// 从内部Context获得设置字段值的方法
const { setInnerField } = useInnerContext();
// 将外部传入的onChange交给children的handleChange函数去invoke
const handleChange = (value) => {
setInnerField(field, value);
// 组件内部触发(组件本身value发生变动)后,先更新Form维护的value,再用新
// value触发用户自定义监听onChange
onChange(value);
};
// 使用cloneElement覆盖children的value和onChange(如input组件上原生的value和onChange
return (
<div>{React.cloneElement(children, { value, onChange: handleChange })}</div>
);
};
在 FormItem 的第一个子组件上传 value 是无效的,因为会被 Form 的 value 直接覆盖
组件 Props 的设计、实现和规范
-
属性命名为 value 时,对应的初始值为 defaultValue(受控组件 )
-
回调事件命名为
on{eventName}
-
配置项使用形容词(isShow, canClear)
-
loading
-
语义化事件名 onAdd
二次封装组件
善用 omit 和{...rest}
,能够定制一个配置的修改
type MyProps = Omit<RawProps, "options"> & { myOptions: string };
const MySelect = ({ myOptions, ...rest }: MyProps) => {
return <Select options={myOptions} {...rest}></Select>;
};
className 与 Style 优先级
理想状态下,我们封装的公共组件不需要去再定制样式,实际场景下可能难以避免 我们可以提供 style:CSSProperties 和 className: string|string[]两个属性,方便用户进行样式覆盖
实现的时候注 意用户传入的类名和 style 属性优先级更高
<div className={classnames(low, high)} style={style}></div>
受控 与 非受控设计
对于受控非受控的设计符合社区规范,建议可以使用 useControlled
跳转
跳转: 链接跳转使用<Link/>标签
,有更好的原生体验(比如 command + 左键点击新开标签页)
z-index
z-index: 组件的 z-index 和基础组件的层级相关联(相对层级)
// 相对层级
@z-index-popup-base: 1000
@z-index-affix: @z-index-popup-base - 1 //999