React 应用的架构模式 Flux

React 组件之间的数据通信在 初探 React 组件 有初步接触,父组件可以通过声明 props 来向子组件传递数据,但是子组件无法向父组件传递数据,兄弟组件之间也不能相互传递数据。React 的这种单向数据流的通信模式能确保数据的流动简单可控,非常严谨,React 也一直秉承着简单严谨的设计思想。

那如上提到的非父子组件的数据通信该怎么办呢?有几种方案:

  • 回调函数 父组件注册回调函数,子组件执行回调函数,这种方案比较简单,适用范围也有限,适合简单的子组件向父组件的数据通信;

  • 事件 事件通信是 JavaScript 最常用的通信方案,这种通信方案不用局限于组件之间的关系,比较灵活,但是当应用复杂庞大的时候,使用这种通信方式会导致管理混乱,不可控;

  • Flux Facebook 官方团队提出来专门应用于 React 的一套应用的架构模式,本文将要讲述的就是该方案。

React 专注在 UI 层上,从 MVC 分层的架构模式上来看,React 主要涉及到的是 V,一个 React 的组件可以是一个 Views 或 Controller-views 的混合体,它完全没有 M 层。Facebook 官方团队并没有照搬比较流行的 MV*,而是提出了更适合 React 的 Flux 架构模式。

Flux 主要包含四个部分:

  • Dispatcher 动作(Action)分发中心,用于分发动作给数据存储(Store)中心;
  • Actions 用户的交互,数据的交互都可以是一个动作;
  • Stores 数据存储中心,响应动作,对数据进行修改,数据有变化就对组件(View)发出通知;
  • Views & Controller-Views 组件本身,发送 Action,接收来自 Store 的通知;

Store 中存储了组件的数据,要更新数据必须发起一个 Action,不能直接去修改,要获取数据,也只能从 Store 中获取,因此 Flux 的数据流也是单向的。而 Dispatcher 只是一个分发器,将 Action 分发到 Store 中。

考虑到 Demo 代码体积较大,不能一一在文章中展现,我在 github 上传了 flux-demo,可以对照 demo 看看。

Demo 中有 3 个组件,FluxDemo 是入口组件,它包含了 2 个子组件 Dropdown 和 Content。Dropdown 组件在选中了某一项时会发起一个请求,请求的数据会在 Content 中展现出来。

接下来将着重讲解 Flux 的应用部分。

Dispatcher 分发中心

官方的 Flux 提供了一个简单的 Dispatcher 库,它有几个简单的 API,最常用的是 register 和 dispatch。一个应用只需要一个 Dispatcher 中心,用于管理分发整个应用的 Action。

register 用于在 Store 中注册回调,并且会返回一个 token 值,这个 token 可以用于卸载回调,还有一个很重要的功能就是用于 waitFor。

dispatch 用于在 Action 中分发动作,但是这个动作的分发是同步的,如果想支持异步的动作分发可以对 Dispatcher 进行扩展,查看dispatcher/dispatcher.js。

// dispatcher/dispatcher.js
class Dispatcher extends Flux.Dispatcher {
    constructor (...args) {
        super(...args);
    }

    dispatch (type, action = {}) {
        if (!type) {
            throw new Error('You forgot to specify type.');
        }

        super.dispatch({type, ...action});
    }

    dispatchAsync (url, types, action = {}) {
        const { request, success, failure } = types;
        // 使用fetch来获取数据
        const promise = fetch(url).then((response) => response.json());
        // 分发请求开始的动作
        this.dispatch(request, action);

        promise.then(
            // 分发请求成功的动作
            (response) => {
                this.dispatch(success, {...action, response});
            },

            // 分发请求错误的动作
            (error) => {
                this.dispatch(failure, {...action, error});
            }
        );
    }
};

export default new Dispatcher();

与服务器的数据交互都是异步的,当我们在分发一个异步动作的时候,这个动作可以拆分成 3 个动作:

  • 请求开始
  • 请求成功
  • 请求错误

Actions 动作

一个组件可以对应一系列的 Actions,建议以组件来划分 Actions 的粒度。在 Demo 中有一个向服务器请求数据的 Action,查看action/flux-demo.js。

// action/flux-demo.js
import dispatcher from '../dispatcher/dispatcher';

const fluxDemoActions = {
    fetchIntroduction (name, path) {
        const url = `http://127.0.0.1:3002${path}`;

        // 一个异步Action拆分成了3个不同状态的Action
        dispatcher.dispatchAsync(url, {
            request: 'FETCH_INTRODUCTION',
            success: 'FETCH_INTRODUCTION_SUCCESS',
            failure: 'FETCH_INTRODUCTION_ERROR'
        },{
            name
        });
    }
};

