使用 Flux 和 Backbone 的 React 应用程序中的简单数据流:示例教程

已发表: 2022-03-11

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

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

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

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

Facebook 的 Flux 简介

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

不幸的是,仅依靠 Backbone 来处理 React 视图之外的整个应用程序流程会带来不幸的复杂性。 当我第一次开始编写 React-Backbone 应用程序代码时,我读到的“复杂事件链”并没有花很长时间就抬起了它们类似 hydra 的头脑。 将事件从 UI 发送到模型,然后从一个模型发送到另一个模型,然后再返回,这使得很难跟踪谁在更改谁、以什么顺序以及为什么更改。

本 Flux 教程将演示 Flux 模式如何轻松简单地处理这些问题。

概述

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

Facebook Flux 使用“单向数据流”模型,与 React 和 Backbone 搭配使用时会发生一些变化。

重要的一点是东西从React --> Dispatcher --> Stores --> React

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

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

Flux 更像是一种模式而不是框架,并且没有任何硬依赖。 但是,我们经常使用 EventEmitter 作为 Stores 的基础,并为我们的 Views 使用 React。 在其他地方不容易获得的 Flux 是 Dispatcher。 该模块可在此处使用以完善您的 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 是一个处理所有修改 Store 的事件的地方。 要使用它,您需要让每个 Store register一个回调来处理所有事件。 然后,每当你想修改一个 Store 时,你就dispatch一个事件。

像 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.dispatchCallbackswitch语句中的案例。
  2. 问:该事件的所有可能来源是什么?
    • 答:只需搜索该actionType

这比寻找MyModel.setMyModel.saveMyCollection.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 修改我的 Store 时变得多么干净,即使不使用waitFor

商店

所以数据通过 Dispatcher流入Store。 知道了。 但是数据如何从 Store 流向 View(即 React)? 如 Flux 文档中所述:

[The] 视图监听它所依赖的商店广播的事件。

好的,太好了。 就像我们向 Store 注册回调一样,我们向 Views(即 React 组件)注册回调。 当通过props传入的 Store 发生变化时,我们告诉 React 重新render

例如:

 # 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

惊人的!

那么我们如何发出那个"change"事件呢? 好吧,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...

总的! 每次我想要一个简单的商店时,我都必须自己写所有这些? 每次我想要显示一条信息时,我应该使用哪个? 一定有更好的方法!

缺失的部分

Backbone 的模型和集合已经具备了 Flux 的基于 EventEmitter 的商店似乎正在做的一切。

通过告诉您使用原始 EventEmitter,Flux 建议您在每次创建商店时重新创建大约 50-75% 的 Backbone 模型和集合。 为您的商店使用 EventEmitter 就像在您的服务器上使用裸 Node.js 时,已经存在像 Express.js 或等效的构建良好的微框架来处理所有基础和样板。

就像 Express.js 建立在 Node.js 上一样,Backbone 的模型和集合也建立在 EventEmitter 上。 它拥有您几乎总是需要的所有东西:Backbone 发出change事件,并具有查询方法、getter 和 setter 等等。 此外,Backbone 的 Jeremy Ashkenas 和他的 230 名贡献者在所有这些事情上做得比我可能做的要好得多。

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

客观上它的代码更少(无需重复工作),主观上更清晰和简洁(例如, this.add(message)而不是_messages[message.id] = message )。

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

FluxBone 模式:Backbone 的通量存储

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

  1. Stores 是实例化的 Backbone 模型或集合,它们已向 Dispatcher 注册了回调。 通常,这意味着它们是单例。
  2. 视图组件从不直接修改 Stores(例如,没有.set() )。 相反,组件将 Action 分派给 Dispatcher。
  3. 查看组件查询 Store 并绑定到它们的事件以触发更新。

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

让我们使用 Backbone 和 Flux 示例依次查看其中的每一部分:

1. Stores是实例化的Backbone Models或Collections,它们已经向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. 组件从不直接修改 Stores(例如,没有.set() )。 相反,组件将 Action 分派给 Dispatcher。

 # 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. 组件查询 Stores 并绑定到它们的事件以触发更新。

 # 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 抗争以便将它们组合到一个应用程序中。

示例混合

每次将 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事件让您知道将事情变成红色。 你可以在这里看到灵感。

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

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

结论

Facebook 的工程团队在使用 React 推动前端 Web 发展方面做了出色的工作,而 Flux 的引入让我们得以窥见真正可扩展的更广泛架构:不仅在技术方面,在工程方面也是如此。 巧妙而谨慎地使用 Backbone(根据本教程的示例)可以填补 Flux 中的空白,使从个人独立商店到大公司的任何人都可以非常轻松地创建和维护令人印象深刻的应用程序。

相关: React 组件如何简化 UI 测试