一个复杂的应用都是由简单的应用发展而来的。随着越来越多的功能加入项目,代码会变得越来越难以控制,本文主要探讨在大型项目中如何对组件进行组织,让项目具备可维护性。
组件设计的基本原则
基本原则
单一职责 这原本来源u面向对象编程,规范定义是“一个类应该只有一个发生变化的原因”,说的简单通俗一点就是:一个类只负责一件事情。不管是什么编程范式,只要是模块化的程序设计都使用单一职责原则,在React中,组件就是模块。
单一职责要求将组件限制在一个合适的粒度,这个粒度是比较主观的概念,换句话说’单一’是一个相对的概念。 单一职责并不是追求职责粒度的最小化,粒度最小化是一个极端,可能会导致大量模块,模块离散化也会让项目变得难以管理。单一职责要的是一个适合被复用的粒度
往往一开始我们设计的组件都可能复合多个职责,后来出现了代码重复或者模块边界被打破,我们才会将可重复的代码抽离,随着越来越多的重构和迭代,模块职责可能会越来越趋于单一。
单一职责的收益:
- 降低组件的复杂度,职责单一组件代码量少,容易被理解,可读性高。
- 降低对其他组件的耦合,当变更到来时可以降低对其他功能的影响,不至于牵一发而动全身。
- 提高可复用性,功能越单一可复用性就越高,就比如一些基础组件。
基本技巧
- 如果组件不需要状态,则使用无状态组件。
- 性能比较上执行:无状态函数 > 有状态函数 > Class组件
- 最小化props,不要传递超过要求的props.
- 如果组件内部存在较多条件控制流,这通常意味着要对组件进行抽取。
- 不要过早优化,只要组件在当前需求下可被复用,然后随机应变。
组件分类
组件主要可分为:容器组件和展示组件
容器组件和展示组件分离是React开发的重要思想,它影响的React应用项目组织和架构总结如下;
容器组件 | 展示组件 | |
---|---|---|
关注点 | 业务 | UI |
数据源 | 状态管理器/后端 | Props |
组件形式 | 高阶组件 | 普通组件 |
- 容器组件主要关注业务处理,一般以高阶组件的形式存在。通常从外部数据源(redux这些状态管理器或者直接请求服务端数据)获取数据,然后 组合展示组件来构建完整的视图。
- 展示组件是一个只关注展示的‘元件’,为了可以在多个地方被复用,它不应该耦合‘业务/功能’,或者说不应该过度耦合。对于展示组件,我们要以一种’第三方组件库’的标准来考虑组件的设计,减少业务的耦合度,考虑各种应用场景,设计好公开的接口。
容器组件和展示组件两者未必是简单的包含和被包含的关系。
按照不同的角度又可以把组件分为:有状态组件和无状态组件,纯组件和非纯组件
类别 | 特征 | 是否是纯组件 |
---|---|---|
有状态组件 | 内部存储状态 | 不一定 |
无状态状态组件 | 内部不存储状态,完全由外部的props来映射,以函数形式存在 | 是 |
纯组件的 ’纯‘来源于函数式编程,指的是对于一个函数而言,给定相同的输入,它总是返回相同的输出,过程没有副作用,没有额外的状态依赖。对应到React中,纯组件指的是props(严格来说还有state和context,它们也是组件的输入)没有变化,组件的输出就不会变动。
和React组件的输入输出模型相比,CycleJS对组件输入/输出的抽象则做的更加彻底,更加函数式。它的组件就是一个普通的函数,只有单向的输入和输出
函数式编程和组件式编程思想某舟意义上是一致的,它们都是’组合‘的艺术,一个大的函数可以有多个职责单一函数组合而成。组件也是如此。我们将一个大的组件拆分为子组件,对组件做更细粒度的控制,保持它们的纯净性,让它们的职责更加单一,更加独立,这带来的好处就是可以复用性,可测性和可预测性。
我们可以很容易地保证一个底层组件的纯净性,因为它本来就很简单。但是对于一个复杂的组件树,则需要花心思进行构建,所以就有了’状态管理‘的需求。这些状态管理器通常都在组件树的外部维护一个或者多个状态库,然后通过依赖注入形式,将局部的状态注入到子树中,通过视图和逻辑分离的原则,来维持组件树的纯净性。
Redux就是一个典型的解决方案,在Redux的世界里可以认为一个复杂的组件树就是一颗状态树的映射,只要状态树不变,组件树就不变,Redux建议保持组件的纯净性,将组件状态交给Redux和配套的异步处理工具来维护,这样就将整个应用抽象成一个"单向数据流",这是一种简单的“输入/输出”关系。
不管是Cyclejs还是Redux,抽象是需要付出一点代价的,就比如redux代码可能就会很啰嗦,一个复杂的状态树,如果缺乏良好的组织,整个应用就会变得很难理解。
示例
永远不要害怕将组件拆分成更小的组件,让一个个可重用组件在大型应用程序中交付使用的过程中,抽离组件期初可能看起来像又脏又累的活儿,所以有一个好的经验法则:如果UI的一部分被使用了好几次(按钮,面板,头像),或者内部比较复杂的东西(app,FeedStory,评论),一个重用的组件对它阿狸说可以达到最大的发挥空间。
原码
import React from 'react';
import ReactDOM from 'react-dom';
function formatDate(date) {
return date.toISOString();
}
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<img className="avatar"
src={props.author.avatarUrl}
alt={props.author.name}
/>
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
ReactDOM.render(
<Comment author={{
avatarUrl: 'https://ss0.bdstatic.com/7Ls0a8Sm1A5BphGlnYG/sys/portrait/item/3ae1dc06.jpg',
name: 'zhangyatao'
}} text={'我的名字叫张亚涛'} date={new Date()}/>,
document.getElementById('root')
);
它接受 author,text和date作为props,用来描述社交媒体网站上的评论。
抽离
首先我们将提取avatar
function Avatar(props){
return(
<img className="Avatar"
src={props.user.avatarUrl}
alt={props.user.name}
/>
);
}
avatar不需要知道它在Comment中呈现。这就是为什么我们给它的prop一个更通用的名称:user而不是author。
我们建议从组件自己的角度命名props,而不是使用它的上下文。
我们现在可以对Commnet组件做一点简化
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<Avatar user={props.author} />
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
接下来,我们将提取一个userInfo组件,该组件在用户名称旁边呈现一个avatar:
function UserInfo(props) {
return (
<div className="UserInfo">
<avatar uer={props.user} />
<div className="UserInfo-name">
{props.user.name}
</div>
</div>
);
}
这使我们可以进一步简化Comment组件
function Comment(props) {
return (
<div className="Comment">
<UserInfo user={props.author} />
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
最终代码如下:
import React from 'react';
import ReactDOM from 'react-dom';
function formatDate(date) {
return date.toISOString();
}
function Avatar(props) {
return (
<img className="Avatar"
src={props.user.avatarUrl}
alt={props.user.name}
/>
);
}
function UserInfo(props) {
return (
<div className="UserInfo">
<Avatar user={props.user}/>
<div className="UserInfo-name">
{props.user.name}
</div>
</div>
);
}
function Comment(props) {
return (
<div className="Comment">
<UserInfo user={props.author}/>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
ReactDOM.render(
<Comment author={{
avatarUrl: 'https://ss0.bdstatic.com/7Ls0a8Sm1A5BphGlnYG/sys/portrait/item/3ae1dc06.jpg',
name: 'zhangyatao'
}} text={'我的名字叫张亚涛'} date={new Date()}/>,
document.getElementById('root')
);