React作为构建用户界面的前端库(View Library),现在已经成为各大公司的技术栈一员,我们组内也早已着手基于React来构建项目,有幸从零开始开发了一些页面,有了一些自己的想法。当使用React来构建一个web页面的时候,我都在思考什么?
这里仅以一个简单的web page为栗,暂且抛开使用Reat-router或Redux的单页应用不说。

概览"#"

  • 前言:组件之间的通信
  • 抽象state(数据流的清晰)
  • 抽取组件(组件划分结构合理,高可复用性)
  • 实现组件(组件类型?实现细节?规范?极端情况处理)
  • 各司其职(React能与不能)
  • 性能优化(shouldComponentUpdate)
  • 还能做些什么?

注:原则上建立在数据(state)存放在顶层组件管理,由props将数据层层传递到底层组件,这样做的好处是逻辑上利于分析,数据流保持相应的清晰,也能够保证底层子组件最大程度的复用(后面会说到)

前言:组件之间的通信"#"

React中并没有类似Angular中的双向绑定功能,所以在开始之前,让我们先探讨一下React组件之间的通信。

上图中,container作为顶层组件,管理着全局state,然后通过props层层传递给子组件A、B,B组件再通过props将数据传递给它的子组件C、D,子组件A、B、C、D的相关的业务逻辑处理也可以通过this.props.onHandleXXX的方式层层传递到顶层组件执行,既保持了底层组件的灵活性(只关心render),也保证了业务逻辑上的解耦(业务逻辑统一在container中进行处理)。
关于组件之间的通信,总结来说无非就这几种情况,以上图为例:

  • 父组件与子组件之间的通信,通信是一个相互的概念,父组件与子组件通信通过props来完成,子组件与父组件通信通过this.props.onHandleXXX执行父组件的回调来完成通信的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import React from 'react';
    import ComponentB from '/component/xxx/componentB';
    // container component
    class Container extends React.Component {
    constructor(props){
    super(props);
    this.handleXXX = this.handleXXX.bind(this);
    }
    render(){
    return (
    <div className="wraper">
    <ComponentB
    data = {this.props.data}
    onHandleXXX = {this.handleXXX}
    />
    </div>
    );
    },
    handleXXX(){
    console.log('ComponentB communicate with me!');
    }
    }
  • 兄弟组件之间的通信类似ComponentA <==> ComponentC | ComponentC <==> ComponentD这种,可以通过抽象出具体的state放在离他们之上的‘最近’的公共组件来实现(对于A<==>B,这个组件是Container,对于C<==>D,这个组件可以是ComponentB也可以是Container),我的建议是视具体的业务逻辑来看,如果这个抽象的state只有ComponentCComponentD需要,则大可放在ComponentB中进行管理,减少顶层组件不必要的state及由于C|D组件通信可能造成的其他组件(例如ComponentA)不必要的更新。但是对这个state来说,如果ComponentA也需要的话统一放到最顶层Container中反而会更好。

  • 祖孙组件之间的通信,类似Container<==>ComponentC | Container<==>ComponentD,这种跨组件的通信,统一使用props来进行逐级传递,可以类似为父子组件的通信,只不过在祖孙组件之间的组件充当的是‘接力’props的工作,本质上不会对props进行处理。

抽象state"#"

可以将React看作状态机,用户的不同交互行为触发了不同的组件回调,组件的状态也会随着改变。得益于React的state,我们仅仅需要告诉React何时改变state,描述组件输入不同数据的UI表现,React会帮我们更新state,重新渲染视图。

React更新视图的时机可能是用户触发的一个交互行为,也可能是代码主动触发的setState(比如说一个倒计时组件,当倒计时结束时,会通过回调来告知父组件倒计时结束,父组件在进行相应的逻辑处理),可以将这个setState的动作放在发生交互的组件,也可以放在父组件,也可以放在祖组件,具体的存放位置要已具体的业务逻辑或组件拆分策略来进行划分。

应该保证state的最简性,一些可以通过其他state计算出来的数据没有必要放到state中,这样只会增加组件维护的复杂性。举一个简单的栗子:

上图是一个简单的tab组件,切换不同的tab展示不同的内容。这个tab组件的数据是一个列表,看起来可能会是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
var tabList = [
{
tabType: 1,
tabName:'最新动态'
},{
tabType: 2,
tabName:'好友圈'
},{
tabType: 3,
tabName:'特别关注'
}
];
class tabList extends React.Component {
constructor(props){
super(props)
}
handleChangeTab(activeType){
var tabMap = {
'1': '最新动态',
'2': '好友圈',
'3': '特别关注'
}
this.setState({
tabType: activeType,
tabName: tabMap[activeType]
})
}
render(){
<div className="m-tab">
{this.props.tabList.map((item, index) => {
if(item.tabType === this.state.tabType){
return (
<div className={'item active'} onClick={(item.type) => this.handleChangeTab(item.type)}>{this.state.tabName}</div>
);
}
return (
<div className="item" onClick={(item.type) => this.handleChangeTab(item.type)}>{item.tabName}</div>
);
})}
</div>
}
}

