ReactiveX مسترجع

كل من يعمل مع Redux سيواجه عاجلاً أو آجلاً مشكلة الإجراءات غير المتزامنة. ولكن لا يمكن تطوير تطبيق حديث بدونها. هذه هي طلبات HTTP إلى الواجهة الخلفية ، وجميع أنواع المؤقتات / التأخير. يتحدث منشئو Redux أنفسهم بشكل لا لبس فيه - بشكل افتراضي ، يتم دعم تدفق البيانات المتزامن فقط ، ويجب وضع جميع الإجراءات غير المتزامنة في البرامج الوسيطة.

بالطبع ، هذا مطوّل وغير مريح للغاية ، لذلك من الصعب العثور على مطور يستخدم الوسيطة "الأصلية" فقط. المكتبات والأطر مثل Thunk و Saga و ما شابه ذلك تأتي دائمًا في الإنقاذ.

بالنسبة لمعظم المهام ، فهي كافية. ولكن ماذا لو كان هناك حاجة إلى منطق أكثر تعقيدًا بقليل من إرسال طلب واحد أو عمل مؤقت واحد؟ هنا مثال صغير:

async dispatch => { setTimeout(() => { try { await Promise .all([fetchOne, fetchTwo]) .then(([respOne, respTwo]) => { dispatch({ type: 'SUCCESS', respOne, respTwo }); }); } catch (error) { dispatch({ type: 'FAILED', error }); } }, 2000); } 

من المؤلم حتى النظر إلى هذا الرمز ، لكن من المستحيل الحفاظ عليه وتوسيعه. ماذا تفعل عندما تكون هناك حاجة إلى معالجة الأخطاء الأكثر تطوراً؟ ماذا لو كنت بحاجة إلى تكرار الطلب؟ وإذا كنت ترغب في إعادة استخدام هذه الميزة؟

اسمي ديمتري ساموفالوف ، وسأخبرك في هذا المنشور ما هو مفهوم Observable وكيفية تنفيذه بالاشتراك مع Redux ، وأيضاً قارن كل هذا بقدرات Redux-Saga.

كقاعدة عامة ، في مثل هذه الحالات ، خذ الملحمة المسترجعة. حسنًا ، نعيد كتابة الملحمات:

 try { yield call(delay, 2000); const [respOne, respTwo] = yield [ call(fetchOne), call(fetchTwo) ]; yield put({ type: 'SUCCESS', respOne, respTwo }); } catch (error) { yield put({ type: 'FAILED', error }); } 

لقد أصبح أفضل بشكل ملحوظ - الكود خطي تقريبًا ، يبدو وقراءة أفضل. لكن التوسع وإعادة الاستخدام لا يزالان صعبين ، لأن الملحمة لا تقل أهمية عن الرهان.

