WebSockets في Angular: إنشاء Angular Service للعمل مع مآخذ الويب

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

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

أولاً ، بعض المعلومات الأساسية.

ما هو WebSocket ولماذا تحتاجه


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

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

على جانب الخادم ، هناك حزم لدعم مقبس الويب ، على العميل هو HTML5 WebSocket API ، والذي يحتوي على واجهة من ثلاث طرق:

WebSocket - الواجهة الرئيسية للاتصال بخادم WebSocket ، ثم إرسال واستقبال البيانات عبر الاتصال ؛
CloseEvent - حدث تم إرساله بواسطة كائن WebSocket عند إغلاق الاتصال ؛
MessageEvent - حدث تم إرساله بواسطة WebSocket عند تلقي رسالة من الخادم.

هذه هي الطريقة التي تبدو بها على مستوى تطبيق JavaSript:

const ws = new WebSocket("ws://www.example.com/socketserver", "protocolOne"); ws.onopen = () => { ws.onmessage = (event) => { console.log(event); } ws.send("Here's some text that the server is urgently awaiting!"); }; 

onmessage - الاستماع إلى الرسائل من الخادم
أرسل - أرسل رسائلك إلى الخادم

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

لماذا لا تخاف من استخدام WebSocket


النقطة الأولى التي يمكن أن تخيف هي دعم المتصفح . اليوم لا توجد مشكلة من هذا القبيل - يتم دعم WebSocket بشكل شبه كامل على الويب وفي قطاع الهاتف المحمول.

الصورة

الصورة

https://caniuse.com/#feat=websockets

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

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

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

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