这个栗子可能不是很恰当,只是为了表达我的意思,像上面这样,将当前active的tabTypetabName都存在state中就没必要,只需要存其中一个就可以,我们完全可以通过计算获取另一个的值。所以,尽量保证组件state的简洁。

抽取组件"#"

组件组合本质"#"

React中的组件的渲染是以一种递归的方式来进行的,实际上组件的展现形式就是通过层层的嵌套来完成的。

每个组件都有一个包含所有需要渲染组件的Container组件,在React中,可以通过ReactDOM.render来将这个Container组件挂载渲染,剩下的事情,就交给React的递归渲染来帮我们完成。

1
2
3
4
import ReactDOM from 'react-dom';
import Container from '../xx/Container.jsx';
//略
ReactDOM.render(<Container/>, document.getElementById('Container'));

组件分类"#"

看起来组件的拆分是一个比较重要的点,如何拆,拆的力度如何这都需要思考。根据组件的业务通用类型,我将组件划分为3种类型:

  • 通用组件,这类组件是很基础的组件,类似一个输入框,一个下拉选择框等,这类组件没有包含任何业务逻辑,可以在任何有需要的业务逻辑中被复用,仅仅包含简单的UI展示和简单的交互逻辑,而交互逻辑是可以通过回调来传递上层组件处理的。
  • 业务复用组件,这类组件的核心是,只专注与在某一类特定的业务中,所以它需要考量的点是如何更好在某一类型的组件中被复用。
  • 特定业务组件,这类组件包含了特定的业务逻辑,从复用的层面看几乎没法用到其他的业务中,对于这类组件来说,它们关注的点应该是特定的业务如何更好的划分和组织。通常我都会给这类组件取名如xxxWraperContainer等,表达的意思也很清晰,就是一个外包组件|顶层组件。如下图所示

复用"#"

复用,一定程度上指的是高可配置,能够通过传递不同的配置参数涵盖不同的业务场景。
相信每一个工程师都不会把时间浪费在1+1上面,组件的复用一方面能够解放工程师的劳动力,另一方面能够真正的从工程化的角度去看待软件工程问题。
上图中,componentA被复用在不同地方,这主要得益于以下几点:

  • componentA不包含业务代码,或者只包含通用的业务代码(对于业务复用组件来说),它们仅仅负责接受数据,然后return View
  • 用户的交互都可以通过this.props.onHandleXXX来交给不同的Wraper来处理,真正做到剥离‘数据’和‘行为’。

拿到一个新的页面,我们可以先根据页面的组成结构将组件进行初步划分,例如Header|Body|sideContent|Footer,接着们对每一个结构组件中的组件再次细分。这个时候需要思考:

  • 思考哪些组件是可以直接复用的:HeaderFooter组件是可以复用的,无须重复编写,我们只需要查看这些组件编写者的注释,按照要求传递相应的props即可,Body中可能还有一个分页功能,这个功能也是已有的组件,可以直接使用,但要注意组件的配置参数。
  • 思考哪些组件是可能被复用的:Body里面可能包含Item,这个Item可能会在其他页面中使用到,所以要在编写Item的时候注意尽量写成通用的,尽可能兼容可能出现的场景(通过defaultProps来处理)
  • 思考哪些组件是wraper组件,需要包含特定的业务逻辑,如何清晰有效的规划这些逻辑。

实现组件"#"

当将业务理顺了之后,尽量保持简洁的情况下抽象出整个page需要的state,根据组件的类型将组件进行划分,一些组件的划分是否合理性,可能要等到开发的时候才能意识到,不过这是正常的情况,随着业务的变更,势必会对现有的组件重新划分,所以在开发前势必要保证一定的‘前瞻性’。

组件类型"#"

React组件按照有无state可以分为以下几类:

  • Functional-Component (函数组件,无状态,无生命周期函数)
  • Class-Component (使用React.createClssAPI或者继承自React.Component的类,可以有state和生命周期函数)

