
In diesem Artikel werde ich versuchen, den engen Umfang der Technologie im Rahmen des Angular-Frameworks und seines bereits integrierten Assistenten - RxJs - detailliert zu behandeln, während wir bewusst nicht auf Server-Implementierungen eingehen, wie z Dies ist ein vollwertiges Thema für einen separaten Artikel.
Dieser Text ist nützlich für diejenigen, die bereits mit Angular vertraut sind, aber ihr Wissen direkt über das Thema vertiefen möchten.
Zunächst einige grundlegende Informationen.
Was ist WebSocket und warum brauchen Sie es?
Laut Wikipedia ist WebSocket ein „Duplex-Kommunikationsprotokoll (es kann gleichzeitig senden und empfangen) über eine TCP-Verbindung, das für Echtzeitnachrichten zwischen einem Browser und einem Webserver entwickelt wurde.
WebSocket ist für die Implementierung in Webbrowsern und Webservern konzipiert, kann jedoch für jeden Client oder jede Serveranwendung verwendet werden. Das WebSocket-Protokoll ist ein unabhängiges Protokoll, das auf dem TCP-Protokoll basiert. Es ermöglicht eine engere Interaktion zwischen dem Browser und der Website und erleichtert die Verteilung interaktiver Inhalte und die Erstellung von Echtzeitanwendungen. “
Mit anderen Worten, WebSocket ermöglicht es dem Server, jederzeit Anforderungen vom Client zu empfangen und Anforderungen an den Client zu senden. Somit erhalten Browser (Client) und Server gleiche Rechte und die Möglichkeit, Nachrichten auszutauschen, wenn eine Verbindung besteht. Eine reguläre AJAX-Anforderung erfordert die Übertragung vollständiger HTTP-Header, was einen erhöhten Datenverkehr in beide Richtungen bedeutet, während der Overhead von Web-Sockets nach dem Herstellen einer Verbindung nur zwei Byte beträgt. Web Socket reduziert die Menge an Informationen, die in HTTP-Headern übertragen werden, um das Hunderttausende und reduziert die Wartezeit erheblich. Web-Socket-Verbindungen unterstützen domänenübergreifende Funktionen wie CORS.
Auf der Serverseite gibt es Pakete zur Unterstützung des Web-Sockets, auf dem Client die HTML5-WebSocket-API mit einer Schnittstelle von drei Methoden:
WebSocket - die Hauptschnittstelle zum Herstellen einer Verbindung zu einem WebSocket-Server und zum Senden und Empfangen von Daten über die Verbindung;
CloseEvent - ein Ereignis, das vom WebSocket-Objekt
ausgelöst wurde, als die Verbindung geschlossen wurde;
MessageEvent - Ein Ereignis, das von einem WebSocket
ausgelöst wird, wenn eine Nachricht vom Server empfangen wird.
So sieht es auf der JavaSript-Implementierungsebene aus:
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 - Abhören von Nachrichten vom Server
Senden - Senden Sie Ihre Nachrichten an den Server
Das heißt, in seiner Grundform ist alles sehr einfach. Wenn Sie sich jedoch mit dem Thema befassen möchten, können Sie sich an
MDN-Webdokumente wenden und gleichzeitig Bibliotheken studieren, die ihre eigenen Ebenen über dieser API implementieren.
Warum haben Sie keine Angst, WebSocket zu verwenden?
Der erste Punkt, der abschrecken kann, ist die
Browserunterstützung . Heute gibt es kein solches Problem - WebSocket wird sowohl im Web als auch im mobilen Segment fast vollständig unterstützt.

