靠谱的React组件设计准则

编写React组件时, 我经常问自己几个问题 :

  • 如何减少使用者在使用过程中不必要的思考顾虑 , 方便快捷的达到使用者的需求和目的?
  • 如何降低与其他组件组合带来的复杂度?
  • 如何让其他开发者积极参与进来, 进行可持续扩展维护?

组件

简介

简单来说就是把页面想象成乐高玩具,需要不同零件组装,然后将各个部分拼到一起 落实到实际应用开发中像这样

一个组件一个目录, 组件可组合相互依赖 , 组件所需的各种资源都在这个目录下就近维护

组件允许你将 UI 拆分为独立可复用的代码片段,并对每个片段进行独立构思。

设计原则

我们需要具有组件化设计思维,它是一种【整理术】帮助我们高效开发整合:

  1. 单一职责

单一职责可以保证组件是最细的粒度,且有利于复用。但太细的粒度有时又会造成组件的碎片化。 因此单一职责组件要建立在可复用的基础上,对于不可复用的单一职责组件,我们仅仅作为独立组件的内部组件即可。

  1. 通用性

组件开发要服务于业务,为了更好的复用,又要从业务中抽离。

  1. 封装

良好的组件封装应该隐藏内部细节和实现意义,并通过props来控制行为和输出。 减少访问全局变量:因为它们打破了封装,创造了不可预测的行为,并且使测试变得困难。可以将全局变量作为组件的props,而不是直接引用。

  1. 组合

具有多个功能的组件,应该转换为多个小组件。 单一责任原则描述了如何将需求拆分为组件,封装描述了如何组织这些组件,组合描述了如何将整个系统粘合在一起。

  1. 可测试

测试不仅仅是自动检测错误,更是检测组件的逻辑。 如果一个组件不易于测试,很大可能是你的组件设计存在问题。

  1. 富有意义

开发人员大部分时间都在阅读和理解代码,而不是实际编写代码。 有意义的函数、变量命名、注释可以让代码具有良好的可读性。

职能分类

组件命名

个人觉得好的命名规范, 能成倍增加使用者的好感, 相反, 晦涩的命名总是让人望而却步

帕斯卡命名法 组件名是由一个或多个帕斯卡单词(主要是名词)串联起来的,比如:<DatePicker><GridItem><SearchList>语义专业化 有意义的名称足以使代码可读, 并且减少了很多不必要的注释

tsx 复制代码
// 一些好的命名,专业易懂,看到就知道用处

// 视频展示面板
<VideoPannel>  
  
// 视频上方覆盖层
<VideoLayer> 
  
// 实现内容可缩放的表格  
<ResizeTable> 


// 一些不好的命名

// 看名字都不知道是什么 大概能猜出来是Tabs、Button、Icon
<Zabs> 
<Zutton>
<Zcon>  
  
// 本身就是通用表单操作栏组件, 改为 FooterToolBar 会更合理
<CommonFormFooterToolBar>  
  
// 名称太过冗长,可读性差, 使用者望而却步  
<TableTextoverflowNofixedNoscroll> 
  
// 通用组件应避免拥有耦合业务过深的组件名或属性名

// 这里是学员选择器,如果要提取该组件作为通用组件,可能业务上会出现如教师、工程师其他职业的人员,改为PersonSeletor更合理
 <StudentSeletor> 

// bad
 <AutoTable
   fetch={}
   isRefresh={}
   leftBtn={}
   rightBtn={}
 />
     
// good
<AutoTable
   request={}
	 refresh={}
   extra={}
/>

封装组合

封装也就是 松耦合, 是我们设计应用结构和组件之间关系的目标。 组合是一种通过将各组件联合在一起以创建更大组件的方式。组合是 React 的核心。

单一责任

组合的一个重要方面在于能够从特定的小组件组成复杂组件的能力。这种分而治之的方式帮助了被组合而成的复杂组件也能符合 SRP 原则。 下面举两个开发过程中的例子:

权限按钮组件目录:

  • AuthButton: 直接对于全局的权限按钮进行状态控制
  • Authorized: 利用render props特性抽象权限点的表现形式, 可能是菜单、链接或具体内容模块
  • useAuthorized: 抽象实现权限业务逻辑

重型Table组件目录:

  • AutoTable: 辅助代码分离、做为内部通信中转站
  • CommonTable: 封装全局定制的表格样式、基本渲染内容和形式
  • useTable: 抽象实现表格的自动请求、搜索、分页、刷新、重置、筛选、排序等业务逻辑
  • SearchForm: Json配置化管理表格搜索表单, 可作为独立组件的内部组件, 也可以抽离为通用搜索表单组件

可复用

组合有可复用的点,使用组合的组件可以重用公共逻辑。 例如,组件 <Comp1><Comp2> 有一些公共代码:

tsx 复制代码
const instance1 = (
    <Comp1>
    /* Common code... */
    /* Specific to Comp1 code... */
    </Composed1>
);
const instance2 = (
    <Comp2>
    /* Common code... */
    /* Specific to Comp2 code... */
    </Comp2>
);

将共同代码封装抽离到一个新组件中, 然后进行组合

