Flux 架构入门教程

作者: 阮一峰

日期: 2016年1月15日

过去一年中,前端技术大发展,最耀眼的明星就是React

React 本身只涉及UI层,如果搭建大型应用,必须搭配一个前端框架。也就是说,你至少要学两样东西,才能基本满足需要:React + 前端框架。

Facebook官方使用的是 Flux 框架。本文就介绍如何在 React 的基础上,使用 Flux 组织代码和安排内部逻辑,使得你的应用更易于开发和维护。

阅读本文之前,我假设你已经掌握了 React 。如果还没有,可以先看我写的《React入门教程》。与以前一样,本文的目标是使用最简单的语言、最好懂的例子,让你一看就会。

一、Flux 是什么?

简单说,Flux 是一种架构思想,专门解决软件的结构问题。它跟MVC 架构是同一类东西,但是更加简单和清晰

Flux存在多种实现(至少15种),本文采用的是Facebook官方实现

二、安装 Demo

为了便于讲解,我写了一个Demo

请先安装一下。


$ git clone https://github.com/ruanyf/extremely-simple-flux-demo.git
$ cd extremely-simple-flux-demo && npm install
$ npm start

然后,访问 http://127.0.0.1:8080 。

你会看到一个按钮。这就是我们的Demo。

三、基本概念

讲解代码之前,你需要知道一些 Flux 的基本概念。

首先,Flux将一个应用分成四个部分。

  • View: 视图层
  • Action(动作):视图层发出的消息(比如mouseClick)
  • Dispatcher(派发器):用来接收Actions、执行回调函数
  • Store(数据层):用来存放应用的状态,一旦发生变动,就提醒Views要更新页面

Flux 的最大特点,就是数据的"单向流动"。

  1. 用户访问 View
  2. View 发出用户的 Action
  3. Dispatcher 收到 Action,要求 Store 进行相应的更新
  4. Store 更新后,发出一个"change"事件
  5. View 收到"change"事件后,更新页面

上面过程中,数据总是"单向流动",任何相邻的部分都不会发生数据的"双向流动"。这保证了流程的清晰。

读到这里,你可能感到一头雾水,OK,这是正常的。接下来,我会详细讲解每一步。

四、View(第一部分)

请打开 Demo 的首页index.jsx ,你会看到只加载了一个组件。


// index.jsx
var React = require('react');
var ReactDOM = require('react-dom');
var MyButtonController = require('./components/MyButtonController');

ReactDOM.render(
  <MyButtonController/>,
  document.querySelector('#example')
);

上面代码中,你可能注意到了,组件的名字不是 MyButton,而是 MyButtonController。这是为什么?

这里,我采用的是 React 的 controller view 模式。"controller view"组件只用来保存状态,然后将其转发给子组件。MyButtonController源码很简单。


// components/MyButtonController.jsx
var React = require('react');
var ButtonActions = require('../actions/ButtonActions');
var MyButton = require('./MyButton');

var MyButtonController = React.createClass({
  createNewItem: function (event) {
    ButtonActions.addNewItem('new item');
  },

  render: function() {
    return <MyButton
      onClick={this.createNewItem}
    />;
  }
});

module.exports = MyButtonController;

上面代码中,MyButtonController将参数传给子组件MyButton。后者的源码甚至更简单。


// components/MyButton.jsx
var React = require('react');

var MyButton = function(props) {
  return <div>
    <button onClick={props.onClick}>New Item</button>
  </div>;
};

module.exports = MyButton;

上面代码中,你可以看到MyButton是一个纯组件(即不含有任何状态),从而方便了测试和复用。这就是"controll view"模式的最大优点。

MyButton只有一个逻辑,就是一旦用户点击,就调用this.createNewItem 方法,向Dispatcher发出一个Action。


// components/MyButtonController.jsx

  // ...
  createNewItem: function (event) {
    ButtonActions.addNewItem('new item');
  }

上面代码中,调用createNewItem方法,会触发名为addNewItem的Action。

五、Action

每个Action都是一个对象,包含一个actionType属性(说明动作的类型)和一些其他属性(用来传递数据)。

在这个Demo里面,ButtonActions 对象用于存放所有的Action。


// actions/ButtonActions.js
var AppDispatcher = require('../dispatcher/AppDispatcher');

