使用 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 測試