作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Alex Rattray's profile image

Alex Rattray

亚历克斯是一名企业家,毕业于沃顿商学院(Wharton),他最近关闭了一家初创公司,并在旅行期间签订了合同.

Previously At

Stripe
Share

React.js is a fantastic library. 有时候,这似乎是继Python之后最好的事情. 然而,React只是前端应用程序栈的一部分. 在管理数据和状态方面,它提供的功能并不多.

Facebook, the makers of React, have offered some guidance there in the form of Flux. Flux是一个“应用架构”(而不是框架),它使用React视图围绕单向数据流构建而成, an Action Dispatcher, and Stores. Flux模式通过体现事件控制的重要原则来解决一些主要问题, 使React应用程序更容易推理, develop, and maintain.

Here, 我将介绍控制流的基本Flux示例, discuss what’s missing for Stores, 以及如何使用骨干模型和集合以“兼容flux”的方式填补空白.

(注意:为了方便和简洁,我在示例中使用CoffeeScript. 非coffeescript开发人员应该能够跟随,并且可以将示例视为伪代码.)

Introduction to Facebook’s Flux

Backbone 是一个优秀的、经过严格审查的小库,包括视图、模型、集合和路由. It’s a de facto 结构化前端应用程序的标准库, 自2013年推出React以来,它一直与React应用配合使用. 大多数React的例子都是在Facebook之外.到目前为止,com已经提到了Backbone的串联使用.

Unfortunately, 仅依靠Backbone来处理React视图之外的整个应用流程会带来不幸的复杂性. 当我第一次开始编写React-Backbone应用程序代码时,我拥有的“复杂事件链” read about 没过多久它们就长出了水螅状的头. 将事件从UI发送到模型, 然后从一个模型到另一个模型,然后再回来, 这样就很难追踪谁在换谁了, in what order, and why.

本Flux教程将演示Flux模式如何以令人印象深刻的轻松和简单的方式处理这些问题.

An Overview

Flux的口号是“单向数据流”。. Here’s a handy diagram from the Flux docs showing what that flow looks like:

Facebook Flux使用“单向数据流”模型,与React和Backbone配对时略有不同.

重要的是物质从 React --> Dispatcher --> Stores --> React.

让我们来看看每个主要组件是什么以及它们是如何连接的:

文档还提供了这个重要的警告:

比起框架,Flux更像是一个模式,并且没有任何硬依赖. 然而,我们经常使用EventEmitter作为store的基础,并使用React作为视图的基础. Flux在其他地方不容易获得的部分是Dispatcher. 这个模块可以在这里完成你的Flux工具箱.

So Flux has three components:

  1. Views (React = require('react'))
  2. Dispatcher (Dispatcher = require('flux').Dispatcher)
  3. Stores (EventEmitter = require('events').EventEmitter)
    • (or, as we’ll soon see, Backbone = require('backbone'))

The Views

I won’t describe React here, 因为有很多关于它的文章, 除了说比起Angular,我更喜欢它. I almost never feel confused 在编写React代码时,不像Angular,但当然,意见会有所不同.

The Dispatcher

Flux Dispatcher是一个处理所有修改store的事件的地方. To use it, you have each Store register 一个处理所有事件的回调. 然后,当您想要修改Store时,您可以 dispatch an event.

像React一样,Dispatcher给我的印象是一个好主意,实现得很好. 例如,允许用户向待办事项列表添加项目的应用程序可能包括以下内容:

# in TodoDispatcher.coffee
Dispatcher = require("flux").Dispatcher

TodoDispatcher = new Dispatcher() #这就是所需要的!.

module.exports = TodoDispatcher    
# in TodoStore.coffee
TodoDispatcher = require("./TodoDispatcher")

TodoStore = {items: []}

TodoStore.dispatchCallback = (payload) ->
  switch payload.actionType
    when "add-item"
      TodoStore.items.push payload.item
    when "delete-last-item"
      TodoStore.items.pop()

TodoStore.dispatchToken = TodoDispatcher.registerCallback(TodoStore.dispatchCallback)

module.exports = TodoStore
# in ItemAddComponent.coffee
TodoDispatcher = require("./TodoDispatcher")

ItemAddComponent = React.createClass
  handleAddItem: ->
    #注意:你不只是直接推到商店!
    #(通过调度程序移动的限制
    #使一切更加模块化和可维护)
    TodoDispatcher.dispatch
      actionType: "add-item"
      item: "hello world"

  render: ->
    React.DOM.button {
      onClick: @handleAddItem
    },
      "Add an Item!"

这让我们很容易回答两个问题:

  1. Q: What are all the events that modify MyStore?
    • A: Just check the cases in the switch statement in MyStore.dispatchCallback.
  2. 问:该事件的所有可能来源是什么?
    • A: Simply search for that actionType.

这比,例如,查找 MyModel.set and MyModel.save and MyCollection.add 等等,在这些地方,找到这些基本问题的答案变得非常困难.

