في الوقت الحاضر ، هناك عدد قليل من الناس يكتبون في بيرل ، ولكن المبدأ الشهير الشهير لاري وول "إبقاء الأشياء البسيطة سهلة وصعبة ممكن" أصبح الصيغة المقبولة عمومًا للتكنولوجيا الفعالة. يمكن تفسيره في جانب ليس فقط تعقيد المهام ، ولكن أيضًا في النهج: يجب أن تسمح التكنولوجيا المثالية ، من ناحية ، بالتطوير السريع للتطبيقات المتوسطة والصغيرة (بما في ذلك "الكتابة فقط") ، من ناحية أخرى ، توفر أدوات للتطوير المدروس التطبيقات المعقدة ، حيث تعتبر الموثوقية والقدرة على الصيانة والهيكلية أمرًا بالغ الأهمية. أو حتى ، ترجمة إلى الطائرة البشرية: لتكون في متناول جونز ، وفي الوقت نفسه لتلبية طلبات Signyors.
يمكن انتقاد المحررين المشهورين الآن من كلا الجانبين - خذ على الأقل حقيقة أن الكتابة حتى الوظيفية الأولية يمكن أن تؤدي إلى العديد من الأسطر متباعدة عبر عدة ملفات - لكننا لن نتعمق ، لأن الكثير قد قيل بالفعل حول هذا الموضوع.
"من المفترض أن تحتفظ بجميع الطاولات في غرفة واحدة ، والكراسي في غرفة أخرى"
- جحا بانانين ، منشئ مكتبة Bacon.js ، عن المحرر
التكنولوجيا التي سيتم مناقشتها اليوم ليست رصاصة فضية ، ولكنها تدعي أنها أكثر اتساقًا مع هذه المعايير.
Mrr هي مكتبة تفاعلية وظيفية تدعي مبدأ "كل شيء يتدفق". تتمثل المزايا الرئيسية التي يوفرها النهج الوظيفي التفاعلي في mrr في الإيجاز والتعبير عن الكود ، فضلاً عن اتباع نهج موحد لتحويلات البيانات المتزامنة وغير المتزامنة.
للوهلة الأولى ، لا يبدو هذا كأنه تقنية يمكن الوصول إليها بسهولة للمبتدئين: يمكن أن يكون مفهوم الدفق صعب الفهم ، فهو ليس واسع الانتشار على الواجهة الأمامية ، ويرتبط بشكل أساسي بمكتبات غبية مثل Rx. والأهم من ذلك ، أنه ليس من الواضح تمامًا كيفية شرح التدفقات استنادًا إلى نظام DOM "لتحديث رد فعل الفعل" الأساسي. لكن ... لن نتحدث بصورة مجردة عن التدفقات! دعونا نتحدث عن أشياء أكثر قابلية للفهم: الأحداث ، الشرط.
الطبخ حسب الوصفة
دون الدخول في براري FRP ، سنتبع خطة بسيطة لإضفاء الطابع الرسمي على مجال الموضوع:
- قم بعمل قائمة من البيانات التي تصف حالة الصفحة وسيتم استخدامها في واجهة المستخدم ، وكذلك أنواعها.
- قم بعمل قائمة بالأحداث التي تحدث أو يتم إنشاؤها بواسطة المستخدم على الصفحة ، وأنواع البيانات التي سيتم إرسالها معهم
- قم بعمل قائمة بالعمليات التي ستحدث على الصفحة
- تحديد الترابط بينهما.
- وصف الاعتمادات المتبادلة باستخدام عوامل التشغيل المناسبة.
في الوقت نفسه ، نحتاج إلى معرفة المكتبة فقط في المرحلة الأخيرة.
لذا ، فلنأخذ مثالًا مبسطًا لمتجر على الإنترنت ، حيث توجد قائمة بالمنتجات ذات ترقيم الصفحات والترشيح حسب الفئة ، بالإضافة إلى سلة.
البيانات على أساسها سيتم بناء الواجهة:
- قائمة السلع (مجموعة)
- الفئة المحددة (خط)
- عدد الصفحات مع البضائع (العدد)
- قائمة المنتجات الموجودة في السلة (الصفيف)
- الصفحة الحالية (رقم)
- عدد المنتجات في السلة (العدد)
الأحداث (بكلمة "الأحداث" تعني الأحداث اللحظية فقط. الإجراءات التي تحدث لفترة من الوقت - العمليات - تحتاج إلى أن تتحلل إلى أحداث منفصلة):
- الصفحة الافتتاحية (باطلة)
- اختيار الفئة (سلسلة)
- إضافة البضائع إلى السلة (كائن "البضائع")
- إزالة البضائع من السلة (هوية البضائع المراد حذفها)
- انتقل إلى الصفحة التالية من قائمة المنتجات (رقم - رقم الصفحة)
العمليات: هذه هي الإجراءات التي تبدأ ومن ثم يمكن أن تنتهي بأحداث مختلفة في وقت واحد أو بعد مرور بعض الوقت. في حالتنا ، سيكون هذا هو تحميل بيانات المنتج من الخادم ، مما قد يستلزم حدثين: الإكمال والإكمال الناجحين مع وجود خطأ.
الترابط بين الأحداث والبيانات. على سبيل المثال ، تعتمد قائمة المنتجات على الحدث: "التحميل الناجح لقائمة المنتجات". و "البدء في تحميل قائمة البضائع" - من "فتح الصفحة" ، "اختيار الصفحة الحالية" ، "اختيار فئة". قم بعمل قائمة بالنموذج [element]: [... تبعيات]:
{ requestGoods: ['page', 'category', 'pageLoaded'], goods: ['requestGoods.success'], page: ['goToPage', 'totalPages'], totalPages: ['requestGoods.success'], cart: ['addToCart', 'removeFromCart'], goodsInCart: ['cart'], category: ['selectCategory'] }
أوه ... ولكن هذا هو تقريبا رمز السيد!

