实例讲解基于 React+Redu 的前端开发流程

前言:在当下的前端界,react 和 redux 发展得如火如荼,react 在 github 的 star 数达 42000 +,超过了 jquery 的 39000+,也即将超过前几年比较火的angular 1 的 49000+;redux 的 star 数也要接近 20000,可见大家对其的热情程度,究竟是什么魔力让大家为之疯狂呢?让我们上车,亲自体验一波试试~~本文章偏向于讲解redux流程。

宅印前端基于 react + redux 的模式开发,我们指定了一套分工明确的并行开发流程。下面通过一个 “苹果篮子” 实例,来看看整个应用开发流程。

首先,我们来看看这个实例的原型:

超级产品经理) }

                摘苹果    );}

}

function select(state) {
return {
state: state.appleBusket
}
}

export default connect(select)(AppleBusket);

可见,动态布局的工作要求只是在 HTML + CSS 布局的基础上,再加上 JSX 语法能力即可。#### 2. 普通显示组件的动态渲染普通显示组件的动态渲染和容器类似,只是这里的state可以自己规定,并给出示例的mockState(模拟state),使用组件的人按照示例传入数据即可使用。AppleItem.jsx 的更新如下:

import React from 'react';

class AppleItem extends React.Component {

shouldComponentUpdate(nextProps) {    return nextProps.state != this.props.state;}render() {    let { state, actions } = this.props;    /      * 这个区域是 mock 数据区,也作为组件文档,请书写清楚     * //在组件发布时,请注释掉,提高性能     */    let mockState = {        id: 1,        weight: 256,        isEaten: false    };    let mockActions = {        eatApple : id => console.log('eatApple',id)    };    /      * 开关这行代码,用于切换装入的数据来源。(为了开关的方便,请把两句代码合成一行)     * 在开发阶段打开,使用内部 state 和 action, 开发完成后请注释关闭     */    state = mockState; actions = mockActions;    if (state.isEaten) return null;    return (

超级产品经理
) }

           dispatch(actions.pickApple())}>摘苹果      ...   )}

}

function selectState(state) {
return {
state: state.appleBusket
}
}
export default connect(selectState)(AppleBusket);

注意这两行。就是装入action的地方

actions={{eatApple: id => dispatch(actions.eatApple(id))}}
dispatch(actions.pickApple())}>摘苹果

上面代码中引入的actions其实是actionCreators。此外,actionCreator还有很简洁的用法:对actionCreator做dispatch级别的封装,这个过程我们可以使用 redux 提供的 bindActionCreators 函数自动完成。然后就可以直接调用action,而不需要使用dispatch方法去调用actionCreator。看下面更新后的代码:

import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

import AppleItem from '../components/AppleItem';
import actions from '../actions/appleActions';

class AppleBusket extends React.Component {
render() {
let { state, actions} = this.props;
...
return (
...

          { state.apples.map(apple =>          ) }          摘苹果      ...   )}

}

function selectState(state) {
return {
state: state.appleBusket
}
}

function buildActionDispatcher(dispatch) {
return {
actions: bindActionCreators(actions, dispatch)
}
}

export default connect(selectState, buildActionDispatcher)(AppleBusket);

注意这三个变动:

let { state, actions } = this.props;
actions={{eatApple: actions.eatApple }}
摘苹果

我们对比一下之前的写法:

let { state, dispatch } = this.props
actions={{eatApple: id => dispatch(actions.eatApple(id))}}
dispatch(actions.pickApple())}>摘苹果

可以发现使用新的方式使代码简洁了很多!但是,这对于有对象属性提示功能编辑器来说,这种方式会使编辑器无法分析对象属性:![超级产品经理](http://upload-images.jianshu.io/upload_images/1234637-cf9e5476a0aa6c5a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)...

AppleItem.jsx

... actions.eatApple(state.id)}>吃掉...

普通显示组件使用统一actions属性接受父级的action,可以在组件内部建立mockActions, 这个mockActions 既有文档功能,也有测试功能,非常实用:

