65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 ES6 的简单 React-Flux 示例

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2017 年 3 月 30 日

CPOL

6分钟阅读

viewsIcon

30567

本文将演示如何使用 React 以及一个可运行的示例来实现一个简单的 Flux 工作流程。

引言

几个月前,我开始学习 React。由于 React 只实现了 MVC 中的“V”,我很快就开始研究 Flux。有很多关于 Flux 的文章,但它们(至少对我而言)没能将它们联系起来。

本文将概述如何使用 React 进行 Flux 开发,并提供一个可运行的示例。您可以从 Github 下载本文的完整源代码。

假设我们的需求是创建一个虚构的文章管理器应用程序,用户可以在其中提交文章,提交后触发审批工作流。在实际应用中,这会涉及调用带有完整 UI 的 API。为简单起见,此应用程序的 UI 将是基本的 HTML 文本框输入。如果文章文本少于 10 个字符,它将被批准,否则将被直接拒绝(在实际应用中,您可能会使用 API 进行抄袭检测、垃圾邮件检查等)。用户最多可以提交 10 篇文章,还可以选择删除文章。用户可以从“审批状态列表”或“已提交文章”中删除文章,删除文章将同时更新这两个列表。

本文假设您已经了解如何创建 React 组件并有效使用 props/state。您还应该安装了 NodeJS 以及 React/Flux 所需的包。如果尚未设置,请安装 NodeJS 并从 **github** 下载 hello world React 应用。在下载了示例代码的工作目录命令提示符下,使用“npm install”恢复包,并使用“npm start”运行应用程序。

Flux 入门

Flux 是一种类似于发布/订阅模式的架构。简单来说,它是这样的:

  1. 组件通过 Action Creators 发布事件
  2. Action Creator 通过 Dispatcher 将事件分派到 Store
  3. Store 注册以接收分派的事件
  4. Store 更新其内部数据结构以进行任何必需的更改,并发出一个更改事件
  5. 组件订阅 Store 发出的更改事件,以修改其内部状态并根据从 Store 返回的数据进行相应的重新渲染。

请注意,可以有多个 Store 监听 Action Creators 分派的事件,同样,多个组件可以注册 Store 的更新并进行相应的重新渲染。

为文章管理器应用程序创建组件

一旦您运行了一个基本的 hello world React 应用(请从 GitHub 下载 hello world React 应用),让我们开始识别文章示例应用的组件。

  • Content 组件(将是其他组件的容器)
  • 一个通用的 Button 组件 - 将用于“提交”和“移除”按钮
  • 一个 List 组件 - 用于显示已提交文章和审批状态
  • 最后是一个 App 组件,它将挂载 Content 组件

Content 组件

import React from 'react'
import Button from './Button.jsx';
import List from './List.jsx'

class Content extends React.Component {

    constructor(props) {
        super(props);
        this.state = { articles: [], articlesApproved: [], message: '' };
    }

   handleClick() {
       
    }
   
    render() {
        var simpleContent =
            <div>
                {this.props.text}
                <br />
                Enter text : <input type="text" 
                name="simpletext" id="simpletext" />
                <Button handleClick={this.handleClick} 
                text="SUBMIT" />
                <br />
                <List articles={this.state.articles} 
                listHeader="Submitted Articles" />
                {this.state.message}
                <List articles={this.state.articlesApproved} 
                listHeader="Approval Status" />
            </div>;

        return simpleContent;
    }
}

export default Content;

请注意,示例代码使用了 ES6 语法,因此每个使用 state 的 React 组件都应派生自 React.Component 并通过 super(props) 调用基类构造函数。

List 和 Button 组件

//Button.jsx
import React from 'react'

const Button = (props) => 
<button onClick={props.handleClick} >{props.text}</button>
export default Button;

//List.jsx
import React from 'react'
import Button from './Button.jsx'

class List extends React.Component {

    handleClick(key) {
             
    }

    render() {
        var articles = this.props.articles != undefined ? 
        this.props.articles.map((article,i) => {
            return <li key={i}> Article {i+1}:{article} 
                        <Button  handleClick={()=>this.handleClick(i)} 
                        text="X"/>
                  </li> 
        }) :[];
        
        return (
            <div>
                <h1>{this.props.listHeader}</h1>
                <ul>
                    {articles} 
                </ul>
            </div>
        );
    }
}

