أساسيات البرمجة التفاعلية باستخدام RxJS. الجزء 2. المشغلين والأنابيب



في مقال سابق ، نظرنا إلى ماهية الجداول وماذا تأكل. في الجزء الجديد ، سوف نتعرف على الأساليب التي توفرها RxJS لإنشاء التدفقات ، ما هي المشغلات والأنابيب وكيفية التعامل معها.

سلسلة من المقالات "أساسيات البرمجة التفاعلية باستخدام RxJS":



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

تمثيل رسومي للخيوط


لإظهار بوضوح كيف يتصرف تدفق معين ، سأستخدم الترميز المعتمد في النهج التفاعلي. أذكر مثالنا من المقال السابق:

const observable = new Observable((observer) => { observer.next(1); observer.next(2); observer.complete(); }); 

إليك الشكل الذي سيبدو عليه تمثيل الرسوم البيانية:



يصور التدفق عادة كخط مستقيم. إذا أرسل الدفق أي قيمة ، فسيتم عرضه على السطر كدائرة. الخط المستقيم في الشاشة هو الإشارة لإنهاء الدفق. لعرض الخطأ ، استخدم الرمز - "×".

 const observable = new Observable((observer) => { observer.error(); }); 



تيارات سطر واحد


في ممارستي ، نادراً ما اضطررت إلى إنشاء مثيلات ملحوظة خاصة بي مباشرة. معظم طرق إنشاء سلاسل الرسائل موجودة بالفعل في RxJS. لإنشاء دفق ينبعث من القيمتين 1 و 2 ، يكفي استخدام الطريقة:

 const observable = of(1, 2); 

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



إذا كنت تريد تمثيل الصفيف كدفق ، فيمكنك استخدام الأسلوب from. تتوقع الطريقة from كوسيطة أي كائن قابل للتكرار (صفيف أو سلسلة أو ما إلى ذلك) أو وعد ، وتقوم بعرض هذا الكائن على الدفق. إليك ما سيبدو عليه الدفق الذي تم الحصول عليه من السلسلة:

 const observable = from('abc'); 



وهكذا ، يمكنك التفاف وعد في دفق:

 const promise = new Promise((resolve, reject) => { resolve(1); }); const observable = from(promise); 



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

هل تتذكر المثال مع الفاصل الزمني من المادة الأولى ؟ هذا التدفق هو مؤقت يقوم بحساب الوقت بالثواني من لحظة الاشتراك.

 const timer = new Observable(observer => { let counter = 0; const intervalId = setInterval(() => { observer.next(counter++); }, 1000); return () => { clearInterval(intervalId); } }); 

إليك كيفية تنفيذ نفس الشيء في سطر واحد:

 const timer = interval(1000); 



وأخيرًا ، طريقة تسمح لك بإنشاء دفق أحداث لعناصر DOM:

 const observable = fromEvent(domElementRef, 'keyup'); 

كقيم ، سوف يتلقى هذا التدفق وينبعث من كائنات حدث keyup.

الأنابيب والمشغلين


توجيه الإخراج هو طريقة فئة ملاحظ المضافة في RxJS في الإصدار 5.5. بفضله ، يمكننا بناء سلاسل من المشغلين للمعالجة المتسلسلة للقيم التي تم استلامها في التدفق. Pipe هي قناة أحادية الاتجاه تربط بين المشغلين. المشغلون أنفسهم وظائف عادية موصوفة في RxJS تعالج القيم من دفق.

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

دعونا نلقي نظرة على المشغلين في العمل. اضرب كل قيمة من الدفق ب 2 باستخدام مشغل الخريطة:

 of(1,2,3).pipe( map(value => value * 2) ).subscribe({ next: console.log }); 

إليك ما يبدو عليه الدفق قبل تطبيق مشغل الخريطة:



بعد بيان الخريطة:



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

 of(1, 2, 3).pipe( //     filter(value => value % 2 !== 0), map(value = value * 2) ).subscribe({ next: console.log }); 

وهذه هي الطريقة التي سيبدو بها مخطط جدول أعمالنا بأكمله:



بعد التصفية:



بعد الخريطة:



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

نحن نكتب الطلب


الآن وبعد أن اكتشفنا ما هي الأنابيب والمشغلين ، يمكنك الانخراط في التدريب. سيقوم تطبيقنا بمهمة واحدة بسيطة: عرض قائمة بمخازن جيثب المفتوحة بواسطة اللقب المستعار للمالك.

سيكون هناك القليل من المتطلبات:

  • لا تقم بتنفيذ طلب API إذا كانت السلسلة المدخلة في الإدخال تحتوي على أقل من 3 أحرف ؛
  • لكي لا تفي بالطلب الخاص بكل حرف يدخله المستخدم ، يجب عليك ضبط التأخير (الإلغاء) على 700 ميلي ثانية قبل الوصول إلى API ؛

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

لنبدأ مع ترميز HTML. دعونا وصف عناصر المدخلات و ul:

 <input type="text"> <ul></ul> 

بعد ذلك ، في ملف js أو ts ، نحصل على روابط للعناصر الحالية باستخدام واجهة برمجة تطبيقات المتصفح:

 const input = document.querySelector('input'); const ul = document.querySelector('ul'); 

نحتاج أيضًا إلى طريقة من شأنها تنفيذ طلب إلى واجهة برمجة تطبيقات github. يوجد أدناه رمز دالة getUsersRepsFromAPI ، التي تقبل كنية المستخدم وتنفذ طلبًا باستخدام ajax باستخدام الجلب. ثم تقوم بإرجاع وعد ، وتحويل الاستجابة الناجحة إلى json على طول الطريق:

 const getUsersRepsFromAPI = (username) => { const url = `https://api.github.com/users/${ username }/repos`; return fetch(url) .then(response => { if(response.ok) { return response.json(); } throw new Error(''); }); } 

بعد ذلك ، نكتب طريقة تسرد أسماء المستودعات:

 const recordRepsToList = (reps) => { for (let i = 0; i < reps.length; i++) { //    ,    if (!ul.children[i]) { const newEl = document.createElement('li'); ul.appendChild(newEl); } //      const li = ul.children[i]; li.innerHTML = reps[i].name; } //    while (ul.children.length > reps.length) { ul.removeChild(ul.lastChild); } } 

الاستعدادات كاملة. لقد حان الوقت لإلقاء نظرة على RxJS في العمل. نحن بحاجة إلى الاستماع إلى الحدث keyup من المدخلات لدينا. بادئ ذي بدء ، يجب أن نفهم أنه في نهج رد الفعل ، ونحن نعمل مع التدفقات. لحسن الحظ ، RxJS يوفر بالفعل خيار مماثل. تذكر طريقة fromEvent التي ذكرتها أعلاه. نستخدمها:

 const keyUp = fromEvent(input, 'keyup'); keyUp.subscribe({ next: console.log }); 

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

 fromEvent(input, 'keyup').pipe( map(event => event.target.value) ).subscribe({ next: console.log }); 

ننتقل إلى تنفيذ المتطلبات. بادئ ذي بدء ، سنقوم بتنفيذ الاستعلام عندما تحتوي القيمة المدخلة على أكثر من حرفين. للقيام بذلك ، استخدم عامل التصفية:

 fromEvent(input, 'keyup').pipe( map(event => event.target.value), filter(value => value.length > 2) ) 

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

 fromEvent(input, 'keyup').pipe( debounceTime(700), map(event => event.target.value), filter(value => value.length > 2) ) 

إليك ما قد يبدو عليه دفقنا دون debounceTime:



وهذه هي الطريقة التي سيبدو بها الدفق نفسه عبر هذا البيان:



مع debounceTime ، سنكون أقل عرضة لاستخدام واجهة برمجة التطبيقات (API) ، والتي ستوفر حركة المرور وتفريغ الخادم.

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

 from('aaabccc').pipe( distinctUntilChanged() ) 

بدون تمييزإلغاء التغيير:



مع متميزةتغيير:



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

اذهب إلى الخادم


بالفعل الآن يمكننا وصف منطق الطلب ومعالجة الاستجابة. بينما يمكننا أن نعمل فقط مع وعد. لذلك ، نحن نصف مشغل خريطة آخر سوف يستدعي طريقة getUsersRepsFromAPI. في المراقب ، نصف منطق معالجة وعدنا:

 /*  !     RxJS    promise,      */ fromEvent(input, 'keyup').pipe( debounceTime(700), map(event => event.target.value), filter(val => val.length > 2), distinctUntilChanged(), map(value => getUsersRepsFromAPI(value)) ).subscribe({ next: promise => promise.then(reps => recordRepsToList(reps)) }); 

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

بالطبع ، يمكننا تعليق الالتزام بالوعد في الطريقة التالية ، ولكن لهذا السبب ، سيبدأ الكود الخاص بنا في التشبه أكثر فأكثر بـ "جحيم معاودة الاتصال". إذا احتجنا فجأة إلى تنفيذ طلب آخر ، فسوف تزداد درجة تعقيد الشفرة.

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

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

يمكن حل هذه المشكلة باستخدام عامل التشغيل mergeMap:

 fromEvent(input, 'keyup').pipe( debounceTime(700), map(event => event.target.value), filter(val => val.length > 2), distinctUntilChanged(), mergeMap(value => from(getUsersRepsFromAPI(value))) ).subscribe({ next: reps => recordRepsToList(reps), error: console.log }) 

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

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

خطأ في التعامل


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

هنا سوف يساعدنا مشغل catchError. يتم رفع catchError فقط عند حدوث خطأ في الدفق. يسمح لك باعتراضها ومعالجتها والعودة إلى الدفق بالقيمة المعتادة ، والتي لن تؤدي إلى إكمالها.

 fromEvent(input, 'keyup').pipe( debounceTime(700), map(event => event.target.value), filter(val => val.length > 2), distinctUntilChanged(), mergeMap(value => from(getUsersRepsFromAPI(value))), catchError(err => of([])) ).subscribe({ next: reps => recordRepsToList(reps), error: console.log }) 

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

الشيء هو أن catchError يستبدل تيارنا الأصلي بآخر جديد. ثم يستمع مراقبنا إليه فقط. عندما ينبعث التيار من صفيف فارغ ، سيتم استدعاء الطريقة الكاملة.

من أجل عدم استبدال مؤشر الترابط الأصلي الخاص بنا ، فإننا ندعو عامل التشغيل catchError الموجود على من مؤشر الترابط من داخل مشغل mergeMap.

 fromEvent(input, 'keyup').pipe( debounceTime(700), map(event => event.target.value), filter(val => val.length > 2), distinctUntilChanged(), mergeMap(value => { return from(getUsersRepsFromAPI(value)).pipe( catchError(err => of([])) ) }) ).subscribe({ next: reps => recordRepsToList(reps), error: console.log }) 

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

استنتاج


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

آمل أن يكون هذا المقال قد ساعدك على فهم كيفية عمل RxJS بشكل أفضل. أتمنى لك التوفيق في دراستك!

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


All Articles