tsx 复制代码
const instance1 = (
    <Comp1>
       <Common />
       <Piece1 />
    </Composed1>
);
const instance2 = (
    <Comp2>
        <Common />
        <Piece2 />
    </Comp2>
);

灵活高效

一个组合式的组件通过给子组件传递 props的方式,来控制其子组件。这就带来了灵活性的好处。 例如,有一个组件,它需要根据用户的设备显示信息,使用组合可以灵活地实现这个需求:

tsx 复制代码
function ByDevice({ children: { mobile, other } }) {
    return Utils.isMobile() ? mobile : other;
}

<ByDevice>{{
    mobile: <div>Mobile detected!</div>,
    other: <div>Not a mobile device</div>
}}</ByDevice>

<ByDevice> 组合组件,对于移动设备,显示: Mobile detected!; 对于非移动设备,显示 Not a mobile device"。

常见抽象逻辑技巧

HOC 高阶组件通过包裹(wrapped)被传入的React组件,经过一系列处理,最终返回一个相对增强(enhanced)的React组件(注意:高阶组件的本质是函数,不是组件, 属于设计模式的装饰器Decorator模式) const HOC = Component => EnhancedComponent 使用方式:

1.属性代理,代理传递给被包装组件的 props, 对 props 进行操作

tsx 复制代码
import React, { Component } from 'react';

export default WrappedComponent => {
  return class extends Component {
    moveRef = React.createRef();

    dragDown = e => {
     ...
    };

    render() {
      return (
        <div
          ref={this.moveRef}
          onMouseDown={e => this.dragDown(e)}
          style={{
            position: 'fixed',
            left: 0,
            top: 0,
            cursor: 'move',
            zIndex: 999,
          }}
        >
          <WrappedComponent {...this.props} />
        </div>
      );
    }
  };
};

@withDrag
class TeamCard extends React.PureComponent {}
// 或者
export default withDrag(TeamCard);

2.反向继承(高阶组件继承被包装的组件)

tsx 复制代码
const withWebSocket = WrappedComponent => {
  return class extends WrappedComponent {
    constructor(props) {
      super(props);
    }

    componentDidMount() {
      this.connection();
    }

    componentWillUnmount() {
      this.disconnect();
    }

    // 建立连接
    connection() {}

    // 断开连接
    disconnect() {    }

    render() {
      return super.render();
    }
  };
};

@withWebSocket
class CommandCenterPage extends React.Component {}

react-router的withRouter(),redux的connect()都是HOC组件 render props 个人觉得是很有意思的一种编码方式, 但是比较冷门 ,通常用于父子组件解藕的同时,又保持着通信

tsx 复制代码
const List = props => <>
    {props.children(props)}
</>

<List>
    {() => <Card />}
</List>

<List>
    {() => <Pannel />}
</List>

自定义hooks 取代高阶组件 react函数式编程的大改革

tsx 复制代码
// 抽象保留状态的前一个值的逻辑

const usePrevious = value => {
  const ref = useRef();

  useEffect(() => {
    ref.current = value;
  }, [value]);

  return ref.current;
};

react-reduxuseSelector()useDispatch()就是抽象状态管理的hooks钩子

**设计不当导致的一些常见问题 : ** 环形依赖 组件间耦合度高,集成测试难 一处修改,处处影响,交付周期长 因为组件之间存在循环依赖,变成了"先有鸡还是先有蛋"的问题 消除环形依赖 , 创建一个共同依赖的新组件

复用第三方库

某个工作日,你刚刚收到了为应用增加新特性的任务,在撩起袖子狂敲代码之前,先稍等几分钟。 你要做的工作在很大概率上已经被解决了。由于 React 非常流行以及其非常棒的开源社区,先搜索一下是否有已存在的解决方案是明智之举 个人是反对重复造轮子的, 一些成熟的第三方库, 可以更高效和高质量的完成部分开发工作 , 提高工作效率, 站在巨人的肩膀上🤤

最近随着项目业务需求疯狂增长, 之前封装的通用组件迎来了很多改变, 由于项目前端架构采用了微前端方案,导致了很多子应用都需要引用重复的通用组件, 这个时候我是通过将组件发布到npm上进行管理的, 但是面临几个问题:

  1. 我开发的组件和发布流程通常是我一个人来维护的
  2. 没有类似于antd组件库的组件api介绍、以及丰富的demo展示

这样一来会让增加了其他开发者的开发成本, 甚至导致对于组件理解偏差导致错误使用造成功能问题, 二来也降低了大家的积极性,可能有的开发同学就自行去重写一套了, 不利于协作

找了一下 , 发现了一个干货 Dumi , 可以快速实现组件库+文档的工具 , 真香~

结尾

最后总结一句话 , 为开发者而设计 组件设计与产品设计思想如出一辙, 致力于提升开发者或用户的使用体验, 因此,要为你未来的用户设计,在一个月内为自己设计,为那些在你离开后必须维护你代码的可怜兄 dei 设计,为开发者设计!

相关资料摘要

react是一门哲学

相关推荐
Captaincc3 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师4 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen5 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒5 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow5 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林8186 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang800786 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明6 小时前
第 6 章 组件进阶
前端·vue.js
江华森6 小时前
HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析
前端
南青6 小时前
Vue 3 中后台实战:我踩过的 10 个坑和最佳实践
前端