Aujourd'hui, je veux vous parler de la façon dont la transition vers Mobx s'est déroulée sur notre projet, quels avantages il offre. Un projet type sera également présenté et des explications sur les principaux problèmes seront données. Mais d'abord, introductif.

Pourquoi avez-vous besoin de passer à quelque chose? En fait, la réponse à cette question est déjà la moitié de la bataille. Beaucoup aiment désormais appliquer les nouvelles technologies uniquement parce qu'elles sont nouvelles. Une bonne ligne dans le CV, la possibilité de s'épanouir, pour être dans la tendance. C'est génial quand vous pouvez simplement avancer.
Et pourtant, chaque outil doit résoudre ses propres problèmes, et nous les repoussons d'une manière ou d'une autre lorsque nous écrivons du code commercial.
Dans notre projet, il existe un certain nombre de widgets où l'utilisateur entre ses données, interagit avec les formulaires. En règle générale, chaque widget dispose de plusieurs écrans. Il était une fois, tout fonctionnait sur le bon vieux moteur de template MarkoJS + requis jQuery sur le client. L'interaction avec les formulaires a été écrite dans un style impératif, si ... sinon, des rappels et c'est tout de même ce qui semble déjà être dans le passé.
Puis vint le moment de
React . La logique métier du client devenait de plus en plus épaisse, il y avait beaucoup d'options d'interaction, le code impératif s'est transformé en un gâchis compliqué. Le code de réaction déclarative s'est avéré beaucoup plus pratique. Il a finalement été possible de se concentrer sur la logique plutôt que sur la présentation, de réutiliser les composants et de répartir facilement les tâches de développement de nouvelles fonctionnalités entre les différents employés.
Mais l'application sur Pure React au fil du temps repose sur des limites tangibles. Bien sûr, nous nous ennuyons d'écrire this.setState pour tout le monde et de penser à son asynchronie, mais lancer des données et des rappels à travers l'épaisseur des composants rend la tâche particulièrement difficile. En bref, le moment est venu de séparer complètement les données et la présentation. Il ne s'agit pas de savoir comment vous pouvez le faire sur Pure React, mais dans les cadres industriels qui implémentent l'architecture Flux des applications frontales ont été populaires récemment.
Selon le nombre d'articles et de références dans les postes vacants,
Redux est le plus célèbre d'entre nous. En fait, j'ai déjà apporté ma main pour l'installer dans notre projet et commencer le développement, comme au tout dernier moment (et c'est littéralement!) Le diable a traversé le Habr, puis il y a eu juste une discussion sur le sujet "Redux ou Mobx?" Voici cet article:
habr.com/en/post/459706 . Après l'avoir lu, ainsi que tous les commentaires en dessous, j'ai réalisé que j'utiliserais toujours
Mobx .
Encore une fois. La réponse à la question la plus importante - pourquoi tout cela? - cela ressemble à ceci: il est temps de séparer la présentation et les données, je voudrais construire la gestion des données dans un style déclaratif (comme le dessin), pas de pollinisation croisée des rappels et des attributs transmis.
Nous sommes maintenant prêts à continuer.
1. À propos de l'application
Nous devons construire à l'avant un concepteur d'écrans et de formulaires, qui pourraient ensuite être rapidement mélangés, connectés les uns aux autres en fonction des exigences changeantes de l'entreprise. Cela nous amène inévitablement à ce qui suit: créer une collection de composants complètement isolés, ainsi que certains composants de base qui correspondent à chacun de nos widgets (en fait, ce sont des SPA distincts créés à chaque fois dans le cadre d'une nouvelle analyse de rentabilité dans l'application générale).
Les exemples montreront une version tronquée de l'un de ces gadgets. Afin de ne pas empiler de code supplémentaire, que ce soit une forme de trois champs de saisie et boutons.
2. Données
Mobx n'est pas essentiellement un framework, c'est juste une bibliothèque. Le manuel déclare explicitement qu'il n'organise pas directement vos données. Vous devez vous-même créer une telle organisation. Soit dit en passant, nous utilisons
Mobx 4 car la version 5 utilise le type de données Sybmol, qui, malheureusement, n'est pas pris en charge par tous les navigateurs.
Ainsi, toutes les données sont allouées à des entités distinctes. Notre application vise un ensemble de deux dossiers:
-
composants où nous mettons toute la vue
- les
magasins , qui contiendront des données, ainsi que la logique de travailler avec eux.
Par exemple, un composant de saisie de données typique pour nous se compose de deux fichiers:
Input.js et
InputStore.js . Le premier fichier est un stupide composant React qui est strictement responsable de l'affichage, le second est les données de ce composant, les règles utilisateur (
onClick ,
onChange , etc ...)
Avant de passer directement aux exemples, nous devons résoudre un autre problème important.
3. Gestion
Eh bien, nous avons des composants complètement autonomes du View-Store, mais comment pouvons-nous nous réunir dans une application entière? Pour l'affichage, nous aurons le composant racine d'
App.js , et pour la gestion des flux de données, le stockage principal est
mainStore.js . Le principe est simple: mainStore sait tout sur tous les référentiels de tous les composants nécessaires (il sera montré ci-dessous comment cela est réalisé). D'autres référentiels ne connaissent rien du monde (enfin, il y aura une exception - les dictionnaires). Ainsi, nous avons la garantie de savoir où vont nos données et où les intercepter.
mainStore de manière déclarative, en modifiant des parties de son état, peut contrôler le reste des composants. Dans la figure suivante,
Actions et
État font référence aux magasins de composants et les
valeurs calculées font référence à
mainStore :

