详解React与Redux应用搭建

本文将主要讲述我对React与Redux单页面应用搭建的理解与当前的应用模式,并通过范例带领大家深入了解其开发模式,共同探讨后台组件化的实现方案。

在文中涉及到的一些技术文档链接如下所示,想对使用技术进行详细了解的同学可以通过下面的链接进行深入学习:

React - 中文文档:http://reactjs.cn/react/index.html
Redux - 中文文档:http://cn.redux.js.org/index.html
Webpack - 基本了解:http://docs.showjoy.net/2016/07/14/cong-gulpdao-webpack-ji-zhu-bi-ji/ 英文文档:http://webpack.github.io/docs/
ECMAScript 6 - ECMAScript 6 入门:http://es6.ruanyifeng.com/#docs/intro

后台组件化的需求

一切的一切,可以从这个业务需求开始说起,通过这个久远的需求我开始接触React和Redux技术,希望可以通过他们来实现业务所需。相信有接触过我们后台管理页面开发的同学应该基本了解,我们当前的后台管理页面存在这样的问题:

  1. 前端页面与服务端数据耦合
    页面和数据耦合的问题并不单一存在于后台管理页面,同样存在于我们当前的业务前台展示页面。这个问题在很大程度上导致了前端和服务端的开发不能完全独立进行,开发信息的不对称与对接效率低下经常导致开发时间的被动拉长,从可扩展性与可维护性上来说也存在缺陷。
  2. 后台页面通用部分缺少复用
    在后台管理页面中有很多可复用的部分,但由于没有进行业务代码的抽离,脚本和样式仍存在在页面中,开发者在实际的开发过程中难以进行复用,开发和维护成本大大提高。

基于此,我们希望可以提供一种解决上述问题的技术解决方案。可以基本明确的是,我们要实现的方案应该包含这两个基本点:数据接口化(数据解耦)组件化(可复用)。那在结合当前相对成熟的实现框架,React的组件化实现方案我觉得是一个不错的选择。

浅识React

React框架是当前仍旧比较流行的前端页面组件化框架,它具有如下两个主要特点:

虚拟DOM

在前端页面的开发过程中,开发者通常需要根据数据的变化改变前端UI的展示,这涉及到频繁而复杂的DOM操作。这些操作一方面导致了页面的性能降低,另一方面使得页面的可维护性下降;而通过React框架的虚拟DOM机制,当数据发生变化时,开发者将不再需要关注DOM元素的操作,转而去关注在不同的数据状态下页面是如何渲染数据的,为页面开发提供了方便。

组件化

React框架带来了组件化开发的思想,它提倡通过组件的方式进行前端页面UI的搭建,将页面上的各个模块分别构建成组件,通过组件之间的嵌套组合实现整个页面的搭建。

下面通过一段代码来对React框架使用过程中常用的方法进行了解。(详细的技术使用请参考React文档)

var Parent = React.createClass({  
  render: function() {
    return (
      <div>
        {this.props.data}
      </div>
    );
  }
});
React.render(  
  <Parent data=‘example’/>,
  document.body
);

其中:

  • React.createClass()用于生成一个组件对应的类,用于创建React组件渲染架构,render()接口定义了组件渲染输出的内容。(render()部分采用的是JSX语法)
  • React.render()将一个React组件渲染到指定的DOM容器中。
  • <Parent data=‘example’/>在通过React.createClass()方法定义了组件类之后,可以通过标签化的方式进行组件的引用,其中data={...}为组件顶层传递数据的接口,可以命名若干个数据接口接收所需内容,然后在组件内部通过this.props获取字段值。

在本小节的最后,将介绍一下 React StateReact State 作为React框架中重要部分的存在,用于表示组件内部的数据状态。在React中,组件相当于一个状态机,通过交互行为导致的状态(数据)改变,将使React框架根据状态的改变重新渲染页面UI部分的内容,保证用户界面和状态(数据)的同步。可以这样去进行理解:State是组件内部的数据,通过控制State就是控制了组件的内部数据流,可以通过改变State来实现不同的界面UI渲染展示。

但是在本文描述的应用中,我们将不采用 State 进行组件的实现。虽然 State 管理了组件内部的数据流动,但是这种数据流动在多层组件嵌套中变得难以控制;组件的组合使得每个组件既要接收其他组件的数据,同时还需要控制自己的组件状态,使得组件之间的状态周期存在耦合,不利于进行组件的解耦,可复用性低下。因此我们放弃了React组件的状态控制,将React作为‘纯组件’进行使用:即放弃组件内部的状态控制,通过不同的顶层数据的传递将导致不同的组件UI渲染结果;然后通过引入Redux框架来实现多组件的状态控制,实现单页面应用的数据状态可控并保证单向清晰的数据流动。通过下一节对Redux的介绍,我们将会对采用这种方式搭建的单页面应用有更清晰的认知。

