写点什么

深入浅出 React(五):使用 Flux 搭建 React 应用程序架构

  • 2015-12-09
  • 本文字数:6203 字

    阅读完需:约 20 分钟

前面几篇文章介绍了 React 相关的基本概念和运行原理,可以看到 React 是一个完全面向 View 的解决方案,它让我们能以一种新的思路去实现 View,让很多复杂的场景可以用一种简单的方法去解决。然而在一个完整的应用程序中,除了实现 View 之外,我们还需要考虑如何同服务器通信、View 之间如何交互以及 View 背后的数据模型如何去设计。那么 Flux 正是 Facebook 提出的解决这些问题的方案。

简单来说,Flux 定义了一种单向数据流的方式,来实现 View 和 Model 之间的数据流动。它更像是一种设计模式而非一个正式的框架,以至于官方的 Flux 参考实现只有一个文件,区区 100 多行源代码。所以 Flux 继承了 React 的简单、直观的设计思想,让人一眼就能看明白其背后的运行原理。当然,要用好 Flux,还是要正确理解其概念和背后的出发点,官方则是提供了两个具体的例子供大家参考。

Flux 的标准实现非常简单,因此还衍生出了很多第三方实现,比较著名的包括 Redux,Reflux,Fluxmm。而如今最为火热的应该属于 Redux,它采用了函数式编程的思想来维护整个应用程序的状态。其实无论哪一种框架,都是以 Flux 的架构为基础而做的演变,其核心都是单向数据流和单向数据绑定。因而本文只会介绍官方的 Flux,理解了标准实现之后也会更容易理解其他的实现方式。大家可以按照自己的兴趣和认可程度选择最适合自己的实现。

Flux 要解决的问题

在传统 MVC 框架中,通常使用双向绑定的方式来将 Model 的数据展现到 View。当 Model 中的数据发生变化时,一个或多个 View 会发生变化;当 View 接受了用户输入时,Model 中的数据则会发生变化。在实际的应用中,当一个 Model 中的数据发生变化时,也有可能另一个相关的 Model 中的数据会被同步更新。这样,很容易出现的一个现象就是连锁更新(Cascading Update),Model 可以更新 Model,Model 可以更新 View,View 也可以更新 Model。你很难去推断一个界面的变化究竟是由哪个局部的功能代码引起。如下图所示, Model 和 View 之间的关系错综复杂,导致出现问题时很难调试;实现新功能时也需要时刻注意代码是否会产生副作用。

对此问题,Flux 的解决方案是让数据流变成单向,引入 Store、Action、Action Creators 和 Dispatcher 等概念来管理信息流。如下图所示:

可以看到,数据流变成单向的。同时,数据如何被处理也被明确的定义了。在 MVC 中,数据如何处理通常由 Controller 来完成,在 Controller 中实现大部分的业务逻辑来处理数据。而现在则被清晰的定义在 Store 或者 Action Creators 中。当然,上图隐藏了一些细节,更为全面的架构图则如下所示:

在 Flux 中,View 完全是 Store 的展现形式,Store 的更新则完全由 Action 触发。得益于 React 的 View 每次更新都是整体刷新的思路,我们可以完全不必关心 Store 的变化细节,只需要监听 Store 的 onChange 事件,每次变化都触发 View 的 re-render。从而也可以看到,尽管 Flux 架构可以离开 React 单独使用,但无疑两者结合是一个更加和谐的方案,能够各发挥所长。

看一个具体的例子

为了对 Flux 有一个总体的印象,我们先考虑一个简单的使用场景:在文章评论页面提交一条评论。为此,我们需要向服务器发送一个请求提交新的评论,同时要将新的评论显示在列表中。这样的场景如果使用 Flux 去实现,大概需要实现以下几个部分:

  1. React 组件用于显示评论列表以及评论框,并绑定到 Store;
  2. 一个 Store 用于存储评论数据;
  3. Action Creator 用于向服务器发送请求;
  4. Store 中监听 Action 并进行处理,从而对 Store 自身进行更新。

整个架构如下图所示:

(点击放大图像)

整个流程的运行大概如下:

  1. 用户点击提交按钮,Action Creator 负责向服务器发送请求;
  2. 请求如果成功,那么将评论本身被添加到 Store;
  3. 请求如果失败,那么在 Store 中标记一个特别的错误状态;
  4. View 监听了 Store 的 onChange 的事件,因此,无论请求成功和失败,Store 都会触发 onChange 事件,这时 View 就会进行整体更新。