Commençons à écrire le code. Le fichier d'application principal 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";
Ici, vous pouvez voir le concept de base de Mobx. Les données (magasins) sont disponibles n'importe où dans l'application via le mécanisme du
fournisseur . Nous emballons notre application en répertoriant les installations de stockage nécessaires. Pour utiliser le
fournisseur, connectez le
module mobx-react . Pour que le magasin principal de contrôle
mainStore ait accès à toutes les autres données depuis le début, nous initialisons les magasins enfants dans
mainStore :
Maintenant
App.js , le squelette de notre application
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> ); } }
Il existe deux autres concepts de base de Mobx: l'
injection et l'
observateur .
inject n'implémente que le magasin nécessaire dans l'application. Différentes parties de notre application utilisent différents référentiels, que nous listons en
inject , séparés par des virgules. Naturellement, les magasins enfichables doivent être initialement répertoriés dans le
fournisseur . Les référentiels sont disponibles dans le composant via
this.props.yourStoreName .
observateur - le décorateur indique que notre composant sera abonné à des données modifiées à l'aide de Mobx. Les données ont changé - une réaction s'est produite dans le composant (elle sera montrée ci-dessous). Ainsi, aucun abonnement ni rappel spéciaux - Mobx fournit les modifications lui-même!
Nous reviendrons sur la gestion de l’ensemble de l’application dans
mainStore , mais pour le moment nous allons faire les composants. Nous en avons trois types -
Fio ,
Email ,
Button . Que le premier et le troisième soient universels et que la
messagerie soit personnalisée. Commençons par lui.
L'écran est le composant muet habituel de React:
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> ); } }
Nous connectons le composant externe de validation, et il est important de le faire une fois que l'élément est déjà inclus dans la mise en page. Par conséquent, la méthode du magasin est appelée dans
componentDidMount .
Maintenant, le référentiel lui-même:
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();
Il convient de prêter attention à deux nouvelles entités.
observable - un objet dont tout changement de champs est surveillé par Mobx (et envoie des signaux à l'
observateur , qui est abonné à notre stockage spécifique).
action - ce décorateur doit envelopper tout gestionnaire qui modifie l'état de l'application et / ou provoque des effets secondaires. Ici, nous modifions la valeur de la
valeur dans les
paramètres @observable .
Voilà, notre composant simple est prêt! Il peut suivre les données des utilisateurs et les enregistrer. Plus tard, nous verrons comment le
référentiel central
mainStore s'abonne pour modifier ces données.
Maintenant un composant
Fio typique. Sa différence avec le précédent est que nous allons utiliser des composants de ce type un nombre illimité de fois dans une seule application. Cela impose des exigences supplémentaires au magasin de composants. En plus de cela, nous ferons plus de conseils sur les caractères d'entrée en utilisant l'excellent service
DaData . Affichage:
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() { 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";
Il y a quelque chose de nouveau ici: nous n'accédons pas directement à l'état du composant, mais via
get :
get(FioStore.items, name)
Le fait est que le nombre d'instances de composants est illimité et que le référentiel est un pour tous les composants de ce type. Par conséquent, lors de l'enregistrement, nous entrons les paramètres de chaque instance dans
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(() => { 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 = []; } } }); }) } @observable items = new Map([]); @action registration = (params) => { const nameExists = get(this.items, params.name); if (!blockValidate({params, nameExists, type: "Fio"})) return false;
L'état de notre composant générique est initialisé comme suit:
@observable items = new Map([]);
Il serait plus pratique de travailler avec un objet JS standard, cependant, il ne sera pas "exploité" lors de la modification des valeurs de ses champs, car les champs sont ajoutés dynamiquement lorsque de nouveaux composants sont ajoutés à la page. La réception des conseils DaData que nous supprimons séparément.
Le composant bouton est similaire, mais il n'y a pas de conseils:
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() {} @observable items = new Map([]) @action registration = (params) => { const nameExists = get(this.items, params.name); if (!blockValidate({params, nameExists, type: "Button"})) return false;
Le composant du
Button est encapsulé par le composant HOC de
ButtonArea . Veuillez noter que l'ancien composant comprend son propre ensemble de magasins et le plus jeune. Dans les chaînes de composants imbriqués, il n'est pas nécessaire de transmettre de paramètres et de rappels. Tout ce qui est nécessaire au fonctionnement d'un composant particulier y est ajouté directement.
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> ); } }
Nous avons donc tous les composants prêts. L'affaire est laissée au directeur de
mainStore. Tout d'abord, tout le code de stockage:
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 } }; componentsMap = { userData : [ ["name", "fio"], ["surname", "fio"], ["email", "email"], ["send_data", "button"] ] }; @observable listenerBlocks = {}; @action fillBlocks = () => { for (let i in this.componentsMap) { const pageBlock = this.componentsMap[i];
Quelques autres entités clés.
calculé est un décorateur pour les fonctions qui suivent les changements dans notre
observable . Un avantage important de Mobx est qu'il ne suit que les données qui sont calculées puis renvoyées en conséquence. La réaction et, par conséquent, le redessin du DOM viral se produit uniquement lorsque cela est nécessaire.
réaction - un outil pour organiser les effets secondaires en fonction d'un état modifié. Il prend deux fonctions: la première calculée, renvoyant l'état calculé, la seconde avec des effets qui devraient suivre les changements d'état. Dans notre exemple, la
réaction est appliquée deux fois. Dans le premier, nous examinons l'état des champs et concluons si le formulaire entier est correct, et enregistrons également la valeur de chaque champ. Dans le second, on clique sur un bouton (plus précisément, s'il y a un signe "bouton enfoncé"), on envoie des données au serveur. L'objet de données s'affiche dans la console du navigateur. Puisque
mainStore connaît tous les référentiels, immédiatement après avoir traité le clic d'un bouton, nous pouvons nous permettre de désactiver le drapeau dans un style impératif:
get(this.ButtonStore.items, "send_data").isClicked = false;
Vous pouvez discuter de l'acceptabilité de la présence d'un tel «impératif», mais dans tous les cas, le contrôle n'a lieu que dans une seule direction - de
mainStore à
ButtonStore .
l'exécution automatique est utilisée lorsque nous voulons exécuter certaines actions directement, pas en réaction à la sauvegarde des modifications. Dans notre exemple, une fonction auxiliaire est lancée, ainsi que le préremplissage des champs du formulaire avec les données du dictionnaire.
Ainsi, la séquence d'actions que nous avons est la suivante. Les composants suivent les événements utilisateur et changent leur état.
mainStore à travers
calculé calcule le résultat basé uniquement sur les états qui ont changé. Différents
calculés recherchent des changements dans différents états dans différents référentiels. De plus, par
réaction, sur
la base des résultats
calculés ,
nous effectuons des actions avec des
observables , ainsi que des effets secondaires (par exemple, nous faisons une demande AJAX).
Obsevables s'abonne aux composants enfants, qui sont redessinés si nécessaire. Un flux de données unidirectionnel avec un contrôle total sur où et ce qui change.
Vous pouvez essayer l' exemple et le code vous-même. Lien vers le référentiel:
github.com/botyaslonim/mobx-habr .
Puis comme d'habitude:
npm i ,
npm run local . Dans le dossier
public , fichier
index.html . Les indices DaData fonctionnent sur mon compte gratuit, par conséquent, ils peuvent probablement tomber à certains moments en raison de l'effet habr.
Je serai heureux de tout commentaire constructif et suggestions sur le travail des applications sur
Mobx!En conclusion, je dirai que la bibliothèque a grandement simplifié le travail avec les données. Pour les petites et moyennes applications, ce sera certainement un outil très pratique pour oublier les propriétés des composants et des rappels et se concentrer directement sur la logique métier.