接触Redux

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。

在开发React组件构成的页面应用时,常会遇到如下棘手的问题:

  1. React组件的状态控制在组件嵌套深的情况难以控制,数据流在父子组件之间的传递存在多向控制,需要对组件生命周期有深入的了解与掌握才能比较好的控制组件内部的数据流动。
  2. 组件间通信存在困难,缺少组件之间的通信机制,交互开发难以进行。

针对以上存在的问题,Redux给出了很好的解决方案。Redux框架确保了页面组件状态变化的可预测性,提供了页面应用数据流单向流动的解决方案,使得页面开发变得明晰而易于扩展,同时针对组件的可复用提供了良好的支持。下面将针对Redux框架的内容进行介绍。 根据上图,我们大致可以把这个框架通过下面的几个部分进行详细的解释:View、Actions、Store、Reducer。其中 View 即为我们的前端UI渲染页面,在本文中我们采用React框架进行前端UI部分的搭建;将在下一节通过代码示例进行详细的说明。

Store

Redux 应用只有一个单一的 store

store 存储了页面的数据状态,而为了确保页面的状态可预测,Redux有如下的约束:
1. 页面的状态 state 是存储在store中的单一对象
2. state为不可变数据(immutable),只读
3. state只能通过Actions来触发更新,更新的逻辑通过reducer执行

Actions

Action 是把数据从应用(这里之所以不叫 view 是因为这些数据有可能是服务器响应,用户输入或其它非 view 的数据 )传到 store 的有效载荷。它是 store 数据的唯一来源。

Action 即为用户或者系统触发的交互动作,该动作从应用被触发,通过 dispatch 的方式传递到 Store 中进行处理。下图展示了一个 Input 定义的 onChange 事件触发了 Action 并通过 dispatch 的方式将 Action 传到 store 中这样的过程。

Action其实本质上是一个JavaScript普通对象,包含这个动作的定义 type 以及一些其他出发该动作应该附带的数据。其中 type 这个字段是必须存在的,用于标识该动作,使得对应的处理可以捕获该 Action 。

{
  type: ‘INPUT_CHANGE’,
  text: ‘E’
}

在设计动作的时候应该尽量减少在 Action 中传递的数据,仅传递触发该动作以及后续逻辑需要处理的数据即可。

Reducer

Action 描述了有事情发生了这一事实,没有指明应用如何更新 state。而这正是 Reducer 要做的事情。

Reducer的功能为接收 state 状态和 Action ,通过匹配 Action 进行逻辑处理,返回新的 state 状态。如果没有匹配任何的 Action (对于这个 reducer 来说是未知的 Action ),则不进行逻辑处理,返回之前的 state 状态。

下面通过一个例子来让大家更加明白其工作原理:

在Reducer中有这样的一个要求:

只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。

React与Redux单页面应用搭建

在本节中,将会结合实际的代码工程会搭建一个Input输入组件作为示例,为大家展示如何通过React和Redux结合应用。

第一步:搭建View层的React组件

首先,我们搭建Input的React组件。在组件搭建的过程中,需要注意我们在组件中不涉及React state的应用,所有的数据通过this.props从组件顶层进行获取,所有的状态通过redux框架进行管理,组件作为纯UI组件存在。 1. componentAction - 在componentAction中定义了这个组件可以触发的方法,通过定义常量的方式进行定义用于保证统一规范化的命名使用,方便管理。
2. this.props.dataSource - 通过this.props的方法获取组件需要的数据,组件所有需要的数据都通过这种方式进行获取,不使用 state。
3. render(){} - 在render接口中完成组件渲染架构的搭建,在这里也可以进行其他已经搭建好的组件的引用。
4. handleChange() - 在组件类中我定义了一个handleChange方法,通过这个调用这个方法把组件触发的方法暴露到组件顶层,再在顶层通过调用其他方法实现 Action 的分发。在图中例子可以看到,其传递的参数包括了前面定义的componentAction里面的常量,后面将在reducer中提供该内容的识别。

第二步:数据模型设置

组件的数据模型也就是组件对应的自己的状态,组件可以通过这个状态进行组件的渲染那。设置数据模型需要关注哪些所需字段是在组件的使用过程中可能会发生改变的,这种字段可以设置在数据模型中,其他内容可以直接设置在组件的使用处,设置成静态数据。当然也可以按照自己的需求来,设置成数据模型将可以提供更多的动态配置。

第三步:reducer的开发

组件的reducer也即组件的逻辑处理设置。在reducer中,我们需要根据这个组件可以匹配的 Action 进行不同的逻辑开发。需要注意reducer的单纯原则以及state的不可变数据原则 Immutable

