الغوص العميق في مزايا NgRx والميزات

نشرت: 2022-03-11

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

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

بدأ معظم المطورين في استخدام إدارة الدولة عندما أصدر دان أبراموف مكتبة Redux. بدأ البعض في استخدام إدارة الدولة لأنها كانت توجهاً ، وليس لأنهم يفتقرون إليها. يمكن للمطورين الذين يستخدمون مشروعًا قياسيًا "Hello World" لإدارة الدولة أن يجدوا أنفسهم سريعًا يكتبون نفس الرمز مرارًا وتكرارًا ، مما يزيد التعقيد بدون مكسب.

في النهاية ، أصيب البعض بالإحباط وتخلوا عن إدارة الدولة تمامًا.

مشكلتي الأولية مع NgRx

أعتقد أن هذا القلق المعياري كان مشكلة رئيسية في NgRx. في البداية ، لم نتمكن من رؤية الصورة الكبيرة وراء ذلك. إن NgRx مكتبة وليست نموذج برمجة أو عقلية. ومع ذلك ، من أجل فهم وظائف هذه المكتبة وإمكانية استخدامها بشكل كامل ، يتعين علينا توسيع معرفتنا أكثر قليلاً والتركيز على البرمجة الوظيفية. هذا هو الوقت الذي قد تكتب فيه كود معياري وتشعر بالسعادة حيال ذلك. (أعني ذلك). كنت ذات مرة أحد المتشككين في NgRx ؛ الآن أنا معجب بـ NgRx.

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

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

 @Component({ selector: 'simple-button', template: ` <button>Hello {{ name }}</button> `, }) export class SimpleButtonComponent { @Input() name!: string; } export default function SimpleButton(props: { name: string }) { return <button>{props.name} </button>; }

كما ترى ، هذا تحول مباشر:

  • SimpleButtonComponent => SimpleButton
  • Input () name => props.name
  • template => قيمة الإرجاع

رسم توضيحي: المكون الزاوي وخطاف التفاعل متشابهان تمامًا.
المكون الزاوي وخطافات التفاعل متشابهة تمامًا.

وظيفة SimpleButton لدينا SimpleButton لها خاصية مهمة واحدة في عالم البرمجة الوظيفية: إنها وظيفة خالصة . إذا كنت تقرأ هذا ، أفترض أنك سمعت هذا المصطلح مرة واحدة على الأقل. يستشهد NgRx.io بالوظائف النقية مرتين في المفاهيم الأساسية:

  • يتم التعامل مع تغييرات الحالة من خلال وظائف نقية تسمى reducers التي تأخذ الحالة الحالية وآخر إجراء لحساب حالة جديدة.
  • المحددات هي وظائف خالصة تستخدم لتحديد أجزاء من الحالة واشتقاقها وتكوينها.

في React ، يتم تشجيع المطورين على استخدام الخطافات كوظائف pures قدر الإمكان. يشجع Angular أيضًا المطورين على تنفيذ نفس النمط باستخدام نموذج Smart-Dumb Component.

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

تشارك هذه المقالة خبرتي التعليمية والمعرفة التي اكتسبتها حول NgRx والبرمجة الوظيفية. أنا لا أشرح API لـ NgRx أو كيفية استدعاء الإجراءات أو استخدام المحددات. بدلاً من ذلك ، أشارك لماذا أقدر أن NgRx مكتبة رائعة: إنها ليست مجرد اتجاه جديد نسبيًا ، فهي توفر مجموعة من الفوائد.

لنبدأ بالبرمجة الوظيفية .

البرمجة الوظيفية

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

هذه المفاهيم الأساسية هي:

  • وظيفة نقية
  • دولة غير قابلة للتغيير
  • اعراض جانبية

أكرر: إنه مجرد نموذج ، لا أكثر. لا توجد مكتبة function.js نقوم بتنزيلها واستخدامها لكتابة برامج وظيفية. إنها مجرد طريقة للتفكير في كتابة التطبيقات. لنبدأ بالمفهوم الأساسي الأكثر أهمية: الوظيفة الصافية .

