[关闭]
@nullcc 2016-06-16T11:18:38.000000Z 字数 8158 阅读 1873

react+flux编程实践(二) 实践篇

react flux 前端组件


这部分要分析一个react+flux的demo。

Data Flow

单向数据流是一种应用架构模式,它关心数据如何存放,如何更改数据,如何通知数据更改等问题。并不是React自带的东西,它是一种用React构建客户端web应用的最佳实践。正因为Data Flow是一种模式,或者说是一种思想,所以市面上现在有不止一个的实现。比较常用的有官方的Flux和Redux。

本篇就要使用react+flux来构建客户端web应用。

Flux

React承担了MVC中View的职责,那Flux则承担了M和C的职责。Flux是Facebook使用的一套前端应用架构模式。一个Flux应用主要包括下面4个部分:

  1. Dispatcher

    处理动作分发

  2. Stores

    数据和逻辑部分

  3. Views

    React组件,这一层可以看做是controller-views,作为视图同时响应用户交互。

  4. Actions

    一般由Views调用,在内部调用Dispatcher分发事件给Stores。

有兴趣的童鞋可以去阅读Flux的源码。

单向数据流

简单看下单向数据流是怎么一回事:

Action -> Dispatcher -> Store -> View

更多时候用户会通过View触发事件,因此更准确的图如下:

Flux概念图

解释一下这个数据流程图:

一句话来概括就是:所有的状态都由Store来维护,Dispatcher通过Action传递数据,View从Store获取数据更新UI

Flux的Github页面还给出了一张更详细的图:

Flux概念图-详细版

Dispatcher

Dispatcher是应用的消息分发中心,一般来说一个应用只需要一个Dispatcher,它管理所有数据流向,分发动作给Store。

Dispatcher分发动作给Store注册的回调函数,这里和一般意义上的发布/订阅模式有点不同:

  1. 回调函数不是订阅到某一个特定的事件或频道上,Dispatcher分发的每个事件都会分发给所有注册的回调函数。
  2. 回调函数可以指定在其他回调之后调用。

在Flux实现中,Dispatcher提供的API很简单:

  1. register(function callback): string

    注册回调函数,返回一个token供在waitFor()中调用。

  2. unregister(string id): void

    通过token移除回调。

  3. waitFor(array ids): void

    在指定的回调函数执行之后才执行当前回调,这个方法只能在分发动作的回调函数中使用。

  4. isDispatching(): boolean

    返回Dispatcher当前是否处于在分发的状态。

Action

我们需要创建一些动作,定义一些action creator来创建,这些方法暴露给外部调用。动作就是用来封装传递数据的,动作就是一个简单的JS对象,包含了动作类型(type)和payload(数据载荷),type是一个字符串常量,我们需要事先定义好。

Store

Store中包含了应用的状态和逻辑,不同的Store管理着应用中的不同部分。

在Store中向Dispatcher注册的回调函数会接受到Dispatcher分发的action,因为每个action都会分发给所有注册的回调函数,因此在回调函数中必须判断action的type,只处理自己关心的action类型,并获取action中的数据后调用Store的内部方法进行更新,然后通知View数据有变更,随后View重新渲染。

注意,Store对外只会暴露getter方法,不会暴露任何setter方法,唯一更新数据的手段就是通过Dispatcher分发action去更新Store。

View

View就是React组件,从Store获取数据,绑定事件处理。一个View可能关联多个Store来管理不同部分的状态(毕竟一个View可能要展示多方面的数据),React在更新View时仅仅是去Store拿最新的数据,然后调用setState,这部分解耦了。


一个实例

下面我们来分析一个官方的实例:todo-mvc

注意 一定要阅读完下面列出的所有代码!

先来围观一下这个demo的文件结构,我们只看js部分:

todo-mvc文件结构

项目文件结构

  1. actions文件夹

    存放应用中的所有类型的Action。

  2. components文件夹

    存放React组件文件。

  3. constants文件夹

    存放每个Action文件对应的Action Type常量。

  4. dispatcher文件夹

    存放Dispatcher文件,一般来说Dispatcher一个就够了。

  5. stores文件夹

    存放应用中的所有Store文件。

  6. app.js

    客户端web应用的入口文件。

  7. bundle.js

    这个文件是由工作流脚本生成的,我们需要在package.json文件中配置生成的命令,然后启动npm start开启监控,每当我们的js文件有变化,都会自动帮我们打包这个文件。bundle.js文件需要在index.html中手动用\标签引入.

index.html

先看一下index.html:

index

在index.html中,只引入了一个bundle.js,这是一个打包文件。

constants文件夹

TodoConstants.js文件:

var keyMirror = require('keymirror');

module.exports = keyMirror({
    TODO_CREATE: null,
    TODO_COMPLETE: null,
    TODO_DESTROY: null,
    TODO_DESTROY_COMPLETED: null,
    TODO_TOGGLE_COMPLETE_ALL: null,
    TODO_UNDO_COMPLETE: null,
    TODO_UPDATE_TEXT: null
});

TodoConstants.js文件中,定义了7个action type,对应了7种不同的更新Store中数据的方式。

dispatcher文件夹


AppDispatcher.js文件:

var Dispatcher = require('flux').Dispatcher;
module.exports = new Dispatcher();

AppDispatcher.js文件没什么特别的,就是导出一个Dispatcher实例而已。

actions文件夹

TodoActions.js文件:

var AppDispatcher = require('../dispatcher/AppDispatcher');
var TodoConstants = require('../constants/TodoConstants');

var TodoActions = {

    create: function(text) {
        AppDispatcher.dispatch({
            actionType: TodoConstants.TODO_CREATE,
            text: text
        });
    },

    updateText: function(id, text) {
        AppDispatcher.dispatch({
            actionType: TodoConstants.TODO_UPDATE_TEXT,
            id: id,
            text: text
        });
    },

    toggleComplete: function(todo) {
        var id = todo.id;
        var actionType = todo.complete ?
            TodoConstants.TODO_UNDO_COMPLETE :
            TodoConstants.TODO_COMPLETE;

        AppDispatcher.dispatch({
            actionType: actionType,
            id: id
        });
    },

    toggleCompleteAll: function() {
        AppDispatcher.dispatch({
            actionType: TodoConstants.TODO_TOGGLE_COMPLETE_ALL
        });
    },

    destroy: function(id) {
        AppDispatcher.dispatch({
            actionType: TodoConstants.TODO_DESTROY,
            id: id
        });
    },

    destroyCompleted: function() {
        AppDispatcher.dispatch({
            actionType: TodoConstants.TODO_DESTROY_COMPLETED
        });
    }
};

module.exports = TodoActions;

TodoActions.js中基本上就是一些对应TodoConstants.js中定义的action type(其中toggleComplete对应了两种),导出的TodoActions中包含这些action creator。这就是Action暴露给外部调用的方法了,通过调用这些action creator,我们很容易就可以让Dispatcher分发我们期望的action给Store。

stores文件夹

TodoStore.js文件:

var AppDispatcher = require('../dispatcher/AppDispatcher');
var EventEmitter = require('events').EventEmitter;
var TodoConstants = require('../constants/TodoConstants');
var assign = require('object-assign');

var CHANGE_EVENT = 'change';

var _todos = {};

function create(text) {
    var id = (+new Date() + Math.floor(Math.random() * 999999)).toString(36);
    _todos[id] = {
        id: id,
        complete: false,
        text: text
    };
}

function update(id, updates) {
    _todos[id] = assign({}, _todos[id], updates);
}

function updateAll(updates) {
    for (var id in _todos) {
        update(id, updates);
    }
}

function destroy(id) {
    delete _todos[id];
}

function destroyCompleted() {
    for (var id in _todos) {
        if (_todos[id].complete) {
            destroy(id);
        }
    }
}

var TodoStore = assign({}, EventEmitter.prototype, {

    areAllComplete: function() {
        for (var id in _todos) {
            if (!_todos[id].complete) {
                return false;
            }
        }
        return true;
    },

    getAll: function() {
        return _todos;
    },

    emitChange: function() {
        this.emit(CHANGE_EVENT);
    },

    addChangeListener: function(callback) {
        this.on(CHANGE_EVENT, callback);
    },

    removeChangeListener: function(callback) {
        this.removeListener(CHANGE_EVENT, callback);
    }
});