Immutable提供了不可变数据的解决方案,具体的内容同学可以参考其文档进行学习:http://facebook.github.io/immutable-js/
在上图中,我们使用了几个关于immutable的方法:

  • immutable.fromJS()用于把JS中的内容转化成不可变对象;该返回一个不可变对象。
  • immutableObj.getIn([path])用于获取不可变对象中的内容,其参数为一个数组,为获取内容的路径path;该方法返回一个不可变对象。
  • immutableObj.setIn([path], value)用于设置不可变对象中的内容,参数包括一个设置的路径和设置的值;该方法返回一个不可变对象。
  • immutableObj.toJS()把一个对象转换成普通JS内容。

引入property和componentAction

他们分别是我们第一步和第二步定义的数据模型和组件可触发的 Actions ,获取这些内容之后我们可以开始进行reducer的开发。

reducer方法

组件的reducer方法接收两个参数,分别为这个组件的在处理前的 state 和触发的 Action 。这上图所示中,如果 state 没有传递内容则采用 property 进行初始化;而 Action ,这里需要注意所有应用触发分发的 Action 都会传递到 reducer 中,只是 reducer 是否提供可匹配项进行逻辑处理。

逻辑实现

这个组件的逻辑实现包括两个部分,一个是对input触发的onChange事件进行处理,另一个针对input的验证操作。我们可以看到在组件中我们没有设置可以触发验证操作的地方,因为这个 Action 并不是通过input组件进行触发的,可以通过其他组件进行这个 Action 的触发,这也完成了组件之间的通信的功能。可以看到具体的代码中,通过switch/case的方式进行Action的匹配,针对不同的action的类型可以进行不同的逻辑操作的处理。 其中还需要说明action.id,之所以有这个字段是因为我们页面可能会引用很多相同的这样的组件,为了区分组件我添加了 id 字段,使得组件可以和 Action 唯一对应。

组件的引用

在完成组件搭建之后我们可以进行组件的引用操作。在开发中,整个页面相当于一个大的组件,其中通过嵌套不同的小组件来实现整个页面。具体我们从如下图所示的代码出发: 下面各说明的序号对应图中的序号:

  1. 在使用组件的时候,我们可以通过引用这个组件的类来获取组件的内容。
  2. 在整个页面组件容器中,我们可以直接使用该组件类的名称进行组件的使用,其中需要注意一些所需数据的定义以及从组件底层所暴露方法的接口的定义。
  3. 由于在组件中有需要在页面中进行 Action 的触发,所以通过一个统一的方式进行 Action 的dispatch,数据的获取通过组件暴露的接口进行获取。
store的应用

最后我们可以设置组件在页面中的数据状态的获取。 1. store获取的数据都是通过组件的 reducer 进行获取的,所以需要把组件的 reducer 进行引入。
2. redux的createStore方法定义了这个页面应用的store,其接收两个参数:组件的 reducer 和组件的初始化状态。其中combineReducer用于把多个组件的 state 联合起来形成单一的 state 树。
3. 这个参数为可选项,为组件的初始化数据状态,只有在第一次初始化的时候才会使用,可以通过这个参数设置页面的初始状态。
4. redux通过使用Provider容器把前面通过createStore创建的页面store的数值内容传递到页面应用中。
5. 在这里为我们的页面应用组件,在这种开发模式中,我们把整个页面当成一个大的组件进行使用。
6. 通过js原生的方式把我们创建的应用组件插入到 DOM 中。
最后,我们还需要在页面组件中定义从顶层容器传递过来的store中state的使用分发方式,具体的方式和我们组件组合搭配的方式相关。connet函数相当于把我们设置好的获取的 state 传递到我们的页面组件类中。

至此,我们完成了整个单页面应用的开发。到这里我们再次看回Redux框架的一个图相信大家会有更加深入的了解。

组件化方案实现探究

最后结合我们的需求,说明一下基于这种框架模式实现的组件化方案。通过React实现的UI组件统一包括以下这几个部分: 在使用组件时存在两种使用的状态:

  1. 直接在页面中使用该组件
    这种情况我们像前面所示的方法直接使用组件即可。
  2. 在组件中嵌套该组件
    通常如果我们在一个组件中嵌套另外一个组件,这时候我们其实相当于创建了一个新的组件,通过开发这个新的组件实现组件的复用。这时候我们的新组件同样包含上述UI组件包含的几个部分,同时其可触发的 Action 可以在使用组件的时候通过传值覆盖掉原组件触发的方法,使其触发新搭建组件的 Action ,达到扩展的目的。

通过这种使用方式进行组件的使用可以达到我们组件解耦使用并提高可维护性的需求。在此之后,我的想法是把React和Redux单页面应用的搭建放到spon中实现,通过类似于fecomponent的方式来管理React组件,即可以达到组件的可维护,也可以使用和上述一样的方式import组件,提高开发效率。

谢谢阅读!欢迎指正与探讨哈哈~

悟空

前端开发码农一枚,爱摄影,爱旅游