أريد أن أخبركم اليوم عن كيفية الانتقال إلى Mobx في مشروعنا ، وما المزايا التي يقدمها. كما سيتم عرض مشروع نموذجي وسيتم تقديم توضيحات حول القضايا الرئيسية. ولكن أولا ، تمهيدية.

لماذا تحتاج إلى التبديل إلى شيء ما؟ في الواقع ، الجواب على هذا السؤال هو بالفعل نصف المعركة. يحب الكثيرون الآن تطبيق تقنيات جديدة فقط لأنها جديدة. خط جيد في السيرة الذاتية ، وإمكانية التنمية الذاتية ، ليكون في هذا الاتجاه. انه لامر رائع عندما يمكنك فقط المضي قدما.
ومع ذلك ، يجب أن تحل كل أداة مشكلاتها الخاصة ، ونصدها بطريقة أو بأخرى عندما نكتب الرمز التجاري.
في مشروعنا ، هناك عدد من عناصر واجهة المستخدم حيث يقوم المستخدم بإدخال بياناته ، ويتفاعل مع النماذج. كقاعدة عامة ، كل عنصر واجهة مستخدم لديه عدة شاشات. ذات مرة ، عملت جميعها على المحرك القديم MarkoJS + templating الذي يتطلب jQuery على العميل. تمت كتابة التفاعل مع النماذج بأسلوب حتمي ، إذا ... آخر ، عمليات الاسترجاعات وهذا هو نفسه الذي يبدو بالفعل في الماضي.
ثم جاء الوقت
لرد الفعل . كان منطق العمل على العميل يزداد سماكة ، وكان هناك الكثير من خيارات التفاعل ، تحولت الكود الإلزامي إلى فوضى معقدة. تحول رمز رد الفعل التعريفي إلى أن يكون أكثر ملاءمة. كان من الممكن أخيرًا التركيز على المنطق بدلاً من العرض التقديمي وإعادة استخدام المكونات وتوزيع المهام بسهولة لتطوير ميزات جديدة بين مختلف الموظفين.
لكن التطبيق على رد الفعل النقي مع مرور الوقت يعتمد على حدود ملموسة. بالطبع ، نحن نشعر بالملل من كتابة this.setState للجميع والتفكير في عدم التزامن بها ، ولكن رمي البيانات وعمليات الاستدعاء من خلال سماكة المكونات يجعل الأمر صعبًا للغاية. باختصار ، لقد حان الوقت لفصل البيانات والعرض تمامًا. لا يتعلق الأمر بالكيفية التي يمكنك بها إدارة ذلك باستخدام React الخالص ، ولكن في الأطر الصناعية التي تطبق بنية Flux للتطبيقات الأمامية أصبحت شائعة في الآونة الأخيرة.
وفقًا لعدد المقالات والمراجع في الوظائف الشاغرة ، فإن
Redux هي الأكثر شهرة بيننا. في الواقع ، لقد أحضرت يدي بالفعل لتثبيته في مشروعنا وبدء التطوير ، كما في اللحظة الأخيرة (وهذا هو حرفيًا!) قام الشيطان بسحب حبر ، ثم كان هناك مجرد مناقشة لموضوع "Redux أو Mobx؟" إليكم هذه المقالة:
habr.com/en/post/459706 . بعد قراءتها ، وكذلك جميع التعليقات التي تحتها ، أدركت أنني ما زلت أستخدم
Mobx .
مرة اخرى الجواب على السؤال الأكثر أهمية - لماذا كل هذا؟ - يبدو الأمر كما يلي: لقد حان الوقت لفصل العرض التقديمي والبيانات ، وأود أن أبني إدارة البيانات بأسلوب تعريفي (مثل الرسم) ، وليس التلقيح المتبادل لعمليات الاسترجاعات والسمات المعاد توجيهها.
الآن نحن على استعداد للمضي قدما.
1. حول التطبيق
نحتاج إلى أن نبني في المقدمة مصممًا للشاشات والأشكال ، والتي يمكن بعد ذلك خلطها سريعًا ، متصلاً مع الآخر بعد المتطلبات المتغيرة للأعمال. هذا ينقلنا حتماً إلى ما يلي: لإنشاء مجموعة من المكونات المعزولة تمامًا ، وكذلك بعض المكونات الأساسية التي تتوافق مع كل أداة من أدواتنا المصغّرة (في الواقع ، هذه هي SPA منفصلة يتم إنشاؤها في كل مرة بموجب حالة عمل جديدة في التطبيق العام).
ستُظهر الأمثلة نسخة مقطوعة من إحدى هذه الأدوات. حتى لا تتراكم التعليمات البرمجية الإضافية ، فليكن شكلًا من ثلاثة حقول وأزرار إدخال.
2. البيانات
Mobx ليس إطارًا أساسيًا ؛ إنه مجرد مكتبة. ينص الدليل صراحة على أنه لا ينظم بياناتك مباشرة. أنت نفسك يجب أن تأتي مع هذه المنظمة. بالمناسبة ، نستخدم
Mobx 4 لأن الإصدار 5 يستخدم نوع بيانات Sybmol ، والذي ، للأسف ، غير معتمد من قبل جميع المتصفحات.
لذلك ، يتم تخصيص جميع البيانات لكيانات منفصلة. يهدف تطبيقنا إلى مجموعة من مجلدين:
-
المكونات حيث نضع كل رأي
-
المخازن ، والتي سوف تحتوي على بيانات ، وكذلك منطق العمل معهم.
على سبيل المثال ، يتكون مكون إدخال البيانات النموذجي الخاص بنا من ملفين:
Input.js و
InputStore.js . الملف الأول هو مكون React غبي وهو مسؤول تمامًا عن العرض ، والثاني هو بيانات هذا المكون ، وقواعد المستخدم (
onClick ،
onChange ، إلخ ...)
قبل أن نذهب مباشرة إلى الأمثلة ، نحتاج إلى حل مشكلة أخرى مهمة.
3. الإدارة
حسنًا ، لدينا مكونات مستقلة تمامًا لـ View-Store ، لكن كيف نلتقي في تطبيق كامل؟ للعرض ، سيكون لدينا المكون الرئيسي لـ
App.js ،
ولإدارة تدفق البيانات ، فإن التخزين الرئيسي هو
mainStore.js . المبدأ بسيط: 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";
هنا يمكنك رؤية المفهوم الأساسي لـ Mobx. تتوفر البيانات (المخازن) في أي مكان في التطبيق من خلال آلية
الموفر . نلتف طلبنا من خلال سرد مرافق التخزين اللازمة. لاستخدام
الموفر ، نقوم بتوصيل وحدة
mobx-react . لكي يتمكن المتجر الرئيسي
mainStore من الوصول إلى جميع البيانات الأخرى من البداية ، نقوم بتهيئة المتاجر الفرعية داخل
mainStore :
الآن
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 الأساسية -
الحقن والمراقب .
حقن فقط بتنفيذ المتجر الضروري في التطبيق. تستخدم أجزاء مختلفة من تطبيقنا مستودعات مختلفة ، نقوم
بإدراجها في
الحقن ، مفصولة بفواصل. وبطبيعة الحال ، يجب أن يتم سرد المخازن القابلة للتوصيل في البداية في
الموفر . تتوفر المستودعات في المكون من خلال
this.props.yourStoreName .
مراقب - يشير الديكور إلى أن المكون الخاص بنا سيتم الاشتراك في البيانات التي يتم تعديلها باستخدام Mobx. لقد تغيرت البيانات - حدث رد فعل في المكون (سيظهر أدناه). وبالتالي ، لا الاشتراكات الخاصة وعمليات الاسترجاعات - Mobx يسلم التغييرات نفسها!
سوف نعود إلى إدارة التطبيق بالكامل في
المتجر الرئيسي ، لكن الآن سنفعل المكونات. لدينا ثلاثة أنواع منها -
Fio ،
Email ،
Button . فليكن الأول والثالث عالميًا ،
والبريد الإلكتروني مخصصًا. لنبدأ معه.
العرض هو المكون المعتاد React 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> ); } }
نقوم بتوصيل المكون الخارجي للتحقق من الصحة ، ومن المهم القيام بذلك بعد تضمين العنصر بالفعل في التخطيط. لذلك ، يتم استدعاء الأسلوب من 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();
يجدر الانتباه إلى كيانين جديدين.
يمكن ملاحظته - كائن ، تتم مراقبة أي تغيير في الحقول منه بواسطة Mobx (ويرسل إشارات إلى
المراقب ، المشترك في تخزيننا المحدد).
الإجراء - يجب على هذا الديكور التفاف أي معالج يغير حالة التطبيق و / أو يتسبب في آثار جانبية. نحن هنا نغير قيمة
القيمة في
paramobsobservable .
هذا كل شيء ، لدينا مكون بسيط جاهز! يمكنه تتبع بيانات المستخدم وتسجيلها. سنرى لاحقًا كيف
يشترك المستودع المركزي في
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() { 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";
يوجد شيء جديد هنا: لا يمكننا الوصول إلى حالة المكون مباشرةً ، ولكن من خلال
الحصول على :
get(FioStore.items, name)
الحقيقة هي أن عدد مثيلات المكون غير محدود ، والمستودع واحد لكل مكونات هذا النوع. لذلك ، أثناء التسجيل ، ندخل معلمات كل مثيل في
الخريطة :
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;
تتم تهيئة حالة المكون العام لدينا على النحو التالي:
@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() {} @observable items = new Map([]) @action registration = (params) => { const nameExists = get(this.items, params.name); if (!blockValidate({params, nameExists, type: "Button"})) return false;
يتم لف مكون
الزر بواسطة مكون
HOC من
ButtonArea . يرجى ملاحظة أن المكون الأقدم يتضمن مجموعة المتاجر الخاصة به ، الأصغر منها. في سلاسل المكونات المتداخلة ، ليست هناك حاجة لإعادة توجيه أي معلمات وعمليات الاسترجاعات. كل ما هو مطلوب لتشغيل مكون معين يضاف إليه مباشرة.
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.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];
عدد قليل من الكيانات الرئيسية.
حساب هو الديكور للوظائف التي تتبع التغييرات في
ملاحظتنا . من أهم ميزات Mobx أنه يتعقب فقط البيانات التي يتم حسابها ثم يتم إرجاعها كنتيجة لذلك. رد الفعل ، ونتيجة لذلك ، إعادة رسم DOM الفيروسية يحدث فقط عند الضرورة.
رد فعل - أداة لتنظيم الآثار الجانبية على أساس الحالة المتغيرة. يستغرق وظيفتين: الأولى المحسوبة ، وإرجاع الحالة المحسوبة ، والثانية مع الآثار التي يجب أن تتبع تغييرات الحالة. في مثالنا ، يتم تطبيق
رد الفعل مرتين. في البداية ، ننظر إلى حالة الحقول ونستنتج ما إذا كان النموذج بأكمله صحيحًا ، وكذلك نسجل قيمة كل حقل. في الثانية ، نضغط على زر (بتعبير أدق ، إذا كان هناك علامة "الضغط على زر") ، فإننا نرسل البيانات إلى الخادم. يتم عرض كائن البيانات في وحدة تحكم المستعرض. نظرًا لأن
mainStore يعرف جميع المستودعات ،
فبمجرد معالجة نقرة زر واحدة ، يمكننا تحمل تعطيل العلامة بأسلوب ضروري:
get(this.ButtonStore.items, "send_data").isClicked = false;
يمكنك مناقشة مدى مقبولية وجود هذه "الضرورة" ، ولكن في أي حال ، يتم التحكم في اتجاه واحد فقط - من
mainStore إلى
ButtonStore .
يتم استخدام
التشغيل التلقائي حيث نريد تشغيل بعض الإجراءات مباشرةً ، وليس كرد فعل لتخزين التغييرات. في المثال الخاص بنا ، يتم إطلاق وظيفة مساعدة واحدة ، بالإضافة إلى ملء حقول النموذج مسبقًا ببيانات من القاموس.
وبالتالي ، فإن تسلسل الإجراءات التي لدينا هي على النحو التالي. مكونات تتبع أحداث المستخدم وتغيير حالتها.
mainStore من خلال
حساب يحسب النتيجة بناء على تلك الحالة التي تغيرت فقط. تبحث
تلك المحسوبة المختلفة عن تغييرات في حالات مختلفة في مستودعات مختلفة. علاوة على ذلك ، من خلال
رد الفعل ، استنادًا إلى النتائج
المحسوبة ،
فإننا ننفذ إجراءات مع عناصر
ملحوظة ، وكذلك ننفذ آثارًا جانبية (على سبيل المثال ، نطلب AJAX). تشترك عناصر
المراقبة في المكونات الفرعية ، والتي يتم إعادة رسمها إذا لزم الأمر. دفق بيانات أحادي الاتجاه مع تحكم كامل في أين وما التغييرات.
يمكنك تجربة المثال والرمز بنفسك. رابط إلى المستودع:
github.com/botyaslonim/mobx-habr .
ثم كالمعتاد:
npm i ،
npm تشغيل محلي . في المجلد
العمومي ، ملف
index.html . تلميحات DaData تعمل على حسابي المجاني ، وبالتالي ، فمن المحتمل أن تقع في بعض النقاط بسبب تأثير habr.
سأكون سعيدًا بأي تعليقات واقتراحات بناءة حول عمل التطبيقات على
Mobx!في الختام ، سأقول إن المكتبة قد سهّلت العمل إلى حد كبير باستخدام البيانات. للتطبيقات الصغيرة والمتوسطة الحجم ، سيكون بالتأكيد أداة مريحة للغاية لنسيان خصائص المكونات وعمليات الاسترجاع والتركيز مباشرة على منطق العمل.