var ButtonActions = {
  addNewItem: function (text) {
    AppDispatcher.dispatch({
      actionType: 'ADD_NEW_ITEM',
      text: text
    });
  },
};

上面代码中,ButtonActions.addNewItem方法使用AppDispatcher,把动作ADD_NEW_ITEM派发到Store。

六、Dispatcher

Dispatcher 的作用是将 Action 派发到 Store、。你可以把它看作一个路由器,负责在 View 和 Store 之间,建立 Action 的正确传递路线。注意,Dispatcher 只能有一个,而且是全局的。

Facebook官方的 Dispatcher 实现输出一个类,你要写一个AppDispatcher.js,生成 Dispatcher 实例。


// dispatcher/AppDispatcher.js
var Dispatcher = require('flux').Dispatcher;
module.exports = new Dispatcher();

AppDispatcher.register()方法用来登记各种Action的回调函数。


// dispatcher/AppDispatcher.js
var ListStore = require('../stores/ListStore');

AppDispatcher.register(function (action) {
  switch(action.actionType) {
    case 'ADD_NEW_ITEM':
      ListStore.addNewItemHandler(action.text);
      ListStore.emitChange();
      break;
    default:
      // no op
  }
})

上面代码中,Dispatcher收到ADD_NEW_ITEM动作,就会执行回调函数,对ListStore进行操作。

记住,Dispatcher 只用来派发 Action,不应该有其他逻辑。

七、Store

Store 保存整个应用的状态。它的角色有点像 MVC 架构之中的Model 。

在我们的 Demo 中,有一个ListStore,所有数据都存放在那里。


// stores/ListStore.js
var ListStore = {
  items: [],

  getAll: function() {
    return this.items;
  },

  addNewItemHandler: function (text) {
    this.items.push(text);
  },

  emitChange: function () {
    this.emit('change');
  }
};

module.exports = ListStore;

上面代码中,ListStore.items用来保存条目,ListStore.getAll()用来读取所有条目,ListStore.emitChange()用来发出一个"change"事件。

由于 Store 需要在变动后向 View 发送"change"事件,因此它必须实现事件接口。


// stores/ListStore.js
var EventEmitter = require('events').EventEmitter;
var assign = require('object-assign');