https://caniuse.com/#feat=websocketsDer zweite Punkt ist die
einfache Implementierung . Ja, das ist zunächst entmutigend.
Die API ist so einfach, dass es auf den ersten Blick schwierig sein kann, zu verstehen, wie man mit einer so bescheidenen Anzahl von Methoden arbeitet, da alle außer einer entweder Fehler oder Verbindungen melden und nur eine von ihnen -
onmessage - trägt für welche Web-Sockets verwendet werden, d.h. um Daten vom Server zu empfangen.
Gleichzeitig ist der Haken, dass der Server normalerweise unterschiedliche Daten sendet, daher benötigen wir mehrere unterschiedliche Nachrichten? Oder müssen Sie für jedes Datenmodell eine eigene Verbindung erstellen?
Also die Aufgabe: Sie müssen das Benutzermodell und das neueste Nachrichtenmodell vom Server akzeptieren, und vielleicht sogar etwas anderes.
Ich bin auf eine so "elegante" Implementierung gestoßen:
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) => { // ... };
Auf den ersten Blick ist alles logisch. Aber stellen Sie sich jetzt vor, wie es aussehen wird, wenn es Dutzende oder Hunderte von ihnen gibt. Bei einem der Projekte, an denen ich gerade gearbeitet habe, gab es ungefähr dreihundert Veranstaltungen.

Wir lösen das Problem.
In allen Bibliotheken von Drittanbietern für die Arbeit mit Web-Sockets können Sie Nachrichten vom Typ addEventListener abonnieren. Es sieht so aus:
ws.on("user", (userData) => { / .. })
Wie wir wissen, können wir mit einer einzigen Methode arbeiten -
onmessage , die alle Daten als Teil ihrer Verbindung empfängt, sodass dieser Code etwas ungewöhnlich aussieht. Dies wird wie folgt implementiert:
onmessage gibt ein
MessageEvent zurück , das das
Datenfeld enthält. Es sind
Daten, die die Informationen enthalten, die der Server an uns sendet. Dieses Objekt sieht folgendermaßen aus:
{ "event": "user", "data": { "name": "John Doe", ... } }
Dabei ist
Ereignis der Schlüssel, mit dem bestimmt werden kann, welche Informationen der Server gesendet hat. Als nächstes wird auf der Front-End-Seite ein Bus erstellt, der Informationen nach Ereignis filtert und an die gewünschte Adresse sendet:
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') { // ... } };
Auf diese Weise können verschiedene Daten innerhalb derselben Verbindung empfangen und über eine Syntax abonniert werden, die der für JS-Ereignisse üblichen ähnelt.
WebSockets in Angular
Schließlich kamen wir zum Wichtigsten - der Verwendung von WebSockets direkt in Angular.
Trotz der Einfachheit der Arbeit mit der nativen WebSocket-API werden wir in diesem Artikel RxJs verwenden, was natürlich darauf zurückzuführen ist, dass es sich um Angular handelt.
Die native WebSocket-API kann in Anwendungen auf Angular verwendet werden, eine benutzerfreundliche Oberfläche auf der Basis von RxJs Observable erstellen, die erforderlichen Nachrichten abonnieren usw., aber RxJs hat bereits die Hauptarbeit für Sie erledigt: WebSocketSubject ist ein reaktiver Wrapper über das Standard-WebSocket API Es wird kein Ereignisbus erstellt oder die Verbindung erneut hergestellt. Dies ist ein reguläres Thema, mit dem Sie mit reaktiven Web-Sockets arbeiten können.
RxJs WebSocketSubject
Daher erwartet
WebSocketSubject WebSocketSubjectConfig und ein optionales Ziel, in dem Sie einen Link zu Ihrem beobachteten Betreff übergeben können, erstellt ein Observable, über das Sie Nachrichten für Web-Sockets abhören und senden können.
Einfach ausgedrückt, Sie übergeben die Verbindungs-URL als WebSocketSubject-Argument und abonnieren alle Aktivitäten des Web-Sockets auf die für RxJs übliche Weise. Wenn Sie eine Nachricht an den Server senden müssen, verwenden Sie dieselbe übliche Methode webSocketSubject.next (data).
Wir bieten einen Service für die Arbeit mit WebSocket Angular
Beschreiben Sie kurz, was wir vom Service erwarten:
- Einheitliche und übersichtliche Oberfläche;
- Möglichkeit der Konfiguration auf Verbindungsebene von DI- Abhängigkeiten;
- Die Möglichkeit der Wiederverwendung;
- Tippen;
- Die Möglichkeit, sich anzumelden, um Informationen per Schlüssel zu erhalten;
- Möglichkeit, ein Abonnement zu kündigen;
- Senden von Nachrichten an den Server;
- Schließen Sie die Verbindung wieder an.
Der letzte Punkt ist besonders zu beachten. Die erneute Verbindung oder die Organisation der erneuten Verbindung zum Server ist ein wichtiger Faktor bei der Arbeit mit Web-Sockets Netzwerkbrüche, Serverabstürze oder andere Fehler, die einen Verbindungsbruch verursachen, können zum Absturz der Anwendung führen.
Es ist wichtig zu beachten, dass Wiederverbindungsversuche
nicht zu häufig sein und nicht auf unbestimmte Zeit fortgesetzt werden sollten Dieses Verhalten kann den Client anhalten.
Fangen wir an.
Zunächst erstellen wir eine Service-Konfigurationsschnittstelle und ein Modul, das die Konfiguration bei Verbindung ermöglicht.
Wenn möglich, werde ich den Code verkürzen, die Vollversion, die Sie
in der russischsprachigen Angular-Community auf GitHub sehen können .
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 }] }; } }
Als nächstes müssen wir die Schnittstelle der Web-Socket-Nachricht beschreiben:
export interface IWsMessage<T> { event: string; data: T; }
Dabei ist
Ereignis der Schlüssel, und die vom Schlüssel erhaltenen
Daten sind ein typisiertes Modell.
Die öffentliche Schnittstelle des Dienstes sieht folgendermaßen aus:
export interface IWebsocketService { on<T>(event: string): Observable<T>; send(event: string, data: any): void; status: Observable<boolean>; }
Service hat Felder:
// 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>;
Im Konstruktor der Serviceklasse erhalten wir das WebSocketConfig-Objekt, das angegeben wurde, als das Modul verbunden wurde:
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(); }
Die Verbindungsmethode selbst ist einfach:
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(); } }); }
Das erneute Verbinden ist etwas komplizierter:
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(); } }); }
Die
on- Methode ist auch sehr einfach, es gibt nichts zu kommentieren.
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) ); } }
Die
Sendemethode ist noch einfacher:
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!'); } }
Das ist der ganze Service. Wie Sie sehen können, fiel der Großteil des Codes auf die Organisation der erneuten Verbindung.
Lassen Sie uns nun sehen, wie Sie es verwenden. Schließen Sie das WebsocketModule-Modul an:
imports: [ WebsocketModule.config({ url: environment.ws // 'ws://www.example.com' }) ]
Im Komponentenkonstruktor injizieren wir den Dienst und abonnieren Nachrichten von '
Nachrichten '. Senden Sie den Text zurück an den Server:
constructor(private wsService: WebsocketService) { this.wsService.on<IMessage[]>('messages') .subscribe((messages: IMessage[]) => { console.log(messages); this.wsService.send('text', 'Test Text!'); }); }
Der Name der Ereignisse ist bequemer, um Konstanten oder eine Aufzählung einzugeben. Wir erstellen irgendwo die Datei websocket.events.ts und schreiben sie hinein:
export const WS = { ON: { MESSAGES: 'messages' }, SEND: { TEXT: 'text' } };
Schreiben Sie Abonnements mit dem erstellten WS-Objekt neu:
this.wsService.on<IMessage[]>(WS.ON.MESSAGES) .subscribe((messages: IMessage[]) => { console.log(messages); this.wsService.send(WS.SEND.TEXT, 'Test Text!'); });

Abschließend
Das ist in der Tat alles. Dies ist ein notwendiges Minimum, das ein Angular-Entwickler über WebSockets wissen muss. Ich hoffe, dass ich dieses Thema ganz klar behandelt habe. Die Vollversion des Dienstes finden Sie auf
GitHub .
Bei allen Fragen können Sie sich in den Kommentaren
an mich per Telegramm oder auf
den dortigen Angular-Kanal wenden.