AppDispatcher.register(function(action) {
    var text;

    switch(action.actionType) {
        case TodoConstants.TODO_CREATE:
            text = action.text.trim();
            if (text !== '') {
                create(text);
                TodoStore.emitChange();
            }
            break;

        case TodoConstants.TODO_TOGGLE_COMPLETE_ALL:
            if (TodoStore.areAllComplete()) {
                updateAll({complete: false});
            } else {
                updateAll({complete: true});
            }
            TodoStore.emitChange();
            break;

        case TodoConstants.TODO_UNDO_COMPLETE:
            update(action.id, {complete: false});
            TodoStore.emitChange();
            break;

        case TodoConstants.TODO_COMPLETE:
            update(action.id, {complete: true});
            TodoStore.emitChange();
            break;

        case TodoConstants.TODO_UPDATE_TEXT:
            text = action.text.trim();
            if (text !== '') {
                update(action.id, {text: text});
                TodoStore.emitChange();
            }
            break;

        case TodoConstants.TODO_DESTROY:
            destroy(action.id);
            TodoStore.emitChange();
            break;

        case TodoConstants.TODO_DESTROY_COMPLETED:
            destroyCompleted();
            TodoStore.emitChange();
            break;

        default:
            // no op
    }
});

module.exports = TodoStore;

TodoStore.js负责管理数据和逻辑。

首先是定义了一个_todos变量用来保存todo数据,这个变量外部无法直接访问到。

在导出对象TodoStore中,定义了一系列的方法供外部调用(大部分属于getter方法,还有一些和事件相关的方法),在View中可以调用Store中的getter方法获取相应的状态数据,然后调用setState方法渲染自身。

另外还定义了一些私有方法(只能从内部访问到的),比如create, update, updateAll, destroy, destroyCompleted,这些私有方法用来直接更新数据,由于私有方法只能从内部访问到,所以基本上只能在注册的回调函数中调用它们。

在最后面,向Dispatcher注册了一个回调函数,这个函数内部用switch判断action type来执行相应的代码。观察可以发现,在回调函数的代码中,针对每个不同的action type,调用了刚才说到的私有方法来更新数据,然后调用TodoStore.emitChange();手动触发一个事件,由于定义了addChangeListener和removeChangeListener,相应的View可以在生命周期函数(比如componentDidMount和componentWillUnmount)中添加和删除监听器,View在监听器回调中可以调用setState进行渲染。

TodoStore.js的代码看似很长,其实并不难理解。总结一下,它分成三大块:

  1. 定义数据和更新数据的私有方法。
  2. 定义导出对象,这个对象对外暴露了一些方法用于获取数据(基本上是一些getter)和注册和销毁监听器的方法。
  3. 向Dispatcher注册回调函数来处理Dispatcher分发的action,回调函数中通过action type进行分支处理,其中action还可以带有payload。

components文件夹

components文件夹中都是React组件,下面是其中一个文件TodoApp.react.js的代码:

var Footer = require('./Footer.react');
var Header = require('./Header.react');
var MainSection = require('./MainSection.react');
var React = require('react');
var TodoStore = require('../stores/TodoStore');

function getTodoState() {
    return {
        allTodos: TodoStore.getAll(),
        areAllComplete: TodoStore.areAllComplete()
    };
}

var TodoApp = React.createClass({

    getInitialState: function() {
        return getTodoState();
    },

    componentDidMount: function() {
        TodoStore.addChangeListener(this._onChange);
    },

    componentWillUnmount: function() {
        TodoStore.removeChangeListener(this._onChange);
    },

    render: function() {
        return (
            <div>
                <Header />
                <MainSection
                    allTodos={this.state.allTodos}
                    areAllComplete={this.state.areAllComplete}
                />
                <Footer allTodos={this.state.allTodos} />
            </div>
        );
    },

    _onChange: function() {
        this.setState(getTodoState());
    }
});

module.exports = TodoApp;

TodoApp.react.js是一个顶层组件,它包含了Header ,MainSection和Footer三个组件(代码就不列出了),并把它们放在一个div中封装起来作为一个整体供使用。

在这个文件中,导出TodoApp对象,TodoApp实际上就是一个React组件了,里面定义了getInitialState, componentDidMount, componentWillUnmount, render和_onChange这些方法。

在getInitialState方法中,调用了一个私有方法getTodoState,在getTodoState中,通过TodoStore对外提供的接口去获取数据。

在componentDidMount和componentWillUnmount中分别注册和销毁了change事件的监听器回调函数。在这里其实我们可以定义自己的事件,不一定是change,比如可以自定义一个open事件,当然在TodoStore中也要有相应的处理。回调函数_onChange负责最终去调用this.setState方法渲染UI。

app.js

app.js代码如下:

var React = require('react');

var TodoApp = require('./components/TodoApp.react');

React.render(
    <TodoApp />,
    document.getElementById('todoapp')
);

app.js是应用的入口,在这里对todoapp这个节点添加了一个\组件,就是这样。

bundle.js

bundle.js是一个打包文件,在配置工作流环境后运行npm start会自动生成,这个不用担心。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注