编写React组件时, 我经常问自己几个问题 :
- 如何减少使用者在使用过程中不必要的思考顾虑 , 方便快捷的达到使用者的需求和目的?
- 如何降低与其他组件组合带来的复杂度?
- 如何让其他开发者积极参与进来, 进行可持续扩展维护?
组件
简介
简单来说就是把页面想象成乐高玩具,需要不同零件组装,然后将各个部分拼到一起 落实到实际应用开发中像这样
一个组件一个目录, 组件可组合相互依赖 , 组件所需的各种资源都在这个目录下就近维护
组件允许你将 UI 拆分为独立可复用的代码片段,并对每个片段进行独立构思。
设计原则
我们需要具有组件化设计思维,它是一种【整理术】帮助我们高效开发整合:
- 单一职责
单一职责可以保证组件是最细的粒度,且有利于复用。但太细的粒度有时又会造成组件的碎片化。 因此单一职责组件要建立在可复用的基础上,对于不可复用的单一职责组件,我们仅仅作为独立组件的内部组件即可。
- 通用性
组件开发要服务于业务,为了更好的复用,又要从业务中抽离。
- 封装
良好的组件封装应该隐藏内部细节和实现意义,并通过props来控制行为和输出。 减少访问全局变量:因为它们打破了封装,创造了不可预测的行为,并且使测试变得困难。可以将全局变量作为组件的props,而不是直接引用。
- 组合
具有多个功能的组件,应该转换为多个小组件。 单一责任原则描述了如何将需求拆分为组件,封装描述了如何组织这些组件,组合描述了如何将整个系统粘合在一起。
- 可测试
测试不仅仅是自动检测错误,更是检测组件的逻辑。 如果一个组件不易于测试,很大可能是你的组件设计存在问题。
- 富有意义
开发人员大部分时间都在阅读和理解代码,而不是实际编写代码。 有意义的函数、变量命名、注释可以让代码具有良好的可读性。
职能分类
组件命名
个人觉得好的命名规范, 能成倍增加使用者的好感, 相反, 晦涩的命名总是让人望而却步
帕斯卡命名法 组件名是由一个或多个帕斯卡单词(主要是名词)串联起来的,比如:<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-redux
的useSelector()
、useDispatch()
就是抽象状态管理的hooks钩子
**设计不当导致的一些常见问题 : ** 环形依赖 组件间耦合度高,集成测试难 一处修改,处处影响,交付周期长 因为组件之间存在循环依赖,变成了"先有鸡还是先有蛋"的问题 消除环形依赖 , 创建一个共同依赖的新组件
复用第三方库
某个工作日,你刚刚收到了为应用增加新特性的任务,在撩起袖子狂敲代码之前,先稍等几分钟。 你要做的工作在很大概率上已经被解决了。由于 React 非常流行以及其非常棒的开源社区,先搜索一下是否有已存在的解决方案是明智之举 个人是反对重复造轮子的, 一些成熟的第三方库, 可以更高效和高质量的完成部分开发工作 , 提高工作效率, 站在巨人的肩膀上🤤
最近随着项目业务需求疯狂增长, 之前封装的通用组件迎来了很多改变, 由于项目前端架构采用了微前端方案,导致了很多子应用都需要引用重复的通用组件, 这个时候我是通过将组件发布到npm上进行管理的, 但是面临几个问题:
- 我开发的组件和发布流程通常是我一个人来维护的
- 没有类似于antd组件库的组件api介绍、以及丰富的demo展示
这样一来会让增加了其他开发者的开发成本, 甚至导致对于组件理解偏差导致错误使用造成功能问题, 二来也降低了大家的积极性,可能有的开发同学就自行去重写一套了, 不利于协作
找了一下 , 发现了一个干货 Dumi , 可以快速实现组件库+文档的工具 , 真香~
结尾
最后总结一句话 , 为开发者而设计 组件设计与产品设计思想如出一辙, 致力于提升开发者或用户的使用体验, 因此,要为你未来的用户设计,在一个月内为自己设计,为那些在你离开后必须维护你代码的可怜兄 dei 设计,为开发者设计!