
اليوم سنقوم بتحليل تفصيلي لتطبيق زاوي تفاعلي (
مستودع جيثب ) ، مكتوب بالكامل على استراتيجية
OnPush . يستخدم تطبيق آخر نماذج تفاعلية ، وهي نموذجية تمامًا لتطبيق المؤسسة.
لن نستخدم Flux و Redux و NgRx وبدلاً من ذلك نستفيد من الإمكانات المتوفرة بالفعل في Typescript و Angular و RxJS. والحقيقة هي أن هذه الأدوات ليست رصاصة فضية ويمكن أن تضيف تعقيدًا غير ضروري حتى إلى التطبيقات البسيطة. لقد تم تحذيرنا بصراحة من هذا الأمر من قبل
أحد مؤلفي Flux ،
ومؤلف Redux ومؤلف NgRx .
لكن هذه الأدوات تمنح تطبيقاتنا ميزات جميلة جدًا:
- تدفق البيانات المتوقع ؛
- دعم OnPush حسب التصميم ؛
- عدم ثبات البيانات وعدم وجود آثار جانبية متراكمة وأشياء ممتعة أخرى.
سنحاول الحصول على نفس الخصائص ، ولكن دون تقديم تعقيد إضافي.
كما سترى في نهاية المقالة ، هذه مهمة بسيطة إلى حد ما - إذا قمت بإزالة تفاصيل Angular و OnPush من المقالة ، فهناك فقط بعض الأفكار البسيطة.
لا تقدم المقالة نمطًا عالميًا جديدًا ، ولكنها تشارك فقط مع القارئ العديد من الأفكار التي ، على الرغم من بساطتها ، لسبب ما لم تتبادر إلى الذهن على الفور. أيضا ، لا يتعارض الحل المطور أو يستبدل Flux / Redux / NgRx. يمكن توصيلها ، إذا كان ذلك
ضروريًا حقًا .
لقراءة مريحة للمقال ، هناك حاجة لفهم المصطلحات الذكية والعرضية ومكونات الحاوية .خطة العمل
يمكن وصف منطق التطبيق ، وكذلك تسلسل عرض المادة ، في شكل الخطوات التالية:
- بيانات منفصلة للقراءة (GET) والكتابة (PUT / POST)
- حالة التحميل كتدفق في مكون الحاوية
- توزيع الحالة على تسلسل هرمي لمكونات OnPush
- أبلغ الزاوي بتغييرات المكونات
- تحرير البيانات المغلفة
لتطبيق OnPush ، نحتاج إلى تحليل جميع الطرق لتشغيل الكشف عن التغيير في Angular. هناك أربع طرق فقط ، وسوف نعتبرها تباعًا طوال المقالة.
لذا دعنا نذهب.
مشاركة البيانات للقراءة والكتابة
بشكل عام ، تستخدم تطبيقات الواجهة الأمامية والخلفية العقود المكتوبة (وإلا لماذا الطباع على الإطلاق؟).
لا يحتوي المشروع التجريبي الذي نفكر فيه على خلفية حقيقية ، ولكنه يحتوي على ملف وصف تم إعداده مسبقًا
swagger.json . وبناءً عليه ، يتم إنشاء عقود
الكتابة النصية بواسطة الأداة المساعدة
sw2dts .
العقود المولدة لها خاصيتان مهمتان.
أولاً ، تتم القراءة والكتابة باستخدام عقود مختلفة. نحن نستخدم اصطلاحًا صغيرًا ونشير إلى قراءة العقود مع اللاحقة "الدولة" ، وكتابة العقود باستخدام اللاحقة "نموذج".
من خلال فصل العقود بهذه الطريقة ، نشارك تدفق البيانات في التطبيق. من الأعلى إلى الأسفل ، يتم نشر حالة القراءة فقط من خلال التسلسل الهرمي للمكونات. لتعديل البيانات ، يتم إنشاء نموذج يتم ملؤه في البداية ببيانات من الحالة ، ولكنه موجود ككائن منفصل. في نهاية التحرير ، يتم إرسال النموذج إلى الواجهة الخلفية كأمر.
النقطة الثانية المهمة هي أن جميع حقول الولاية مميزة بمعدّل للقراءة فقط. لذلك نحصل على دعم مناعة على مستوى الكتابة. الآن لا يمكننا تغيير الحالة في الشفرة عن طريق الخطأ أو الربط بها باستخدام [(ngModel)] - عند تجميع التطبيق في وضع AOT ، سنحصل على خطأ.
حالة التحميل كتدفق في مكون الحاوية
لتحميل الحالة وتهيئتها ، سوف نستخدم الخدمات الزاوية العادية. سيكونون مسؤولين عن السيناريوهات التالية:
- يتم تحميل مثال كلاسيكي عبر HttpClient باستخدام معلمة id التي حصل عليها المكون من جهاز التوجيه.
- تهيئة حالة فارغة عند إنشاء كيان جديد. على سبيل المثال ، إذا كانت الحقول تحتوي على قيم افتراضية أو لتهيئة ، تحتاج إلى طلب بيانات إضافية من الواجهة الخلفية.
- إعادة تشغيل حالة تم تحميلها بالفعل بعد قيام المستخدم بإجراء تغيير البيانات إلى الواجهة الخلفية.
- إعادة التشغيل من خلال إشعار الدفع ، على سبيل المثال ، عند تحرير البيانات بشكل مشترك. في هذه الحالة ، تقوم الخدمة بدمج الدولة المحلية والدولة التي تم الحصول عليها من الواجهة الخلفية.
في التطبيق التجريبي ، سننظر في أول سيناريوهين على أنهما الأكثر شيوعًا. أيضًا ، هذه السيناريوهات بسيطة وتسمح بتنفيذ الخدمة ككائنات بسيطة عديمة الجنسية وعدم تشتيت الانتباه بسبب التعقيد ، وهو ليس موضوع هذه المقالة المحددة.
يمكن العثور على مثال للخدمة في ملف
some-لكيان service.ts .
يبقى الحصول على الخدمة من خلال DI في مكون الحاوية وحالة التحميل. عادة ما يتم ذلك على النحو التالي:
route.params .pipe( pluck('id'), filter((id: any) => { return !!id; }), switchMap((id: string) => { return myFormService.get(id); }) ) .subscribe(state => { this.state = state; });
ولكن مع هذا النهج ، تنشأ مشكلتان:
- يجب عليك إلغاء الاشتراك يدويًا من الاشتراك الذي تم إنشاؤه ، وإلا سيحدث تسرب للذاكرة.
- إذا قمت بتبديل المكون إلى استراتيجية OnPush ، فسوف يتوقف عن الاستجابة لتحميل البيانات.
يأتي
الأنابيب غير المتزامن لإنقاذ. يستمع مباشرة للملاحظة وغير المشترك منه عند الضرورة. أيضًا ، عند استخدام الأنبوب غير المتزامن ، تقوم Angular تلقائيًا بتشغيل كشف التغيير في كل مرة ينشر فيها Observable قيمة جديدة.
يمكن العثور على مثال لاستخدام ماسورة غير متزامنة في القالب
لمكون بعض كيانات .
وفي كود المكون ، أزلنا المنطق المتكرر إلى عوامل RxJS المخصصة ، وأضفنا النص البرمجي لإنشاء حالة فارغة ، ودمج مصدري الحالة في دفق واحد مع عامل الدمج وإنشاء نموذج للتحرير ، والذي سنناقشه لاحقًا:
this.state$ = merge( route.params.pipe( switchIfNotEmpty("id", (requestId: string) => requestService.get(requestId) ) ), route.params.pipe( switchIfEmpty("id", () => requestService.getEmptyState()) ) ).pipe( tap(state => { this.form = new SomeEntityFormGroup(state); }) );
هذا هو كل ما كان مطلوبًا القيام به في مكون الحاوية. ونضع في الخنازير الطريقة الأولى لاستدعاء كشف التغيير في مكون OnPush - الأنبوب غير المتزامن. سيكون مفيدا لنا أكثر من مرة.
توزيع الحالة على تسلسل هرمي لمكونات OnPush
عندما تحتاج إلى عرض حالة معقدة ، نقوم بإنشاء تسلسل هرمي للمكونات الصغيرة - هذه هي الطريقة التي نتعامل بها مع التعقيد.
كقاعدة ، يتم تقسيم المكونات إلى تسلسل هرمي مشابه للتسلسل الهرمي للبيانات ، ويتلقى كل مكون قطعة البيانات الخاصة به من خلال معلمات الإدخال لعرضها في القالب.
نظرًا لأننا سنقوم بتطبيق جميع المكونات مثل OnPush ، فلنبدأ للحظة ونناقش ما هي وكيف تعمل Angular مع مكونات OnPush. إذا كنت تعرف هذه المادة بالفعل - فلا تتردد في التمرير إلى نهاية القسم.
أثناء تجميع التطبيق ، تقوم Angular بإنشاء كاشف تغيير فئة خاص لكل مكون ، والذي "يتذكر" جميع الارتباطات المستخدمة في قالب المكون. في وقت التشغيل ، يبدأ الفصل الذي تم إنشاؤه في فحص التعبيرات المخزنة مع كل حلقة كشف عن التغيير. إذا أظهر الاختيار أن نتيجة أي تعبير قد تغيرت ، فإن Angular تعيد رسم المكون.
بشكل افتراضي ، لا تعرف Angular أي شيء عن مكوناتنا ولا يمكنها تحديد المكونات التي ستؤثر عليها ، على سبيل المثال ، setTimeout الذي تم تشغيله للتو أو طلب AJAX الذي انتهى. لذلك ، يضطر إلى التحقق من التطبيق بالكامل حرفياً لكل حدث داخل التطبيق - حتى التمرير البسيط في النافذة يؤدي بشكل متكرر إلى الكشف عن التغيير للتسلسل الهرمي الكامل لمكونات التطبيق.
هنا يكمن مصدر محتمل لمشاكل الأداء - كلما كانت قوالب المكونات أكثر تعقيدًا ، زادت صعوبة فحص كاشف التغيير. وإذا كان هناك الكثير من المكونات ويتم تشغيل الفحوصات كثيرًا ، فإن اكتشاف التغيير يبدأ في أخذ وقت طويل.
ماذا تفعل؟
إذا كان المكون لا يعتمد على أي تأثيرات عالمية (بالمناسبة ، فمن الأفضل تصميم المكونات بهذه الطريقة) ، ثم يتم تحديد حالته الداخلية من خلال:
- معلمات الإدخال ( Input ) ؛
- الأحداث التي حدثت في المكون نفسه ( Output ).
سنؤجل النقطة الثانية في الوقت الحالي ونفترض أن حالة المكون الخاص بنا تعتمد فقط على معلمات الإدخال.
إذا كانت جميع معلمات الإدخال للمكون كائنات ثابتة ، فيمكننا وضع علامة على المكون على أنه OnPush. ثم ، قبل تشغيل الكشف عن التغيير ، ستتحقق Angular مما إذا كانت الروابط إلى معلمات الإدخال للمكون قد تغيرت منذ الفحص السابق. وإذا لم تتغير ، فإن Angular ستتخطى اكتشاف التغيير للمكون نفسه وجميع مكوناته الفرعية.
وبالتالي ، إذا قمنا ببناء تطبيقنا بالكامل وفقًا لاستراتيجية OnPush ، فسوف نتخلص من فئة كاملة من مشاكل الأداء من البداية.
نظرًا لأن الحالة في تطبيقنا غير قابلة للتغيير بالفعل ، يتم أيضًا نقل الكائنات غير القابلة للتغيير إلى معلمات الإدخال للمكونات الفرعية. أي أننا مستعدون لتمكين OnPush للمكونات الفرعية وسوف يستجيبون لتغيرات الحالة.
على سبيل المثال ، هذه هي
مكونات readonly-info.component و
nested-items.componentالآن دعونا نرى كيفية تنفيذ التغيير في حالة المكونات في نموذج OnPush.
تحدث إلى Angular عن حالتك
حالة العرض - هذه هي المعلمات المسؤولة عن مظهر المكون: مؤشرات التحميل ، وأعلام رؤية العناصر أو إمكانية الوصول إلى مستخدم إجراء معين ، يتم لصقها من ثلاثة حقول إلى سطر واحد من اسم المستخدم ، إلخ.
في كل مرة تتغير فيها حالة العرض التقديمي للمكون ، يجب أن نعلم Angular حتى يتمكن من عرض التغييرات على واجهة المستخدم.
اعتمادًا على مصدر حالة المكون ، هناك عدة طرق لإخطار Angular.
حالة العرض التقديمي ، محسوبة على أساس معلمات الإدخال
هذا هو الخيار الأسهل. وضعنا منطق حساب حالة العرض التقديمي في ربط ngOnChanges. سيبدأ الكشف عن التغيير نفسه بتغيير معلمات @ Input. في العرض التوضيحي ، هذا هو
readonly-info.component .
export class ReadOnlyInfoComponent implements OnChanges { @Input() public state: Backend.SomeEntityState; public traits: ReadonlyInfoTraits; public ngOnChanges(changes: { state: SimpleChange }): void { this.traits = new ReadonlyInfoTraits(changes.state.currentValue); } }
كل شيء بسيط للغاية ، ولكن هناك نقطة واحدة يجب الانتباه إليها.
إذا كانت حالة العرض للمكون معقدة ، وخاصة إذا تم حساب بعض حقوله على أساس البعض الآخر ، محسوبة أيضًا بواسطة معلمات الإدخال ، ضع حالة المكون في فئة منفصلة ، وجعله غير قابل للتغيير وإعادة إنشاء ngOnChanges في كل مرة يبدأ فيها. في مشروع تجريبي ، مثال على ذلك هو فئة
ReadonlyInfoComponentTraits . باستخدام هذا النهج ، فإنك تحمي نفسك من الحاجة إلى مزامنة البيانات التابعة عندما تتغير.
في الوقت نفسه ، يجدر التفكير في ذلك: ربما يكون للمكون حالة صعبة نظرًا لوجود الكثير من المنطق فيه. المثال النموذجي هو محاولة في مكون واحد لملاءمة تمثيلات لمستخدمين مختلفين لديهم طرق مختلفة جدًا للعمل مع النظام.
الأحداث الأصلية المكونة
للتواصل بين مكونات التطبيق ، نستخدم أحداث الإخراج. هذه هي أيضًا الطريقة الثالثة لتشغيل الكشف عن التغيير. تفترض Angular بشكل معقول أنه إذا كان المكون يولد حدثًا ، فيمكن أن يتغير شيء ما في حالته. لذلك ، يستمع Angular لجميع أحداث إخراج المكونات ويطلق اكتشاف التغيير عند حدوثها.
في المشروع التجريبي ، إنه اصطناعي تمامًا ، ولكن أحد الأمثلة هو المكون
Submit-button.component ، الذي يلقي بحدث
formSaved . يشترك مكون الحاوية في هذا الحدث ويعرض تنبيهًا بإشعار.
استخدم أحداث الإخراج للغرض المقصود منها ، أي إنشاءها للتواصل مع المكونات الرئيسية ، وليس من أجل تشغيل الكشف عن التغيير. خلاف ذلك ، فمن المحتمل ، بعد أشهر وسنوات ، ألا نتذكر لماذا هذا الحدث غير ضروري لأي شخص هنا ، وحذفه ، وكسر كل شيء.
التغييرات في المكونات الذكية
في بعض الأحيان يتم تحديد حالة المكون من خلال المنطق المعقد: استدعاء الخدمة بشكل غير متزامن ، والاتصال بمقبس الويب ، والتحقق من التشغيل من خلال setInterval ، ولكنك لا تعرف أبدًا أي شيء آخر. تسمى هذه المكونات المكونات الذكية.
بشكل عام ، كلما كانت المكونات الأقل ذكاءً في التطبيق التي ليست مكونات حاويات ، كان من الأسهل العيش. لكن في بعض الأحيان لا يمكنك الاستغناء عنها.
إن أبسط طريقة لربط حالة المكون الذكي بكشف التغيير هي تحويله إلى عنصر قابل للرصد واستخدام
الأنبوب غير المتزامن الذي تمت مناقشته بالفعل أعلاه. على سبيل المثال ، إذا كان مصدر التغييرات هو استدعاء خدمة أو حالة شكل تفاعلي ، فإن هذا يمكن ملاحظته. إذا تم تشكيل الحالة من شيء أكثر تعقيدًا ، يمكنك استخدام
fromPromise ،
websocket ،
timer ،
الفاصل الزمني من تكوين RxJS. أو قم بإنشاء دفق بنفسك باستخدام
الموضوع .
إذا لم يكن أي من الخيارات مناسبًا
في الحالات التي لا تكون فيها أي من الطرق الثلاث التي سبق دراستها مناسبة ، لا يزال لدينا خيار مضاد للرصاص - باستخدام
ChangeDetectorRef مباشرة. نحن نتحدث عن طرق DetChanges و MarkForCheck لهذه الفئة.
التوثيق الشامل يجيب على جميع الأسئلة ، لذلك لن نتحدث عن عملها. ولكن لاحظ أن استخدام
ChangeDetectorRef يجب أن يقتصر على الحالات التي تفهم فيها بوضوح ما تفعله ، حيث لا يزال هذا هو المطبخ الزاوي الداخلي.
طوال الوقت وجدنا بضع حالات فقط حيث قد تكون هناك حاجة لهذه الطريقة:
- العمل اليدوي مع الكشف عن التغيير - يستخدم في تنفيذ المكونات ذات المستوى المنخفض وهو مجرد حالة "تفهم بوضوح ما تفعله".
- العلاقات المعقدة بين المكونات - على سبيل المثال ، عندما تحتاج إلى إنشاء ارتباط لمكون في قالب وتمريره كمعلمة لمكون آخر موجود في أعلى التسلسل الهرمي أو حتى في فرع آخر من التسلسل الهرمي للمكونات. تبدو معقدة؟ هكذا هي. ومن الأفضل إعادة صياغة هذا الرمز فقط ، لأنه سيجلب الألم ليس فقط مع الكشف عن التغيير.
- تفاصيل سلوك Angular نفسه - على سبيل المثال ، عند تنفيذ ControlValueAccessor مخصص ، قد تواجه تغيير قيمة التحكم بواسطة Angular بشكل غير متزامن ولا يتم تطبيق التغييرات على دورة الكشف عن التغيير المطلوب.
كأمثلة للاستخدام في التطبيق التجريبي ، هناك الفئة الأساسية
OnPushControlValueAccessor ، والتي تحل المشكلة الموضحة في الفقرة الأخيرة. أيضا في المشروع هناك وريث لهذا الفصل - custom
radio-button.component .
لقد ناقشنا الآن جميع الطرق الأربع لتشغيل الكشف عن التغيير وخيارات تنفيذ OnPush لجميع الأنواع الثلاثة من المكونات: الحاوية ، الذكية ، العرضية. ننتقل إلى النقطة الأخيرة - تحرير البيانات بأشكال تفاعلية.
تحرير البيانات المغلفة
تحتوي الأشكال التفاعلية على عدد من القيود ، ولكن لا يزال هذا أحد أفضل الأشياء التي حدثت في النظام البيئي الزاوي.
بادئ ذي بدء ، فإنها تغلف العمل بشكل جيد مع الدولة وتوفر جميع الأدوات اللازمة للاستجابة للتغيرات بطريقة رد فعل.
في الواقع ، النموذج التفاعلي هو نوع من المتاجر الصغيرة التي تغلف العمل مع الحالة: البيانات والحالات معطلة / صالحة / معلقة.
يبقى لنا أن ندعم هذا التغليف قدر الإمكان وتجنب مزج منطق العرض التقديمي ومنطق النموذج.
في التطبيق التجريبي ، يمكنك مشاهدة
فئات النماذج الفردية التي تتضمن تفاصيل عملهم: التحقق من الصحة ، إنشاء FormGroups الفرعية ، العمل مع حالة تعطيل حقول الإدخال.
نقوم بإنشاء النموذج الجذر في مكون الحاوية عند تحميل حالة الوقت ، ومع إعادة تشغيل كل حالة ، يتم إعادة إنشاء النموذج. هذا ليس شرطًا أساسيًا ، ولكن بهذه الطريقة يمكننا التأكد من عدم وجود تأثيرات متراكمة في منطق النموذج متبقية من حالة التحميل السابقة.
داخل النموذج نفسه ، نقوم ببناء عناصر التحكم و "نقل" البيانات التي تأتي منها ، وتحويلها من عقد الدولة إلى عقد النموذج. يتطابق هيكل النماذج ، قدر الإمكان ، مع عقود النماذج. ونتيجة لذلك ، تعطينا خاصية القيمة للنموذج نموذجًا جاهزًا للإرسال إلى الواجهة الخلفية.
إذا تغيرت الحالة أو بنية النموذج في المستقبل ، فسوف نحصل على خطأ تجميع مطبوع تمامًا في المكان الذي نحتاج فيه إلى إضافة / إزالة الحقول ، وهو أمر مناسب جدًا.
أيضًا ، إذا كانت كائنات الحالة والنموذج لها بنية متطابقة تمامًا ، فإن الكتابة الهيكلية المستخدمة في الكتابة النصية تلغي الحاجة إلى إنشاء خرائط لا معنى لها لأحدها في الآخر.
المجموع ، يتم عزل منطق النموذج عن منطق العرض التقديمي في المكونات ويعيش "بمفرده" ، دون زيادة تعقيد تدفق البيانات لتطبيقنا ككل.
هذا كل شيء تقريبًا. هناك حالات حدود متبقية عندما يتعذر علينا عزل منطق النموذج عن بقية التطبيق:
- تغييرات في النموذج تؤدي إلى تغيير في حالة العرض التقديمي - على سبيل المثال ، رؤية كتلة البيانات اعتمادًا على القيمة التي تم إدخالها. نقوم بتطبيقه في المكون بالاشتراك في تشكيل الأحداث. يمكنك القيام بذلك من خلال السمات الثابتة التي نوقشت سابقًا.
- إذا كنت بحاجة إلى مدقق غير متزامن يستدعي الواجهة الخلفية ، فإننا نقوم بإنشاء AsyncValidatorFn في المكون ونقوم بتمريره إلى مُنشئ النموذج ، وليس الخدمة.
وبالتالي ، يظل كل منطق "الحدود" في مكان بارز - في المكونات.
الاستنتاجات
دعونا نلخص ما حصلنا عليه وما هي النقاط الأخرى للدراسة والتطوير.
بادئ ذي بدء ، يدفعنا تطوير استراتيجية OnPush إلى تصميم تدفق البيانات للتطبيق بعناية ، حيث أننا الآن نملي قواعد اللعبة على Angular ، وليس عليه.
هناك نتيجتان لهذا الوضع.
أولاً ، نحصل على شعور لطيف بالسيطرة على التطبيق. لم يعد هناك أي سحر "يعمل بطريقة أو بأخرى". أنت على دراية بما يحدث في أي وقت في طلبك. يتطور الحدس تدريجيًا ، مما يسمح لك بفهم سبب الخطأ الذي تم العثور عليه ، حتى قبل فتح الشفرة.
ثانيًا ، علينا الآن قضاء المزيد من الوقت في تصميم التطبيق ، ولكن النتيجة ستكون دائمًا "الأكثر مباشرة" ، وبالتالي ، الحل الأبسط. هذا يجلب بشكل ملحوظ احتمالية وضع حيث ، مع نمو التطبيق ، يصبح وحشًا من التعقيد الهائل ، فقد المطورون السيطرة على هذا التعقيد ويبدو التطور الآن أشبه بالطقوس الغامضة.
إن التعقيد المتحكم فيه وغياب "السحر" يقللان من احتمالية فئة كاملة من المشكلات الناشئة ، على سبيل المثال ، من تحديثات البيانات الدورية أو الآثار الجانبية المتراكمة. بدلاً من ذلك ، نحن نتعامل مع مشاكل ملحوظة بالفعل أثناء التطوير ، عندما لا يعمل التطبيق ببساطة. وبالطبع ، عليك أن تجعل التطبيق يعمل ببساطة ووضوح.
ذكرنا أيضا آثار جيدة على الأداء. الآن ، باستخدام أدوات بسيطة للغاية ، مثل
profiler.timeChangeDetection ، يمكننا التحقق في أي وقت من أن تطبيقنا لا يزال في حالة جيدة.
أيضا الآن من
الخطأ عدم محاولة
تعطيل NgZone . أولاً ، سيسمح لك بعدم تحميل المكتبة بأكملها عند بدء تشغيل التطبيق. ثانيًا ، سيزيل مقدارًا لا بأس به من السحر من تطبيقك.
هنا ننهي قصتنا.
سنكون على اتصال!