وظيفة نقية

تعتبر الوظيفة وظيفة خالصة إذا اتبعت قاعدتين بسيطتين:

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

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

دعنا نلقي نظرة على ثلاثة أمثلة بسيطة:

 //Pure function function add(a,b){ return a + b; } //Impure function breaking rule 1 function random(){ return Math.random(); } //Impure function breaking rule 2 function sayHello(name){ console.log("Hello " + name); }
  • الوظيفة الأولى نقية لأنها ستعيد دائمًا نفس الإجابة عند تمرير نفس الوسيطات.
  • الوظيفة الثانية ليست نقية لأنها غير حتمية وتعيد إجابات مختلفة في كل مرة يتم استدعاؤها.
  • الوظيفة الثالثة ليست نقية لأنها تستخدم تأثيرًا جانبيًا (استدعاء console.log ).

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

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

تأتي الوظائف البحتة بوظيفة سهلة الاستخدام وصديقة للأداء تسمى الذاكرة. إذا علمنا أن استدعاء نفس الوسيطات سيعيد نفس القيمة ، فيمكننا ببساطة تخزين النتائج مؤقتًا وعدم إضاعة الوقت في استدعاءها مرة أخرى. NgRx يجلس بالتأكيد على قمة memoization ؛ هذا أحد الأسباب الرئيسية لكونه سريعًا.

يجب أن يكون التحول بديهيًا وشفافًا.

قد تسأل نفسك ، "ماذا عن الآثار الجانبية؟ الى اين يذهبون؟" في حديثه في GOTO ، يمزح روس أولسن أن عملائنا لا يدفعون لنا مقابل الوظائف البحتة ، بل يدفعون لنا مقابل الآثار الجانبية. هذا صحيح: لا أحد يهتم بوظيفة الآلة الحاسبة الصافية إذا لم تتم طباعتها في مكان ما. الآثار الجانبية لها مكانها في عالم البرمجة الوظيفية. سنرى ذلك قريبا.

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

دولة غير قابلة للتغيير

يوجد تعريف بسيط للحالة الثابتة:

  • يمكنك فقط إنشاء أو حذف حالة. لا يمكنك تحديثه.

بعبارات بسيطة ، لتحديث عمر كائن المستخدم ...:

 let user = { username:"admin", age:28 }

... يجب أن تكتبه على هذا النحو:

 // Not like this newUser.age = 30; // But like this let newUser = {...user, age:29 }

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

String و Boolean و Number كلها حالات ثابتة: لا يمكنك إلحاق أو تعديل قيم موجودة. في المقابل ، التاريخ هو كائن قابل للتغيير: أنت تتعامل دائمًا مع كائن التاريخ نفسه.

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

 function updateAge(user, age) { return {...user, age: age) } let user = {username: 'admin', age: 29}; let newUser = updateAge(user, 32);

لماذا يجب أن نكرس الوقت والاهتمام لهذا؟ هناك نوعان من الفوائد تستحق التأكيد عليها.

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

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

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

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

الآن بعد أن قمنا بمراجعة الثبات والنقاء ، دعنا نتناول المفهوم الأساسي المتبقي: الآثار الجانبية .

اعراض جانبية

يمكننا تعميم تعريف التأثير الجانبي:

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

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

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

دعونا نفحص كيف يمكننا تنفيذ جمال هذه التقنية داخل إطار العمل الزاوي.

شكل توضيحي: تأثير جانبي زاوي NgRx

البرمجة الزاويّة الوظيفية

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

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

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

لنلقِ نظرة على مثال مكون مضاد. العداد هو مكون يحتوي على زرين يزيدان القيمة أو displayField ، وحقل عرض واحد يعرض القيمة currentValue . لذلك انتهى بنا الأمر إلى أربعة مكونات:

  • كاونتر كونتينر
  • زيادة الزر
  • إنقاص
  • القيمة الحالية

