التوقف عن استخدام Ngrx / الآثار لهذا الغرض

تمثال نصفي مع الآثار

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

ملاحظات المترجم

كُتب المقال في عام 2017 ، لكنه ذو صلة بهذا اليوم. إنه موجه للأشخاص ذوي الخبرة في RxJS و Ngrx ، أو الذين يرغبون في تجربة Redux in Angular.


تم تحديث مقتطفات الكود بناءً على صيغة RxJS الحالية وتم تعديلها قليلاً لتحسين قابلية القراءة وسهولة الفهم.

Ngrx / store هي مكتبة Angular تساعد على احتواء تعقيد الوظائف الفردية. أحد الأسباب هو أن ngrx / store يشتمل على البرمجة الوظيفية ، والتي تقيد ما يمكن القيام به داخل إحدى الوظائف لتحقيق المزيد من المعقولية خارجها. في ngrx / store ، تعتبر أشياء مثل المخفضات (المشار إليها فيما يلي باسم المخفضات) ، والمحددات (المشار إليها فيما يلي باسم المحددات) ومشغلي RxJS وظائف خالصة.


الوظائف الصافية أسهل في الاختبار والتصحيح والتحليل والموازنة والجمع. وظيفة نظيفة إذا:


  • مع نفس المدخلات ، فإنها ترجع دائما نفس المخرجات.
  • لا آثار جانبية.

لا يمكن تجنب الآثار الجانبية ، ولكنها معزولة في ngrx / store ، وبالتالي فإن بقية التطبيق قد تتكون من وظائف خالصة.


آثار جانبية


عندما يرسل المستخدم النموذج ، نحتاج إلى إجراء تغييرات على الخادم. يعد تغيير الخادم والاستجابة للعميل أحد الآثار الجانبية. يمكن معالجة هذا في المكون:


this.store.dispatch({ type: 'SAVE_DATA', payload: data, }); this.saveData(data) // POST    .pipe(map(res => this.store.dispatch({ type: 'DATA_SAVED' }))) .subscribe(); 

سيكون من الرائع أن نتمكن من إرسال الإجراء (المشار إليه فيما يلي باسم الإجراء) داخل المكون عندما يقوم المستخدم بإرسال النموذج والتعامل مع التأثير الجانبي في مكان آخر.


Ngrx / تأثيرات هي الوسيطة لمعالجة الآثار الجانبية في ngrx / store. يستمع إلى الإجراءات المرسلة في الخيط الذي يمكن ملاحظته ، ويؤدي تأثيرات جانبية ، ويعيد إجراءات جديدة على الفور أو بشكل غير متزامن. يتم تمرير الإجراءات التي تم إرجاعها إلى المخفض.


القدرة على التعامل مع الآثار الجانبية بطريقة RxJS تجعل الشفرة أكثر نظافة. بعد إرسال الإجراء الأولي SAVE_DATA من المكون ، يمكنك إنشاء فئة تأثير للتعامل مع الباقي:


 @Effect() saveData$ = this.actions$.pipe( ofType('SAVE_DATA'), pluck('payload'), switchMap(data => this.saveData(data)), map(res => ({ type: 'DATA_SAVED' })), ); 

هذا يبسط تشغيل المكون فقط قبل إرسال الإجراءات والاشتراك في يمكن ملاحظتها.


من السهل إساءة استخدام Ngrx / تأثيرات


Ngrx / effects هو حل قوي للغاية ، لذلك من السهل إساءة استخدامه. فيما يلي بعض أنماط مكافحة ngrx / store الشائعة التي يبسطها Ngrx / تأثيرات:


1. مكررة الدولة


افترض أنك تعمل على تطبيق من نوع ما للوسائط المتعددة ، وأن لديك الخصائص التالية في شجرة الحالة:


 export interface State { mediaPlaying: boolean; audioPlaying: boolean; videoPlaying: boolean; } 

نظرًا لأن الصوت هو نوع من الوسائط ، كلما كان audioPlaying صحيحًا ، يجب أن يكون تشغيل الوسائط صحيحًا أيضًا. لذا ، إليك السؤال التالي: "كيف أتأكد من تحديث mediaPlaying عند تحديث audioPlaying؟"


إجابة غير صالحة : استخدام Ngrx / تأثيرات!


 @Effect() playMediaWithAudio$ = this.actions$.pipe( ofType('PLAY_AUDIO'), map(() => ({ type: 'PLAY_MEDIA' })), ); 