هناك نهج آخر. هذا هو النهج بالضبط ، وليس مكتبة أخرى لكتابة التعليمات البرمجية غير المتزامنة. يطلق عليه Rx (وهي أيضًا عناصر ملحوظة ، وتدفقات تفاعلية ، وما إلى ذلك). سنستخدمه ونعيد كتابة المثال على الملاحظة:

 action$ .delay(2000) .switchMap(() => Observable.merge(fetchOne, fetchTwo) .map(([respOne, respTwo]) => ({ type: 'SUCCESS', respOne, respTwo })) .catch(error => ({ type: 'FAILED', error })) 

لم يصبح الكود مسطحًا فقط وانخفض في الحجم ، فقد تغير مبدأ وصف الإجراءات غير المتزامنة. الآن نحن لا نعمل مباشرة مع الاستعلامات ، ولكننا ننفذ عمليات على كائنات خاصة تسمى Observable.

من المريح تمثيل "الملاحظة" كدالة تعطي دفقًا (تسلسل) من القيم. للملاحظة ثلاث حالات رئيسية - التالي ("إعطاء القيمة التالية") ، خطأ ("حدث خطأ") وكاملة ("القيم قد انتهت ، لا يوجد شيء أكثر لإعطاء"). في هذا الصدد ، يشبه إلى حد ما وعد ، لكنه يختلف في أنه من الممكن التكرار على هذه القيم (وهذه واحدة من القوى العظمى التي يمكن ملاحظتها). يمكنك التفاف أي شيء في الملاحظة - المهلات ، طلبات http ، أحداث DOM ، كائنات js فقط.



القوة العظمى الثانية التي يمكن ملاحظتها هي المشغلين. المشغل عبارة عن وظيفة تقبل وترجع الملاحظة ، ولكنها تقوم ببعض الإجراءات على مجموعة القيم. أقرب القياس هو الخريطة والتصفية من javascript (بالمناسبة ، هؤلاء المشغلون في Rx).



الأكثر فائدة بالنسبة لي شخصيا كانت مشغلي الرمز البريدي ، forkJoin ، و flatMap. باستخدام مثالهم ، من الأسهل شرح عمل المشغلين.

يعمل مشغل zip بكل بساطة - يستغرق الأمر عددًا قليلًا من الملاحظات (لا يزيد عن 9) ويعيد في الصفيف القيم التي ينبعث منها.

 const first = fromEvent("mousedown"); const second = fromEvent("mouseup"); zip(first, second) .subscribe(e => console.log(`${e[0].x} ${e[1].x}`)); //output [119,120] [120,233] … 

بشكل عام ، يمكن تمثيل عمل الرمز البريدي بواسطة المخطط:



يتم استخدام Zip إذا كان لديك العديد من الملاحظات التي يجب ملاحظتها وتحتاج إلى تلقي قيم منها باستمرار (على الرغم من إمكانية إرسالها على فترات مختلفة ، بشكل متزامن أم لا). إنه مفيد للغاية عند العمل مع أحداث DOM.

يشبه بيان forkJoin الرمز البريدي مع استثناء واحد - فهو يعرض فقط أحدث القيم من كل ملاحظة.



وفقًا لذلك ، من المعقول استخدامه عند الحاجة إلى قيم محددة فقط من الدفق.
أكثر تعقيدا قليلا هو مشغل flatMap. يستغرق الأمر "ملاحظة" كمدخلات ويعيد "ملاحظة جديدة" ، ويقوم بتعيين القيم منه إلى "الملاحظة الجديدة" ، إما باستخدام وظيفة محدد أو ملاحظة أخرى. يبدو الأمر مربكًا ، لكن المخطط بسيط جدًا:



أكثر وضوحا في الكود:

 const observable = of("Hello"); const promise = value => new Promise(resolve => resolve(`${value} World`); observable .flatMap(value => promise(value)) .subscribe(result => console.log(result)); //output "Hello World" 

في معظم الأحيان ، يتم استخدام flatMap في طلبات الواجهة الخلفية ، جنبًا إلى جنب مع switchMap و concatMap.
كيف يمكنني استخدام Rx في Redux؟ هناك مكتبة رائعة يمكن رؤيتها مسترجعة لهذا الغرض. هيكلها يشبه هذا:



جميع المشغلات التي يمكن ملاحظتها والإجراءات المتعلقة بها مصنوعة في شكل برامج وسيطة خاصة تسمى ملحمة. كل ملحمة تتخذ إجراءات كمدخلات ، وتلفها في ملحوظة ، ويجب أن تعيد العمل ، كما يمكن ملاحظتها. لا يمكنك إرجاع إجراء منتظم ، وهذا يخلق حلقة لا نهاية لها. دعنا نكتب ملحمة صغيرة تقدم طلبًا إلى api.

 const fetchEpic = action$ => action$ .ofType('FETCH_INFO') .map(() => ({ type: 'FETCH_START' })) .flatMap(() => Observable .from(apiRequest) .map(data => ({ type: 'FETCH_SUCCESS', data })) .catch(error => ({ type: 'FETCH_ERROR', error })) ) 

من المستحيل القيام بذلك دون مقارنة الملحمة المسترجعة والملاحظة المسترجعة. يبدو للكثيرين أنهم قريبون في الوظائف والقدرات ، ولكن هذا ليس هو الحال على الإطلاق. تعد Sagas أداة ضرورية تمامًا ، وهي في الأساس مجموعة من الأساليب للتعامل مع الآثار الجانبية. الملاحظة هي أسلوب مختلف تمامًا في كتابة التعليمات البرمجية غير المتزامنة ، إذا أردت ، فلسفة مختلفة.

كتبت عدة أمثلة لتوضيح الإمكانيات والنهج لحل المشكلات.

لنفترض أننا نحتاج إلى تطبيق مؤقت يتوقف عن العمل. إليك ما يبدو عليه في القصص المصورة:

 while(true) { const timer = yield race({ stopped: take('STOP'), tick: call(wait, 1000) }) if (!timer.stopped) { yield put(actions.tick()) } else { break } } 

الآن استخدم Rx:

 interval(1000) .takeUntil(action$.ofType('STOP')) 


لنفترض أن هناك مهمة لتنفيذ طلب مع الإلغاء في sagas:

 function* fetchSaga() { yield call(fetchUser); } while (yield take('FETCH')) { const fetchSaga = yield fork(fetchSaga); yield take('FETCH_CANCEL'); yield cancel(fetchSaga); } 

كل شيء أبسط على Rx:

 switchMap(() => fetchUser()) .takeUntil(action$.ofType('FETCH_CANCEL')) 

وأخيرا ، المفضل لدي. قم بتنفيذ طلب api ، في حالة الفشل ، لا تقدم أكثر من 5 طلبات متكررة مع تأخير لمدة ثانيتين. هنا ما لدينا في الملحمة:

 for (let i = 0; i < 5; i++) { try { const apiResponse = yield call(apiRequest); return apiResponse; } catch (err) { if(i < 4) { yield delay(2000); } } } throw new Error(); } 

ما يحدث في آر إكس:

 .retryWhen(errors => errors .delay(1000) .take(5)) 

إذا قمت بتلخيص إيجابيات وسلبيات الملحمة ، فستحصل على الصورة التالية:



يسهل تعلم Sagas وشعبية كبيرة ، لذلك في المجتمع يمكنك العثور على وصفات لجميع المناسبات تقريبًا. لسوء الحظ ، فإن الأسلوب الحتمي يمنع استخدام الساجا بمرونة.

لدى Rx موقف مختلف تمامًا:



قد يبدو أن Rx هو مطرقة سحرية ورصاصة فضية. لسوء الحظ ، هذا ليس كذلك. الحد الأدنى لإدخال Rx أعلى بكثير ، لذلك يصعب إدخال شخص جديد إلى مشروع يستخدم Rx بنشاط.

بالإضافة إلى ذلك ، عند العمل مع Observable ، من المهم بشكل خاص توخي الحذر وفهم جيدًا لما يحدث دائمًا. خلاف ذلك ، قد تتعثر بسبب أخطاء غير واضحة أو سلوك غير محدد.

 action$ .ofType('DELETE') .switchMap(() => Observable .fromPromise(deleteRequest) .map(() => ({ type: 'DELETE_SUCCESS'}))) 

بمجرد أن كتبت ملحمة قامت بمهمة بسيطة إلى حد ما - مع كل إجراء من النوع "DELETE" ، تم استدعاء طريقة API التي أنتجت إزالة العنصر. ومع ذلك ، كانت هناك مشاكل أثناء الاختبار. شكا المختبر من السلوك الغريب - أحيانًا عند النقر فوق زر الحذف ، لم يحدث شيء. اتضح أن مشغل switchMap يدعم تنفيذ ملاحظة واحدة فقط في وقت واحد ، وهو نوع من الحماية ضد حالة السباق.

نتيجة لذلك ، سأقدم بعض التوصيات التي أتبعها وأحث كل من يبدأ العمل مع Rx على اتباعها:

  • كن حذرا.
  • تحقق من الوثائق.
  • تحقق في رمل.
  • اكتب الاختبارات.
  • لا تطلق النار على العصافير من المدفع.

Source: https://habr.com/ru/post/ar460155/


All Articles