可以看到,无论请求成功和失败,都是去修改组件之外的 Store,由 Store 通知 UI 进行变化。在这样一个架构中,Store 中存储的是整个或者一部分应用程序的状态,React 实现的 View 只需要监听 Store 的变化,而无需知道变化的细节,这也是由 React 组件的特点决定的。这样,我们就使用 Flux 完成了评论功能,不同于双向绑定,在 Flux 的流程中,数据如何流转和变化,变得非常清晰明确。虽然可能需要写更多的代码,但是带来了更清楚的架构。下面,我们来具体看其中的每个具体组件的概念和用法。

View 和 Store

在 Flux 架构中,View 即 React 的组件,而 Store 则存储的是应用程序的状态。在前面的文章中我们已经介绍过,React 是完全面向 View 的解决方案,它提供了一种始终都是整体刷新的思路来构建界面。在 React 的思路中,UI 就是一个状态机,每个确定的状态对应着一个确定的界面。对于一个小的组件,它的状态可能是在其内部进行维护;而对于多个组件组成的应用程序,如果某些状态需要在组件之间进行共享,则可以将这部分状态放到 Store 中进行维护。在 Flux 中,Store 并不是一个复杂的机制,甚至 Flux 的官方实现中并没有任何 Store 相关的机制和接口,而是仅仅通过示例来描述了一个 Store 应该是什么样的数据结构。例如,在官方提供的 TodoMVC 例子 ( https://github.com/facebook/flux/tree/master/examples/flux-todomvc/ ) 中,Store 的实现如下:

复制代码
var _todos = [];
var TodoStore = assign({}, EventEmitter.prototype, {
/**
* Get the entire collection of TODOs.
* @return {object}
*/
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);
}
});

可以看到,一个 Flux 的 Store 就是一个能触发 onChange 事件的对象,能够让其它对象订阅(addChangeListener)或者取消订阅(removeChangeListener)。同时,它提供了一些 API 供 View 来获取自己需要的状态。因此,也可以将 Store 理解为需要被不同 View 共享的公用状态。

那么,已经有了 Store,React 的组件(View)该如何使用它们呢?其实很简单,只需要在 Store 每次变化时都去获取一下最新的数据即可。我们可以看下 TodoMVC 中的实现:

复制代码
var TodoStore = require('../stores/TodoStore');
/**
* Retrieve the current TODO data from the 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);
},
/**
* @return {object}
*/
render: function() {
return (
<div>
<Header />
<MainSection
allTodos={this.state.allTodos}
areAllComplete={this.state.areAllComplete}
/>
<Footer allTodos={this.state.allTodos} />
</div>
);
},
/**
* Event handler for 'change' events coming from the TodoStore
*/
_onChange: function() {
this.setState(getTodoState());
}
});

可以看到,在组件的 componentDidMount 方法中,开始监听 Store 的 onChange 事件,在 componentWillUnmount 方法中,取消监听 onChange 事件。在 Store 的每次变化后,都去重新获取自己需要的状态数据:getTodoState()。

通过这样一种很简单的机制,我们建立了从 Store 到 View 的数据绑定,每当 Store 发生变化,View 也会进行相应的更新。那么底下我们需要关心当 View 接收用户交互,需要将新的状态存入到 Store 中,应该如何去实现。这就需要引入 Flux 的另外两个概念 Dispatcher 和 Action。

Dispatcher,Action

顾名思义,Dispatcher 就是负责分发不同的 Action。在一个 Flux 应用中,只有一个中心的 Dispatcher,所有的 Action 都通过它来分发。而 Facebook 的官方 Flux 实现其实就仅仅是提供了 Dispatcher。使用 Dispatcher 只需要将其作为 npm 模块引入:

var Dispatcher = require('flux').Dispatcher;典型的,Dispatcher 有两个方法:

  1. dispatch:分发一个 Action;
  2. register:注册一个 Action 处理函数。

这样,当 View 接受了一个用户的输入之后,通过 Dispatcher 来分发一个特定的 Action,而对应的 Action 处理函数会负责去更新 Store。这个流程在文章开始的图中可以清楚的看到。因此,通常来说 Action 的处理函数会和 Store 放在一起,因为 Store 的更新都是由 Action 处理函数来完成的。例如在 TodoMVC 中,TodoStore 中会处理如下 Action:

复制代码
Dispatcher.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
}
});

无论是添加、删除还是修改一个 Todo 项,都是由 Action 来触发的。在 Action 处理函数中,不仅对 Store 进行了更新,还触发了 Store 的 onChange 事件,从而让所有监听组件能够得到通知。完整的代码可以参考: https://github.com/facebook/flux/blob/master/examples/flux-todomvc/js/stores/TodoStore.js

通过 Dispatcher 和 Action,实现了从 View 到 Store 的数据流,进而实现了整个 Flux 的单向数据流循环。从这里可以看到,Dispatcher 是全局唯一的,相当于是所有 Action 的总 hub,而每个 Action 处理函数都能够收到所有的 Action,至于需要对哪些进行处理,则由处理函数自己决定。例子中是通过 switch 来判断 Action 的 type 属性来决定如何进行处理。因此,虽然不是必须,但是一般 Action 都会有一个 type 属性来标识其类型。

Action Creators

有了上述概念和机制,基本上就已经有了 Flux 的整个架构的模型。那么 Action Creators 又是什么呢?顾名思义,Action Creators 即 Action 的创建者。它们负责去创建具体的 Action。一个 Action 可以由一个界面操作产生,也可以由一个 Ajax 请求的返回结果产生。为了让这部分逻辑更加清晰,让 View 更少的去关心数据流的细节,于是有了 Action Creators。例如,对于一个 TodoItem 组件,当用户点击其中的 Checkbox 时,会产生一个 COMPLETE_TODO 的 Action,直观的看,完全可以在 View 的内部去实现 Dispatcher.dispatch({type: ‘COMPLETE_TODO’, payload: {…}),而为了保持 View 的简单和直观,通常会在独立的 Action Creators 去封装这部分逻辑,例如:

复制代码
var AppDispatcher = require('../dispatcher/AppDispatcher');
var TodoConstants = require('../constants/TodoConstants');
var TodoActions = {
/**
* @param {string} text
*/
create: function(text) {
AppDispatcher.dispatch({
actionType: TodoConstants.TODO_CREATE,
text: text
});
},
/**
* @param {string} id The ID of the ToDo item
* @param {string} text
*/
updateText: function(id, text) {
AppDispatcher.dispatch({
actionType: TodoConstants.TODO_UPDATE_TEXT,
id: id,
text: text
});
},
}

这里的 TodoActions 就是一个 Action Creator,它把具体分发 Action 的动作封装成具有语义的方法,供 View 去使用,那么在一个 TodoItem 的组件中,其界面 JSX 可能就是:

复制代码
render: function() {
var todo = this.props.todo;
var input;
if (this.state.isEditing) {
input =
<TodoTextInput
className="edit"
onSave={this._onSave}
value={todo.text}
/>;
}
return (
<li
className={classNames({
'completed': todo.complete,
'editing': this.state.isEditing
})}
key={todo.id}>
<div className="view">
<input
className="toggle"
type="checkbox"
checked={todo.complete}
onChange={this._onToggleComplete}
/>
<label onDoubleClick={this._onDoubleClick}>
{todo.text}
</label>
<button className="destroy" onClick={this._onDestroyClick} />
</div>
{input}
</li>
);
},
_onToggleComplete: function() {
TodoActions.toggleComplete(this.props.todo);
},

通过将 Action 的创建逻辑放到 Action Creators,可以让 View 更加简单和纯粹,View 不需要知道背后是否有 Flux,而只需要知道调用某个方法来实现某个功能,从而让 View 的开发更加流畅和直观 。完整代码可以参考: https://github.com/facebook/flux/blob/master/examples/flux-todomvc/js/components/TodoItem.react.js

小结

本文介绍了 Facebook 提出的面向 React 的一种新的应用架构模式 Flux,这种架构也已经在 Facebook 内部被广泛使用,其概念和原理虽然很简单直观,但是确实被证明有能力去组织一个完整的大型应用。然而 Flux 的中心 Dispatcher 的模式,以及 Action 作为全局可知的数据流的方式,仍然有一些争论。因此,社区中也是出现了非常多的类 Flux 实现,如今最成熟的莫过于 Redux,其最大的特点在于 Action 能够分层次的去负责一个全局 Store 的不同部分,从而更容易去模块化应用状态的管理。理解了本文介绍的 Flux 概念,对于理解 Redux 也会有很大的帮助。


感谢徐川对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群(已满),InfoQ 读者交流群(#2))。

2015-12-09 16:2913802

评论

发布
暂无评论
发现更多内容

激活SBOM内生价值 推动供应链体系建设健康有序|2023开放原子全球开源峰会软件物料清单(SBOM)分论坛成功举办

开放原子开源基金会

开源 开放原子全球开源峰会 开放原子 软件物料清单(SBOM)

华为云数据库GaussDB:数字化转型的可信之选

YG科技

华为云数据库GaussDB:给世界一个更优选择

YG科技

中国科学院院士梅宏:推进构建面向全球的开源生态治理体系

开放原子开源基金会

开源 开放原子 梅宏

PDF Expert for mac:pdf编辑工具中文最新

背包客

macos PDF Mac软件 PDF Expert PDF Expert for Mac

以商业实践反哺开源技术跃升|2023开放原子全球开源峰会开源商业化创新发展分论坛成功举行

开放原子开源基金会

开源 开源商业化 开放原子全球开源峰会

2023开放原子全球开源峰会OpenCloudOS社区年会顺利举行

开放原子开源基金会

开源 开放原子 OpenCloudOS社区

深度学习应用篇-元学习[15]:基于度量的元学习:SNAIL、RN、PN、MN

汀丶人工智能

人工智能 深度学习 元学习 元强化学习 6 月 优质更文活动

对线面试官-Sychronized和ReentrantLock

派大星

Java 面试题

行业翘楚论道未来想象|2023开放原子全球开源峰会云原生分论坛圆满落幕

开放原子开源基金会

开源 云原生 开放原子

OneCode-流程引擎设计

codebee

Alibaba官方上线!SpringBoot+SpringCloud全彩指南

互联网架构师小马

Java 微服务 Spring Cloud Spring Boot

阿里资深专家撰写出的Nginx底层与源码分析手册,GitHub已爆赞

互联网架构师小马

nginx

华为云数据库GaussDB:数字化转型和可持续发展的更优选择

YG科技

C语言编程—命令行参数

芯动大师

C语言 命令行参数解析 6 月 优质更文活动

深入理解Pod的核心知识

穿过生命散发芬芳

pod 6 月 优质更文活动

TS接口类型

不叫猫先生

6 月 优质更文活动

深度学习应用篇-元学习[16]:基于模型的元学习-Learning to Learn优化策略、Meta-Learner LSTM

汀丶人工智能

人工智能 深度学习 元学习 元强化学习 6 月 优质更文活动

Nautilus Chain测试网迎阶段性里程碑,模块化区块链拉开新序幕

股市老人

VSD Viewer for Mac:Visio绘图文件阅读器

背包客

macos Mac软件 Visio文件查看工具 Visio Viewer for Mac VSD Viewer

ubuntu服务器安装图形化界面总结

Studying_swz

6 月 优质更文活动

MySQL8新特性窗口函数详解

越长大越悲伤

MySQL sql

NestJs 管道(Pipe)

小鑫同学

typescript 后端 nestjs 6 月 优质更文活动

Generative AI 新世界:文本生成领域论文解读

亚马逊云科技 (Amazon Web Services)

机器学习

柏睿向量存储查询引擎基于LLM实现特定垂直领域的知识问答功能

新消费日报

这项国家级安全检测,腾讯云慧眼安全能力再获认可!

牵着蜗牛去散步

人工智能 腾讯云 网络安全 腾讯云AI 腾讯云慧眼人脸核身

DEVONthink Pro 3 for Mac:Mac强大的文档管理工具

背包客

macos 文档管理软件 Mac软件 DEVONthink Pro DEVONthink

STM32L0 ADC使用HAL库关于校准问题的说明

矜辰所致

STM32L051 ADC 6 月 优质更文活动

Nautilus Chain测试网迎阶段性里程碑,模块化区块链拉开新序幕

BlockChain先知

基于STM32设计的智能灌溉控制系统

DS小龙哥

6 月 优质更文活动

深入浅出React(五):使用Flux搭建React应用程序架构_语言 & 开发_王沛_InfoQ精选文章