React + Mobx:有什么意义?

今天,我想告诉您有关Mobx过渡是如何在我们的项目中进行的,它具有什么优势。 还将显示一个典型项目,并对主要问题进行解释。 但首先,介绍一下。

图片

为什么需要切换到某些内容? 实际上,这个问题的答案已经成功了一半。 现在,许多人只因为新技术而喜欢应用新技术。 简历中有一条不错的路线,即自我发展的可能性,这是趋势。 只要您能前进就很好。

但是,每种工具都必须解决自己的问题,并且在编写商业代码时我们会以一种或另一种方式排斥它们。

在我们的项目中,有许多小部件,用户可在其中输入数据并与表单进行交互。 通常,每个小部件都有几个屏幕。 曾几何时,它们都可以在客户端上使用的旧版MarkoJS +模板引擎上运行。 与表单的交互以命令式的方式编写,如果...否则,则使用回调,而这一切似乎都已经过去了。

然后是时候到React了 。 客户端上的业务逻辑越来越厚,有很多交互选项,命令性代码变成了一个复杂的混乱局面。 事实证明,声明式反应代码更加方便。 最终有可能专注于逻辑而不是表示形式,重用组件并轻松地在不同员工之间分配用于开发新功能的任务。

但是随着时间的流逝,纯React上的应用仍然存在于切实的边界上。 当然,我们为每个人编写this.setState并考虑它的异步性很无聊,但是在组件的整个厚度中抛出数据和回调使它特别困难。 简而言之,现在是将数据和表示完全分离的时刻了。 这不是如何在纯React上做到这一点的问题,但是在实现前端应用程序Flux架构的行业框架中,最近很流行。

根据空缺中的文章和参考文献的数量, Redux是我们中最著名的。 实际上,到最后一刻,我已经把手伸进了我们的项目中并开始开发(从字面上看!)恶魔穿过了哈伯,然后才开始讨论“ Redux或Mobx?”主题。 这是本文: habr.com/en/post/459706 。 阅读了它以及其中的所有注释之后,我意识到我仍然会使用Mobx

再说一遍。 最重要问题的答案-为什么所有这些? -看起来像这样:现在该将演示文稿和数据分开了,我想以声明式(以及呈现)的方式构建数据管理,不对回调和转发的属性进行交叉授粉。

现在我们准备继续。

1.关于申请


我们需要在前端构建一个屏幕和表单的设计器,然后可以将其快速改组,并根据业务需求的变化相互连接。 这不可避免地将我们带到以下位置:创建完全隔离的组件的集合,以及与我们的每个小部件相对应的一些基本组件(实际上,这些是在常规应用程序中每次在新业务案例下每次创建的单独的SPA)。

这些示例将显示其中一个小工具的截短版本。 为了不堆积多余的代码,请使其成为三个输入字段和按钮的形式。

2.资料


Mobx本质上不是一个框架;它只是一个库。 该手册明确指出它不会直接组织您的数据。 您自己必须提出这样的组织。 顺便说一下,我们使用Mobx 4,因为版本5使用Sybmol数据类型,不幸的是,并非所有浏览器都支持该数据类型。

因此,所有数据都分配给单独的实体。 我们的应用程序针对两个文件夹:
-放置所有视图的组件
-store ,其中将包含数据以及使用它们的逻辑。
例如,对我们而言,典型的数据输入组件由两个文件组成: Input.jsInputStore.js 。 第一个文件是一个严格负责显示的愚蠢的React组件,第二个文件是该组件的数据,用户规则( onClickonChange等)

在直接查看示例之前,我们需要解决另一个重要问题。

3.管理


好吧,我们拥有View-Store的完全自主组件,但是如何在整个应用程序中融合在一起呢? 为了进行显示,我们将拥有App.js的根组件,对于管理数据流,主存储为mainStore.js 。 原理很简单:mainStore知道所有必需组件的所有存储库的所有信息(将在下面显示如何实现)。 其他存储库对世界一无所知(当然,会有一个例外-字典)。 因此,我们可以保证知道我们的数据将流向何处以及在何处进行拦截。

图片

mainStore以声明方式,通过更改其状态的一部分,可以控制其余组件。 在下图中,“ 操作”和“ 状态”是指组件存储,而“ 计算值”是指mainStore

图片

让我们开始编写代码。 主应用程序文件index.js:

import React from "react"; import ReactDOM from "react-dom"; import {Provider} from "mobx-react"; import App from "./components/App"; import mainStore from "./stores/mainStore"; import optionsStore from "./stores/optionsStore"; //  IE11 require("es6-object-assign").polyfill(); require( "./static/less/main.less"); const stores = { mainStore, optionsStore, ButtonStore : mainStore.ButtonStore, FioStore : mainStore.FioStore, EmailStore : mainStore.EmailStore }; ReactDOM.render(( <Provider {...stores}> <App /> </Provider> ), document.getElementById('reactContainer')); 

在这里,您可以看到Mobx的基本概念。 数据(存储)可通过提供程序机制在应用程序中的任何位置使用。 我们通过列出所需的存储设备来包装我们的应用程序。 要使用提供程序,我们连接mobx-react模块 。 为了使主控制存储区mainStore从一开始就可以访问所有其他数据,我们在mainStore中初始化子存储

 // mainStore.js import optionsStore from "./optionsStore"; import ButtonStore from "./ButtonStore"; import FioStore from "./FioStore"; import EmailStore from "./EmailStore"; .... class mainStore { constructor() { /** *    */ this.ButtonStore = new ButtonStore(); this.FioStore = new FioStore(); this.EmailStore = new EmailStore(); ... 


现在是App.js ,它是我们应用程序的框架

 import React from "react"; import {observer, inject} from "mobx-react"; import ButtonArea from "./ButtonArea"; import Email from "./Email"; import Fio from "./Fio"; import l10n from "../../../l10n/localization.js"; @inject("mainStore") @observer export default class App extends React.Component { constructor(props) { super(props); }; render() { const mainStore = this.props.mainStore; return ( <div className="container"> <Fio label={l10n.ru.profile.name} name={"name"} value={mainStore.userData.name} daData={true} /> <Fio label={l10n.ru.profile.surname} name={"surname"} value={mainStore.userData.surname} daData={true} /> <Email label={l10n.ru.profile.email} name={"email"} value={mainStore.userData.email} /> <ButtonArea /> </div> ); } } 

Mobx还有另外两个基本概念: 注入观察器
注入仅在应用程序中实现必要的存储。 我们应用程序的不同部分使用不同的存储库,这些存储库在inject中列出,并用逗号分隔。 自然地,可插拔存储应该首先在Provider中列出。 可以通过this.props.yourStoreName在组件中使用存储库。
观察者 -装饰者指示我们的组件将订阅使用Mobx修改的数据。 数据已更改-组件中发生了反应(如下所示)。 因此,没有特殊的订阅和回调-Mobx会自己提供更改!

我们将返回在mainStore中管理整个应用程序,但现在我们将处理组件。 我们有三种类型-Fio电子邮件按钮 。 让第一个和第三个通用,并使用电子邮件 -自定义。 让我们从他开始。

该显示是通常的哑响应组件:

Email.js
 import React from "react"; import {inject, observer} from 'mobx-react'; @inject("EmailStore") @observer export default class Email extends React.Component { constructor(props) { super(props); }; componentDidMount = () => { this.props.EmailStore.validate(this.props.name); }; componentWillUnmount = () => { this.props.EmailStore.unmount(this.props.name); }; render() { const name = this.props.name; const EmailStore = this.props.EmailStore; const params = EmailStore.params; let status = "form-group email "; if (params.isCorrect && params.onceValidated) status += "valid"; if (params.isWrong && params.onceValidated) status += "error"; return ( <div className={status}> <label htmlFor={name}>{this.props.label}</label> <input type="email" disabled={this.props.disabled} name={name} id={name} value={params.value} onChange={(e) => EmailStore.bindData(e, name)} /> </div> ); } } 

我们连接了验证的外部组件,在元素已经包含在布局中之后执行此操作很重要。 因此,store中的方法在componentDidMount中被调用。

现在仓库本身:

