前端组件设计

49 篇文章 3 订阅
16 篇文章 0 订阅
文章探讨了前端组件设计的重要性,提出了如何提升组件的易用性和可扩展性的方法。合理的组件封装、规范的API编写以及插槽的使用是提高易用性的关键。同时,采用开闭原则和设计可扩展的API能增强组件的灵活性。HeadlessUI作为一种极端的扩展性实现,关注组件的状态和交互,解耦UI展示层,适合跨业务或跨端的通用组件开发。
摘要由CSDN通过智能技术生成

以下文章来源于ELab团队 ,作者ELab.yangyuqin

为何要进行前端组件设计?

与仅承担数据处理逻辑的后端不同,前端需要负责界面渲染、数据处理、和接口调用,在框架诞生前,更多地是编写页面维度的顺序脚本代码。随着前端继续的持续发展,ES6推出了class语法糖,React提出了函数式组件,Vue则以模版语法的形式组织代码,前端代码逐渐从“平铺”转变到了“层级”结构,从“面向过程”进阶为“面向对象”,前端组件也成为了近几年来的热门议题。

“组件是对数据和方法的简单封装,是软件中具有相对独立功能、接口由契约指定、和语境有明显依赖关系、可独立部署、可组装的软件实体。”这段百科中摘取的组件定义,揭示了组件所需要具备的特性:功能独立、约定一致、可集成、服务于场景。

在软件工程中,软件设计是软件开发流程中的必要阶段,在需求分析后、软件开发前进行。软件复杂度是每一个项目演进的产物,随着需求和代码行数的增加,复杂度将持续提升。软件设计的优劣为对复杂度带来的影响是不同的,优雅、合理的设计使待开发的代码复杂度可控,而拙劣的设计将会给软件带来无序、偶然的复杂度变更。一个优秀的前端组件需要在满足需求的前提下,具备高易用性和良好的可扩展性,这是我们进行前端组件设计的目标。

💡如何提升组件易用性?

合理的组件封装

组件既生于页面,又能够独立于页面。我们不能将整个页面杂糅为一个组件,也不能将每一小块UI都封装为组件。前端组件按类型可以分为容器组件、功能组件和展示组件,一个优秀的组件应该保证:功能内聚、样式统一、并且与父元素仅通过Props通信。

组件的封装粒度并不是越小越好,很多时候一个组件是在其他一个或多个组件的基础上开发的,无法完全以功能点的数量衡量是否遵循单一职责原则,组件开发者需要根据组件功能和目标来确定组件封装粒度:

  1. 当该组件需要承载具体的额外功能时,相较于新增 API ,封装成独立的组件是更好的选择。
    🌰:InputTag组件 在 Input、Tag 的基础上,增加了部分交互功能,API整合了两个组件的属性,作为一个全新的组件提供给开发者使用。相似的,InputNumber、AutoComplete、Mentions等组件也是基于单一职责原则封装的特定功能组件。
    在这里插入图片描述
  2. 当组件中存在可能被单独使用、可以承载独立功能的子组件时,可以将其以内部组件的形式提供。
    🌰:图片预览功能通常依托着图片组件使用,在实际系统中,唤起图片预览的触发器不一定是图片,可能是按钮或其他触发事件,因此预览组件需要单独提供给开发者使用。预览组件作为 Image 的内部组件,开发者能够以Image.Preview、Image.PreviewGroup的方式使用,并提供左右切换、图片缩放等功能,用户可以通过 srcList、visible、actions、scales等API来控制并定制化预览组件。
    在这里插入图片描述
    在这里插入图片描述

规范的API编写

一个易用的组件,使用者无需阅读文档或仅快速浏览文档即可上手使用,并且应当在使用过程中给予清晰的注释和代码提示。希望以下API编写建议能够给组件开发者一些参考:

  1. 减少必填的API项,尽可能多地提供默认值,降低组件的使用成本;
  2. 使用通用且有意义的API命名:
    • onXXX:命名监听/触发方法
    • renderXXX:命名渲染方法
    • beforeXXX/afterXXX:命名前置/后置动作
    • xxxProps:命名子组件属性
    • 优先使用常见单词进行命名,如:value、visible、size、disabled、label、type等等
  3. 单独维护类型文件,并将其打包至组件产物包中,这样使用者在开发过程中能够实时看到对应的类型提示;
  4. 在类型文件中,为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;

