你将要创建的内容
在本教程中,您将了解Facebook 的 Flux架构以及如何使用它来处理基于 react 的应用程序中的数据流。我们将首先介绍 Flux 的基础知识并了解其开发背后的动机,然后我们将通过构建一个简单的虚拟钱包应用程序来实践我们所学到的知识。
在整个教程中,我假设您以前使用过 React,但没有使用 Flux 的经验。如果您已经了解 Flux 的基础知识并希望获得更深入的了解,您可能会从中有所收获。
如果您对 React 场景完全陌生,我建议您阅读David East的React 入门课程,在 Envato Tuts+ 上。这是一门很棒的课程,可以让您立即上手。
什么是通量?
Flux 主要是 Facebook 开发的应用架构概念,但同一个术语也指代表官方实现的库。
Facebook 推出 Flux 是为了解决由 MVC 模式在其庞大的代码库中引起的问题。他们努力解决操作触发级联更新导致不可预测的结果和难以调试的代码的问题。如果您以前使用过 MVC 框架,这听起来可能很熟悉,因为在它们中的大多数中,一切都趋于紧密耦合。将观察者和双向数据绑定添加到组合中,您会感到头疼。
我的建议是避免试图在 Flux 和 MVC 之间找到共同点。除了增加你的困惑之外,它不会有太大帮助。Flux 尝试以不同的方式解决问题,并且尝试将其与其他模式进行比较也无济于事。
项目设置
如果您想按照本教程进行操作,请首先确保您已安装require d 软件。完成后,从我准备在本文随附的 GitHub 存储库boilerplate中克隆分支。
以下是我在撰写本文时安装的软件要求和版本:
吉特:2.11
节点.js:6.9
新PM:3.10
纱线:0.22
您选择的编辑器
样板文件是我们即将构建的小型虚拟钱包应用程序的起点。它包含用于将 ES6 语法转换为纯javascript和 WDS 以提供文件的webpack配置。它还具有一些css组件样式,因此您可以直接开始编码。
为了安装所有必需的依赖项,cd进入项目目录并运行yarn.
在下一节中,您将在集成 Flux 之前设置应用程序的核心组件。我没有将它们包含在样板文件中,因为我相信这会造成更多混乱。如果您对构建应用程序不感兴趣,可以跳过这些步骤并跳转到下一部分。
组件设置
首先在 中包含以下代码js/index.js,作为应用程序的入口点:
import React from 'react'; import Reactdom from 'react-dom'; import App from './components/App'; ReactDOM.render((<App />), document.getElementById('app'));
对于主 <App />组件,在里面创建一个新文件,js/components并App.js 添加以下代码:
import React from 'react'; import AddNewItem from './AddNewItem'; import ItemsList from './ItemsList'; class App extends React.Component { render() { return ( <div className="container"> <h1 className="app-title">Flux Wallet</h1> <AddNewItem /> <ItemsList /> </div> ); } } export default App;
该 <App />组件包装了另外两个组件,一个用于负责添加新项目的表单,另一个用于项目列表。要创建组件,请在其中创建<AddNewItem />一个新文件并添加以下代码:AddNewItem.jsjs/components
import React from 'react'; class AddNewItem extends React.Component { // Set the initial state. constructor(props) { super(props); this._getFreshItem = this._getFreshItem.bind(this); this.state = { item: this._getFreshItem() }; } // Return a fresh item. _getFreshItem() { return { description: '', amount: '' }; } // Update the state. _updateState(event) { let field = event.target.name; let value = event.target.value; // If the amount is changed and it's not a float, return. if (value && field === 'amount' && !value.match(/^[a-z0-9.\+\-]+$/g)) { return; } this.state.item[field] = value; this.setState({ item : this.state.item }); } // Add a new item. _addNewItem(event) { // ... } render() { return ( <div> <h3 className="total-budget">$0</h3> <form className="form-inline add-item" onSubmit={this._addNewItem.bind(this)}> <input type="text" className="form-control description" name="description" value={this.state.item.description} placeholder="Description" onChange={this._updateState.bind(this)} /> <div className="input-group amount"> <div className="input-group-addon">$</div> <input type="text" className="form-control" name="amount" value={this.state.item.amount} placeholder="Amount" onChange={this._updateState.bind(this)} /> </div> <button type="submit" className="btn btn-primary add">Add</button> </form> </div> ) } } export default AddNewItem;
该组件捆绑了一些用于在表单字段更新时更新状态的逻辑以及一些基本验证。js/components/ItemsList.js让我们通过在项目列表中创建最后一个来完成组件设置,使用以下代码:
import React from 'react'; class ItemsList extends React.Component { constructor(props) { super(props); this.state = { items: [] }; } render() { let noItemsMessage; // Show a friendly message instead if there are no items. if (!this.state.items.length) { noItemsMessage = (<li className="no-items">Your wallet is new!</li>); } return ( <ul className="items-list"> {noItemsMessage} {this.state.items.map((itemDetails) => { let amountType = parseFloat(itemDetails.amount) > 0 ? 'positive' : 'negative'; return (<li key={itemDetails.id}>{itemDetails.description} <span className={amountType}>{itemDetails.amount}</span></li>); })} </ul> ); } } export default ItemsList;
而已!您已完成设置项目的组件。最重要的是,它们还带有免费造型。
运行yarn start并等待构建包。如果您将浏览器指向localhost:8080,您应该会看到该应用程序没有任何功能。
接下来,我们将介绍 Flux 是什么以及如何使用它为虚拟钱包应用程序添加功能。
通量构建块
在高层次上,Flux 分为四个主要部分:动作、调度程序、存储和视图:
动作描述在应用程序中发生的动作。
调度程序是callback的单例注册表。它通过将操作传递给订阅它的所有商店来充当中间人。
存储管理应用程序特定部分更新状态和逻辑所需的状态。
视图是普通的旧 React 组件。
在 Flux 中,所有数据都沿一个方向流动:
使用称为动作创建者的便利类将动作传递给调度程序。
调度程序将操作发送(正在调度)到所有订阅它的商店。
最后,如果商店关心接收到(或更多)的特定操作,它们会更新它们的状态并向视图发出信号,以便它们可以重新渲染。
下面是这个过程的可视化表示。
行动
使用称为操作的纯 JavaScript 对象“通过线路”在单个方向上发送数据。他们的工作是描述应用程序中发生的事件并将新数据传输到商店。每个操作都必须有一个类型和一个包含数据的可选负载键。一个操作类似于以下操作:
{ actionType: "UPDATE_TITLE", payload: "This is a new title." }
动作的类型必须由描述性和一致的大写字符串表示——类似于定义常量的通用约定。它们用作商店将用来识别操作并做出相应响应的唯一 ID。
一种常见的做法是在常量对象中定义所有动作类型,并在应用程序中引用该对象以保持一致性。我们的虚拟钱包将支持单个操作,将项目添加到列表中——费用和财务收益都将被视为一个项目——因此我们的常量文件将非常小。
在文件夹中创建一个index.js文件js/constants并使用以下代码创建您的第一个操作类型:导出默认 {
export default { ADD_NEW_ITEM: 'ADD_NEW_ITEM' }
使用称为操作创建者的便利类帮助器将操作传递给调度程序,这些帮助程序处理创建操作并将其发送到调度程序的简单任务。在创建我们的 action creator 之前,让我们先看看 dispatcher 做了什么,并了解它在 Flux 中的作用。
调度员
调度程序用于协调动作创建者和商店之间的通信。您可以使用它来注册商店的操作处理程序回调,也可以将操作发送到订阅的商店。
dispatcher 的api很简单,它只有五个方法可用:
register():注册商店的操作处理程序回调。
unregister() :取消注册商店的回调。
waitFor():等待指定的回调首先运行。
dispatch(): 调度一个动作。
isDispatching():检查调度程序当前是否正在调度动作。
最重要的是register()它们dispatch() 用于处理大部分核心功能。让我们看看它们在幕后的外观和工作方式。
let _callbacks = []; class Dispatcher { // Register a store callback. register(callback) { let id = 'callback_' + _callbacks.length; _callbacks[id] = callback; return id; } // Dispatch an action. dispatch(action) { for (var id in _callbacks) { _callbacks[id](action); } } }
当然,这是基本要点。该register()方法将所有回调存储在私有_callbacks数组中,dispatch()并使用接收到的操作迭代并调用存储的每个回调。
为简单起见,我们不会编写自己的调度程序。相反,我们将使用 Facebook 库中提供的那个。我鼓励您查看 Facebook 的 GitHub 存储库,看看它是如何实现的。
在js/dispatcher文件夹中,创建一个新文件index.js并添加以下代码片段:
import { Dispatcher } from 'flux'; export default new Dispatcher();
它从之前使用 yarn 安装的库中导入调度flux程序,然后导出它的新实例。
现在调度程序已准备就绪,我们可以返回到操作并设置应用程序的操作创建器。在 js/actions 文件夹中,创建一个名为walletActions.js并添加以下代码的新文件:
import Dispatcher from '../dispatcher'; import ActionTypes from '../constants'; class WalletActions { addNewItem(item) { // Note: This is usually a good place to do API calls. Dispatcher.dispatch({ actionType: ActionTypes.ADD_NEW_ITEM, payload: item }); } } export default new WalletActions();
该类WalletActions公开了一个addNewItem()处理三个基本任务的方法:
它接收一个item作为参数。
它使用调度程序来调度具有ADD_NEW_ITEM我们之前创建的动作类型的动作。
然后它将接收到的item作为有效负载与动作类型一起发送。
在使用这个动作创建器之前,让我们看看商店是什么以及它们如何适合我们的 Flux 驱动的应用程序。
专卖店
我知道,我说过你不应该将 Flux 与其他模式进行比较,但 Flux 存储在某种程度上类似于 MVC 中的模型。它们的作用是处理应用程序中特定顶级组件的逻辑和存储状态。
所有 Flux 存储都必须定义一个操作处理程序方法,然后将其注册到调度程序。这个回调函数主要由一个关于接收到的动作类型的 switch 语句组成。如果满足特定的操作类型,它会相应地采取行动并更新本地状态。最后,商店广播一个事件来通知视图关于更新状态的信息,以便它们可以相应地更新。
为了广播事件,商店需要扩展事件发射器的逻辑。有各种可用的事件发射器库,但最常见的解决方案是使用 node 的事件发射器。对于像虚拟钱包这样的简单应用程序,不需要多个商店。
在js/stores文件夹中,创建一个名为的新文件walletStore.js,并为我们的应用商店添加以下代码:
import { EventEmitter } from 'events'; import Dispatcher from '../dispatcher'; import ActionTypes from '../constants'; const CHANGE = 'CHANGE'; let _walletState = []; class WalletStore extends EventEmitter { constructor() { super(); // Registers action handler with the Dispatcher. Dispatcher.register(this._registerToActions.bind(this)); } // Switches over the action's type when an action is dispatched. _registerToActions(action) { switch(action.actionType) { case ActionTypes.ADD_NEW_ITEM: this._addNewItem(action.payload); break; } } // Adds a new item to the list and emits a CHANGED event. _addNewItem(item) { item.id = _walletState.length; _walletState.push(item); this.emit(CHANGE); } // Returns the current store's state. getAllItems() { return _walletState; } // Calculate the total budget. getTotalBudget() { let totalBudget = 0; _walletState.forEach((item) => { totalBudget += parseFloat(item.amount); }); return totalBudget; } // Hooks a React component's callback to the CHANGED event. addChangeListener(callback) { this.on(CHANGE, callback); } // Removes the listener from the CHANGED event. removeChangeListener(callback) { this.removeListener(CHANGE, callback); } } export default new WalletStore();
我们首先导入存储所需的依赖项,从 Node 的事件发射器、调度程序开始,然后是 ActionTypes。你会注意到它下面有一个常量CHANGE,类似于你之前了解的动作类型。
它实际上不是一个,也不应该混淆。当商店的数据发生变化时,它是一个用于事件触发的常量。我们将把它保存在这个文件中,因为它不是应用程序其他部分中使用的值。
初始化时,WalletStore该类首先_registerToAction()向调度程序注册回调。在幕后,这个回调将被添加到调度程序的_callbacks 数组中。
switch当分派动作时,该方法对从分派器接收到的动作类型有一个单独的语句。如果它满足ADD_NEW_ITEM动作类型,它就会运行该_addNewItem()方法并传递它接收到的有效负载。
该_addNewItem()函数id为项目设置一个,将其推送到现有项目的列表中,然后发出一个CHANGE事件。接下来,getAllItems()andgetTotalBudget()方法是基本的 getter,我们将使用它来检索当前商店的状态和总预算。
最后两个方法addChangeListener()和removeChangeListener()将用于将 React 组件链接到 ,WalletStore以便它们在 store 的数据更改时得到通知。
控制器视图
使用 React 允许我们将应用程序的各个部分分解为不同的组件。我们可以嵌套它们并构建有趣的层次结构,这些层次结构在我们的页面中形成工作元素。
在 Flux 中,位于链顶端的组件倾向于存储生成动作和接收新数据所需的大部分逻辑;因此,它们被称为控制器视图。这些视图直接连接到存储中,并监听存储更新时触发的更改事件。
发生这种情况时,控制器视图会调用该setState方法,该方法会触发该render()方法运行并更新视图并通过 props 向子组件发送数据。从那里开始,React 和虚拟 DOM 发挥了它们的魔力并尽可能高效地更新 DOM。
我们的应用程序很简单,并且不遵守这本书的规则。但是,根据复杂性,较大的应用程序有时可能需要多个控制器视图以及应用程序主要部分的嵌套子组件。
装配在一起
我们已经完成了 Flux 的主要部分,但虚拟钱包应用程序尚未完成。在最后一节中,我们将回顾从操作到视图的整个流程,并填写完成 Flux 单向数据流所需的缺失代码。
调度动作
回到<AddNewItem />组件,您现在可以包含WalletActions模块并使用它在方法中生成新操作_addNewItem()。
import React from 'react'; import WalletActions from '../actions/walletActions'; // … _addNewItem(event) { event.preventDefault(); this.state.item.description = this.state.item.description || '-'; this.state.item.amount = this.state.item.amount || '0'; WalletActions.addNewItem(this.state.item); this.setState({ item : this._getFreshItem() }); } // ...
现在,当表单被提交时,一个动作被调度并且所有的商店——在我们的例子中——被通知新的数据。
监听存储变化
在您的WalletStore中,当前当一个项目被添加到列表中时,它的状态会发生变化并CHANGE触发事件,但没有人在听。<ItemsList />让我们通过在组件中添加一个更改***器来关闭循环。
import React from 'react'; import WalletStore from '../stores/walletStore'; class ItemsList extends React.Component { constructor(props) { super(props); this.state = { items: WalletStore.getAllItems() }; this._onChange = this._onChange.bind(this); } _onChange() { this.setState({ items: WalletStore.getAllItems() }); } componentWillMount() { WalletStore.addChangeListener(this._onChange); } componentWillUnmount() { WalletStore.removeChangeListener(this._onChange); } render() { // ... } } export default ItemsList;
更新后的组件关闭了 Flux 的单向数据流。请注意,我跳过包含整个render()方法以节省一些空间。让我们逐步了解新功能:
该WalletStore模块包含在顶部。
初始状态被更新为使用商店的状态。
一种新_onChange()方法用于使用来自商店的新数据更新状态。
使用 React 的生命周期钩子,_onChange()回调作为 store 的更改***器回调添加和删除。
结论
恭喜!您已经构建了一个由 Flux 提供支持的可工作的虚拟钱包应用程序。您已经了解了所有 Flux 组件如何相互交互,以及如何使用它向 React 应用程序添加结构。
当您对自己的 Flux 技能充满信心时,请确保您还检查了其他 Flux 实现,例如Alt、Delorean、Flummox或Fluxxor ,看看哪一个适合您。
- 调度动作
- 监听存储变化