var ListStore = assign({}, EventEmitter.prototype, {
  items: [],

  getAll: function () {
    return this.items;
  },

  addNewItemHandler: function (text) {
    this.items.push(text);
  },

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

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

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

上面代码中,ListStore继承了EventEmitter.prototype,因此就能使用ListStore.on()ListStore.emit(),来监听和触发事件了。

Store 更新后(this.addNewItemHandler())发出事件(this.emitChange()),表明状态已经改变。 View 监听到这个事件,就可以查询新的状态,更新页面了。

八、View (第二部分)

现在,我们再回过头来修改 View ,让它监听 Store 的 change 事件。


// components/MyButtonController.jsx
var React = require('react');
var ListStore = require('../stores/ListStore');
var ButtonActions = require('../actions/ButtonActions');
var MyButton = require('./MyButton');

var MyButtonController = React.createClass({
  getInitialState: function () {
    return {
      items: ListStore.getAll()
    };
  },

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

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

  _onChange: function () {
    this.setState({
      items: ListStore.getAll()
    });
  },

  createNewItem: function (event) {
    ButtonActions.addNewItem('new item');
  },

  render: function() {
    return <MyButton
      items={this.state.items}
      onClick={this.createNewItem}
    />;
  }
});

上面代码中,你可以看到当MyButtonController 发现 Store 发出 change 事件,就会调用 this._onChange 更新组件状态,从而触发重新渲染。


// components/MyButton.jsx
var React = require('react');

var MyButton = function(props) {
  var items = props.items;
  var itemHtml = items.map(function (listItem, i) {
    return <li key={i}>{listItem}</li>;
  });

  return <div>
    <ul>{itemHtml}</ul>
    <button onClick={props.onClick}>New Item</button>
  </div>;
};

module.exports = MyButton;

九、致谢

本文受到了Andrew Ray 的文章《Flux For Stupid People》的启发。

(完)

珠峰培训

一灯学院

留言(66条)

好复杂,看一遍根本不懂

action不一定要放在controller view里,要是组件层次很深,传props会很累的,可以直接放在需要和action打交道的view里,fb的官方示例(todomcv和chat)就是这么做的

文章中 MyButtonController.jxs 的示例中的类名误写成 MyButton 了.

第一部分:
var MyButton = React.createClass({
......
});

这里的 MyButton 应该是 MyButtonController 吧?

Flux 的设计理念确实很棒,但是一个简单的列表就可以搞这么复杂,实在是不太利于做项目

@abc,@夜幕下的猫:

谢谢指出,已经改过来了。

学海无涯

刚看了一遍没有看懂

我表示非常不认同把Flux与MVC放在一起说,Flux完全是另外一种Pattern。

刚好前两天看到有人在抱怨前端技术…… https://medium.com/@wob/the-sad-state-of-web-development-1603a861d29f#.iuy0tz462

你们说的我都不懂,我就看看热闹。

前端现在就是黎明前的黑暗,大家各自寻找解决方案,一两年后就会清晰。

看了一遍大概明白Flux的设计思路了,谢谢阮老师能把教程写的如此简单易懂,理解起来倒不困难就是感觉实现一个简单的功能都这么复杂,实在有点累觉不爱啊。

使用Angular1.x版本中,未来会接触React,加油!

楼主为什么不用ES6写呢?

引用kingguy的发言:

楼主为什么不用ES6写呢?

很简单,作为入门教程来讲,降低学习门槛以达到分享知识见解的目的是很重要的,很多初学者不一定接触过ES6,只是想了解下Flux,React官网教程也没有在主篇幅用Es6,只是在一些变化处写了说明,对于想用Es6写的这也不是什么问题

买一本书,支持一下,阮老师的书还是很好的

对于入门的demo来说简单才是王道。看了你的demo,再看看官方的“Flux TodoMVC Example,思路渐渐清晰,非常感谢楼主分享,当初react就是看楼主的文章入门。

感谢分享,思路很清晰

监听 Action 的回调那段写的不对吧?应该写在 Store 里,而不是 AppDispatcher;

比如官方 example 里的这一段https://github.com/facebook/flux/blob/master/examples%2Fflux-todomvc%2Fjs%2Fstores%2FTodoStore.js

而且例子里如果能举一下 waitFor 的用法,更能体现 Flux 的精髓

贴个一年前翻译的启发文章链接。http://caichao.me/2015-01-14-FluxForStupidPeople.html

多谢ruan大神的分享,之前看flux一直没看懂,现在清晰多了

阮老师的博客,思路清晰,赞赞赞....

引用edokeh的发言:

监听 Action 的回调那段写的不对吧?应该写在 Store 里,而不是 AppDispatcher;

比如官方 example 里的这一段https://github.com/facebook/flux/blob/master/examples%2Fflux-todomvc%2Fjs%2Fstores%2FTodoStore.js

而且例子里如果能举一下 waitFor 的用法,更能体现 Flux 的精髓

同问 AppDispatcher 这段回调为什么不写在store里

@edokeh,@tata:

我觉得,与派发器有关的逻辑,都放在AppDispatch比较好。这样,Store就不用加载AppDispatch了。

一直没能下定决心去学react,就是因为代码看起来好复杂啊,看一遍根本没看懂,代码可读性不高,看起来乱七八糟的的感觉

阮大大,其实我觉得flux是mvc的更好的实践吧,action类似于controller的角色。
第一:改了名字之后,感觉更加向ui靠拢,意为ui的动作,更加直接的指向了ui输入。另外,这样做的好处也是为每一个ui输入留下一笔记录。
第二:也更加明确了action的范围,只是向model发送事件,不对model做任何直接的改变,model内部通过action发来的事件自己处理数据。这也使得model的改变有了稳定的位置,有利于控制和追踪代码。

不知道我这样的理解是否正确?

阮老师啥时候讲下Redux,很是期待哈。

Flux For Stupid People

这到底是在客户端运行的,还是在node.js中运行的。
现在出的东西,对于我这种菜鸟来说真的搞不懂

Facebook画的那个MVC的图,View和Model之间是直接互相调用的,而且这么多个View和Model才对应一个Controller,似乎跟我对MVC的理解有很大出入。
我分别用Flux和MVC各实现了一次Todo。老实说,没看出Flux的优点,反倒觉得有以下缺点:
1. 规定Dispatcher为全局的单例。个人认为任何全局对象或者单例都会降低对它有依赖的类的内聚性,也会造成对View和Store单元测试上的障碍。比方说,定义一个class A,它内部调用了单例class B。首先A的接口没有体现出这种依赖,单元测试也不方便mock。其次对A的所有测试用例,都要把B的行为考虑进去。
2. View同时依赖于Store和Dispatcher。在MVC中,Model和View的所有事件都通过Controller来中转,保证了View和Model的独立。而Flux中,View触发事件(Action)时是通过Dispatcher间接传到Store上,但取数据时又是直接访问Store。导致View需要同时依赖于Store和Dispatcher,这更像是一种倒退。

就说那么多吧。限于个人水平,认识可能有许多误解,请大神帮忙指正,感激不尽~~

_onChange: function () {
this.setState({
items: ListStore.getAll()
});
},
这个触发后, this 对象变为了 store 对象。没有setState 方法,老师遇到这个问题没

额,一遍看下来表示完全不懂,还是多看几遍把。

对flux的action dispatch表示费解, 和 PubSub一样吗?
我直接放弃flux,用pubsub来处理。
最上层的组件(Controller)subscribe事件, 子组件(View)publish触发事件去该表store(Model)。
store变化由最顶层setState, 子组件通过 shouldComponentUpdate(nextProps, nextState) 来决定是否更新。
感觉本质上应该是一样的。

之前看了一遍没看懂,看了几遍之后,再自己写写,终于搞懂了flux。不过在实际项目中,如果项目不是很大,发觉这个东西没什么用处。而且如果一个项目好几个人一起做,有些人会flux,有些不会,沟通成本其实还蛮大的

阮老师的这篇讲解Flux技术的文章比之于其它草草翻译官方文档的文章来说的确非常容易理解,深入浅出。仔细看了两三遍后,的确明白了这种架构数据流的过程。但是,其实还有一点不甚清楚的就是为什么要如此来处理数据流,这样有什么好处等等,关于这几点感觉文章并没有提到。因为,单纯从例子来看,反而觉得不用Flux会更简洁、方便一些。

> webpack-dev-server --progress 0% compileevents.js:141 throw er; // Unhandled 'error' event

npm ERR! node v4.2.4
npm ERR! npm v2.14.12
npm ERR! code ELIFECYCLE
npm ERR! flux-for-stupid-people-demo@1.0.0 start: `webpack-dev-server --progress`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the flux-for-stupid-people-demo@1.0.0 start script 'webpack-dev-server --progress'.
npm ERR! This is most likely a problem with the flux-for-stupid-people-demo package,
npm ERR! not with npm itself.
npm ERR! Tell the author that this fails on your system:
npm ERR! webpack-dev-server --progress
npm ERR! You can get their info via:
npm ERR! npm owner ls flux-for-stupid-people-demo
npm ERR! There is likely additional logging output above.


请问阮老师,npm start启动的时候,控制台报了以上错误,应该怎么解决呢?没有接触过webpack,问题应该很低级,实在抱歉。

@二点零:

我这里是正常的,没法复现这个错误,猜不出原因啊。要不你删掉 node_modules 目录,重装一次。

引用阮一峰的发言:

@二点零:

我这里是正常的,没法复现这个错误,猜不出原因啊。要不你删掉 node_modules 目录,重装一次。

问了你之后不久就解决了,因为之前没有了解和学习过webpack,后面专门去入门了一下。问题原因是webpack没有全局安装。感谢阮老师,学习了很多!!!

> webpack-dev-server --progress --port 8888
0% compile http://localhost:8888/webpack-dev-server/
webpack result is served from /
content is served from D:\git\extr 95% emit
npm ERR! Windows_NT 6.1.7600
npm ERR! argv "C:\\Program Files\\nodejs\\node.exe" "C:\\Program Files\\nodejs\\ node_modules\\npm\\bin\\npm-cli.js" "start"
npm ERR! node v4.4.4
npm ERR! npm v2.15.1
npm ERR! code ELIFECYCLE
npm ERR! flux-for-stupid-people-demo@1.0.0 start: `webpack-dev-server --progress --port 8888`
npm ERR! Exit status 3221225501
npm ERR!
npm ERR! npm bugs flux-for-stupid-people-demo
npm ERR! D:\git\extremely-simple-flux-demo\npm-debug.log
windows上报这个错误有没有遇到过

请收下我的膝盖,顺便啥时候出个 redux 的教程呢?

阮老师好,问个题外话,为什么执行demo的时候不能直接用webpack-dev-server来执行?而一定要用package.json中的npm start? npm start不也是执行webpack-dev-server --progress么?

老师讲讲webpack哈

刚开始学flux,有点看不懂,看来得多看几遍啊。哈哈

确实是一开始有点儿费解,头一遍完全晕掉

其实我的理解 就是广播机制更细化了而已……
先说说对广播机制的理解----有一个radio center,任何对象都可以订阅它
如radio.on(xxx,function(){ do....})
然后也可以通过它来发消息
Radio.push(xxx)
订阅了该消息的对象,响应的做些行为

而flux就是把on和push的写法抽离到了action和dispatch里统一管理,但这种抽离不见的一定好。

不知道我的理解对不对……

之前一直接触的是js和页面,对于安装的指令看不明白,现在都无法启动例子。有没有简单的教程呢?

@diper:

没安装全局webpack. 执行npm install webpack -g

git clone之后是不是还要npm install啊

阮老师什么时候讲讲redux和webpack啊?

引用front-thinking的发言:

阮老师的这篇讲解Flux技术的文章比之于其它草草翻译官方文档的文章来说的确非常容易理解,深入浅出。仔细看了两三遍后,的确明白了这种架构数据流的过程。但是,其实还有一点不甚清楚的就是为什么要如此来处理数据流,这样有什么好处等等,关于这几点感觉文章并没有提到。因为,单纯从例子来看,反而觉得不用Flux会更简洁、方便一些。


组件化开发之后就明白好处了啊。细流合成江湖,公用状态方便获取等。
二是数据都汇起来了方便做实时监测。

阮老师博客最棒的地方就是能把复杂的东西变成简单的。让大家都能读懂。感谢。

感觉确实不如redux思路清晰,只是作为了解皆可以了。

讲的很棒,一遍就懂了。

之前用过vuex,但概念上一直有点模糊,看了阮大神的文章,感觉清晰多了。

在store的数据层为什么addChangeListener里面的this后面跟on而在removeChangeListener里面的this后面不是更on的?
有点不理解求解中

请问阮老师MyButton那个组件是怎么回事?为什么是function的?import了React为什么没有用到的?








大神我下载了代码,在自己的机器上看,为啥没有这两个js文件(init.js,bundle.js)呢?访问报错了

引用tylerdong的发言:

大神我下载了代码,在自己的机器上看,为啥没有这两个js文件(init.js,bundle.js)呢?访问报错了

webpack构建

学React没多久,看您这篇文章遇到一个困惑。在您的MyButtonController.jsx中,return了标签,按理说这个MyButton应该是一个组件,但是为什么在Mybutton.jsx中却没有用React.createClass去创建这个组件,而Mybutton却是一个function呢?求解惑...

引用wikiJane的发言:

请问阮老师MyButton那个组件是怎么回事?为什么是function的?import了React为什么没有用到的?

同问,在MyButtonController.jsx引用了这个《MyButton /》这个标签,这个标签按说应该是一个组件,可是在MyButton.jsx却是用function去定义的这个?为什么这样,如果这样定义,他为什么可以通过标签《MyButton /》去引用?

为什么在启动的时候,会报错:
C:\Users\myliu\WebstormProjects\lmy_developer\node_modules\open-browser-webpack-plugin\index.js:50
throw err+url;
^
Error: Command failed: C:\Windows\system32\cmd.exe /s /c "start "" "http://localhost:8080""
ϵͳ�޷�ִ��ָ���ij�����
然后在webpack.config.js中删除插件openBrowser在启动就可以了呢?

和MVC有什么不同,感觉是一样的

_onChange那里确实会报this.setState is undefined

引用LiShuxue的发言:

同问,在MyButtonController.jsx引用了这个《MyButton /》这个标签,这个标签按说应该是一个组件,可是在MyButton.jsx却是用function去定义的这个?为什么这样,如果这样定义,他为什么可以通过标签《MyButton /》去引用?

看下创建React组件的方式,有三种方式,大神用的这种叫无状态组件,

createCLass方式是es5的方式,还有一种es6的方式,就是class Mybutton extends React.component{ //xxxx }

这个组件 如果多次使用的话 就是一个页面出现多个 不能独立开来 数据都是共享的

终于把整个流程搞明白了

我要发表看法

«-必填

«-必填,不公开

«-我信任你,不会填写广告链接