
Dans cet article, je vais essayer de couvrir en détail l'étendue étroite de la technologie dans le cadre du cadre angulaire et de son assistant déjà intégré - RxJs, alors que nous n'aborderons pas délibérément les implémentations de serveur, comme Il s'agit d'un sujet à part entière pour un article séparé.
Ce texte sera utile à ceux qui connaissent déjà Angular, mais qui souhaitent approfondir leurs connaissances directement sur le sujet.
Tout d'abord, quelques informations de base.
Qu'est-ce que WebSocket et pourquoi en avez-vous besoin
Selon Wikipedia , WebSocket est un «protocole de communication duplex (il peut transmettre et recevoir en même temps) sur une connexion TCP, conçu pour la messagerie en temps réel entre un navigateur et un serveur Web.
WebSocket est conçu pour être implémenté dans les navigateurs Web et les serveurs Web, mais il peut être utilisé pour n'importe quelle application client ou serveur. Le protocole WebSocket est un protocole indépendant basé sur le protocole TCP. Il permet une interaction plus étroite entre le navigateur et le site Web, facilitant la distribution de contenu interactif et la création d'applications en temps réel. »
En d'autres termes, WebSocket permet au serveur de recevoir des demandes du client et d'envoyer des demandes au client à tout moment souhaité, ainsi, le navigateur (client) et le serveur reçoivent des droits égaux et la possibilité d'échanger des messages lors de la connexion. Une demande AJAX régulière nécessite la transmission d'en-têtes HTTP complets, ce qui signifie un trafic accru dans les deux sens, tandis que la surcharge des sockets Web après l'établissement d'une connexion n'est que de deux octets. La socket Web réduit la quantité d'informations transmises dans les en-têtes HTTP par des centaines et des milliers de fois et réduit considérablement le temps d'attente. Les connexions de socket Web prennent en charge plusieurs domaines comme CORS.
Côté serveur, il existe des packages pour la prise en charge du socket web, sur le client c'est l'API HTML5 WebSocket, qui a une interface de trois méthodes:
WebSocket - l'interface principale pour se connecter à un serveur WebSocket, puis envoyer et recevoir des données via la connexion;
CloseEvent - un événement distribué par l'objet WebSocket lorsque la connexion a été fermée;
MessageEvent - Un événement distribué par un WebSocket lorsqu'un message est reçu du serveur.
Voici à quoi cela ressemble au niveau d'implémentation 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 - écoute les messages du serveur
envoyer - envoyez vos messages au serveur
Autrement dit, dans sa forme de base, tout est extrêmement simple, mais si vous souhaitez vous plonger dans le sujet, vous pouvez vous tourner vers
MDN Web Docs et en même temps étudier des bibliothèques qui implémentent leurs propres couches au-dessus de cette API.
Pourquoi ne pas avoir peur d'utiliser WebSocket
Le premier point qui peut effrayer est la
prise en charge du navigateur . Aujourd'hui, il n'y a pas un tel problème - WebSocket est pris en charge presque complètement à la fois sur le Web et dans le segment mobile.

