تطوير تطبيقات الويب باستخدام Angular


الدرس: مراقبة البيانات مع RxJS


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

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

Observables


ولكن ما هو Observable؟

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

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

للوصول إلى Observables والطرق التي ستستخدمها ، يجب عليك إضافة عمليتي استيراد:

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/interval';
يتم استخدام الاستيراد الأول لإتاحة النوع القابل للرصد ، والثاني يمنحك الوصول إلى الطريقة التي ستستخدمها: الطريقة   interval() ، التي تنشئ Observable يمكن أن تصدر عددًا متزايدًا على فترات منتظمة والتي تأخذ العدد المطلوب من المللي ثانية كمُدخل.
تنفيذ   OnInit  وإنشاء المرصد في   ngOnInit()  :

import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/interval';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {

  ngOnInit() {
    const counter = Observable.interval(1000);
  }
}
الآن بعد أن أصبح لديك Observable ، يجب عليك مراقبته! لهذا ، ستستخدم الوظيفة   subscribe() ، التي ستأخذ كوسيطة بين واحدة وثلاث وظائف مجهولة لإدارة الأنواع الثلاثة من المعلومات التي يمكن أن يرصدها هذا الـ Observable : البيانات أو الخطأ أو الرسالة   complete  .

إنشاء متغير   secondes  في   AppComponent  وبعد ذلك في القالب:

export class AppComponent implements OnInit {

  secondes: number;

  ngOnInit() {
    const counter = Observable.interval(1000);
  }
}
<ul class="nav navbar-nav">
    <li routerLinkActive="active"><a routerLink="auth">Authentification</a></li>
    <li routerLinkActive="active"><a routerLink="appareils">Appareils</a></li>
</ul>
<div class="navbar-right">
    <p>Vous êtes connecté depuis { { secondes }} secondes !</p>
</div>

ستقوم الآن بالاشتراك في Observable وإنشاء ثلاث وظائف: سيتم تشغيل الوظيفة الأولى في كل مرة يرسل فيها Observable البيانات وستخصص هذه القيمة للمتغير "secondes" ؛ والثاني سيدير ​​أي أخطاء محتملة ؛ والثالث سيتم تشغيله إذا انتهى Observable :

ngOnInit() {
    const counter = Observable.interval(1000);
    counter.subscribe(
      (value) => {
        this.secondes = value;
      },
      (error) => {
        console.log('Uh-oh, an error occurred! : ' + error);
      },
      () => {
        console.log('Observable complete!');
      }
    );
}
في الحالة الحالية ، لن تخطئ Observable التي قمت بإنشائها ولن تكمل نفسها. أريد فقط أن أريك التكامل التام للوظيفة   subscribe()  . من الممكن الاشتراك في Observable عن طريق دمج الوظيفة الأولى فقط.

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

الاشتراكات Subscriptions


في الفصل السابق ، تعلمت كيفية إنشاء observable قابلة للاشتراك والاشتراك فيها. للتذكير ، subscribe()   تأخذ الدالة   ثلاث وظائف مجهولة:
  • يتم تشغيل الأولى في كل مرة يرسل فيها observable بيانات جديدة ، ويتلقى هذه البيانات كمُدخل ؛
  • يتم تشغيل الثانية إذا أرسل المرصد خطأ ، ويتلقى هذا الخطأ كوسيطة ؛
  • يتم تشغيل الثالثة إذا انتهى observable ، ولا يتلقى مُدخلا.

في الوقت الحالي ، لا يتم تخزين هذا الاشتراك (subscription) في متغير: لذلك لم يعد بإمكاننا لمسه بمجرد إطلاقه ، وهذا قد يسبب لك أخطاء! في الواقع ، فإن الاشتراك في أحد observables التي تستمر إلى ما لا نهاية سيستمر في تلقي البيانات ، سواء تم استخدامها أم لا ، وقد تعاني من سلوك غير متوقع.
ليس هذا هو الحال بالنسبة لجميع المرصدات. بشكل عام ، يتم حذف الاشتراكات في Observables التي تقدمها Angular من تلقاء نفسها عند تدمير المكون.

لتجنب أي مشكلة ، عند استخدام Observables المخصصة ، يُنصح بشدة بتخزين الاشتراك في كائن اشتراك (للاستيراد من rxjs / Subscription ) :

export class AppComponent implements OnInit {

  secondes: number;
  counterSubscription: Subscription;

  ngOnInit() {
    const counter = Observable.interval(1000);
    this.counterSubscription = counter.subscribe(
      (value) => {
        this.secondes = value;
      },
      (error) => {
        console.log('Uh-oh, an error occurred! : ' + error);
      },
      () => {
        console.log('Observable complete!');
      }
    );
  }
}

يعمل الرمز بالطريقة نفسها التي كان عليها من قبل ، ولكن يمكنك الآن إضافة الرمز إليه والذي سيتجنب الأخطاء المتعلقة بـ Observables . ستقوم بتطبيق واجهة جديدة   OnDestroy  (تستوردها من   @angular/core  ) ، مع وظيفتها   ngOnDestroy()  التي يتم تشغيلها عند تدمير أحد المكونات:

export class AppComponent implements OnInit, OnDestroy {

  secondes: number;
  counterSubscription: Subscription;

