编写React组件时, 我经常问自己几个问题 :
- 如何减少使用者在使用过程中不必要的思考顾虑 , 方便快捷的达到使用者的需求和目的?
- 如何降低与其他组件组合带来的排斥度?
- 如何让其他开发者积极参与进来, 进行可持续扩展维护?
组件
简介
简单来说就是把页面想象成乐高玩具,需要不同零件组装,然后将各个部分拼到一起
落实到实际应用开发中像这样
一个组件一个目录, 组件可组合相互依赖 , 组件所需的各种资源都在这个目录下就近维护
组件允许你将 UI 拆分为独立可复用的代码片段,并对每个片段进行独立构思。
设计原则
我们需要具有组件化设计思维,它是一种【整理术】帮助我们高效开发整合:
- 单一职责
单一职责可以保证组件是最细的粒度,且有利于复用。但太细的粒度有时又会造成组件的碎片化。
因此单一职责组件要建立在可复用的基础上,对于不可复用的单一职责组件,我们仅仅作为独立组件的内部组件即可。
- 通用性
组件开发要服务于业务,为了更好的复用,又要从业务中抽离。
- 封装
良好的组件封装应该隐藏内部细节和实现意义,并通过props来控制行为和输出。
减少访问全局变量:因为它们打破了封装,创造了不可预测的行为,并且使测试变得困难。可以将全局变量作为组件的props,而不是直接引用。
- 组合
具有多个功能的组件,应该转换为多个小组件。
单一责任原则描述了如何将需求拆分为组件,封装描述了如何组织这些组件,组合描述了如何将整个系统粘合在一起。
- 可测试
测试不仅仅是自动检测错误,更是检测组件的逻辑。
如果一个组件不易于测试,很大可能是你的组件设计存在问题。
- 富有意义
开发人员大部分时间都在阅读和理解代码,而不是实际编写代码。
有意义的函数、变量命名、注释可以让代码具有良好的可读性。
职能分类
组件命名
个人觉得好的命名规范, 能成倍增加使用者的好感, 相反, 晦涩的命名总是让人望而却步
帕斯卡命名法
组件名是由一个或多个帕斯卡单词(主要是名词)串联起来的,比如:<DatePicker>
、<GridItem>
、<SearchList>
语义专业化
有意义的名称足以使代码可读, 并且减少了很多不必要的注释
// 一些好的命名,专业易懂,看到就知道用处// 视频展示面板<VideoPannel>// 视频上方覆盖层<VideoLayer>// 实现内容可缩放的表格 <ResizeTable>// 一些不好的命名// 看名字都不知道是什么 大概能猜出来是Tabs、Button、Icon<Zabs><Zutton><Zcon>// 本身就是通用表单操作栏组件, 改为 FooterToolBar 会更合理<CommonFormFooterToolBar>// 名称太过冗长,可读性差, 使用者望而却步 <TableTextoverflowNofixedNoscroll>// 通用组件应避免拥有耦合业务过深的组件名或属性名// 这里是学员选择器,如果要提取该组件作为通用组件,可能业务上会出现如教师、工程师其他职业的人员,改为PersonSeletor更合理<StudentSeletor>// bad<AutoTablefetch={} isRefresh={} leftBtn={} rightBtn={} />// good<AutoTablerequest={} refresh={} extra={} />
封装组合
封装也就是 松耦合, 是我们设计应用结构和组件之间关系的目标。
组合是一种通过将各组件联合在一起以创建更大组件的方式。组合是 React 的核心。
单一责任
组合的一个重要方面在于能够从特定的小组件组成复杂组件的能力。这种分而治之的方式帮助了被组合而成的复杂组件也能符合 SRP 原则。
下面举两个开发过程中的例子:
权限按钮组件目录
- AuthButton: 直接对于全局的权限按钮进行状态控制
- Authorized: 利用
render props
特性抽象权限点的表现形式, 可能是菜单、链接或具体内容模块 - useAuthorized: 抽象实现权限业务逻辑
重型Table组件目录
- AutoTable: 辅助代码分离、做为内部通信中转站
- CommonTable: 封装全局定制的表格样式、基本渲染内容和形式
- useTable: 抽象实现表格的自动请求、搜索、分页、刷新、重置、筛选、排序等业务逻辑
- SearchForm: Json配置化管理表格搜索表单, 可作为独立组件的内部组件, 也可以抽离为通用搜索表单组件
可复用
组合有可复用的点,使用组合的组件可以重用公共逻辑。
例如,组件<Comp1>
和<Comp2>
有一些公共代码:
constinstance1= ( <Comp1>/* Common code... *//* Specific to Comp1 code... */</Comp1>); constinstance2= ( <Comp2>/* Common code... *//* Specific to Comp2 code... */</Comp2>);
将共同代码封装抽离到一个新组件中, 然后进行组合
constinstance1= ( <Comp1><Common/><Piece1/></Comp1>); constinstance2= ( <Comp2><Common/><Piece2/></Comp2>);
灵活高效
一个组合式的组件通过给子组件传递 props
的方式,来控制其子组件。这就带来了灵活性的好处。
例如,有一个组件,它需要根据用户的设备显示信息,使用组合可以灵活地实现这个需求:
functionByDevice({ children: { mobile, other } }) { returnUtils.isMobile() ?mobile : other; } <ByDevice>{{ mobile: <div>Mobiledetected!</div>,other: <div>Notamobiledevice</div>}}</ByDevice>
ByDevice
组合组件,对于移动设备,显示: Mobile detected!; 对于非移动设备,显示 Not a mobile device"。
常见抽象逻辑技巧
HOC
高阶组件通过包裹(wrapped)被传入的React组件,经过一系列处理,最终返回一个相对增强(enhanced)的React组件(注意:高阶组件的本质是函数,不是组件, 属于设计模式的装饰器Decorator模式)
const HOC = Component => EnhancedComponent
使用方式:
1.属性代理,代理传递给被包装组件的 props, 对 props 进行操作
mportReact, { Component } from'react'; exportdefaultWrappedComponent=> { returnclassextendsComponent { moveRef=React.createRef(); dragDown=e=> { }; render() { return ( <divref={this.moveRef} onMouseDown={e=>this.dragDown(e)} style={{ position: 'fixed', left: 0, top: 0, cursor: 'move', zIndex: 999, }} ><WrappedComponent {this.props} /></div> ); } }; }; @withDragclassTeamCardextendsReact.PureComponent {} // 或者exportdefaultwithDrag(TeamCard);
2.反向继承(高阶组件继承被包装的组件)
constwithWebSocket=WrappedComponent=> { returnclassextendsWrappedComponent { constructor(props) { super(props); } componentDidMount() { this.connection(); } componentWillUnmount() { this.disconnect(); } // 建立连接connection() {} // 断开连接disconnect() { } render() { returnsuper.render(); } }; }; @withWebSocketclassCommandCenterPageextendsReact.Component {}
react-router的withRouter(
),redux的connect()
都是HOC组件
render props
个人觉得是很有意思的一种编码方式, 但是比较冷门 ,通常用于父子组件解藕的同时,又保持着通信
constList=props=><> {props.children(props)} </><List> {() =><Card/>} </List><List> {() =><Pannel/>} </List>
自定义hooks 取代高阶组件
react函数式编程的大改革
// 抽象保留状态的前一个值的逻辑constusePrevious=value=> { constref=useRef(); useEffect(() => { ref.current=value; }, [value]); returnref.current; };
react-redux的useSeletor()
、useDispatch()
就是抽象状态管理的hooks钩子
设计不当导致的一些常见问题 :
环形依赖
组件间耦合度高,集成测试难 一处修改,处处影响,交付周期长 因为组件之间存在循环依赖,变成了“先有鸡还是先有蛋”的问题
消除环形依赖 , 创建一个共同依赖的新组件
复用第三方库
某个工作日,你刚刚收到了为应用增加新特性的任务,在撩起袖子狂敲代码之前,先稍等几分钟
你要做的工作在很大概率上已经被解决了。由于 React 非常流行以及其非常棒的开源社区,先搜索一下是否有已存在的解决方案是明智之举
个人是反对重复造轮子的, 一些成熟的第三方库, 可以更高效和高质量的完成部分开发工作 , 提高工作效率, 站在巨人的肩膀上🤤
如: React Hooks 库ahooks、UI组件库 Ant Design、一些精选React组件库、业务开发组件库 Orca-team
最近随着项目业务需求疯狂增长, 之前封装的通用组件迎来了很多改变, 由于项目前端架构采用了微前端方案,导致了很多子应用都需要引用重复的通用组件, 这个时候我是通过将组件发布到npm上进行管理的, 但是面临几个问题:
- 我开发的组件和发布流程通常是我一个人来维护的
- 没有类似于antd组件库的组件api介绍、以及丰富的demo展示
这样一来会让增加了其他开发者的开发成本, 甚至导致对于组件理解偏差导致错误使用造成功能问题, 二来也降低了大家的积极性,可能有的开发同学就自行去重写一套了, 不利于协作
找了一下 , 发现了一个干货 Dumi , 可以快速实现组件库+文档的工具 , 真香~
结尾
最后总结一句话 , 为开发者而设计
组件设计与产品设计思想如出一辙, 致力于提升开发者或用户的使用体验, 因此,要为你未来的用户设计,在一个月内为自己设计,为那些在你离开后必须维护你代码的可怜兄 dei 设计,为开发者设计!