يبقى فقط لإضافة وظائف تصف العلاقة. قد يكون لديك أحداث وبيانات وعمليات متوقعة لتكون كيانات مختلفة في السيد - ولكن لا ، كل هذا خيوط! مهمتنا هي توصيلهم بشكل صحيح.
كما ترون ، لدينا نوعان من التبعيات: "البيانات" من "الأحداث" (على سبيل المثال ، الصفحة من goToPage) و "البيانات" من "البيانات" (goodsInCart من السلة). لكل منهم هناك النهج المناسبة.
أسهل طريقة هي "البيانات من البيانات": هنا نضيف ببساطة وظيفة خالصة ، "الصيغة":
goodsInCart: [arr => arr.length, 'cart'],
في كل مرة يتم تغيير صفيف السلة ، سيتم إعادة حساب قيمة goodsInCart.
إذا كانت بياناتنا تعتمد على حدث واحد ، فسيكون كل شيء بسيطًا أيضًا:
category: 'selectCategory', goods: [resp => resp.data, 'requestGoods.success'], totalPages: [resp => resp.totalPages, 'requestGoods.success'],
تصميم النموذج [دالة ، ... خيط الوسيطات] هو أساس السيد. لفهم حدسي ، رسم تشبيه مع Excel ، تدفقات في mrr تسمى أيضًا الخلايا ، والوظائف التي يتم حسابها تسمى الصيغ.
إذا كانت بياناتنا تعتمد على عدة أحداث ، فيجب علينا تحويل قيمها بشكل فردي ، ثم دمجها في دفق واحد باستخدام عامل الدمج:
page: ['merge', [a => a, 'goToPage'], [(a, prev) => a < prev ? a : prev, 'totalPages', '-page'] ], cart: ['merge', [(item, arr) => [...arr, item], 'addToCart', '-cart'], [(id, arr) => arr.filter(item => item.id !== id), 'removeFromCart', '-cart'], ],
في كلتا الحالتين ، نشير إلى القيمة السابقة للخلية. لمنع حلقة لا نهائية ، نشير إلى العربة وخلايا الصفحة بشكل سلبي (ناقص علامة أمام اسم الخلية): سيتم استبدال قيمها في الصيغة ، ولكن إذا تغيرت ، فلن تبدأ عملية إعادة الحساب.
يتم بناء كل سلاسل العمليات إما على أساس سلاسل العمليات الأخرى أو المنبعثة من DOM. ولكن ماذا عن تيار "الصفحة الافتتاحية"؟ لحسن الحظ ، لست مضطرًا إلى استخدام componentDidMount: في mrr ، هناك بداية دفق خاصة ، والتي تشير إلى أنه تم إنشاء المكون وتثبيته.
يتم احتساب "العمليات" بشكل غير متزامن ، بينما نصدر بعض الأحداث منها ، سيساعدنا المشغل "المتداخل" هنا:
requestGoods: ['nested', (cb, page, category) => { fetch("...") .then(res => cb('success', res)) .catch(e => cb('error', e)); }, 'page', 'category', '$start'],
عند استخدام عامل التشغيل المتداخل ، سيتم تمرير الوسيطة الأولى إلينا وظيفة رد اتصال لإصدار أحداث معينة. في هذه الحالة ، يمكن الوصول إليها من الخارج من خلال مساحة اسم الخلية الجذرية ، على سبيل المثال ،
cb('success', res)
داخل صيغة requestGoods ، سيؤدي تحديث خلية requestGoods.success.
لعرض الصفحة بشكل صحيح قبل حساب بياناتنا ، يمكنك تحديد قيمها الأولية:
{ goods: [], page: 1, cart: [], },
إضافة العلامات. نقوم بإنشاء مكون React باستخدام دالة withMrr ، التي تقبل مخطط الارتباط التفاعلي ووظائف التجسيد. من أجل "وضع" قيمة في دفق ، نستخدم الدالة $ ، التي تنشئ (وتخزين ذاكرات) معالجات الأحداث. الآن يبدو تطبيقنا العامل بالكامل كما يلي:
import { withMrr } from 'mrr'; const App = withMrr({ $init: { goods: [], cart: [], page: 1, }, requestGoods: ['nested', (cb, page = 1, category = 'all') => { fetch('https://reqres.in/api/products?page=', page, category).then(r => r.json()) .then(res => cb('success', res)) .catch(e => cb('error', e)) }, 'page', 'selectCategory', '$start'], goods: [res => res.data, 'requestGoods.success'], page: ['merge', 'goToPage', [(a, prev) => a < prev ? a : prev, 'totalPages', '-page']], totalPages: [res => res.total_pages, 'requestGoods.success'], category: 'selectCategory', cart: ['merge', [(item, arr) => [...arr, item], 'addToCart', '-cart'], [(id, arr) => arr.filter(item => item.id !== id), 'removeFromCart', '-cart'], ], }, (state, props, $) => { return (<section> <h2>Shop</h2> <div> Category: <select onChange={$('selectCategory')}> <option>All</option> <option>Electronics</option> <option>Photo</option> <option>Cars</option> </select> </div> <ul className="goods"> { state.goods.map((item, i) => { const cartI = state.cart.findIndex(a => a.id === item.id); return (<li key={i}> { item.name } <div> { cartI === -1 && <button onClick={$("addToCart", item)}>Add to cart</button> } { cartI !== -1 && <button onClick={$("removeFromCart", item.id)}>Remove from cart</button> } </div> </li>); }) } </ul> <ul className="pages"> { new Array(state.totalPages).fill(true).map((_, p) => { const page = Number(p) + 1; return ( <li className="page" onClick={$('goToPage', page)} key={p}> { page } </li> ); }) } </ul> </section> <section> <h2>Cart</h2> <ul> { state.cart.map((item, i) => { return (<li key={i}> { item.name } <div> <button onClick={$("removeFromCart", item.id)}>Remove from cart</button> </div> </li>); }) } </ul> </section>); }); export default App;
البناء
<select onChange={$('selectCategory')}>
يعني أنه عند تغيير الحقل ، سيتم "دفع" القيمة إلى دفق selectCategory. ولكن ما هو المعنى؟ بشكل افتراضي ، هذا هو event.target.value ، ولكن إذا كنا بحاجة إلى دفع شيء آخر ، فنحن نحدده باستخدام الوسيطة الثانية ، كما يلي:
<button onClick={$("addToCart", item)}>
كل شيء هنا - الأحداث والبيانات والعمليات - هذه هي التدفقات. يؤدي تشغيل حدث ما إلى إعادة حساب البيانات أو الأحداث التي تعتمد عليها ، وهكذا على طول السلسلة. يتم حساب قيمة الدفق التابع باستخدام صيغة يمكن أن تُرجع قيمة ، أو وعد (عندئذٍ ، سينتظر السيد السيد الدقة الخاصة به).
mrr API موجزة وموجزة للغاية - بالنسبة لمعظم الحالات ، نحتاج فقط إلى 3-4 مشغلين أساسيين ، ويمكن القيام بالعديد من الأشياء بدونهم. أضف رسالة خطأ عند عدم تحميل قائمة المنتجات بنجاح ، وسيتم عرضها لمدة ثانية واحدة:
hideErrorMessage: [() => new Promise(res => setTimeout(res, 1000)), 'requestGoods.error'], errorMessageShown: [ 'merge', [() => true, 'requestGoods.error'], [() => false, 'hideErrorMessage'], ],
ملح ، فلفل السكر حسب الذوق
هناك أيضا السكر النحوي في السيد ، وهو اختياري للتنمية ، ولكن يمكن تسريعها. على سبيل المثال ، مشغل التبديل:
errorMessageShown: ['toggle', 'requestGoods.error', [() => new Promise(res => setTimeout(res, 1000)), 'showErrorMessage']],
سيؤدي التغيير في الوسيطة الأولى إلى تعيين الخلية على "صحيح" ، وفي الثانية إلى "خطأ".
إن النهج المتبع في "تحليل" نتائج المهمة غير المتزامنة إلى النجاح وعوامل الخطأ الفرعية واسع الانتشار أيضًا بحيث يمكنك استخدام مشغل الوعد الخاص (الذي يلغي تلقائيًا حالة السباق):
requestGoods: [ 'promise', (page = 1, category = 'all') => fetch('https://reqres.in/api/products?page=', page, category).then(r => r.json()), 'page', 'selectCategory', '$start' ],
تناسب الكثير من الوظائف في بضع عشرات من الخطوط. اقتنعنا بشرط شهر يونيو - لقد تمكن من كتابة كود عمل ، والذي اتضح أنه مضغوط للغاية: كل المنطق ملائم في ملف واحد وعلى شاشة واحدة. لكن علامة يدق بشكل لا يصدق: Eka غير مرئي ... يمكنك كتابة هذا على السنانير / إعادة التركيب / الخ
نعم بالفعل! بطبيعة الحال ، من غير المرجح أن تكون الشفرة أكثر تشابكًا وتنظيمًا ، لكن هذا ليس هو الموضوع. دعنا نتخيل أن المشروع قيد التطوير ، ونحن بحاجة إلى تقسيم الوظيفة إلى صفحتين منفصلتين: قائمة بالمنتجات وسلة. علاوة على ذلك ، من الواضح أنه يجب تخزين بيانات السلة على مستوى العالم لكلا الصفحتين.
نهج واحد ، واجهة واحدة
هنا نأتي إلى مشكلة أخرى لتطوير التفاعل: وجود نُهج غير متجانسة لإدارة الحالة محليًا (داخل مكون) وعلى مستوى عالمي على مستوى التطبيق بأكمله. أنا متأكد من أن الكثير منهم واجهوا معضلة: لتنفيذ بعض المنطق محليًا أو عالميًا؟ أو موقف آخر: اتضح أن هناك حاجة إلى حفظ جزء من البيانات المحلية على الصعيد العالمي ، وعليك إعادة كتابة جزء من الوظيفة ، على سبيل المثال ، من recompos إلى المحرر ...
التباين ، بطبيعة الحال ، مصطنع ، وهو ليس كذلك: إنه جيد بنفس القدر ، والأهم من ذلك أنه موحد! - مناسبة لكل من إدارة الدولة المحلية والعالمية. بشكل عام ، لا نحتاج إلى أي حالة عالمية ، فنحن لدينا القدرة على تبادل البيانات بين المكونات ، وبالتالي ستكون حالة المكون الجذر "عالمية".
مخطط تطبيقنا الآن على النحو التالي: المكون الجذر الذي يحتوي على قائمة البضائع في السلة ، واثنين من المكونات الفرعية: البضائع والسلة ، والمكون العالمي "يستمع" إلى التدفقات "إضافة إلى سلة" و "إزالة من سلة" من المكونات الفرعية.
const App = withMrr({ $init: { cart: [], currentPage: 'goods', }, cart: ['merge', [(item, arr) => [...arr, item], 'addToCart', '-cart'], [(id, arr) => arr.filter(item => item.id !== id), 'removeFromCart', '-cart'], ], }, (state, props, $, connectAs) => { return ( <div> <menu> <li onClick={$('currentPage', 'goods')}>Goods</li> <li onClick={$('currentPage', 'cart')}>Cart{ state.cart && state.cart.length ? '(' + state.cart.length + ')' : '' }</li> </menu> <div> { state.currentPage === 'goods' && <Goods {...connectAs('goods', ['addToCart', 'removeFromCart'], ['cart'])}/> } { state.currentPage === 'cart' && <Cart {...connectAs('cart', { 'removeFromCart': 'remove' }, ['cart'])}/> } </div> </div> ); })
const Goods = withMrr({ $init: { goods: [], page: 1, }, goods: [res => res.data, 'requestGoods.success'], requestGoods: [ 'promise', (page = 1, category = 'all') => fetch('https://reqres.in/api/products?page=', page, category).then(r => r.json()), 'page', 'selectCategory', '$start' ], page: ['merge', 'goToPage', [(a, prev) => a < prev ? a : prev, 'totalPages', '-page']], totalPages: [res => res.total, 'requestGoods.success'], category: 'selectCategory', errorShown: ['toggle', 'requestGoods.error', [cb => new Promise(res => setTimeout(res, 1000)), 'requestGoods.error']], }, (state, props, $) => { return (<div> ... </div>); });
const Cart = withMrr({}, (state, props, $) => { return (<div> <h2>Cart</h2> <ul> { state.cart.map((item, i) => { return (<div> { item.name } <div> <button onClick={$('remove', item.id)}>Remove from cart</button> </div> </div>); }) } </ul> </div>); });
إنه لأمر مدهش كيف تغير القليل! لقد وضعنا ببساطة التدفقات في المكونات المقابلة ووضعنا "جسور" بينهما! من خلال توصيل المكونات باستخدام وظيفة mrrConnect ، فإننا نحدد التعيين للتدفقات المتلقية للمعلومات:
connectAs( 'goods', ['addToCart', 'removeFromCart'], ['cart'] )
هنا ، ستذهب تيارات addToCart و removeFromCart من المكون الفرعي إلى الوالد ، وسيعود دفق العربة. لسنا ملزمين باستخدام أسماء الدفق نفسها - إذا لم تتطابق ، فسنستخدم التعيين:
connectAs('cart', { 'removeFromCart': 'remove' })
سيكون دفق الإزالة من المكون الفرعي هو مصدر دفق الإزالةFromCart في الأصل.
كما ترون ، تتم إزالة مشكلة اختيار موقع تخزين البيانات في حالة السيد بشكل كامل: يمكنك تخزين البيانات حيث يتم تحديدها منطقيا.
هنا مرة أخرى ، لا يمكن للمرء أن يفشل في ملاحظة عيب المحرر: حيث يجب حفظ جميع البيانات في مستودع مركزي واحد. حتى البيانات التي قد يتم طلبها واستخدامها بواسطة مكون منفصل واحد فقط أو الشجرة الفرعية الخاصة به! إذا كتبنا في "أسلوب التحرير" ، فسننتقل أيضًا إلى تحميل البضائع وترقيمها في صفحات إلى المستوى العالمي (في الإنصاف - هذا النهج ، بفضل مرونة السيد ، ممكن أيضًا وله الحق في الحياة ، شفرة المصدر ).
ومع ذلك ، هذا ليس ضروريا. يتم استخدام البضائع المحملة فقط في مكون البضائع ، وبالتالي ، فنقلها إلى المستوى العالمي ، فنحن نسد ونضخّم الحالة العالمية فقط. بالإضافة إلى ذلك ، سيتعين علينا مسح البيانات القديمة (على سبيل المثال ، صفحة ترقيم الصفحات) عندما يعود المستخدم إلى صفحة المنتج مرة أخرى. عند اختيار المستوى الصحيح لتخزين البيانات ، نتجنب هذه المشاكل تلقائيًا.
ميزة أخرى لهذا النهج هي أن منطق التطبيق يتم دمجه مع العرض التقديمي ، والذي يسمح لنا بإعادة استخدام مكونات React الفردية باعتبارها أدوات تعمل بكامل طاقتها ، وليس كقوالب "غبية". أيضًا ، مع الاحتفاظ بحد أدنى من المعلومات على المستوى العالمي (من الناحية المثالية ، هذه هي بيانات الجلسة فقط) واستخراج معظم المنطق في مكونات صفحة منفصلة ، فإننا نحد من تماسك الشفرة إلى حد كبير. بالطبع ، هذا النهج غير قابل للتطبيق دائمًا ، ولكن هناك عددًا كبيرًا من المهام التي تكون فيها الحالة العالمية صغيرة للغاية وتكون "الشاشات" الفردية مستقلة تمامًا عن بعضها البعض: على سبيل المثال ، أنواع مختلفة من المسؤولين ، إلخ. على عكس المحرر ، الذي يستفزنا إلى أخذ كل ما هو مطلوب وليس مطلوبًا على المستوى العالمي ، يسمح لك السيد بتخزين البيانات في مجموعات فرعية منفصلة ، مما يشجع على جعل التغليف ممكنًا ، وبالتالي تحويل تطبيقنا من "فطيرة" متجانسة إلى "كعكة" ذات طبقات.
تجدر الإشارة إلى: بالطبع ، لا يوجد شيء ثوري جديد في النهج المقترح! تعتبر المكونات ، عناصر واجهة المستخدم المستقلة ، أحد الأساليب الأساسية المستخدمة منذ ظهور أطر عمل js. والفرق الوحيد المهم هو أن السيد يتبع المبدأ التصريحي: يمكن للمكونات فقط الاستماع إلى تدفقات المكونات الأخرى ، ولكن لا يمكنها التأثير عليها (ما الذي يتعين عليها فعله من الاتجاه من الأسفل إلى الأعلى ، أو الاتجاه من أعلى إلى أسفل ، والذي يختلف عن التمويه) النهج). تتوافق المكونات الذكية التي يمكنها فقط تبادل الرسائل مع المكونات الأساسية والمكونات الأصلية مع نموذج الممثل الشهير ولكن غير المعروف في تطوير الواجهة الأمامية (يتم تناول موضوع استخدام العناصر والخيوط على الواجهة الأمامية جيدًا في المقالة مقدمة في البرمجة التفاعلية ).
بالطبع ، هذا أبعد ما يكون عن التنفيذ الكنسي للجهات الفاعلة ، لكن الجوهر هو هذا بالضبط: يتم لعب دور الجهات الفاعلة من خلال مكونات تبادل الرسائل من خلال تدفقات MPP ؛ يمكن للمكون (بشكل تعريفي!) إنشاء وحذف العناصر الفاعلة للمكونات الفرعية بفضل DOM الظاهري و React: تحدد وظيفة التجسيد ، في جوهرها ، بنية العناصر التابعة.
بدلاً من الوضع القياسي لـ React ، عندما "نسقط" المكون الأصل في رد اتصال معين عبر الدعائم ، يجب أن نستمع إلى تدفق المكون الفرعي من الوالد. نفس الشيء في الاتجاه المعاكس ، من الأب إلى الطفل. على سبيل المثال ، قد تسأل: لماذا تنقل بيانات سلة التسوق إلى مكون سلة التسوق كتيار ، إذا أمكننا ، دون مزيد من اللغط ، تمريرها كدعائم؟ ما هو الفرق؟ في الواقع ، يمكن أيضًا استخدام هذا النهج ، ولكن فقط حتى تكون هناك حاجة إلى الاستجابة للتغيرات في الدعائم. إذا سبق لك استخدام طريقة componentWillReceiveProps ، فأنت تعرف ماذا عن هذا. هذا نوع من "تفاعل الفقراء": تستمع إلى كل تغييرات الدعائم ، وتحدد ما الذي تغير ، وتتفاعل. ولكن هذه الطريقة ستختفي قريبًا من React ، وقد تنشأ الحاجة إلى رد فعل على "إشارات من الأعلى".
في السيد ، لا يتدفق "التدفق" لأعلى فحسب ، بل أيضًا لأسفل التسلسل الهرمي للمكون ، بحيث يمكن للمكونات الاستجابة بشكل مستقل لتغيرات الحالة. في القيام بذلك ، يمكنك استخدام القوة الكاملة للأدوات التفاعلية السيد.
const Cart = withMrr({ foo: [items => {
أضف القليل من البيروقراطية
المشروع ينمو ، يصبح من الصعب تتبع أسماء التدفقات ، التي هي - يا رعب! - يتم تخزينها في الصفوف. حسنًا ، يمكننا استخدام الثوابت لأسماء الدفق وكذلك عبارات السيد. أصبح الآن كسر أحد التطبيقات عن طريق الخطأ المطبعي الصغير أكثر صعوبة.
import { withMrr } from 'mrr'; import { merge, toggle, promise } from 'mrr/operators'; import { cell, nested, $start$, passive } from 'mrr/cell'; const goods$ = cell('goods'); const page$ = cell('page'); const totalPages$ = cell('totalPages'); const category$ = cell('category'); const errorShown$ = cell('errorShown'); const addToCart$ = cell('addToCart'); const removeFromCart$ = cell('removeFromCart'); const selectCategory$ = cell('selectCategory'); const goToPage$ = cell('goToPage'); const Goods = withMrr({ $init: { [goods$]: [], [page$]: 1, }, [goods$]: [res => res.data, requestGoods$.success], [requestGoods$]: promise((page, category) => fetch('https://reqres.in/api/products?page=', page).then(r => r.json()), page$, category$, $start$), [page$]: merge(goToPage$, [(a, prev) => a < prev ? a : prev, totalPages$, passive(page$)]), [totalPages$]: [res => res.total, requestGoods$.success], [category$]: selectCategory$, [errorShown$]: toggle(requestGoods$.error, [cb => new Promise(res => setTimeout(res, 1000)), requestGoods$.error]), }, ...);
ماذا يوجد في الصندوق الاسود؟
ماذا عن الاختبار؟ من السهل الفصل بين المنطق الموصوف في مكون mrr ، ثم اختباره.
دعونا نجعل هيكل السيد بشكل منفصل عن ملفنا.
const GoodsStruct = { $init: { [goods$]: [], [page$]: 1, }, ... } const Goods = withMrr(GoodsStruct, (state, props, $) => { ... }); export { GoodsStruct }
ثم نستوردها في اختباراتنا. مع غلاف بسيط نستطيع
ضع القيمة في الدفق (كما لو كانت قد تم إجراؤها من DOM) ، ثم تحقق من قيم مؤشرات الترابط الأخرى التي تعتمد عليها.
import { simpleWrapper} from 'mrr'; import { GoodsStruct } from '../src/components/Goods'; describe('Testing Goods component', () => { it('should update page if it\'s out of limit ', () => { const a = simpleWrapper(GoodsStruct); a.set('page', 10); assert.equal(a.get('page'), 10); a.set('requestGoods.success', {data: [], total: 5}); assert.equal(a.get('page'), 5); a.set('requestGoods.success', {data: [], total: 10}); assert.equal(a.get('page'), 5); }) })
تألق وفقر التفاعل
تجدر الإشارة إلى أن التفاعل هو مجرد تجسيد لمستوى أعلى مقارنة بالتشكيل "اليدوي" للدولة بناءً على الأحداث في المحرر. تسهيل التنمية ، من ناحية ، يخلق فرصًا لإطلاق النار على قدمك. خذ بعين الاعتبار هذا السيناريو: ينتقل المستخدم إلى الصفحة رقم 5 ، ثم يقوم بالتبديل "عامل تصفية" الفئة. يجب علينا تحميل قائمة منتجات الفئة المحددة في الصفحة الخامسة ، ولكن قد يتضح أن البضائع في هذه الفئة لا تحتوي إلا على ثلاث صفحات. في حالة الواجهة الخلفية "الغبية" ، تكون خوارزمية أفعالنا كما يلي:
- صفحة بيانات الطلب = 5 & الفئة =٪ فئة٪
- خذ من الإجابة قيمة عدد الصفحات
- إذا تم إرجاع عدد صفر من السجلات ، فاطلب أكبر صفحة متاحة
إذا أردنا تنفيذ هذا على "المحرر" ، فسيتعين علينا إنشاء إجراء غير متزامن واحد كبير مع المنطق الموضح. في حالة التفاعل على السيد ، ليست هناك حاجة لوصف هذا السيناريو بشكل منفصل. كل شيء موجود بالفعل في هذه السطور:
[requestGoods$]: ['nested', (cb, page, category) => { fetch('https://reqres.in/api/products?page=', page).then(r => r.json()) .then(res => cb('success', res)) .catch(e => cb('error', e)) }, page$, category$, $start$], [totalPages$]: [res => res.total, requestGoods$.success], [page$]: merge(goToPage$, [(a, prev) => a < prev ? a : prev, totalPages$, passive(page$)]),
إذا كانت قيمة totalPages الجديدة أقل من الصفحة الحالية ، فسنقوم بتحديث قيمة الصفحة وبالتالي نبدأ في تقديم طلب ثانٍ إلى الخادم.
ولكن إذا كانت وظيفتنا تُرجع نفس القيمة ، فسيظل يُنظر إليها على أنها تغيير في دفق الصفحة ، تليها استعادة جميع التدفقات التابعة. لتجنب هذا ، السيد لديه معنى خاص - تخطي. عند إعادته ، فإننا نشير إلى أنه: لم تحدث أي تغييرات ، فلا يجب تحديث أي شيء.
import { withMrr, skip } from 'mrr'; [requestGoods$]: nested((cb, page, category) => { fetch('https://reqres.in/api/products?page=', page).then(r => r.json()) .then(res => cb('success', res)) .catch(e => cb('error', e)) }, page$, category$, $start$), [totalPages$]: [res => res.total, requestGoods$.success], [page$]: merge(goToPage$, [(a, prev) => a < prev ? a : skip, totalPages$, passive(page$)]),
وبالتالي ، يمكن أن يؤدي بنا خطأ صغير إلى حلقة لا نهائية: إذا لم نرجع إلى "تخطي" ، ولكن "السابق" ، ستتغير خلية الصفحة وسيحدث طلب ثان ، وهكذا في دائرة. بطبيعة الحال ، فإن احتمال حدوث مثل هذا الموقف ليس "عيبًا معيبًا" لـ FRP أو السيد ، حيث أن إمكانية التكرار اللانهائي أو حلقة لا تشير إلى الأفكار المعيبة للبرمجة الهيكلية. ومع ذلك ، ينبغي أن يكون مفهوما أن السيد لا يزال يتطلب بعض الفهم لآلية التفاعل. بالعودة إلى الاستعارة المعروفة للسكاكين ، فإن mrr هو سكين حاد للغاية يعمل على تحسين كفاءة العمل ، ولكنه قد يؤدي أيضًا إلى إصابة عامل غير كفء.
بالمناسبة ، الخصم المباشر للسيد هو سهل للغاية دون تثبيت أي ملحقات:
const GoodsStruct = { $init: { ... }, $log: true, ... }
ما عليك سوى إضافة $ log: إلى هيكل mrr ، وسيتم إخراج جميع التغييرات على الخلايا إلى وحدة التحكم ، حتى تتمكن من رؤية التغييرات وماهية ذلك.
مفاهيم مثل الاستماع السلبي أو معنى التخطي ليست "عكازين" محددة: فهي توسع من إمكانيات التفاعل بحيث يمكن بسهولة وصف المنطق الكامل للتطبيق دون اللجوء إلى أساليب حتمية. آليات مماثلة ، على سبيل المثال ، في Rx.js ، ولكن واجهتها أقل ملاءمة. : Mrr: FRP
.
ملخص
- FRP, mrr ,
- : ,
- , ,
- , , - ( - !)
- mrr : " , !"
- ,
- , , ( ). !
- : , , , TMTOWTDI: , - .
PS
. , mrr , , :
import useMrr from 'mrr/hooks'; function Foo(props){ const [state, $, connectAs] = useMrr(props, { $init: { counter: 0, }, counter: ['merge', [a => a + 1, '-counter', 'incr'], [a => a - 1, '-counter', 'decr'] ], }); return ( <div> Counter: { state.counter } <button onClick={ $('incr') }>increment</button> <button onClick={ $('decr') }>decrement</button> <Bar {...connectAs('bar')} /> </div> ); }