但是按照React官方说法,未来将会考虑为Functional-Component引入state,这样以来,组件的分类可能要变为下面这样:

  • Functional-Component
  • stateLess-Functional-Component
  • stateLess-Component
  • Class Component
    所以到底使用Functional-Component 还是class xxx extends React.Component创建的component,是我们需要考虑的。
    函数组件没有周期函数和state,看起来十分简洁,一些存文案展示的组件或者一些中间件组件(例如一个itemList组件,仅仅负责遍历渲染item,没有其他逻辑,但是一些情况下我们需要使用生命周期函数的情况下则是不得不使用传统的Component,比如需要在在列表DOM渲染完毕后,对相关的DOM进行操作,就需要使用componentDidMount或者componentDidUpdat方法)

组件规范"#"

一个人的项目可以随意编写代码,但是放到一个团队中则需要统一编码规范。否则,1+1<2是很正常的事情,如果没有统一规范,你会发现做了很多重复劳动,组件的规范包括但不限于:

  • 组件头部的注释,组件功能概述|作者|编写日期|需要传入的props(哪些是必须的,哪些是可选的)
  • 组件的回调规范,在组件内部处理的事件处理函数,统一命名为handleXXX,传入组件的回调props命名为onHandleXXX
  • 组件的类名规范,模块以m-开头,组件以w-等等
  • 组件的ajax获取时机,初始化在componentDidMount,关于为什么不建议在componentWillMount中进行ajax请求,可以看这里这里)
  • 组件内函数的参数注释,对组件内的各个方法,需要标注参数类型
  • 保持render函数的纯,仅仅负责描述Virtual DOM
  • 其他规范

各司其职"#"

在选取框架/库的角度,衡量的标准是不一致的,每个框架/库都有它所擅长的一面,大部分场景下React表现可能十分出色,但是在某些特殊场景下,使用React就显得不那么有优势,在编写组件的时候,不能完全要想着使用React来完成业务需求,当使用React实现起来比较繁盛/损耗性能的时候,要敢于跳出React的既定圈子,尝试使用jQuery或许会更好。

性能优化"#"

追求页面打开的极致是每一个前端工程师的追求,React高效的diff算法虽然已经减少了许多不必要的真实DOM操作,但是我们依旧需要避免一些无意义的重复渲染,这其中的关键点是shouldComponentUpdate函数,告知接下来的propsstate,如果无须更新,可以在shouldComponentUpdate中返回false

1
2
3
4
5
6
7
// 略
shouldComponentUpdate: function(nextProps, nextState){
if(nextPros.a.b === this.props.a.b || nextState.c.d === this.state.c.d){
return false;
}
return true;
}

就这么简单,如果判断无须更新,我们仅需返回false即可,说到这里又牵扯出了如何判断propsstate的问题上。如果是判断基本的类型还好,可以保证达到我们想要的效果。但是如果我们要判断的是对象的话就会有问题了:

1
2
3
4
5
6
7
8
9
10
11
12
this.state.arr = [1,2];
this.state.arr.push(3);
this.setState({
arr: this.state.arr
})
//
shouldComponentUpdate: function(nextProps, nextState){
if(nextState.arr === this.state.arr){// error! 永远相等
return false;
}
return true;
}

如果比较的是引用类型,其实真正判断的是指向的引用是否相等,但是还好,我们可以使用FaceBook推出的immutable.js,这个库的核心思想就是对于相同对象的修改返回不同的引用,这样就很好的解决了上面判断错误的情况。

还能做些什么?"#"

  • 特殊情况处理(列表为空|ajax数据获取异常)
  • 抽象后端接口,后端传来的数据接口不一定完全满足前端展示需要,某些情况下,需要通过一个wraper组件将数据转化为符合组件展示需求的格式
  • propType校验 | chrome DevToll测试页面渲染更新性能
  • 回顾组件的整体数据流,找到可能阻塞性能的地方(警惕setState这个动作,它可能会导致该组件及其子组件的递归更新,进而触发它或子组件的componentWillReceiveProps|componentDidUpdat等方法,而这些方法都是可能潜在运行一些逻辑代码的地方)
  • 回顾componentWillUnmount方法,别忘了在这里注销通过第三方注册的事件handle!
  • HOC(Hight-Order-Component),如何结合高阶组件来使用,让你的组件更加的通用和flexible.
  • 业务复杂时是否要引入状态管理库-Redux|Reflux..etc
  • 组件测试

总结"#"

在新建一个页面的时候,需要由浅入深的考虑组件划分,总是免不了抽组件这个话题,从何种维度抽,以何种标准抽,这都没有一个固定的答案,要依据组内的规范,业务的需求来衡量。也不需要对划分组件很担忧,当你以一个标准划分之后,随着业务的发展,自然而然的就会知道组件分的合不合理,心中对于组件的划分也就会逐级清晰起来,这是一个过程,需要我们不断的自我反思和总结。
当你从仅仅为了应付需求而简单的完成一个页面,到能够使用一种更为工程化的角度去看待页面时,我想说:你好,前端工程师!