كل المنطق موجود داخل CounterContainer ، لذلك الثلاثة هم مجرد عارضين. هذا هو رمز الثلاثة منهم:

 @Component({ selector: 'decrease-button', template: `<button (click)="increase.emit()" [disabled]="disabled"> Decrease </button>`, }) export class DecreaseButtonComponent { @Input() disabled!: boolean; @Output() increase = new EventEmitter(); } @Component({ selector: 'current-value', template: `<button> {{ currentValue }} </button>`, }) export class CurrentValueComponent { @Input() currentValue!: string; } @Component({ selector: 'increase-button', template: `<button (click)="increase.emit()" [disabled]="disabled"> Increase </button>`, }) export class IncreaseButtonComponent { @Input() disabled!: boolean; @Output() increase = new EventEmitter(); }

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

لذلك يتم نقل الوظائف النقية في البرمجة الوظيفية إلى مكونات نقية في Angular. لكن إلى أين يذهب كل هذا المنطق؟ لا يزال المنطق موجودًا ولكن في مكان مختلف قليلاً ، وهو CounterComponent .

 @Component({ selector: 'counter-container', template: ` <decrease-button [disabled]="decreaseIsDisabled" (decrease)="decrease()"> </decrease-button> <current-value [currentValue]="currentValue"> </current-value> <increase-button (increase)="increase()" [disabled]="increaseIsDisabled"> </increase-button> `, }) export class CounterContainerComponent implements OnInit { @Input() disabled!: boolean; currentValue = 0; get decreaseIsDisabled() { return this.currentValue === 0; } get increaseIsDisabled() { return this.currentValue === 100; } constructor() {} ngOnInit(): void {} decrease() { this.currentValue -= 1; } increase() { this.currentValue += 1; } }

كما ترى ، فإن منطق السلوك موجود في CounterContainer ولكن جزء العرض مفقود (يقوم بتعريف المكونات داخل القالب) لأن جزء العرض مخصص لمكونات نقية.

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

يمكننا القفز بسهولة من هذا النمط إلى مكتبة NgRx نفسها ، والتي هي طبقة واحدة فقط فوقها.

مكتبة NgRx

يمكننا تقسيم أي تطبيق ويب إلى ثلاثة أجزاء أساسية:

  • منطق الأعمال
  • حالة التطبيق
  • تقديم المنطق

توضيح لمنطق العمل وحالة التطبيق ومنطق التقديم.

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

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

يشمل منطق العرض التقديم ، مثل عرض البيانات باستخدام DOM ، وإنشاء العناصر أو إزالتها ، وما إلى ذلك.

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

باستخدام NgRx يمكنك فصل كل هذه الأجزاء. هناك ثلاثة أجزاء رئيسية:

  • مخفضات
  • أجراءات
  • المحددات

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

مخفضات

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

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

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

هذا مثال تافه لمخفض:

 function usernameReducer(oldState, username) { return {...oldState, username} }

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