لقد صادفت مثل هذا التنفيذ "الأنيق":

 const wsUser = new WebSocket("ws://www.example.com/user"); wsUser.onmessage = (event) => { // ... }; const wsNews = new WebSocket("ws://www.example.com/news"); wsNews.onmessage = (event) => { // ... }; const wsTime = new WebSocket("ws://www.example.com/time"); wsTime.onmessage = (event) => { // ... }; const wsDinner = new WebSocket("ws://www.example.com/dinner"); wsDinner.onmessage = (event) => { // ... }; const wsCurrency = new WebSocket("ws://www.example.com/currency"); wsCurrency.onmessage = (event) => { // ... }; const wsOnline = new WebSocket("ws://www.example.com/online"); wsOnline.onmessage = (event) => { // ... }; const wsLogin = new WebSocket("ws://www.example.com/login"); wsLogin.onmessage = (event) => { // ... }; const wsLogout = new WebSocket("ws://www.example.com/logout"); wsLogout.onmessage = (event) => { // ... }; 

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

الصورة

نحل المشكلة.

تسمح لك كافة مكتبات الجهات الخارجية للعمل باستخدام مآخذ الويب بالاشتراك في رسائل من نوع addEventListener. يبدو هذا:

 ws.on("user", (userData) => { / .. }) 

كما نعلم ، يمكننا العمل باستخدام طريقة واحدة - onmessage ، التي تتلقى جميع البيانات كجزء من اتصالها ، لذلك يبدو هذا الرمز غير معتاد إلى حد ما. يتم تنفيذ ذلك على النحو التالي: تُرجع onmessage رسالة MessageEvent تحتوي على حقل البيانات . هي البيانات التي تحتوي على المعلومات التي يرسلها الخادم إلينا. يبدو هذا الكائن مثل هذا:

 { "event": "user", "data": { "name": "John Doe", ... } } 

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

 const ws = new WebSocket("ws://www.example.com"); ws.onmessage = (event) => { const data = JSON.parse(event.data); if (data.event === 'user') { // ... } if (data.event === 'news') { // ... } }; 


هذا يجعل من الممكن تلقي بيانات مختلفة داخل نفس الاتصال والاشتراك فيها من خلال بناء جملة مماثل للعادة لأحداث JS.

WebSockets الزاوي


أخيرًا ، وصلنا إلى أهم شيء - باستخدام WebSockets مباشرةً في Angular.

على الرغم من بساطة العمل مع WebSocket API الأصلي ، سنستخدم في هذه المقالة RxJs ، والتي بالطبع ، لأننا نتحدث عن Angular.

يمكن استخدام واجهة برمجة تطبيقات WebSocket الأصلية في تطبيقات Angular ، وإنشاء واجهة سهلة الاستخدام ، و RxJs Observable ، والاشتراك في الرسائل الضرورية ، وما إلى ذلك ، ولكن RxJs قد قامت بالفعل بالعمل الرئيسي نيابة عنك: WebSocketSubject عبارة عن غلاف تفاعلي عبر WebSocket القياسي API لا يقوم بإنشاء ناقل حدث أو إعادة معالجة المعالجة. هذا موضوع عادي ، والذي يمكنك من خلاله التعامل مع مقابس الويب ذات النمط التفاعلي.

RxJs WebSocketSubject


لذلك ، يتوقع WebSocketSubject WebSocketSubjectConfig ووجهة اختيارية ، حيث يمكنك تمرير رابط إلى الموضوع الذي تمت ملاحظته ، وإنشاء ملاحظة يمكن من خلالها الاستماع وإرسال رسائل لمقابس الويب.

ببساطة ، تقوم بتمرير عنوان URL للاتصال كوسيطة WebSocketSubject والاشتراك في جميع أنشطة مقبس الويب بالطريقة المعتادة لـ RxJs. وإذا كنت بحاجة إلى إرسال رسالة إلى الخادم ، فاستخدم نفس الطريقة المعتادة webSocketSubject.next (البيانات).

نقدم خدمة للعمل مع WebSocket Angular


صف بإيجاز ما نتوقعه من الخدمة:

  • واجهة موحدة وموجزة ؛
  • إمكانية التكوين على مستوى اتصال تبعيات DI ؛
  • إمكانية إعادة الاستخدام ؛
  • الكتابة
  • القدرة على الاشتراك لتلقي المعلومات عن طريق المفتاح ؛
  • القدرة على إنهاء الاشتراك ؛
  • إرسال رسائل إلى الخادم ؛
  • أعد الاتصال.

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

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

دعنا نبدأ.

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

إن أمكن ، سأقوم بتقصير الرمز ، النسخة الكاملة التي يمكنك رؤيتها في المجتمع الزاوي الناطق بالروسية على GitHub .

 export interface WebSocketConfig { url: string; reconnectInterval?: number; reconnectAttempts?: number; } export class WebsocketModule { public static config(wsConfig: WebSocketConfig): ModuleWithProviders { return { ngModule: WebsocketModule, providers: [{ provide: config, useValue: wsConfig }] }; } } 

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

 export interface IWsMessage<T> { event: string; data: T; } 

حيث يكون الحدث هو المفتاح ، والبيانات التي يتم الحصول عليها بواسطة المفتاح هي نموذج مكتوب.

تبدو الواجهة العامة للخدمة كما يلي:

 export interface IWebsocketService { on<T>(event: string): Observable<T>; send(event: string, data: any): void; status: Observable<boolean>; } 

تحتوي الخدمة على مجالات:

 //   WebSocketSubject private config: WebSocketSubjectConfig<IWsMessage<any>>; private websocketSub: SubscriptionLike; private statusSub: SubscriptionLike; // Observable    interval private reconnection$: Observable<number>; private websocket$: WebSocketSubject<IWsMessage<any>>; // ,      private connection$: Observer<boolean>; //  Observable       private wsMessages$: Subject<IWsMessage<any>>; //       private reconnectInterval: number; //    private reconnectAttempts: number; //      private isConnected: boolean; //   public status: Observable<boolean>; 

في مُنشئ فئة الخدمة ، نحصل على كائن WebSocketConfig المحدد عند توصيل الوحدة:

 constructor(@Inject(config) private wsConfig: WebSocketConfig) { this.wsMessages$ = new Subject<IWsMessage<any>>(); //  ,  ,     this.reconnectInterval = wsConfig.reconnectInterval || 5000; this.reconnectAttempts = wsConfig.reconnectAttempts || 10; //      connection$   websocket$ this.config = { url: wsConfig.url, closeObserver: { next: (event: CloseEvent) => { this.websocket$ = null; this.connection$.next(false); } }, //     connection$ openObserver: { next: (event: Event) => { console.log('WebSocket connected!'); this.connection$.next(true); } } }; // connection status this.status = new Observable<boolean>((observer) => { this.connection$ = observer; }).pipe(share(), distinctUntilChanged()); //      this.statusSub = this.status .subscribe((isConnected) => { this.isConnected = isConnected; if (!this.reconnection$ && typeof(isConnected) === 'boolean' && !isConnected) { this.reconnect(); } }); // ,  -    this.websocketSub = this.wsMessages$.subscribe( null, (error: ErrorEvent) => console.error('WebSocket error!', error) ); //  this.connect(); } 

طريقة الاتصال نفسها بسيطة:

 private connect(): void { this.websocket$ = new WebSocketSubject(this.config); //  //   ,    , //  ,  // ,    this.websocket$.subscribe( (message) => this.wsMessages$.next(message), (error: Event) => { if (!this.websocket$) { // run reconnect if errors this.reconnect(); } }); } 

إعادة الاتصال أكثر تعقيدًا:

 private reconnect(): void { //  interval    reconnectInterval this.reconnection$ = interval(this.reconnectInterval) .pipe(takeWhile((v, index) => index < this.reconnectAttempts && !this.websocket$)); //     ,        this.reconnection$.subscribe( () => this.connect(), null, () => { // Subject complete if reconnect attemts ending this.reconnection$ = null; if (!this.websocket$) { this.wsMessages$.complete(); this.connection$.complete(); } }); } 

طريقة التشغيل ، هي أيضًا بسيطة للغاية ، لا يوجد شيء للتعليق عليه.

 public on<T>(event: string): Observable<T> { if (event) { return this.wsMessages$.pipe( filter((message: IWsMessage<T>) => message.event === event), map((message: IWsMessage<T>) => message.data) ); } } 

طريقة الإرسال أكثر بساطة:

 public send(event: string, data: any = {}): void { if (event && this.isConnected) { //   any ,   ""   string //      :) this.websocket$.next(<any>JSON.stringify({ event, data })); } else { console.error('Send error!'); } } 

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

دعنا نرى الآن كيفية استخدامه. قم بتوصيل الوحدة النمطية WebsocketModule:

 imports: [ WebsocketModule.config({ url: environment.ws //      'ws://www.example.com' }) ] 

في مُنشئ المكون ، نقوم بإدخال الخدمة والاشتراك في الرسائل من " الرسائل " ، وإرسال النص مرة أخرى إلى الخادم:

 constructor(private wsService: WebsocketService) { this.wsService.on<IMessage[]>('messages') .subscribe((messages: IMessage[]) => { console.log(messages); this.wsService.send('text', 'Test Text!'); }); } 

اسم الأحداث أكثر ملاءمة لوضع ثوابت أو تعداد. نقوم بإنشاء ملف websocket.events.ts في مكان ما ونكتبه فيه:

 export const WS = { ON: { MESSAGES: 'messages' }, SEND: { TEXT: 'text' } }; 

إعادة كتابة الاشتراكات باستخدام كائن WS الذي تم إنشاؤه:

 this.wsService.on<IMessage[]>(WS.ON.MESSAGES) .subscribe((messages: IMessage[]) => { console.log(messages); this.wsService.send(WS.SEND.TEXT, 'Test Text!'); }); 

الصورة

في الختام


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

لجميع الأسئلة يمكنك الاتصال في التعليقات لي على Telegram أو على قناة Angular في نفس المكان.

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


All Articles