使用 ES6 的简单 React-Flux 示例





5.00/5 (1投票)
本文将演示如何使用 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 是一种类似于发布/订阅模式的架构。简单来说,它是这样的:
- 组件通过 Action Creators 发布事件
- Action Creator 通过 Dispatcher 将事件分派到 Store
- Store 注册以接收分派的事件
- Store 更新其内部数据结构以进行任何必需的更改,并发出一个更改事件
- 组件订阅 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
组件使用了无状态函数,因为它没有状态且不使用任何生命周期方法(componentDidMount
、componentWillUnmount
等)。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
类进行 Store
和 ActionCreators
,使用 export default new 语法。我还会尝试写更多文章,通过易于理解的可运行示例和 API 调用来演示 Flux 或 Redux 的使用。