export default List;

我为 Button 组件使用了无状态函数,因为它没有状态且不使用任何生命周期方法(componentDidMountcomponentWillUnmount 等)。List 组件稍微复杂一些,它需要使用 this 关键字引用内部的点击事件,因此它需要一个派生自 React.Component 的 ES6 类。

App.jsx 组件入口点,用于挂载应用程序的容器

import React from 'react'
import Content from './components/Content.jsx';

const App = () => <div> 
<Content text="A simple flux implementation with React" /> </div>   

export default App;

实现 ActionCreator 以分派事件

我们已经识别并创建了应用程序所需的组件。现在,我们需要引入一个数据结构来存储数据,并使用 Flux 架构来分派事件。

回顾需求,用户应该能够:

  • 提交文章(最多 10 篇文章,超过限制将收到错误消息)
  • 提交文章应触发审批工作流(文章文本少于 10 个字符 - 将被批准,否则将被拒绝)
  • 移除文章

Action creator 正是做这个的 - 它通过 AppDispatcher.dispatch({..}) 分派 'SUBMIT_ARTICLE'、'APPROVE_ARTICLE' 和 'REMOVE_ARTICLE' 事件。事件分派后,Store 会捕获事件,更新/移除存储在数组中的文章,并发出一个更改事件。

//AppDispatcher.js
import { Dispatcher } from 'flux';
export default new Dispatcher();

//AppActions.js
import AppDispatcher from './AppDispatcher';

class AppActions {

    submitArticle(data) {
        AppDispatcher.dispatch({
            actionType: 'SUBMIT_ARTICLE',
            value: data
        });

        AppDispatcher.dispatch({
            actionType: 'APPROVE_ARTICLE',
            value: data
        });
    }

    removeArticle(key)
    {
         AppDispatcher.dispatch({
            actionType: 'REMOVE_ARTICLE',
            value: key
        });
    }
}

export default new AppActions() //Note: Using a new keyword 
                 //will make this work like a static class

既然 Action Creator 已经准备好分派事件,让我们修改 Content 组件以满足需求 #1 和 #2。请注意,AppActions.submitArticle(document.getElementById('simpletext').value) 会将事件发送到 ActionCreator,最终分派到 Store。

//Component.jsx

    constructor(props) {
        super(props);
        this.state = { articles: [], articlesApproved: [], message: '' };
        this.handleClick = this.handleClick.bind(this);       
    }

    handleClick() {
        if (document.getElementById('simpletext').value.length > 
        0 && this.state.articles.length < 10) {
            AppActions.submitArticle(document.getElementById('simpletext').value)
            document.getElementById('simpletext').value = ''
        }
    }

删除文章将从 List 组件处理。为了从数组中删除一个项目,需要维护一个唯一的索引。这就是为什么 li 标签使用 <li key={i}> 进行渲染的原因。

 //List.jsx 

    handleClick(key) {
        AppActions.removeArticle(key)       
    }

    render() {
        var articles = this.props.articles != undefined ? 
        this.props.articles.map((article,i) => {
            return <li key={i}> Article {i+1}:{article} 
            <Button  handleClick={()=>this.handleClick(i)} 
            text="X"/></li> 
        }) :[];
        
        return (
            <div>
                <h1>{this.props.listHeader}</h1>
                <ul>
                    {articles} 
                </ul>
            </div>
        );
    }

实现 Store

Store 是一个 数据结构,用于监听 ActionCreators 分派的事件,并相应地更改其内部状态。为了让 Store 在内部状态修改后发出事件,它需要派生自 EventEmmiter

AppDispatcher.register 注册以接收 Action creator 分派的事件,并将其绑定到回调方法。

this.dispatchToken = AppDispatcher.register(this.dispatcherCallback.bind(this))

dispatcherCallback(action) {
        switch (action.actionType) {
            case 'SUBMIT_ARTICLE':
                this.submitArticle(action.value);
                break;
            case 'APPROVE_ARTICLE':
                this.approveArticle(action.value);
                break;
            case 'REMOVE_ARTICLE':
                this.removeArticle(action.value);
        }

        this.emitChange(action.actionType);

        return true;
    }