可以看到,这个业务卡片组件是由多个不同组件组合而成,其承载了渲染和操作(选中操作、图片切换和弹层操作),这个设计的缺陷是显而易见的:

  1. 需要编写很多分散的JSX代码,无论是写在Props中还是定义成单独的组件,其可读性都不高;
  2. 需要在Card组件中杂糅许多额外的Props,例如triggerProps和descriptionsProps,增加了该组件的学习成本;

如果以插槽的方式对Card组件进行改造,通过内部组件间的组合来实现需求,避免了大量组件Props的堆砌,层次清晰、可读性高,这样的组件结构明显易用性更高。

<Card type="verticle" {...cardProps}>
  <CardImage {...ImageProps} />
  <CardContent {...InfoProps} >
      <div className="card-title">title</div>
      <div className="card-content">content</div>
      <Tag>Tag</Tag>
  </CardContent>
  <CardTrigger {...triggerProps}>
      <Description {...descriptionProps}/>
  </CardTrigger>
</Card>
什么是插槽(Slot)?

在这里插入图片描述
Slot是Vue框架提出的概念,可以理解为临时占位,可以用其他组件进行填充,Slot能够实现父组件向子组件分发内容的功能。Vue框架中提供了

  1. 使用 props.children 获取子组件,若需要区分使用不同子组件,只能通过数组下标读取。
  2. 使用 Props 传递 ReactNode 元素。
  3. 将组件划分为多个内部组件,交由开发者自行组装。
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

目录 1. 介绍 5 1.1 项目概述 5 1.2 范围 5 1.3 参考 5 2. 用例视图 6 2.1 WAS - SAP R/3 集成用例 6 2.1.1 车辆列表功能 6 2.1.2 车辆订购申请单的创建功能 7 2.1.3 车辆订购申请单查询功能 7 2.1.4 车辆订购申请单的修改功能 7 2.1.5 索赔单的创建 8 2.1.6 数据交换需求 8 2.2 PORTAL集成的用例 8 2.2.1 经销商 Portal 框架 9 2.2.2 车辆销售系统和Portal的整合 9 2.2.3 Nadcon system 和Portal系统的整合 10 2.2.4 车辆销售系统和Nadcon 的整合 10 3. 逻辑视图 10 3.1 兼容性 10 3.2 系统架构 10 3.2.1 逻辑架构 10 3.2.2 Web 应用的包设计 12 3.3 组件设计 - J2EE WEB APPLICATION 13 3.3.1 MVC 框架 – Struts 13 3.3.2 日志 14 3.3.3 BAPI代理结构 15 3.3.4 销售商用户信息组件和安全组件 16 3.3.5 页面表现框架 17 3.3.6 车辆列表功能 18 3.3.7 车辆订购请求单创建 24 3.3.8 车辆订购申请单查询列表 32 3.3.9 车辆订购申 请单修改 37 3.3.10 索赔单创建 43 3.3.11 数据交换 50 3.3.12 登录 & 退出 53 4. 数据视图 56 4.1 车辆列一表 57 4.2 车辆订购申请单创建 58 4.3 车辆订购申请单列表 59 4.4 车辆订购申请单修改 60 4.5 索赔单创建 61 5. 实现视图 62 5.1 缓存策略 62 5.2 会话管理 62 5.3 连接管理 62 5.4 集成的需要 62 5.4.1 WAS – SAP 集成 63 5.4.2 单点登陆 63 5.4.3 Vehicle Sale 系统 和 Nadcon的集成 63 6. 部署视图 64 6.1 安装需求 64 6.1.1 服务器的安装 64 6.2 服务支持的考虑 64 6.2.1 安全 64 6.2.2 服务器管理 64 7. 实现环境视图 64 7.1 开发环境 64 7.2 测试环境 64 7.3 生产环境 65 7.3.1 网络 65 7.4 域信息 65
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值