https://caniuse.com/#feat=websocketsLe deuxième point est la
facilité de mise en œuvre . Oui, au début, c'est décourageant.
L'API est si simple qu'à première vue, il peut être difficile de comprendre comment travailler avec un nombre aussi modeste de méthodes, car toutes, sauf une, signalent des erreurs ou des connexions, et une seule d'entre elles -
onmessage - contient pour quelles prises Web sont utilisées, c.-à-d. pour recevoir des données du serveur.
Dans le même temps, le problème est que le serveur envoie généralement des données différentes, par conséquent, nous avons besoin de plusieurs messages différents? Ou avez-vous besoin de créer votre propre connexion pour chaque modèle de données?
Donc, la tâche: vous devez accepter le modèle utilisateur et le dernier modèle d'actualités du serveur, et peut-être même autre chose.
J'ai rencontré une telle implémentation "élégante":
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) => { // ... };
À première vue, tout est logique. Mais imaginez maintenant à quoi cela ressemblera s'il y en a des dizaines ou des centaines. Dans l'un des projets sur lesquels j'ai travaillé, il y a eu environ trois cents événements.

Nous résolvons le problème.
Toutes les bibliothèques tierces pour travailler avec des sockets Web vous permettent de vous abonner aux messages du type addEventListener. Cela ressemble à ceci:
ws.on("user", (userData) => { / .. })
Comme nous le savons, nous pouvons fonctionner avec une seule méthode -
onmessage , qui reçoit toutes les données dans le cadre de sa connexion, donc ce code semble quelque peu inhabituel. Ceci est implémenté comme suit:
onmessage renvoie un
MessageEvent qui contient le champ de
données . Ce sont des
données qui contiennent les informations que le serveur nous envoie. Cet objet ressemble à ceci:
{ "event": "user", "data": { "name": "John Doe", ... } }
où
événement est la clé permettant de déterminer les informations envoyées par le serveur. Ensuite, du côté frontal, un bus est créé qui filtre les informations par événement et les envoie à l'adresse souhaitée:
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') { // ... } };
Cela permet de recevoir différentes données au sein d'une même connexion et de s'y abonner via une syntaxe similaire à celle habituelle pour les événements JS.
WebSockets en angulaire
Enfin, nous sommes arrivés à la chose la plus importante: utiliser WebSockets directement dans Angular.
Malgré la simplicité de travailler avec l'API WebSocket native, dans cet article, nous utiliserons RxJs, ce qui bien sûr, car nous parlons d'Angular.
L'API WebSocket native peut être utilisée dans des applications angulaires, créer une interface facile à utiliser, RxJs Observable, s'abonner aux messages nécessaires, etc., mais RxJs a déjà fait le travail principal pour vous: WebSocketSubject est un wrapper réactif sur le WebSocket standard API Il ne crée pas de bus d'événements ni ne reconnecte le traitement. Il s'agit d'un sujet standard, avec lequel vous pouvez travailler avec des sockets Web de style réactif.
RxJs WebSocketSubject
Ainsi,
WebSocketSubject attend
WebSocketSubjectConfig et une destination facultative, dans laquelle vous pouvez passer un lien vers votre sujet observé, crée un observable à travers lequel vous pouvez écouter et envoyer des messages pour les sockets Web.
Autrement dit, vous passez l'URL de connexion en tant qu'argument WebSocketSubject et vous abonnez à toute l'activité du socket Web de la manière habituelle pour les RxJ. Et si vous devez envoyer un message au serveur, utilisez la même méthode habituelle webSocketSubject.next (data).
Nous faisons un service pour travailler avec WebSocket Angular
Décrivez brièvement ce que nous attendons du service:
- Interface unifiée et concise;
- Possibilité de configuration au niveau de la connexion des dépendances DI ;
- La possibilité de réutilisation;
- Dactylographie;
- La possibilité de s'abonner pour recevoir des informations par clé;
- Possibilité de résilier un abonnement;
- Envoi de messages au serveur;
- Reconnectez-vous.
Le dernier point mérite une attention particulière. La reconnexion, ou l'organisation de la reconnexion au serveur, est un facteur primordial lorsque vous travaillez avec des sockets Web, comme les ruptures de réseau, les pannes de serveur ou d'autres erreurs qui provoquent une rupture de connexion peuvent entraîner le blocage de l'application.
Il est important de noter que les tentatives de reconnexion
ne devraient pas être trop fréquentes et ne devraient pas se poursuivre indéfiniment, car ce comportement peut suspendre le client.
Commençons.
Tout d'abord, nous allons créer une interface de configuration de service et un module qui offrira la possibilité de configurer une fois connecté.
Si possible, je raccourcirai le code, la version complète que vous pouvez voir
dans la communauté angulaire russophone sur 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 }] }; } }
Ensuite, nous devons décrire l'interface du message de socket Web:
export interface IWsMessage<T> { event: string; data: T; }
où
événement est la clé et les
données obtenues par la clé sont un modèle typé.
L'interface publique du service ressemble à ceci:
export interface IWebsocketService { on<T>(event: string): Observable<T>; send(event: string, data: any): void; status: Observable<boolean>; }
Le service a des champs:
// 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>;
Dans le constructeur de la classe de service, nous obtenons l'objet WebSocketConfig spécifié lorsque le module était connecté:
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(); }
La méthode de connexion elle-même est simple:
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(); } }); }
La reconnexion est un peu plus compliquée:
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(); } }); }
La méthode on, c'est aussi extrêmement simple, il n'y a même rien à commenter.
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) ); } }
La méthode d'
envoi est encore plus simple:
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!'); } }
C'est tout le service. Comme vous pouvez le voir, l'essentiel du code est tombé sur l'organisation de la reconnexion.
Voyons maintenant comment l'utiliser. Connectez le module WebsocketModule:
imports: [ WebsocketModule.config({ url: environment.ws // 'ws://www.example.com' }) ]
Dans le constructeur du composant, nous injectons le service et souscrivons aux messages de '
messages ', renvoyons le texte au serveur:
constructor(private wsService: WebsocketService) { this.wsService.on<IMessage[]>('messages') .subscribe((messages: IMessage[]) => { console.log(messages); this.wsService.send('text', 'Test Text!'); }); }
Le nom des événements est plus pratique pour mettre des constantes ou une énumération. Nous créons quelque part le fichier websocket.events.ts et l'écrivons dedans:
export const WS = { ON: { MESSAGES: 'messages' }, SEND: { TEXT: 'text' } };
Réécrivez les abonnements à l'aide de l'objet WS créé:
this.wsService.on<IMessage[]>(WS.ON.MESSAGES) .subscribe((messages: IMessage[]) => { console.log(messages); this.wsService.send(WS.SEND.TEXT, 'Test Text!'); });

En conclusion
En fait, c'est tout. C'est un minimum nécessaire qu'un développeur Angular doit connaître sur WebSockets. J'espère avoir couvert ce sujet assez clairement. La version complète du service est disponible sur
GitHub .
Pour toutes questions que vous pouvez contacter dans les commentaires,
à moi sur Telegram ou sur
le canal angulaire au même endroit.