跳到主要内容

公共组件设计

核心目的:在一开始考虑组件的指责范围和分层,能避免后期大的重构和迭代

设计案例

封装前

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