EmailStore.js
 import {action, observable} from 'mobx'; import reactTriggerChange from "react-trigger-change"; import Validators from "../../../helpers/Validators"; import { getTarget } from "../../../helpers/elementaries"; export default class EmailStore { @observable params = { value : "", disabled : null, isCorrect : null, isWrong : null, onceValidated : null, prevalidated : null } /** *    */ @action bindData = (e, name) => { this.params.value = getTarget(e).value; }; /** *   */ @action validate = (name) => { const callbacks = { success : (formatedValue) => { this.params.value = formatedValue; this.params.isCorrect = true; this.params.isWrong = false; this.params.onceValidated = true; }, fail : (formatedValue) => { this.params.value = formatedValue; this.params.isCorrect = false; this.params.isWrong = true; } }; const options = { type : "email" }; const element = document.getElementById(name); new Validators(element, options, callbacks).init(); //    reactTriggerChange(element); this.params.prevalidated = true; }; } 


值得关注两个新实体。

可观察的 -一个对象,其任何字段的更改都由Mobx监视(并将信号发送给观察者观察者已预订了我们的特定存储)。
动作 -此装饰器应包装任何更改应用程序状态和/或引起副作用的处理程序。 在这里,我们在@observable params中更改value的

就是这样,我们的简单组件已准备就绪! 他可以跟踪用户数据并进行记录。 稍后,我们将看到mainStore中央存储库如何订阅以更改此数据。

现在是典型的Fio组件。 它与上一个组件的区别在于,我们将在一个应用程序中无限次使用这种类型的组件。 这对组件存储施加了一些其他要求。 最重要的是,我们将使用出色的DaData服务对输入字符进行更多提示。 显示方式

Fio.js
 import React from "react"; import {inject, observer} from 'mobx-react'; import {get} from 'mobx'; @inject("FioStore") @observer export default class Fio extends React.Component { constructor(props) { super(props); }; componentDidMount = () => { /** *       */ this.props.FioStore.registration(this.props); }; componentWillUnmount = () => { this.props.FioStore.unmount(this.props.name); }; render() { /** *       *  DaData: * data.surname -  * data.name -  * https://dadata.ru/api/suggest/name */ const FioStore = this.props.FioStore; const name = this.props.name; const item = get(FioStore.items, name); if (item && item.isCorrect && item.onceValidated && !item.prevalidated) status = "valid"; if (item && item.isWrong && item.onceValidated) status = "error"; //    store let value = this.props.value; if (item) value = item.value; return ( <div className="form-group fio"> <label htlmfor={name}>{this.props.label}</label> <input type="text" disabled={this.props.disabled} name={name} id={name} value={value} onChange={(e) => FioStore.bindData(e, name)} /> {(item && item.suggestions && item.suggestions.length > 0) && <div className="hint-container" id={"hint-container-" + item.id}>{item.suggestions.map((suggestion, i) => { return ( <div className={"suggestion-item fs-" + i} key={i} value={suggestion.data[name]} onClick={(e) => FioStore.setSuggestion(e, name)}> <span className="suggestion-text">{suggestion.data[name]}</span> </div>) })}</div>} </div> ); } } 

这里有一些新内容:我们不直接访问组件的状态,而是通过get进行访问
 get(FioStore.items, name) 

事实是,组件实例的数量是无限的,并且此类型的所有组件的存储库都是一个。 因此,在注册期间,我们在Map中输入每个实例的参数:

Fiostore.js
 import {action, autorun, observable, get, set} from 'mobx'; import reactTriggerChange from "react-trigger-change"; import Validators from "../../../helpers/Validators"; import { getDaData, blockValidate } from "../../../helpers/functions"; import { getAttrValue, scrollToElement, getTarget } from "../../../helpers/elementaries"; export default class FioStore { constructor() { autorun(() => { /** *        .         setSuggestion() */ const self = this; $("body").click((e) => { if (e.target.className !== "suggestion-item" && e.target.className !== "suggestion-text") { const items = self.items.entries(); for (var [key, value] of items) { value.suggestions = []; } } }); }) } /** *   items       Fio    */ @observable items = new Map([]); /** *    */ @action registration = (params) => { const nameExists = get(this.items, params.name); if (!blockValidate({params, nameExists, type: "Fio"})) return false; //  items   const value = { value : params.value, disabled : params.disabled, isCorrect : null, isWrong : null, suggestions : [], daData : params.daData, startValidation : true, //      onceValidated : false, //      prevalidated : false }; set(this.items, params.name, value); this.validate(params.name); }; /** *    */ @action unmount = (name) => { this.items.delete(name); }; /** *    *    */ @action bindData = (e, name) => { const value = getTarget(e).value; const item = get(this.items, name); /** *     DaData */ if (item.daData && !item.startValidation) { getDaData({value, type: "fio", name}) .then((result) => { item.suggestions = result.suggestions; }) .catch((error) => {console.log(error)}) } else { item.startValidation = false; item.value = value; } }; /** *   */ @action setSuggestion = (e, name) => { if (e) e.preventDefault(); get(this.items, name).value = getAttrValue(e); //     get(this.items, name).suggestions = []; get(this.items, name).isCorrect = true; get(this.items, name).isWrong = false; }; /** *   */ @action validate = (name) => { const callbacks = { success : (formatedValue) => { get(this.items, name).value = formatedValue; get(this.items, name).isCorrect = true; get(this.items, name).isWrong = false; get(this.items, name).onceValidated = true; }, fail : (formatedValue) => { get(this.items, name).value = formatedValue; get(this.items, name).isCorrect = false; get(this.items, name).isWrong = true; } }; const options = { type : "fio" }; const element = document.getElementById(name); new Validators(element, options, callbacks).init(); //    reactTriggerChange(element); get(this.items, name).prevalidated = true; }; } 

我们通用组件的状态初始化如下:

 @observable items = new Map([]); 

使用常规JS对象会更方便,但是,在更改其字段的值时不会被“点击”, 将新组件添加到页面时,将动态添加字段。 收到DaData提示后,我们将单独取出。

按钮组件看起来相似,但是没有提示:

Button.js
 import React from "react"; import {inject, observer} from 'mobx-react'; @inject("ButtonStore") @observer export default class CustomButton extends React.Component { constructor(props) { super(props); }; componentDidMount = () => { /** *       */ this.props.ButtonStore.registration(this.props); }; componentWillUnmount = () => { this.props.ButtonStore.unmount(this.props.name); }; render() { const name = this.props.name; return ( <div className="form-group button"> <button disabled={this.props.disabled} onClick={(e) => this.props.ButtonStore.bindClick(e, name)} name={name} id={name} >{this.props.text}</button> </div> ); } } 


ButtonStore.js
 import {action, observable, get, set} from 'mobx'; import {blockValidate} from "../../../helpers/functions"; export default class ButtonStore { constructor() {} /** *   items       Button    */ @observable items = new Map([]) /** *    */ @action registration = (params) => { const nameExists = get(this.items, params.name); if (!blockValidate({params, nameExists, type: "Button"})) return false; //  items   const value = { disabled : params.disabled, isClicked : false }; set(this.items, params.name, value); }; /** *    */ @action unmount = (name) => { this.items.delete(name); }; /** *    */ @action bindClick = (e, name) => { e.preventDefault(); get(this.items, name).isClicked = true; }; } 


Button的组件由ButtonArea的HOC组件包装 。 请注意,较老的商店包括自己的商店,而最小的商店。 在嵌套组件链中,无需转发任何参数和回调。 特定组件操作所需的所有内容都直接添加到该组件中。

ButtonArea.js
 import React from "react"; import {inject, observer} from 'mobx-react'; import l10n from "../../../l10n/localization.js"; import Button from "./Button"; @inject("mainStore", "optionsStore") @observer export default class ButtonArea extends React.Component { constructor(props) { super(props); }; render() { return ( <div className="button-container"> <p>{this.props.optionsStore.dict.buttonsHeading}</p> <Button name={"send_data"} disabled={this.props.mainStore.buttons.sendData.disabled ? true : false} text={l10n.ru.common.continue} /> </div> ); } } 


因此,我们已经准备好所有组件。 这件事留给了mainStore的经理 首先,所有的存储代码:

mainStore.js
 import {observable, computed, autorun, reaction, get, action} from 'mobx'; import optionsStore from "./optionsStore"; import ButtonStore from "./ButtonStore"; import FioStore from "./FioStore"; import EmailStore from "./EmailStore"; import { fetchOrdinary, sendStats } from "../../../helpers/functions"; import l10n from "../../../l10n/localization.js"; class mainStore { constructor() { /** *    */ this.ButtonStore = new ButtonStore(); this.FioStore = new FioStore(); this.EmailStore = new EmailStore(); autorun(() => { this.fillBlocks(); this.fillData(); }); /** *      */ reaction( () => this.dataInput, (result) => { let isIncorrect = false; for (let i in result) { for (let j in result[i]) { const res = result[i][j]; if (!res.isCorrect) isIncorrect = true; this.userData[j] = res.value; } }; if (!isIncorrect) { this.buttons.sendData.disabled = false } else { this.buttons.sendData.disabled = true }; } ); /** *     */ reaction( () => this.sendDataButton, (result) => { if (result) { if (result.isClicked) { get(this.ButtonStore.items, "send_data").isClicked = false; const authRequestSuccess = () => { console.log("request is success!") }; const authRequestFail = () => { console.log("request is fail!") }; const request = { method : "send_userdata", params : { name : this.userData.name, surname : this.userData.surname, email : this.userData.email } }; console.log("Request body is:"); console.log(request); fetchOrdinary( optionsStore.OPTIONS.sendIdentUrl, JSON.stringify(request), { success: authRequestSuccess, fail: authRequestFail } ); } } } ); } @observable userData = { name : "", surname : "", email : "" }; @observable buttons = { sendData : { disabled : true } }; /** *     * @key -   * @value -      (name, type),          */ componentsMap = { userData : [ ["name", "fio"], ["surname", "fio"], ["email", "email"], ["send_data", "button"] ] }; /** *     listener'  stores */ @observable listenerBlocks = {}; /** *    */ @action fillBlocks = () => { for (let i in this.componentsMap) { const pageBlock = this.componentsMap[i]; //      (key)     (value) const blocks = {}; pageBlock.forEach((item, i) => { const _name = item[0]; const _type = item[1]; if (!blocks[_type]) { blocks[_type] = [_name] } else { blocks[_type].push(_name) } }) this.listenerBlocks[i] = blocks; } }; /** *    */ @action fillData = () => { if (optionsStore.preset) { // ,   undefined,     if (optionsStore.preset.name) this.userData.name = optionsStore.preset.name; if (optionsStore.preset.surname) this.userData.surname = optionsStore.preset.surname; } }; /** * Listener    */ @computed get dataInput() { const mappedResult = { fio : {}, email : { email : {} } }; if (this.FioStore && this.FioStore.items) { this.listenerBlocks.userData.fio.forEach((item) => { const i = get(this.FioStore.items, item); if (i) { mappedResult.fio[item] = { isCorrect : i.isCorrect, value : i.value } } }) } if (this.EmailStore && this.EmailStore.params) { mappedResult.email.email = { isCorrect : this.EmailStore.params.isCorrect, prevalidated : this.EmailStore.params.prevalidated, value : this.EmailStore.params.value } } return mappedResult } /** * Listener     */ @computed get sendDataButton() { let result = {}; const i = get(this.ButtonStore.items, "send_data"); if (i) { result = { isClicked : i.isClicked } } return result } } export default new mainStore(); 


其他一些关键实体。

计算的是一个装饰器,用于跟踪我们可观察的物体中的变化的函数。 Mobx的一个重要优点是它仅跟踪经过计算然后作为结果返回的数据。 仅在必要时才会发生反应,并因此重新绘制虚拟DOM。
反应 -用于根据状态变化组织副作用的工具。 它具有两个功能:第一个计算出状态,返回计算出的状态,第二个具有随状态变化而变化的效果。 在我们的示例中, 反应进行了两次。 首先,我们查看字段的状态并得出整个表格是否正确的结论,并记录每个字段的值。 在第二个步骤中,我们单击一个按钮(更确切地说,如果有一个标志“按钮被按下”),我们将数据发送到服务器。 数据对象显示在浏览器控制台中。 因为mainStore知道所有存储库,所以在处理完按钮的单击后,我们可以立即以命令式禁用标记:

 get(this.ButtonStore.items, "send_data").isClicked = false; 

您可以讨论这种强制性命令的接受程度如何,但是在任何情况下,控件仅在一个方向上进行-从mainStoreButtonStore
自动运行用于我们要直接运行某些操作的地方,而不是作为存储更改的反应。 在我们的示例中,启动了一个辅助功能,以及用字典中的数据预填充表单字段。

因此,我们采取的行动顺序如下。 组件跟踪用户事件并更改其状态。 mainStore通过计算仅基于那些已更改的状态计算结果。 不同的计算对象在不同的存储库中寻找不同状态的变化。 此外,通过反应,基于计算结果, 我们将执行带有observable的操作 ,并执行副作用(例如,我们发出AJAX请求)。 Obsevables订阅了子组件,必要时可以重画这些子组件。 单向数据流,可以完全控制更改的位置和内容。

您可以自己尝试该示例和代码。 链接到存储库: github.com/botyaslonim/mobx-habr
然后像往常一样: npm inpm run local 。 在公用文件夹中,为index.html文件。 DaData提示适用于我的免费帐户,因此,由于habr效应,它们可能会在某些时候掉线。

对于Mobx上应用程序工作的任何建设性意见和建议,我将感到高兴

最后,我要说的是,该库大大简化了数据处理工作。 对于中小型应用程序,它无疑是一个非常方便的工具,它无需考虑组件和回调的属性,而直接专注于业务逻辑。

Source: https://habr.com/ru/post/zh-CN471048/


All Articles