本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com
大厂技术 坚持周更 精选好文
本文为来自 字节跳动 - 国际化电商 - S 项目团队 成员的文章,已授权 ELab 发布。
为何要进行前端组件设计?
与仅承担数据处理逻辑的后端不同,前端需要负责界面渲染、数据处理、和接口调用,在框架诞生前,更多地是编写页面维度的顺序脚本代码。随着前端继续的持续发展,ES6 推出了 class 语法糖,React 提出了函数式组件,Vue 则以模版语法的形式组织代码,前端代码逐渐从 “平铺” 转变到了 “层级” 结构,从 “面向过程” 进阶为“面向对象”,前端组件也成为了近几年来的热门议题。
“组件是对数据和方法的简单封装,是软件中具有相对独立功能、接口由契约指定、和语境有明显依赖关系、可独立部署、可组装的软件实体。” 这段百科中摘取的组件定义,揭示了组件所需要具备的特性:功能独立、约定一致、可集成、服务于场景。
在软件工程中,软件设计是软件开发流程中的必要阶段,在需求分析后、软件开发前进行。软件复杂度是每一个项目演进的产物,随着需求和代码行数的增加,复杂度将持续提升。软件设计的优劣为对复杂度带来的影响是不同的,优雅、合理的设计使待开发的代码复杂度可控,而拙劣的设计将会给软件带来无序、偶然的复杂度变更。一个优秀的前端组件需要在满足需求的前提下,具备高易用性和良好的可扩展性,这是我们进行前端组件设计的目标。
💡如何提升组件易用性?
合理的组件封装
组件既生于页面,又能够独立于页面。我们不能将整个页面杂糅为一个组件,也不能将每一小块 UI 都封装为组件。前端组件按类型可以分为容器组件、功能组件和展示组件,一个优秀的组件应该保证:功能内聚、样式统一、并且与父元素仅通过 Props 通信。
组件的封装粒度并不是越小越好,很多时候一个组件是在其他一个或多个组件的基础上开发的,无法完全以功能点的数量衡量是否遵循单一职责原则,组件开发者需要根据组件功能和目标来确定组件封装粒度:
- 当该组件需要承载具体的额外功能时,相较于新增 API ,封装成独立的组件是更好的选择。
🌰:InputTag 组件 在 Input、Tag 的基础上,增加了部分交互功能,API 整合了两个组件的属性,作为一个全新的组件提供给开发者使用。相似的,InputNumber、AutoComplete、Mentions 等组件也是基于单一职责原则封装的特定功能组件。
- 当组件中存在可能被单独使用、可以承载独立功能的子组件时,可以将其以内部组件的形式提供。
🌰:图片预览功能通常依托着图片组件使用,在实际系统中,唤起图片预览的触发器不一定是图片,可能是按钮或其他触发事件,因此预览组件需要单独提供给开发者使用。预览组件作为 Image 的内部组件,开发者能够以 Image.Preview、Image.PreviewGroup 的方式使用,并提供左右切换、图片缩放等功能,用户可以通过 srcList、visible、actions、scales等 API 来控制并定制化预览组件。
规范的 API 编写
一个易用的组件,使用者无需阅读文档或仅快速浏览文档即可上手使用,并且应当在使用过程中给予清晰的注释和代码提示。希望以下 API 编写建议能够给组件开发者一些参考:
减少必填的 API 项,尽可能多地提供默认值,降低组件的使用成本;
使用通用且有意义的 API 命名:
onXXX:命名监听 / 触发方法
renderXXX:命名渲染方法
beforeXXX/afterXXX:命名前置 / 后置动作
xxxProps:命名子组件属性
优先使用常见单词进行命名,如:value、visible、size、disabled、label、type 等等
单独维护类型文件,并将其打包至组件产物包中,这样使用者在开发过程中能够实时看到对应的类型提示;
在类型文件中,为 API 编写注释;
[Slot] 与 [Props] 的选择
使用 Props 存在的问题?
当我们需要实现一个较为复杂的卡片需求组件时,为了最大程度地还原 UI、减少用户的样式开发成本,首次设计时我们会设计出这样的 API:
export type CardProps = { // 底部信息展示 infoProps?: { title?: ReactNode; content?: ReactNode; }; // 弹层信息展示 moreInfoProps?: { info?: ReactNode; triggerProps?: TriggerProps; descriptionsProps?: DescriptionsProps; }; className?: string; style?: CSSStyleSheet; width?: number | string; imageProps?: { srcList?: Array<SrcList>; // 图片url数组 afterImgs?: React.ReactNode; // 插槽,在图片dom节点中 aspectRatio?: string; // 宽高比 默认3:4 buttonProps?: ButtonProps; current?: number; // 受控展示图 defaultCurrent?: number; // 默认展示图 onChangeCurrent?: (current: number) => undefined; // 设置current PreviewGroupProps?: ImagePreviewGroupProps; src?: string; }; children?: React.ReactNode;} & CardCheckboxProps;可以看到,这个业务卡片组件是由多个不同组件组合而成,其承载了渲染和操作(选中操作、图片切换和弹层操作),这个设计的缺陷是显而易见的:
需要编写很多分散的 JSX 代码,无论是写在 Props 中还是定义成单独的组件,其可读性都不高;
需要在 Card 组件中杂糅许多额外的 Props,例如 triggerProps 和 descriptionsProps,增加了该组件的学习成本;
如果以插槽的方式对 Card 组件进行改造,通过内部组件间的组合来实现需求,避免了大量组件 Props 的堆砌,层次清晰、可读性高,这样的组件结构明显易用性更高。
<Card type="verticle" {...cardProps}> <CardImage {...ImageProps} /> <CardContent {...InfoProps} > <div class>content</div> <Tag>Tag</Tag> </CardContent> <CardTrigger {...triggerProps}> <Description {...descriptionProps}/> </CardTrigger></Card>什么是插槽 (Slot)?
Slot 是 Vue 框架提出的概念,可以理解为临时占位,可以用其他组件进行填充,Slot 能够实现父组件向子组件分发内容的功能。Vue 框架中提供了
使用
props.children获取子组件,若需要区分使用不同子组件,只能通过数组下标读取。使用 Props 传递 ReactNode 元素。
将组件划分为多个内部组件,交由开发者自行组装。
Slot(内部组件) 的使用时机?
布局类组件优先使用 Slot,为开发者提供更灵活的使用方式,参考 Typography、Layout、Card 等组件,开发者可以随意地在这些组件内部插入自定义实现。上文提到的业务卡片组件,实质上也是一个封装了多图预览功能的布局组件,因此更适合使用 Slot 来组织代码。
内容复杂、定制化程度高的组件更适合使用 Slot
功能类组件中,以 Props 传递 ReactNode 的方式来接管内部元素,尽量避免传递基础类型元素进行展示。
// ❌ 扩展性低type CardProps { title?: string; tags?: string[];}// ✅ 为开发者提供对应的“插槽”type CardProps { title?: string | ReactNode; tag?: string[] | ReactNode;}💡如何提升组件可扩展性?
开闭原则:对扩展开放,模块的行为可以被扩展;对修改关闭,模块中的源代码不应该被修改
将 DOM 交予用户接管
在前端组件中,应该提供对应的 API 属性或方法来支持额外的功能,给予开发者更充分的扩展空间,而不是有部分需求无法满足时放弃使用组件。以 Cascader 组件 为例(以下例子若无特殊说明,均来自 Arco 组件库),在通常情况下,仅需要使用左侧的基础选择器即可满足需求。
此时若开发者需要在级联选择器底部添加操作按钮或文字展示,可能会直接修改组件源代码、甚至放弃使用该组件。作为组件开发者,为了提升组件可扩展性,在此处增加了 dropdownRender属性,接收一个返回 ReactNode 的函数,并将此 ReactNode 渲染在级联选择器的特定位置。
<Cascader options={options} dropdownRender={(menu) => { // menu 为所有选择器元素 return ( <div> {menu} <Divider style={{ margin: 0 }} /> <div style={{ margin: 4 }}> The footer content </div> </div> ); }} />与此类似的,还有 renderFooter、renderOption、renderFormat 等 API,这些 API 实现难度并不高,一定程度上将 DOM 元素的掌控权交予组件使用者,作为通用组件,为开发者提供了部分功能和样式的可扩展性。我们在设计前端组件时,多多留意组件中能够接管给使用者的渲染逻辑和操作逻辑,并将这些逻辑暴露出去。
设计可扩展的 API
组件开发前,整理组件所需实现的功能,并以功能为维度设计组件 API。以下是一个设计移动端选择器的例子,这个选择器需要支持单选、多选和时间选择,于是这样写了第一版 API👇,它可以满足我们当前的选择器需求。
type PickerProps { dataSource?: PickerData[] | PickerData[][]; multiple?: boolean; // 是否多选 time?: boolean; // 是否时间选择 value?: (string|number)[]; onChange?: (value: (string|number)[]) => void; ...}在后续迭代中发现还有地区选择和级联选择的需求,选择器需要进行优化更新,在以上 API 的基础上只能通过添加 cascader、region 两个布尔值字段用于标识不同选择需求。这样做的缺陷是很明显的,每当我们新增不同类型的选择功能,都需要新增一个 API 字段,并且这些字段还是互斥关系。理清问题后,更新了第二版 API👇。在组件库中很多 API 都设计为常量枚举值的形式,即使其只有两个取值,这样扩展性相较于布尔值类型更好。
type PickerProps { dataSource?: PickerData[] | PickerData[][]; type?: "single" | "multiple" | "cascader" | "region" | "time"; // 选择器类型 value?: (string|number)[]; onChange?: (value: (string|number)[]) => void; ...}除上述例子外,还可以利用 ts 的泛型和可选类型来实现 API 扩展,例如 Table 组件的 pagination、border 等字段,既可以直接设置为 true/false,也能够以对象的形式进行更详细的配置。
极致的扩展性——Headless UI
首次接触 headless 概念是在 chrome 浏览器中,在 headless 模式下用户无需看到网页界面即可进行网页操作,现在广泛用于 web 自动化测试和爬虫场景中。与之相似的,Headless UI 是基于 React Hooks 的组件开发设计理念,强调只负责组件的状态及交互逻辑,不关注组件的样式实现。其本质思想其实就是关注点分离,将组件的 “状态及交互逻辑” 和“UI 展示层”实现解耦。
Headless UI 目前有两种主流实现方式,其一是将组件划分为多个原子组件,使用者可以通过填充组件或修改样式的方式来实现自己的需求,其二是以 Hooks 的方式暴露内置交互功能的子组件属性,使用者可以将这些属性应用于任意组件上,由于没有将样式封装到组件中,Headless 组件实现了最大程度的视图层可扩展性。
<NumberInput>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>// chakra-ui 提供的数字增减框组件function HookUsage() { const { getInputProps, getIncrementButtonProps, getDecrementButtonProps } = useNumberInput({ step: 0.01, defaultValue: 1.53, min: 1, max: 6, precision: 2, }) const inc = getIncrementButtonProps() const dec = getDecrementButtonProps() const input = getInputProps() return ( <HStack maxW='320px'> <Button {...inc}>+</Button> <Input {...input} /> <Button {...dec}>-</Button> </HStack> )}由于 Headless 组件的抽象程度较高,所需的设计成本也更高,因此并不适用于所有前端场景。其更适合用于开发横跨多个不同业务或跨端的通用组件,开发者需要在开发底层 Headless 组件和开发多套组件中衡量成本、作出抉择。
Reference
https://arco.design/react/docs/start
https://juejin.cn/post/7160223720236122125
https://juejin.cn/post/6844904032700481550
❤️ 谢谢支持
以上便是本次分享的全部内容,希望对你有所帮助 ^_^
喜欢的话别忘了 分享、点赞、收藏 三连哦~。
欢迎关注公众号 ELab 团队 收货大厂一手好文章~
使用下方内推码或内推链接,可投递 字节跳动 - 国际化电商 - S 项目团队 相关岗位
- END -