بالنسبة لمكون العداد ، يمكن أن تبدو حالتنا ومخفضاتنا كما يلي:

 interface State{ decreaseDisabled:boolean; increaseDisabled:boolean; currentValue:number; } const MIN_VALUE=0; const MAX_VALUE =100; function decreaseReducer(oldState) { const newValue = oldState.currentValue -1 return {...oldState,currentValue : newValue, decreaseDisabled: newValue===MIN_VALUE } function increaseReducer(oldState) { const newValue = oldState.currentValue + 1 return {...oldState,currentValue : newValue, decreaseDisabled: newValue===MAX_VALUE }

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

أجراءات

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

 enum CounterActions { IncreaseValue = '[Counter Component] Increase Value', DecreaseValue = '[Counter Component] Decrease Value', } on(CounterActions.IncreaseValue,increaseReducer); on(CounterActions.DecreaseValue,decreaseReducer);

أفعالنا مرتبطة بالمخفضات. يمكننا الآن تعديل مكون الحاوية بشكل أكبر واستدعاء الإجراءات المناسبة عند الضرورة:

 @Component({ selector: 'counter-container', template: ` <decrease-button [disabled]="decreaseIsDisabled" (decrease)="decrease()"> </decrease-button> <current-value [currentValue]="currentValue"> </current-value> <increase-button (increase)="increase()" [disabled]="increaseIsDisabled"> </increase-button> `, }) export class CounterContainerComponent implements OnInit { constructor(private store: Store<any>) {} decrease() { this.store.dispatch(CounterActions.DicreaseValue); } increase() { this.store.dispatch(CounterActions.IncreaseValue); } }

ملحوظة: أزلنا الدولة وسنضيف مرة أخرى قريبًا .

الآن ليس لدينا CounterContainer أي منطق لتغيير الحالة. إنه يعرف فقط ما يجب إرساله. نحتاج الآن إلى طريقة ما لعرض هذه البيانات على العرض. هذه هي فائدة المحددات.

المحددات

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

 function selectCurrentValue(state) { return state.currentValue; } function selectDicreaseIsDisabled(state) { return state.decreaseDisabled; } function selectIncreaseIsDisabled(state) { return state.increaseDisabled; }

باستخدام هذه المحددات ، يمكننا تحديد كل شريحة من الحالات داخل مكون CounterContainer الذكي الخاص بنا.

 @Component({ selector: 'counter-container', template: ` <decrease-button [disabled]="ecreaseIsDisabled$ | async" (decrease)="decrease()" > </decrease-button> <current-value [currentValue]="currentValue$ | async"> </current-value> <increase-button (increase)="increase()" [disabled]="increaseIsDisabled$ | async" > </increase-button> `, }) export class CounterContainerComponent implements OnInit { decreaseIsDisabled$ = this.store.select(selectDicreaseIsDisabled); increaseIsDisabled$ = this.store.select(selectIncreaseIsDisabled); currentValue$ = this.store.select(selectCurrentValue); constructor(private store: Store<any>) {} decrease() { this.store.dispatch(CounterActions.DicreaseValue); } increase() { this.store.dispatch(CounterActions.IncreaseValue); } }

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

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

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

في رأيي المتواضع ، الاختبار هو أفضل جزء من NgRx. يعتبر اختبار هذا المشروع النموذجي أشبه بلعب لعبة tic-tac-toe. لا يوجد سوى وظائف نقية ومكونات نقية ، لذلك يعد اختبارها أمرًا سهلاً. تخيل الآن إذا أصبح هذا المشروع أكبر ، بمئات المكونات. إذا اتبعنا نفس النمط ، فسنضيف المزيد والمزيد من القطع معًا. لن تصبح نقطة فوضويّة وغير قابلة للقراءة من التعليمات البرمجية المصدر.

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

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

آثار جانبية

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

إذا فكرنا في الآثار الجانبية ، فمعظمها لها سببان رئيسيان للوجود:

  • فعل أي شيء خارج بيئة الدولة
  • تحديث حالة التطبيق

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

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

 function startTimer(){ setInterval(()=>{ console.log("Hello application"); },3000) } on(CounterActions.StartTime,startTimer) ... // We start timer by dispatching an action dispatch(CounterActions.StartTime);

انها تافهة جدا. كما ذكرت سابقًا ، فإن الآثار الجانبية إما تحدث شيئًا ما أم لا. إذا لم يحدث أحد الآثار الجانبية أي شيء ، فلا يوجد ما تفعله ؛ نحن فقط نتركه. لكن إذا أردنا تحديث دولة ، فكيف نفعل ذلك؟ بنفس الطريقة التي يحاول بها المكوِّن تحديث حالة: استدعاء إجراء آخر. لذلك نسمي إجراءً داخل التأثير الجانبي ، والذي يقوم بتحديث الحالة:

 function startTimer(store) { setInterval(()=> { // We are dispatching another action dispatch(CounterActions.IncreaseValue) }, 3000) } on(CounterActions.StartTime, startTimer); ... // We start timer by dispatching an action dispatch(CounterActions.StartTime);

لدينا الآن تطبيق كامل الوظائف.

تلخيص تجربة NgRx لدينا

هناك بعض الموضوعات المهمة التي أود ذكرها قبل أن ننتهي من رحلة NgRx:

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