  ngOnInit() {
    const counter = Observable.interval(1000);
    this.counterSubscription = counter.subscribe(
      (value) => {
        this.secondes = value;
      },
      (error) => {
        console.log('Uh-oh, an error occurred! : ' + error);
      },
      () => {
        console.log('Observable complete!');
      }
    );
  }

  ngOnDestroy() {
    this.counterSubscription.unsubscribe();
  }
}
 تعمل الوظيفة unsubscribe()على    تدمير الاشتراك وتمنع السلوك غير المتوقع المتعلق بـ Infinite Observables ، لذلك لا تنس إلغاء الاشتراك!

الموضوعات Subjects


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

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

الخطوة الأولى (في AppareilService ) :

 private appareils = [
    {
      id: 1,
      name: 'Machine à laver',
      status: 'éteint'
    },
    {
      id: 2,
      name: 'Frigo',
      status: 'allumé'
    },
    {
      id: 3,
      name: 'Ordinateur',
      status: 'éteint'
    }
  ];

الخطوة الثانية ( في AppareilService ) :

import { Subject } from 'rxjs/Subject';

export class AppareilService {
  
  appareilsSubject = new Subject<any[]>();
  
  private appareils = [
عندما تعلن عن موضوع ، عليك أن تقول نوع البيانات التي ستديرها. نظرًا لأننا لم ننشئ واجهة للأجهزة (يمكنك القيام بذلك إذا أردت) ، فستقوم بإدارة صفائف النوع   any[]  . لا تنس الاستيراد!

الخطوة الثالثة ، لا تزال في   AppareilService  :

emitAppareilSubject() {
    this.appareilsSubject.next(this.appareils.slice());
  }

switchOnAll() {
    for(let appareil of this.appareils) {
      appareil.status = 'allumé';
    }
    this.emitAppareilSubject();
}

switchOffAll() {
    for(let appareil of this.appareils) {
      appareil.status = 'éteint';
      this.emitAppareilSubject();
    }
}

switchOnOne(i: number) {
    this.appareils[i].status = 'allumé';
    this.emitAppareilSubject();
}

switchOffOne(i: number) {
    this.appareils[i].status = 'éteint';
    this.emitAppareilSubject();
}

الخطوة الأخيرة في AppareilViewComponent :

import { Component, OnDestroy, OnInit } from '@angular/core';
import { AppareilService } from '../services/appareil.service';
import { Subscription } from 'rxjs/Subscription';

@Component({
  selector: 'app-appareil-view',
  templateUrl: './appareil-view.component.html',
  styleUrls: ['./appareil-view.component.scss']
})
export class AppareilViewComponent implements OnInit, OnDestroy {

  appareils: any[];
  appareilSubscription: Subscription;

  lastUpdate = new Promise((resolve, reject) => {
    const date = new Date();
    setTimeout(
      () => {
        resolve(date);
      }, 2000
    );
  });

  constructor(private appareilService: AppareilService) { }

  ngOnInit() {
    this.appareilSubscription = this.appareilService.appareilsSubject.subscribe(
      (appareils: any[]) => {
        this.appareils = appareils;
      }
    );
    this.appareilService.emitAppareilSubject();
  }

  onAllumer() {
    this.appareilService.switchOnAll();
  }

  onEteindre() {
    if(confirm('Etes-vous sûr de vouloir éteindre tous vos appareils ?')) {
      this.appareilService.switchOffAll();
    } else {
      return null;
    }
  }

  ngOnDestroy() {
    this.appareilSubscription.unsubscribe();
  }

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

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

عوامل التشغيل


يحتوي RxJS API على العديد من الاحتمالات - الكثير لرؤيته في هذه الدورة. ومع ذلك ، أود أن أخبرك بسرعة عن وجود عوامل تشغيل.
عامل التشغيل هو وظيفة يتم وضعها بين المرصد والمراقب (الاشتراك ، على سبيل المثال) ، والتي يمكنها تصفية و/أو تعديل البيانات المستلمة قبل وصولها إلى الاشتراك. إليك بعض الأمثلة السريعة:
  • map()  : تعديل القيم المستلمة - يمكن إجراء الحسابات على الأرقام ، وتحويل النص ، وإنشاء كائنات ...
  • filter()  : كما يوحي الاسم ، يقوم بتصفية القيم المستلمة وفقًا للدالة التي تم تمريرها إليها كوسيطة.
  • throttleTime()  : يفرض حدًا أدنى من التأخير بين قيمتين - على سبيل المثال ، إذا بث أحد الملاحظات القابلة للرصد خمس قيم في الثانية ، ولكن القيم التي يتم تلقيها في كل ثانية فقط هي التي تهمك ، يمكنك تمريرها   throttleTime(1000)  كعامل تشغيل.
  • scan()   و   reduce()  : تسمح لك بتنفيذ دالة تجمع بين جميع القيم المستلمة وفقًا للدالة التي تمررها إليها - على سبيل المثال ، يمكنك إضافة جميع القيم المستلمة. الفرق الأساسي بين العاملين:   reduce()   إرجاع القيمة النهائية فقط ، بينما   scan()  يُرجع كل خطوة من الحساب.

هذا الفصل هو مجرد مقدمة لعالم RxJS Observables لتقديم العناصر التي يمكن أن تكون مفيدة لك بسرعة كبيرة وتلك التي تم دمجها في خدمات Angular معينة. لمزيد من المعلومات ، لا تتردد في الرجوع إلى الوثائق الموجودة على موقع ReactiveX .