个性化阅读
专注于IT技术分析

使用Flux和Backbone的React App数据流(带有示例)

本文概述

React.js是一个很棒的库。有时, 这似乎是自切片Python以来最好的事情。但是, React只是前端应用程序堆栈的一部分。在管理数据和状态方面, 它没有太多提供。

React的创建者Facebook以Flux的形式提供了一些指导。 Flux是一种使用React View, Action Dispatcher和Stores围绕单向数据流构建的”应用程序体系结构”(不是框架)。 Flux模式通过体现事件控制的重要原理解决了一些主要问题, 这使React应用程序更易于推理, 开发和维护。

在这里, 我将介绍基本的Flux控制流示例, 讨论商店缺少的内容, 以及如何使用骨干模型和集合以”符合Flux的方式”填补空白。

(注意:为了方便和简洁起见, 我在示例中使用CoffeeScript。非CoffeeScript开发人员应该可以遵循, 并将示例视为伪代码。)

Facebook Flux简介

Backbone是一个出色且经过严格审查的小程序库, 其中包括视图, 模型, 集合和路线。它是用于结构化前端应用程序的事实上的标准库, 并且自从2013年推出后就已经与React应用程序配对。到目前为止, Facebook.com之外的大多数React示例都提到了Backbone串联使用。

不幸的是, 仅靠Backbone来处理React的Views之外的整个应用程序流程会带来不幸的麻烦。当我刚开始研究React-Backbone应用程序代码时, 我读到的”复杂事件链”并没有花很长时间来抬起他们像九头蛇一样的头脑。将事件从UI发送到模型, 然后从一个模型发送到另一个模型, 然后再发送回去, 这使得很难跟踪谁在更改谁, 以什么顺序以及为什么更改。

该Flux教程将演示Flux模式如何以令人印象深刻的简便性来处理这些问题。

概述

Flux的口号是”单向数据流”。这是Flux文档中的一个方便图表, 显示了该流程的样子:

Facebook Flux使用"单向数据流"模型,该模型与React和Backbone配对时会略有不同。

重要的一点是, 东西来自React-> Dispatcher-> Stores-> React。

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

文档还提供了以下重要警告:

Flux与其说是框架, 不如说是一种模式, 并且没有任何硬性依赖。但是, 我们经常使用EventEmitter作为Stores和View的基础。分派器是其他地方不容易获得的一种助焊剂。此模块可在此处完成Flux工具箱。

因此, Flux具有三个组成部分:

  1. 视图(React = require(‘react’))
  2. 分派器(Dispatcher = require(‘flux’)。Dispatcher)
  3. 商店(EventEmitter = require(‘events’)。EventEmitter)
    • (或者, 正如我们将很快看到的, Backbone = require(‘backbone’))

观点

我不会在这里描述React, 因为已经写了很多关于它的文章, 除了说我非常喜欢Angular。与Angular不同, 我在编写React代码时几乎从未感到困惑, 但是当然, 观点会有所不同。

调度员

Flux Dispatcher是在单个地方处理所有修改商店的事件。要使用它, 你需要让每个商店都注册一个回调来处理所有事件。然后, 无论何时要修改商店, 都将调度事件。

像React一样, Dispatcher让我印象深刻, 实施得很好。例如, 允许用户将项目添加到待办事项列表的应用可能包括以下内容:

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

TodoDispatcher = new Dispatcher() # That's all it takes!.

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: ->
    # note: you're NOT just pushing directly to the store!
    # (the restriction of moving through the dispatcher
    # makes everything much more modular and maintainable)
    TodoDispatcher.dispatch
      actionType: "add-item"
      item: "hello world"

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

这使得回答两个问题非常容易:

  1. 问:所有修改MyStore的事件是什么?
    • 答:只需检查MyStore.dispatchCallback中switch语句中的情况。
  2. 问:该事件的所有可能来源是什么?
    • 答:只需搜索该actionType。

这比查找MyModel.set和MyModel.save和MyCollection.add等要容易得多, 在这些地方很难快速找到这些基本问题的答案。

Dispatcher还允许你使用waitFor以简单, 同步的方式按顺序运行回调。例如:

# 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

在实践中, 我震惊地看到使用Dispatcher修改我的商店时, 即使不使用waitFor时, 我的代码有多干净。

商店

因此, 数据通过分派器流入商店。得到它了。但是数据如何从商店流向视图(即React)?如Flux文档所述:

[the]视图侦听其所依赖的商店广播的事件。

好, 太棒了。就像我们在商店中注册回调一样, 我们在视图(React组件)中注册回调。我们告诉React只要在通过其道具传递的Store中发生更改时就重新渲染。

例如:

# 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

太棒了!

那么, 我们如何发出”变更”事件呢?好吧, Flux建议使用EventEmitter。从一个官方的例子:

var MessageStore = merge(EventEmitter.prototype, {

  emitChange: function() {
    this.emit(CHANGE_EVENT);
  }, /**
   * @param {function} callback
   */
  addChangeListener: function(callback) {
    this.on(CHANGE_EVENT, callback);
  }, get: function(id) {
    return _messages[id];
  }, getAll: function() {
    return _messages;
  }, // etc...

毛!每当我想要一个简单的Store时, 我都必须自己写下所有内容?每当我要显示一条信息时, 我应该使用哪个信息?一定有更好的方法!

失踪的一块

骨干网的模型和集合已经拥有Flux基于EventEmitter的商店似乎正在做的所有事情。

通过告诉你使用原始的EventEmitter, Flux建议你每次创建商店时都重新创建大约Backbone的模型和集合的50-75%。在商店中使用EventEmitter就像在服务器上使用裸Node.js一样, 当已经存在精心构建的Express.js或类似的微框架来处理所有基础知识和样板时。

就像Express.js建立在Node.js上一样, Backbone的模型和集合也是建立在EventEmitter上。它具有你几乎一直需要的所有东西:Backbone发出更改事件, 并具有查询方法, getter和setter等。另外, Backbone的Jeremy Ashkenas和他的230名贡献者组成的军队在所有这些方面做得比我可能做的要好得多。

作为本Backbone教程的示例, 我将上面的MessageStore示例转换为Backbone版本。

从客观上讲, 它的代码更少(无需重复工作), 主观上则更加简洁明了(例如, 使用this.add(message)而不是_messages [message.id] = message)。

因此, 让我们将Backbone用于商店!

FluxBone模式:Backbone的Flux商店

本教程是我自豪地称为FluxBone的方法的基础, FluxBone是使用Backbone for Stores的Flux架构。这是FluxBone架构的基本模式:

  1. 商店是实例化的骨干模型或集合, 它们已在Dispatcher中注册了回调。通常, 这意味着它们是单例。
  2. 视图组件从不直接修改商店(例如, 没有.set())。而是, 组件将操作分派给分派器。
  3. 查看组件查询存储并绑定到其事件以触发更新。
本Backbone教程旨在研究Backbone和Flux在React应用程序中协同工作的方式。

让我们使用Backbone和Flux示例依次查看每个示例:

1.商店是实例化的骨干模型或集合, 它们已在Dispatcher中注册了回调。

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

TodoDispatcher = new Dispatcher() # That's all it takes!

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"

  # we register a callback with the Dispatcher on init.
  initialize: ->
    @dispatchToken = TodoDispatcher.register(@dispatchCallback)

  dispatchCallback: (payload) =>
    switch payload.actionType
      # remove the Model instance from the Store.
      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.组件永远不要直接修改商店(例如, 没有.set())。而是, 组件将操作分派给分派器。

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

TodoListComponent = React.createClass
  handleTodoDelete: ->
    # instead of removing the todo from the TodoStore directly, # we use the Dispatcher
    TodoDispatcher.dispatch
      actionType: "todo-delete"
      todo: @props.todoItem
  # ... (see below) ...

module.exports = TodoListComponent

3.组件查询存储并绑定到其事件以触发更新。

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

TodoListComponent = React.createClass
  handleTodoDelete: ->
    # instead of removing the todo from the TodoStore directly, # we use the dispatcher. #flux
    TodoDispatcher.dispatch
      actionType: "todo-delete"
      todo: @props.todoItem
  # ...
  componentDidMount: ->
    # the Component binds to the Store's events
    @props.TodoStore.on "add remove reset", =>
      @forceUpdate()
    , @
  componentWillUnmount: ->
    # turn off all events and callbacks that have this context
    @props.TodoStore.off null, null, this
  render: ->
    React.DOM.ul {}, @props.TodoStore.items.map (todoItem) ->
        # TODO: TodoItemComponent, which would bind to
        # `this.props.todoItem.on('change')`
        TodoItemComponent {
          todoItem: todoItem
        }

module.exports = TodoListComponent

我已经将这种Flux和Backbone方法应用于我自己的项目, 并且一旦我重新构建React应用程序以使用此模式, 几乎所有丑陋的地方就消失了。这真是一个奇迹:一个接一个地, 让我咬牙切齿寻找更好方法的代码片段被明智的流程所取代。而且Backbone似乎可以以这种方式集成的平滑性非常出色:我不觉得要与Backbone, Flux或React战斗是为了将它们整合到一个应用程序中。

Example Mixin

每次将FluxBone Store添加到组件时, 编写this.on(…)和this.off(…)代码可能会有点陈旧。

这是一个示例React Mixin, 尽管非常幼稚, 但肯定会使迭代变得更加容易:

# 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")
)

与Web API同步

在原始的Flux图中, 你仅通过ActionCreators与Web API进行交互, 在将操作发送到Dispatcher之前, 它需要服务器的响应。从来没有坐在我身边;在服务器之前, 商店不应该是第一个知道更改的人吗?

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

数据完整性得到维护, 没有问题。当.set()一个新属性时, change事件触发React重新渲染, 乐观地显示新数据。当你尝试将.save()保存到服务器时, request事件使你知道显示一个加载图标。一切顺利时, sync事件使你知道要删除加载图标, 而error事件使你知道将事物变成红色。你可以在这里看到灵感。

还为第一道防线提供了验证(以及相应的无效事件), 并通过.fetch()方法从服务器提取了新信息。

对于不太标准的任务, 通过ActionCreators进行交互可能更有意义。我怀疑Facebook不会做太多”纯粹的CRUD”工作, 在这种情况下, 他们并不将商店放在首位也就不足为奇了。

总结

Facebook的工程团队做了出色的工作, 以通过React推动前端Web的发展, 而Flux的引入使人们可以窥视真正可扩展的更广泛的体系结构:不仅在技术方面, 而且在工程方面。巧妙, 谨慎地使用Backbone(按照本教程的示例)可以填补Flux的空白, 从而使从单人独立商店到大型公司的任何人都可以轻松创建和维护令人印象深刻的应用程序。

相关:React组件如何使UI测试变得容易

赞(0)
未经允许不得转载:srcmini » 使用Flux和Backbone的React App数据流(带有示例)

评论 抢沙发

评论前必须登录!