let mockActions = {    eatApple : id => console.log('eatApple',id), //指定了函数的签名    foo: (arg1,arg2) => console.log('foo',arg1,arg2) //也响应了调用测试};/   * 开关这行代码,用于切换装入的state和actions来源。(为了开关的方便,请把两句代码合成一行)  * 在开发阶段打开,使用内部 state 和 action, 开发完成后请注释关闭  */state = mockState; actions = mockActions;    

点击 “摘苹果” 和 “吃掉” 按钮,我们看看控制台,已经可以发出我们想要的action啦:

好啦,actions 开发的内容就介绍到这里。我们来总结一下我们对action所做的封装:

action -> actionCreator -> actionDispatcher

任务2:reducer 开发

开发内容: reducer的其实就是action的处理器。其开发的内容很明确清晰,就是开发一类函数,接受action 和 当前的state,返回新的state。

技术要求:要求对js比较熟悉,需要会使用 immutable.js 这个数据静态化库。

下面是reducer功能的图解:

我们先看看我们苹果板块的state的数据结构,非常简单,这里是某个时刻的状态:

{    isPicking : false,    newAppleId: 1,    apples: [        {            id: 0,            weight: 235,            isEaten: false        }    ]}

有三个一级属性:

  1. isPicking :表示是否正在摘苹果,我们在上面已经知道,摘苹果其实是发送一个ajax请求,向后台摘一个苹果,这个请求在进行时我们会把 isPicking 设置为ture, 表明正在摘苹果,同时禁止在完成前再发送摘苹果请求

  2. newAppleId:表示新苹果的编号

  3. apples:是苹果列表数组,存放着苹果对象,苹果对象的结构在apples数组里有给出实例。

我们上面提及actions分为广义的action和狭义的普通action。其实,非普通action, 如thunk,一般会以发起普通action结束。我们reducer只需要处理狭义上的普通action,。在我们摘苹果应用里,总共有这4个普通action:

//通知store应用开始摘苹果beginPickApple: () => ({    type: 'apple/BEGIN_PICK_APPLE'}),//摘苹果成功    donePickApple: appleWeight => ({    type: 'apple/DONE_PICK_APPLE',    payload: appleWeight}),//摘苹果失败    failPickApple: error => ({    type: 'apple/FAIL_PICK_APPLE',    payload: error,    error: true}),//吃苹果eatApple: appleId => ({    type: 'apple/EAT_APPLE',    payload: appleId})

下面是根据action,写出的 reducer 的基本结构:

 (state = defaultState, action) => {    switch (action.type) {        case 'apple/BEGIN_PICK_APPLE':            //TODO            return state;        case 'apple/DONE_PICK_APPLE':            //TODO            return state;        case 'apple/FAIL_PICK_APPLE':            //TODO            return state;        case 'apple/EAT_APPLE':            //TODO            return state;        default:            return state;    }};

我们可以看到,reducer是一个函数,接受state和action两个参数,在函数内部,根据 action.type 来确定要做哪些操作,并且每种操作都要返回state(或者是新的,或者是原来的)。

我们以 apple/EAT_APPLE动作为例,讲解如何书写reducer。EAT_APPLE 动作的含义是吃苹果,我们可以非常简单地处理这个动作:直接把对应苹果对象的 isEaten 属性设为true即可。

按照一般的思维,我们会这样处理:

...    case 'apple/EAT_APPLE':        state.apples[action.payload].isEaten = true;        return state;...

但是,这种方法在 redux 应用里看不到作用,因为这种写法不会使store触发react进行重新渲染,为什么呢?因为 newState == oldState ! 下面我们来做一些解释:

首先,要先从js对象的相等判断运算说起,我们看下面的代码

let a = { foo: 'bar'};let b = { foo: 'bar'};console.log( a == b ); //结果是 false

a 和 b 看起来一样,但为什么是false呢?因为对象和数组的赋值是引用赋值, a 和 b 只是一个引用符号,其所指向的对象实体不同(比如 a -> object# 001, b -> object# 002),js的对象(数组)相等判断是根据是否指向同一个对象实体来的确定的 (object# 001 ?= object# 002 // false),详见 MDN。

再看看下面的例子:

let a = {foo: 'bar'};let b = a;b.foo = 'good';console.log( a == b ); //结果是 true

现在应该可以理解刚才为什么newState == oldState了吧~

redux 是根据返回的state是否改变来决定是否通知 react 更新的。根据这种情况所,可能有人会这样改进刚才的reducer:

    state.apples[action.payload].isEaten = true;    newState = Object.assign({},state);    return newState;

这样一来,点击 “吃掉”按钮,看到了有效果了,苹果不见了!但是,这种写法只是迎合了redux更新视觉组件的触发条件,还具有很大的局限性,不是我们redux规定的reducer。下面我们来看看正确的reducer:

首先,reducer有这样的重要约束:在reducer里,不可以修改原来的state,需要保持使每个版本的state不变

这种保持数据不变(Persistent data structure)的方式在函数式编程(Functional programming)非常常见。在我们的redux应用里,其意义在于:

1. 调试意义:保持每个版本的state的不变性,使得我们可以跟踪每个时刻的state, 跟踪应用的“发展史”,这个特性为调试带来了很大的方便。

2. 性能意义:保持state不变这个约束引导我们使用局部更新对象的方法,这样会可以非常有效地提高react或其他显示框架的渲染效率。我们先来看看为了保持数据不变性,要怎么对state做更新,以我们的苹果篮子state为例:

例子:通知开始摘苹果:apple/BEGIN_PICK_APPLE

为了保证每个版本的state不变性,我们有两种实现方式:“深复制”,“浅复制”。我们来看两种模式的内部原理:

深复制方式:有人会这样想:“保持state的不变性很容易,只需要深复制一个state, 让后在新的state要怎么修改就怎么修改,不ok了吗?”,如下就是深复制

这种方式是一种很低级保持不变性的方式:

  1. 深复制操作运行效率低

  2. 没有为渲染环节提供提高渲染效率的铺垫

它只是简单迎合保持数据不变性的约束,虽然有一定调试意义,但是,不但没有提高程序的性能,反而降低了程序的总体性能!没有实践意义。

浅复制方式:浅复制模式只对内部数据发生变化的引用做更新,如下

“state” 对象的内部数据发生变化,所以创建新的state引用;而apples array 内部数据不发生变化,所以就不对该引用做更新!在这个操作中,这种浅复制的方法运行效率比较高,而且其简单地实现了数据不变性,为调试带来方便,同时,也是更重要的,这种浅复制的方式极大地提高了视觉组件渲染阶段的运行效率!我们来对比一下:当用户点击摘苹果时,如果使用“深复制”,渲染程序需要重新遍历整个state对象树来做视觉更新,而使用浅复制来实现数据不变性时,渲染程序只需要遍历state对象的一级子节点即可,而不需要对apples array 做遍历,性能大大地提高。尤其是当苹果很多的时候,两种方式的性能差距是非常明显的。

备注:在react组件里面,要开启条件更新这个生命周期函数 shouldComponentUpdate, 才会对把这个性能提高点释放出来,类似这样:

...shouldComponentUpdate(nextProps) {    return nextProps.state != this.props.state;}...

下面我们再给出 “吃苹果” reducer 进行浅复制的例子:

现在大家应该理解了用浅复制实现数据不变性的原理和意义了,下面我们来看具体的代码实现。

我们的代码用 es6 编写,这里要用到 es6 两个非常方便的特性:

  1. Obejct.assign() 方法,该方法用于产生新的对象

  2. 延展操作符 Spread operator : ...

大家可以稍微看一下文档,或者看我下面的例子就知道其用法了:

// apple basket reducerexport default (state = {    isPicking: false,    newAppleId: 1,    apples: [        {            id: 0,            weight: 235,            isEaten: false        }    ]}, action) => {    let newState ;    switch (action.type) {        case 'apple/BEGIN_PICK_APPLE':            newState = Object.assign({}, state, {                isPicking: true            });            return newState;        case 'apple/DONE_PICK_APPLE':            newState = Object.assign({}, state, {                apples: [                    ...state.apples,                    {                        id: state.newAppleId,                        weight: action.payload,                        isEaten: false                    }                ],                newAppleId: state.newAppleId + 1,                isPicking: false            })            return newState;        case 'apple/FAIL_PICK_APPLE':            //这里只是简单处理            newState = Object.assign({}, state, {                isPicking: false            });            return newState;        case 'apple/EAT_APPLE':            newState = Object.assign({}, state, {                apples: [                    ...state.apples.slice(0, action.payload),                    Object.assign({}, state.apples[action.payload], { isEaten: true }),                    ...state.apples.slice(action.payload + 1)                ]            })            return newState;        default:            return state;    }};

大家会发现这种浅复制操作在开发的过程会复杂一点,于是就有了 immutable.js 这个专门处理不变性数据的库(也是facebook出品),它可以使用类似赋值的方式生成浅复制的不变性数据,下面来看看它怎么简化我们的开发:

我们用 apple/EAT_APPLE 这个reducer来说明。

这是原来的 reducer:

...case 'apple/EAT_APPLE':    newState = Object.assign({}, state, {        apples: [            ...state.apples.slice(0, action.payload),            Object.assign({}, state.apples[action.payload], { isEaten: true }),            ...state.apples.slice(action.payload + 1)        ]    })    return newState;...

这是使用 immutable.js 库的reducer :

import { fromJS } from 'immutable';...case 'apple/EAT_APPLE':    return fromJS(state).setIn(['apples',action.payload,'isEaten'], true).toJS();...

用了immutable.js后,轻松一行代码搞定!如果团队约定 state 都用 immutable 内部的数据类型,就可以连 fromJS 和 toJS 的转化都省了,超级方便!

到这里, reducer 任务的介绍就结束啦~

总结

至此,我们四个任务都介绍完了,大家应该对redux流程有一定概念了,我们来回顾一下我们的4个任务:

这样子,我们通过流程化把 react + redux 的主要流程都定义好了,这种模式的可构建性很高,可以构建非常复杂的单页面应用,不会因为应用的业务复杂性增加而增加开发复杂性。

并且在这种分工里面,布局组对专注于写样式布局,大多是基本的HTML+CSS 工作;逻辑组专注于开发应用逻辑,基本都是JS工作,分工得到非常明确的规划,人们可以更好地做精自己负责的工作,各个部件的耦合性很低,人员安排灵活性比较大。

这就是我们用苹果篮子实例讲解的react+redux开发流程,大家形成redux流程基本的概念就好,具体的理解还是要在实践中慢慢参透。

一些依赖的JS库没有在这里给大家介绍,大家可以在后面的相关js库中查看。

参考资料

  1. 《MDN Javascript Documents》

  2. 阮一峰 《ECMAScript 6入门》

  3. IVAN ROGIC 《React, Redux and Immutable.js: Ingredients for Efficient Web Applications》

项目相关js库列表:

  1. webpack : js开发环境和打包器

  2. babel : es6 编译器

  3. react : 当下非常火的显示框架

  4. react-router : 与react搭配的前端路由器

  5. redux : web应用的状态容器(定义了一套非常清晰的数据传递 流程)

  6. react-redux : react 和 redux 的连接器

  7. redux-logger : redux 的控制台 log 中间件

  8. redux-thunk: redux 的 thunk 中间件

  9. react-router-redux : react-router和 redux 配套使用的连接器

  10. immutable.js: js 持久化数据框架

  11. mock.js : 用于产生模拟后台数据的框架

  12. jquery: 在项目中,我们仅使用它的非常通行的 ajax 功能

感谢您的阅读,希望这篇文章对大家有帮助,欢迎回复和讨论。

关键字:JavaScript, react.js, Redux, es6

版权声明

本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处。如若内容有涉嫌抄袭侵权/违法违规/事实不符,请点击 举报 进行投诉反馈!

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部