对于封装react组件的一些思考

5,858 阅读10分钟

由于近期在涉及到封装组件的时候遇到了一些问题,于是我认真地了解了一下react封装组件过程中应该要涉及和思考到的一些问题,写了下来。(以下主要是针对UI组件,由于水平有限不保证内容正确性,仅仅是一些个人的思考)

一、什么是组件

组件可以将UI切分成一些的独立的、可复用的部件,这样就只需专注于构建每一个单独的部件。

所谓组件,即封装起来的具有独立功能的UI部件。

在 React 中,一切皆是组件,因此理解组件的工作流与核心尤为重要。

且react中有多种创建组件的方式和各种各样的组件概念,因此在设计组件的时候应该使用哪种组件的创建方式且应该设计一个怎样的组件都值得深入思考。

那么在React里面一个组件应该有什么特征呢?在react中认为组件应该具有如下特征:

  1. 可组合(Composeable):一个组件易于和其它组件一起使用,或者嵌套在另一个组件内部。如果一个组件内部创建了另一个组件,那么说父组件拥有(own)它创建的子组件,通过这个特性,一个复杂的UI可以拆分成多个简单的UI组件;
  2. 可重用(Reusable):每个组件都是具有独立功能的,它可以被使用在多个UI场景;
  3. 可维护(Maintainable):每个小的组件仅仅包含自身的逻辑,更容易被理解和维护;

二、一个设计良好的组件应该有什么特性?

(一)高内聚、低耦合

我们经常谈一个设计良好的系统应该是高内聚低耦合的,那么其实我认为一个好的组件也应该是具有高内聚低耦合的特性。

那么我们应该要怎么去做到使一个组件实现高内聚低耦合的特点呢?

  1. 高内聚:将逻辑紧密相关的内容放在一个组件内。
    React可以将展示内容的JSX、定义行为的JavaScript代码、甚至定义样式的css,
    都可以放在一个JavaScript文件中,因此React天生具有高内聚的特点。
  2. 低耦合:不同组件之间的依赖关系要尽量弱化。
    也就是每个组件要尽量独立,
    一个组件不应该掌握着其他组件的细节,
    而是要尽量做到对其他组件了解很少,甚至是一无所知。

为什么需要实现低耦合呢?

因为低耦合会带来以下的好处:

  1. 在系统中的局部改变不会影响到其他地方
  2. 任何组件都可以被替代品取代
  3. 系统之间的组件可以复用
  4. 可以轻易测试独立的组件,提高了应用的测试代码覆盖率

而高耦合的组件间会很容易出现一个问题,
就是无法或者很艰难去修改一个大量依赖其他组件的组件,
甚至只是改一个用来传递数据的字段都会导致大量的修改。

(二)隐藏内部结构

一个封装良好的组件应该是要隐藏其内部结构的,并通过一组 props来提供控制其行为的途径。

隐藏内部结构是必须的。内部结构或实现细节不应该能被其他组件知道或关联。

React 组件可以是函数式的,也可以是基于类的,可以定义实例方法、设置 refs、维护 state或是使用生命周期方法。而这些实现细节被封装在组件自身中,其他组件不应该窥见其中的任何细节。

基于此特点来设计的组件对其他组件的依赖是极低的,带来的是低耦合的特点和好处。

(三)职责单一

我认为组件应该要符合单一职责原则,
一个组件应该尽量只负责一件事情,并且把这件事情做好,
因为我觉得一个组件如果负责处理的事情过多,
在修改其中一件事情的时候很有可能也会影响到它负责的其他事情,且不利于维护和复用。

三、在封装一个组件的时候应该先思考什么?

  1. 这个组件应该是做什么的
  2. 这个组件应该至少需要知道那些信息
  3. 这个组件会反馈什么东西

在设计一个组件的时候我们不应该仅限于实现当前的需求,
设计出一个只适用于单一项目的组件,而是应该是一个可以适应大部分同种需求的通用组件。
所以我们在碰到一个需求的时候应该首先对需求进行抽象,而不是看到设计稿就撸着袖子上。

例如碰到一个轮播图组件的需求的时候,我们拆分以下这个需求,可以得到:

(1) 这个组件要做什么:

  1. 可以展示多张图片
  2. 可以向左向右翻页,或者是可以是上下翻页
  3. PageControl的状态会根据图片的滚动而相应改变 还有可能有一些隐藏的需求,类似于:
  4. 应该支持左右两侧或者上下无限循环滚动
  5. 可以选择的是否自动轮播
  6. 支持手动滑动切换图片
  7. 图片有点击事件,可以点击来进行相关的事件反应

(2)这个组件至少应该知道什么信息

一个好的组件应该是要像存在魔法一样,只需要极其少数的参数和条件就可以得到期望的效果。就像这个轮播图组件一样,组件应该至少知道的信息有:

  1. 图片的url地址数组
  2. 当图片不存在时候的占位图

其他可以知道也可以不知道的信息可以有:

  1. 是否开启自动轮播,默认是开启或者不开启

  2. 图片滚动是左右还是上下,默认是左右

    等等 ....................................

(3)这个组件会反馈什么

一个可用的轮播图效果

四、组件的通信

父组件向封装好的子组件通信通常是通过props