الإجابة الصحيحة هي : إذا كانت حالة mediaPlaying التنبؤ بها بالكامل من جانب جزء آخر من شجرة الحالة ، فإن هذه ليست حالة حقيقية. هذه حالة مشتقة. إنه ينتمي إلى المحدد وليس إلى المتجر.


 audioPlaying$ = this.store.select('audioPlaying'); videoPlaying$ = this.store.select('videoPlaying'); mediaPlaying$ = combineLatest(this.audioPlaying$, this.videoPlaying$).pipe( map(([audioPlaying, videoPlaying]) => audioPlaying || videoPlaying), ); 

الآن يمكن أن تظل حالتنا نظيفة وطبيعية ، ونحن لا نستخدم Ngrx / تأثيرات لشيء ليس من الآثار الجانبية.


2. تسلسل الإجراءات مع المخفض


تخيل أن لديك هذه الخصائص في شجرة الولاية الخاصة بك:


 export interface State { items: { [index: number]: Item }; favoriteItems: number[]; } 

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


إجابة غير صالحة : استخدام Ngrx / تأثيرات!


 @Effect() removeFavoriteItemId$ = this.actions$.pipe( ofType('DELETE_ITEM_SUCCESS'), map(() => ({ type: 'REMOVE_FAVORITE_ITEM_ID' })), ); 

لذلك ، سيكون لدينا الآن إجراءان يتم إرسالهما واحدًا تلو الآخر ، ويعيد مخفضان حالة جديدة واحدة تلو الأخرى.


الإجابة الصحيحة : يمكن معالجة DELETE_ITEM_SUCCESS بواسطة كل من مخفض items DELETE_ITEM_SUCCESS items favoriteItems .


 export function favoriteItemsReducer(state = initialState, action: Action) { switch (action.type) { case 'REMOVE_FAVORITE_ITEM': case 'DELETE_ITEM_SUCCESS': const itemId = action.payload; return state.filter(id => id !== itemId); default: return state; } } 

الهدف من الإجراء هو فصل ما حدث عن كيفية تغيير الدولة. ما حدث كان DELETE_ITEM_SUCCESS . مهمة المخفضات هي التسبب في تغيير مماثل في الحالة.


لا تعتبر إزالة معرف من العناصر favoriteItems من الآثار الجانبية لحذف Item . تتم مزامنة العملية برمتها بالكامل ويمكن معالجتها بواسطة المخفضات. ليست هناك حاجة Ngrx / الآثار.


3. طلب ​​بيانات للمكون


يحتاج المكون الخاص بك إلى بيانات من المتجر ، ولكن عليك أولاً الحصول عليها من الخادم. والسؤال هو ، كيف يمكننا وضع البيانات في المتجر بحيث يمكن للمكون الحصول عليها؟


طريقة مؤلمة : استخدام Ngrx / الآثار!


في المكون ، نبدأ الطلب عن طريق إرسال إجراء:


 ngOnInit() { this.store.dispatch({ type: 'GET_USERS' }); } 

في فئة التأثيرات ، نستمع إلى GET_USERS :


 @Effect getUsers$ = this.actions$.pipe( ofType('GET_USERS'), withLatestFrom(this.userSelectors.needUsers$), filter(([action, needUsers]) => needUsers), switchMap(() => this.getUsers()), map(users => ({ type: 'RECEIVE_USERS', users })), ); 

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


 ngOnDestroy() { this.store.dispatch({ type: 'CANCEL_GET_USERS' }); } 

الآن في فئة التأثيرات نستمع إلى كلا الإجراءين:


 @Effect getUsers$ = this.actions$.pipe( ofType('GET_USERS', 'CANCEL_GET_USERS'), withLatestFrom(this.userSelectors.needUsers$), filter(([action, needUsers]) => needUsers), map(([action, needUsers]) => action), switchMap( action => action.type === 'CANCEL_GET_USERS' ? of() : this.getUsers().pipe(map(users => ({ type: 'RECEIVE_USERS', users }))), ), ); 