export default fluxDemoActions;

每一个动作都要有一个唯一的动作类型,在上面的代码中,它定义了 3 个动作类型:

  • FETCH_INTRODUCTION 请求开始
  • FETCH_INTRODUCTION_SUCCESS 请求数据成功
  • FETCH_INTRODUCTION_ERROR 请求数据失败

这 3 个动作在 Stores 可以有相对应的动作处理函数。

Stores 数据存储中心

Stores 部分要复杂一些。Stores 的数据变更需要发送和接收通知,故需要用到事件系统,可以直接使用 EventEmmiter 模块,可以创建一个工具函数,用于创建 Store。

const createStore = (methods) => {
    let name;

    class Store extends EventEmitter {};

    for (name in methods) {
        Store.prototype[name] = methods[name];
    }

    return new Store();
};

组件不能直接访问 Store 中存储的数据,如果能直接访问也意味着可以直接修改数据,为了确保数据流是单向的,这是不允许的,必须通过 Store 本身提供的数据访问接口来获取数据,最后,需要在 Store 中注册一些与 Action 中相对应的回调函数。

// 数据存储对象,外部不能直接访问
const data = {
    introduction: {}
};

// 创建store
const fluxDemoStore = createStore({
    getIntroduction (name) {
        return data.introduction[name];
    }
});

// 注册回调函数的时候会返回一个token
fluxDemoStore.dispatchToken = dispatcher.register((action) => {
    switch(action.type) {
        case 'FETCH_INTRODUCTION':
            // do something
        break;

        case 'FETCH_INTRODUCTION_SUCCESS':
            // 请求成功后,将数据存储到私有的对象中
            data.introduction[action.name] = action.response.data;
            // 同时通过事件系统发送数据变更的通知
            fluxDemoStore.emit('change');
        break;

        case 'FETCH_INTRODUCTION_ERROR':
            // do something
        break;
    }
});

export default fluxDemoStore;

这里需要注意的是,在注册回调函数时用到了 switch,分发任何一个 Action 时,Store 中的回调函数就通过 action.type 来区分到底分发的是哪一个 Action,查看store/flux-demo.js。

Views 组件

上面把 flux 部分都分析完后,还是回到组件层面。

Dropdown 组件在选择了某一项时,会触发一个请求数据的 Action。如果想让一个组件尽量能复用,那么它最好不要和 flux 有关联,这样能确保组件的独立性,所以在选择的时候提供一个回调函数,那么 Action 就可以在回调函数中调用,查看component/flux-demo.js。

selectCallback = (name) => {
    const path = `/api/${name.toLowerCase()}`;

    this.setState({
        selected: name
    }, () => {
        // 发起请求数据的Action
        fluxDemoActions.fetchIntroduction(name, path);
    });
}

那么该在哪个组件中获取数据更好呢?首先,消费数据的是 Content 组件,最简单的方案就是直接在 Content 组件中来监听数据的变化更新数据。当然也可以在父组件 FluxDemo 中进行,然后再通过声明 props 来传递过去。一个简单的划分原则就是看消费数据的组件是否需要复用,如果为了复用,要确保其通用性,肯定是不包含 flux 部分更好,而如果不会有复用,那么直接在组件内获取数据更好,因为数据少了一层传递,查看component/content.js

// content.js
import fluxDemoStore from '../store/flux-demo';

class Content extends React.Component {
    constructor (props) {
        super(props);

        this.state = {};
    }

    // 组件装载完毕则绑定数据更新的事件
    componentDidMount () {
        fluxDemoStore.on('change', this.refreshContent);
    }

    // 组件卸载的时候同时卸载事件
    componentWillUnmount () {
        fluxDemoStore.off('change', this.refreshContent);
    }

    refreshContent = () => {
        // 从store获取数据
        const introduction = fluxDemoStore.getIntroduction(this.props.name);

        // 获取到数据通过setState来更新组件的状态
        this.setState({
            introduction
        });
    }

    render () {
        return (
            <div>
                <p>{this.state.introduction}</p>
            </div>
        );
    }
};

export default Content;

Flux 的架构模式初次接触的话可能不是那么容易消化,可以仔细看看 Demo,然后自己尝试去写一个应用,你就会真正理解 Flux 单向数据流的魅力。

我建立了一个 React 技术交流群,欢迎大家入群一起交流,群号:471410265。

Ballade: 重新诠释 Flux 架构

原载于:雨夜带刀’s Blog
本文链接:https://blog.yiguochen.com/react-flux.html
如需转载请以链接形式注明原载或原文地址。

“React 应用的架构模式 Flux”目前已有 5 条评论