作为组件的输入,props的值应该最好是js基本类型 (如 string、number、boolean)
但是props可以传入的不仅仅只是这些,它可是一个神奇的东西,它可以传入包括:

  1. js基本类型(如 string、number、boolean)
<Message text="Hello world!" modal={false} />;  
  1. 对象
<Message
  data={{ 
  thexAxis:  thexAxis ,     
  lineData : lineData
   }} 
  />
  1. 数组
<MoviesList items={['Batman Begins', 'Blade Runner']} />  
  1. 作为事件处理和异步操作时,可以指定为函数:
<Message type="text" onChange={handleChange} />  
  1. prop 甚至可以是一个组件构造器。组件可被用来处理其他组件的实例化:
function If({ Component, condition }) {  
 return condition ? <Component /> : null;
  }
<If condition={false} component={LazyComponent} />  

为避免破坏封装,要谨慎对待 props 传递的细节。
父组件对子组件设置 props 时,也不应该暴露自身的结构。
比如,把整个组件实例或 refs 当成 props 传递之类的神奇操作。

访问全局变量是另一个对封装造成负面影响的问题。

我们可以通过 proptypes来对传入的数据进行类型限制。

五、react中创建组件的方法

react创建组件有三种方法,分别是:

  1. function式无状态组件
  2. es5方式React.createClass组件
  3. es6方式extends React.Component

而目前react推荐ES5方式和ES6方式创建组件的写法中推荐的是ES6的写法,所以这里就不对ES5的写法进行讨论了。

React.Component

React.Component是以ES6的形式来创建React组件,也是现在React官方推荐的创建组件的方式,
其和React.createClass创建的组件一样,也是创建有状态的组件。

相比React.createClass方式,React.Component带来了诸多语法上的改进

1.import

ES6使用import方式替代ES5的require方式来导入模块,其中import { }可以直接从模块中导入变量名,此种写法更加简洁直观。

2.初始化 state

在ES6的语法规则中,React的组件使用的类继承的方式来实现,去掉了ES5的getInitialState的hook函数,state的初始化则放在constructor构造函数中声明。

引申内容:

如何正确定义State

React把组件看成一个状态机。通过与用户的交互,实现不同状态,然后渲染UI,让用户界面和数据保持一致。 组件的任何UI改变,都可以从State的变化中反映出来; State中的所有状态都用于反映UI的变化,不应有多余状态。

那么什么样的变量应该做为组件的State呢:

  1. 可以通过props从父组件中获取的变量不应该做为组件State。
  2. 这个变量如果在组件的整个生命周期中都保持不变就不应该作为组件State。
  3. 通过其他状态(State)或者属性(Props)计算得到的变量不应该作为组件State。
  4. 没有在组件的render方法中使用的变量不用于UI的渲染,那么这个变量不应该作为组件的State。这种情况下,这个变量更适合定义为组件的一个普通属性。

function和class创建组件的区别

React内部是通过调用组件的定义来获取被渲染的节点,而对于不同的组件定义方式,其获取节点的步骤也不一样。如下:

//function方式定义
function Example() {
  return <div>this is a div</div>;
}

const node = Example(props);

// 类方式定义
class Example extends React.Component {
  render() {
    return <div>this is a div</div>;
  }
}

const instance = new Example(props);
const node = instance.render();

在这里,函数直接调用,类则需要先实例化再去调用实例化对象上的render方法;

如果将类按照普通函数去调用则会报错

六、Component 和 PureComponent

因为这方面没有详细去了解过,所以也只是粗浅总结一下其区别:

PureComponent除了提供了一个具有浅比较的shouldComponentUpdate方法,
PureComponent和Component基本上完全相同。
当组件更新时,如果组件的 props 和 state 都没发生改变, render 方法就不会触发,省去 Virtual DOM 的生成和比对过程,达到提升性能的目的。

如果我们想用PureComponent去代替Component的时候不需要去做太多的事情,
仅仅是把Component改成PureComponent即可。 但是我们并非可以在所有地方都用PureComponent去代替Component,
具体还是要按照实际情况来选择,因为了解不深就不在此处详谈了。

七、有状态组件和无状态组件

无状态组件更多的是用来定义模板,接收来自父组件props传递过来的数据,
使用{props.xxx}的表达式把props塞到模板里面。
无状态组件应该保持模板的纯粹性,以便于组件复用,所以通常UI组件应该是无状态组件。
类似于:

    var Header = (props) = (
        <div>{props.xxx}</div>
   );

而有状态组件通常是用来处理定义交互逻辑和业务数据
(使用{this.state.xxx}的表达式把业务数据挂载到容器组件的实例上(有状态组件也可以叫做容器组件,无状态组件也可以叫做展示组件),
然后传递props到展示组件,展示组件接收到props,把props塞到模板里面。
类似于:

class Home extends React.Component {
  constructor(props) {
      super(props);
      };
   render() {
      return (
         <Header/> 
      )
   }
}

八、高阶组件

高阶组件给我的感觉类似于高阶函数,都是接受一个东西的输入,
然后再给输入的东西添加新的特性作为一个新的东西输出,
看起来类似于装饰器模式的实现。
但是因为目前为止没有写过高阶组件,所以就不在这里讨论了。