حسنا. الآن يضيف مطور آخر مكونًا يتطلب نفس طلب HTTP (لن نقوم بأي افتراضات حول المكونات الأخرى). يرسل المكون نفس الإجراءات في نفس الأماكن. إذا كان كلا المكونين نشطين في نفس الوقت ، يبدأ المكون الأول في طلب HTTP لتهيئته. عند تهيئة المكون الثاني ، لن يحدث شيء إضافي ، لأن needUsers سيكون false . ! رائع


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


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


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


 @Effect() actionX$ = this.actions$.pipe( ofType('ACTION_X'), map(toPayload), switchMap(payload => this.api.callApiX(payload).pipe( map(data => ({ type: 'ACTION_X_SUCCESS', payload: data })), catchError(err => of({ type: 'ACTION_X_FAIL', payload: err })), ), ), ); @Effect() actionY$ = this.actions$.pipe( ofType('ACTION_Y'), map(toPayload), withLatestFrom(this.store.select(state => state.someBoolean)), switchMap(([payload, someBoolean]) => { const callHttpY = v => { return this.api.callApiY(v).pipe( map(data => ({ type: 'ACTION_Y_SUCCESS', payload: data, })), catchError(err => of({ type: 'ACTION_Y_FAIL', payload: err, }), ), ); }; if (someBoolean) { return callHttpY(payload); } return of({ type: 'ACTION_X', payload }).merge( this.actions$.pipe( ofType('ACTION_X_SUCCESS', 'ACTION_X_FAIL'), first(), switchMap(action => { if (action.type === 'ACTION_X_FAIL') { return of({ type: 'ACTION_Y_FAIL', payload: 'Because ACTION_X failed.', }); } return callHttpY(payload); }), ), ); }), ); 

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


  .  .  . 


لذا ، لماذا توجد الكثير من المشاكل في إدارة تبعية البيانات عندما ينبغي أن تجعل RxJS الأمر سهلاً حقًا؟


على الرغم من أن البيانات الواردة من الخادم هي من الناحية الفنية تأثير جانبي ، لا يبدو لي أن Ngrx / تأثيرات هي أفضل طريقة للتعامل مع هذا.


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


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


  .  .  . 


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


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


إنشاء هذه يمكن ملاحظتها في الخدمة:


 requireUsers$ = this.store.pipe( select(selectNeedUser), filter(needUsers => needUsers), tap(() => this.store.dispatch({ type: 'GET_USERS' })), switchMap(() => this.getUsers()), tap(users => this.store.dispatch({ type: 'RECEIVE_USERS', users })), finalize(() => this.store.dispatch({ type: 'CANCEL_GET_USERS' })), share(), ); users$ = muteFirst( this.requireUsers$.pipe(startWith(null)), this.store.pipe(select(selectUsers)), ); 

سيتم إرسال الاشتراك إلى users$ إلى كل من requireUsers$ و this.store.pipe(select(selectUsers)) ، ولكن لن يتم استلام البيانات إلا من this.store.pipe(select(selectUsers)) ( muteFirst تنفيذ muteFirst و muteFirst الثابت مع اختبارها .)


في المكون:


 ngOnInit() { this.users$ = this.userService.users$; } 

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


يمكن معالجة سلسلة تبعية البيانات مثل هذا:


 requireUsers$ = this.store.pipe( select(selectNeedUser), filter(needUsers => needUsers), tap(() => this.store.dispatch({ type: 'GET_USERS' })), switchMap(() => this.getUsers()), tap(users => this.store.dispatch({ type: 'RECEIVE_USERS', users })), share(), ); users$ = muteFirst( this.requireUsers$.pipe(startWith(null)), this.store.pipe(select(selectUsers)), ); requireUsersExtraData$ = this.users$.pipe( withLatestFrom(this.store.pipe(select(selectNeedUsersExtraData))), filter(([users, needData]) => Boolean(users.length) && needData), tap(() => this.store.dispatch({ type: 'GET_USERS_EXTRA_DATA' })), switchMap(() => this.getUsers()), tap(users => this.store.dispatch({ type: 'RECEIVE_USERS_EXTRA_DATA', users, }), ), share(), ); public usersExtraData$ = muteFirst( this.requireUsersExtraData$.pipe(startWith(null)), this.store.pipe(select(selectUsersExtraData)), ); 

فيما يلي مقارنة موازية للطريقة أعلاه مع هذه الطريقة:


مقارنة موازية

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


آثار مثل الكرز في كوكتيل

استنتاج


Ngrx / الآثار هي أداة عظيمة! لكن ضع في اعتبارك هذه الأسئلة قبل استخدامها:


  • هل هذا حقا تأثير جانبي؟
  • هل Ngrx / تأثيرات هي أفضل طريقة للقيام بذلك؟

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


All Articles