事件已分派,Store 已根据分派的事件进行了更新,并通过 this.emitChange(action.actionType) 发出了更改,那么组件如何知道 Store 发生了更改并需要重新渲染呢?这就是 addChangeListener 的作用,使用下面的代码片段,组件会注册 Store 的更改并进行相应地重新渲染。

请注意,最佳实践是让一个 Store 只发出一个事件,因为一个 Store 应该处理应用程序中的一个单一域。我让 Store 发出了多个事件,以演示一个 Store可以发出多个事件,并且一个组件可以监听来自 Store 的多个更改。

//Content.jsx
class Content extends React.Component {
.
.
.
componentDidMount() {
        AppStore.addChangeListener('SUBMIT_ARTICLE', this.onSubmit);
        AppStore.addChangeListener('REMOVE_ARTICLE', this.onRemove);
}

Store 的完整源代码

import AppDispatcher from './AppDispatcher';
import { EventEmitter } from 'events';

let _articles = [];
let _articlesApproved = []

class AppStore extends EventEmitter {

    constructor() {
        super();
        this.dispatchToken = AppDispatcher.register(this.dispatcherCallback.bind(this))
    }

    emitChange(eventName) {
        this.emit(eventName);
    }

    getAll() {
        return _articles;
    }

    getApproved() {
         return _articlesApproved;
    }

    submitArticle(article) {
        _articles.push(article);
    }

    removeArticle(key)
    {
        _articles.splice(key,1);
        _articlesApproved.splice(key,1)
    }

    approveArticle(article) {
        if (article.length <= 10) {
            _articlesApproved.push('[Approved]:' + article);
        }
        else {
            _articlesApproved.push('[Rejected]:' + article);
        }
    }

    addChangeListener(eventName, callback) {
        this.on(eventName, callback);
    }

    removeChangeListener(eventName, callback) {
        this.removeListener(eventName, callback);
    }

    dispatcherCallback(action) {
        switch (action.actionType) {
            case 'SUBMIT_ARTICLE':
                this.submitArticle(action.value);
                break;
            case 'APPROVE_ARTICLE':
                this.approveArticle(action.value);
                break;
            case 'REMOVE_ARTICLE':
                this.removeArticle(action.value);
        }

        this.emitChange('STORE_' + action.actionType);

        return true;
    }
}

export default new AppStore();

根据 Store 中的事件重新渲染组件

组件需要监听 Store 的更改才能相应地重新渲染自身。这就是在 componentDidMount 方法中拥有 AppStore.addChangeListener 的目的,表明当组件挂载时,它应该监听来自 Store 的任何更改。Store 的任何更改都应强制组件重新渲染。这通过使用 this.setState 更新组件的内部状态来完成,如下面的代码片段所示。请注意,我特意让 Content 组件监听多个事件,以演示这是可行的,并更好地理解 Store 事件。是的,作为最佳实践,理想情况下,您只会有一个与域模型相关的事件。

    componentDidMount() {
        AppStore.addChangeListener('STORE_SUBMIT_ARTICLE', this.onSubmit);
        AppStore.addChangeListener('STORE_REMOVE_ARTICLE', this.onRemove);
    }

    onRemove() {
        this.listArticles()
    }

    onSubmit() {
         this.listArticles()
    }

    listArticles()
    {
        let usermessage = ''

        if (this.state.articles.length > 9) {
            usermessage = 'You have exceeded the number of articles you can submit,You cannot add more articles'
        }

        this.setState({
            articles: AppStore.getAll(),
            articlesApproved: AppStore.getApproved(),
            message: usermessage
        })
    }

关注点

Flux 架构在概念上易于理解,但在实际应用实现中有点棘手。示例应用程序不使用 API,而在实际应用中通常会使用 API。为了在 ActionCreators 中使用 API,请使用一个实用类来发出 API 调用,然后将 API 返回的结果分派给 Store。此外,本文使用 Singleton 类进行 StoreActionCreators,使用 export default new 语法。我还会尝试写更多文章,通过易于理解的可运行示例和 API 调用来演示 Flux 或 Redux 的使用。

© . All rights reserved.