Dispatcher还允许您在一个简单的, synchronous fashion, using waitFor. For example:

# in MessageStore.coffee
MyDispatcher = require("./MyDispatcher")
TodoStore = require("./TodoStore")

MessageStore = {items: []}

MessageStore.dispatchCallback = (payload) ->
  switch payload.actionType
    when "add-item"
      # synchronous event flow!
      MyDispatcher.waitFor [TodoStore.dispatchToken]

      MessageStore.items.push "You added an item! It was: " + payload.item

module.exports = MessageStore

In practice, 当使用Dispatcher修改store时,我惊讶地发现代码变得如此简洁, even without using waitFor.

The Stores

So data flows into Stores through the Dispatcher. Got it. 但是数据是如何从store流向view的呢.e., React)? As stated in the Flux docs:

视图监听由它所依赖的商店广播的事件.

Okay, great. 就像我们在store中注册回调一样, 我们用视图(React组件)注册回调函数。. We tell React to re-render 在Store中发生更改时,该更改通过其 props.

For example:

# in TodoListComponent.coffee
React = require("react")

TodoListComponent = React.createClass
  componentDidMount: ->
    @props.TodoStore.addEventListener "change", =>
      @forceUpdate()
    , @

  componentWillUnmount: ->
    # remove the callback

  render: ->
    # show the items in a list.
    React.DOM.ul {}, @props.TodoStore.items.map (item) ->
      React.DOM.li {}, item

Awesome!

So how do we emit that "change" event? Well, Flux recommends using EventEmitter. From an official example:

var MessageStore = merge(EventEmitter.prototype, {

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

  /**
   * @param {function} callback
   */
  addChangeListener:函数(回调){
    this.on(CHANGE_EVENT, callback);
  },

  get: function(id) {
    return _messages[id];
  },

  getAll: function() {
    return _messages;
  },
// etc...

Gross! 每次我想要一个简单的Store,我都得自己写? 每次我想要显示一条信息时,我应该使用哪个? There has to be a better way!

The Missing Piece

Backbone的模型和集合已经具备了Flux的基于eventemitter的store似乎正在做的一切.

By telling you to use raw EventEmitter, Flux建议你重新创建Backbone模型的50-75% & 集合,每次创建一个Store. 为你的商店使用EventEmitter就像使用裸节点.当构建良好的微框架(如Express.Js或类似的东西已经存在,可以处理所有的基础和样板.

Just like Express.js is built on Node.在js中,Backbone的模型和集合是建立在EventEmitter上的. 它拥有你几乎总是需要的所有东西:Backbone排放 change 事件,并有查询方法,getter和setter等等. Plus, Backbone’s Jeremy Ashkenas and his army of 230 contributors 在这些事情上做得比我可能做得更好吗.

作为本主干教程的一个示例,我将上面的MessageStore示例转换为 a Backbone version.

客观上代码更少(不需要重复工作),主观上更清晰和简洁(例如, this.add(message) instead of _messages[message.id] = message).

So let’s use Backbone for Stores!

FluxBone模式:通过主干存储通量

本教程是我自豪地称之为方法的基础 FluxBone使用Backbone for Stores的Flux架构. 下面是FluxBone架构的基本模式:

  1. 存储是实例化的主干模型或集合, 哪些已经向Dispatcher注册了回调. 通常,这意味着它们是单例的.
  2. View components never 直接修改存储(例如:no .set()). 相反,组件将action分派给Dispatcher.
  3. 查看组件查询store并绑定到它们的事件以触发更新.

本骨干教程旨在了解骨干和Flux在React应用程序中的协同工作方式.

让我们用Backbone和Flux的例子来依次看看其中的每一部分:

1. 存储是实例化的主干模型或集合, 哪些已经向Dispatcher注册了回调.

# in TodoDispatcher.coffee
Dispatcher = require("flux").Dispatcher

TodoDispatcher = new Dispatcher() #这就是所需要的!

module.exports = TodoDispatcher
# in stores/TodoStore.coffee
Backbone = require("backbone")
TodoDispatcher = require("../dispatcher")

TodoItem = Backbone.Model.extend({})

TodoCollection = Backbone.Collection.extend
  model: TodoItem
  url: "/todo"

  #我们在init时向Dispatcher注册一个回调.
  initialize: ->
    @dispatchToken = TodoDispatcher.register(@dispatchCallback)

  dispatchCallback: (payload) =>
    switch payload.actionType
      #从Store中移除Model实例.
      when "todo-delete"
        @remove payload.todo
      when "todo-add"
        @add payload.todo
      when "todo-update"
        # do stuff...
        @add payload.todo,
          merge: true
      # ... etc


# the Store is an instantiated Collection; a singleton.
TodoStore = new TodoCollection()
module.exports = TodoStore

2. Components never 直接修改存储(例如:no .set()). 相反,组件将action分派给Dispatcher.

# components/TodoComponent.coffee
React = require("react")

TodoListComponent = React.createClass
  handleTodoDelete: ->
    #不是直接从TodoStore中删除todo,
    # we use the Dispatcher
    TodoDispatcher.dispatch
      actionType: "todo-delete"
      todo: @props.todoItem
  # ... (see below) ...

module.exports = TodoListComponent

3. 组件查询store并绑定到它们的事件以触发更新.

# components/TodoComponent.coffee
React = require("react")

TodoListComponent = React.createClass
  handleTodoDelete: ->
    #不是直接从TodoStore中删除todo,
    # we use the dispatcher. #flux
    TodoDispatcher.dispatch
      actionType: "todo-delete"
      todo: @props.todoItem
  # ...
  componentDidMount: ->
    组件绑定到Store的事件
    @props.TodoStore.on "add remove reset", =>
      @forceUpdate()
    , @
  componentWillUnmount: ->
    #关闭所有具有此上下文的事件和回调
    @props.TodoStore.off null, null, this
  render: ->
    React.DOM.ul {},
      @props.TodoStore.items.map (todoItem) ->
        # TODO: TodoItemComponent,它将绑定到
        # `this.props.todoItem.on('change')`
        TodoItemComponent {
          todoItem: todoItem
        }

module.exports = TodoListComponent

我已经将这种Flux和Backbone方法应用到我自己的项目中, 有一次我重新架构了我的React应用程序来使用这个模式, almost all the ugly bits disappeared. It was a little miraculous: one by one, 那些让我咬牙切齿寻找更好方法的代码片段被合理的流程所取代. 而Backbone在这种模式中整合的流畅性是非常了不起的:我不觉得我在和Backbone战斗, Flux, 或者React,以便将它们整合到一个应用程序中.

Example Mixin

Writing the this.on(...) and this.off(...) 每次将FluxBone Store添加到组件时,代码都可能变得有点陈旧.

Here’s an example React Mixin that, while extremely naive, 肯定会使快速迭代变得更加容易:

# in FluxBoneMixin.coffee
module.exports = (propName) ->
  componentDidMount: ->
    @props[propName].on "all", =>
      @forceUpdate()
    , @

  componentWillUnmount: ->
    @props[propName].off "all", =>
      @forceUpdate()
    , @
# in HelloComponent.coffee
React = require("react")

UserStore = require("./stores/UserStore")
TodoStore = require("./stores/TodoStore")

FluxBoneMixin = require("./FluxBoneMixin")


MyComponent = React.createClass
  mixins: [
    FluxBoneMixin("UserStore"),
    FluxBoneMixin("TodoStore"),
  ]
  render: ->
    React.DOM.div {},
      "Hello, #{ @props.UserStore.get('name') },
      you have #{ @props.TodoStore.length }
      things to do."

React.renderComponent(
  MyComponent {
    UserStore: UserStore
    TodoStore: TodoStore
  }
  , document.body.querySelector(".main")
)

Syncing with a Web API

In the original Flux diagram, 你只能通过actioncreator与Web API交互, 在向Dispatcher发送操作之前需要服务器的响应. That never sat right with me; shouldn’t the Store be the first to know about changes, before the server?

我选择翻转图表的这一部分:商店通过Backbone直接与RESTful CRUD API交互 sync(). 这非常方便,至少在使用实际的RESTful CRUD API时是这样.

数据完整性的维护没有问题. When you .set() a new property, the change 事件触发React重新渲染,乐观地显示新数据. When you try to .save() it to the server, the request 事件让您知道要显示加载图标. When things go through, the sync 事件让您知道是要删除加载图标,还是要删除 error event lets you know to turn things red. You can see inspiration here.

还有验证(和相应的 invalid 事件)作为第一层防御,以及 .fetch() 方法从服务器提取新信息.

对于不太标准的任务,通过actioncreator进行交互可能更有意义. 我怀疑Facebook并没有做太多“纯粹的垃圾”。, 在这种情况下,他们不把商店放在第一位也就不足为奇了.

Conclusion

Facebook的工程团队做了出色的工作 push the front-end web forward with React, Flux的引入让我们看到了一个真正可扩展的更广泛的架构:不仅仅是在技术方面, but engineering as well. 聪明而谨慎地使用主干(根据本教程的例子)可以填补Flux的空白, 让从单人独立商店到大公司的任何人都可以轻松地创建和维护令人印象深刻的应用程序.

Hire a Toptal expert on this topic.
Hire Now
Alex Rattray's profile image
Alex Rattray

Located in San Francisco, CA, United States

Member since September 7, 2014

About the author

亚历克斯是一名企业家,毕业于沃顿商学院(Wharton),他最近关闭了一家初创公司,并在旅行期间签订了合同.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Previously At

Stripe

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Toptal Developers

Join the Toptal® community.