WebSockets In Angular: Create An Angular Service For Working With Web Sites
In this article, I’ll try to cover in detail the narrow scope of the technology application within the framework of the Angular framework and its already integral help – RxJs, and we will not intentionally touch on server implementations, because this is a full-fledged topic for a separate article.
This article will be useful to those who are already familiar with Angular but want to deepen their knowledge directly on the topic.
To begin with, some basic information.
What is WebSocket and why do you need it
In other words, WebSocket allows the server to receive requests from the client and send requests to the client at any desired time, thus, the browser (client) and the server on connection receive equal rights and the ability to exchange messages. A typical AJAX request requires the transmission of complete HTTP headers, which means an increase in traffic in both directions, whereas the overhead of web concepts after connection establishment is only two bytes. The WebSocket reduces the number of transmitted information in HTTP headers hundreds and thousands of times and reduces the waiting time several times. WebSocket connections support cross-domain, like CORS.
On the server side, there are packages for Webcam support, on the client, it’s the HTML5 WebSocket API, which has an interface of three methods:
WebSocket – the main interface for connecting to the WebSocket server, and then sending and receiving data on the connection;
CloseEvent – the event sent by the WebSocket object when the connection is closed;
MessageEvent – an event sent by a WebSocket object when the message was received from the server.
This is how it looks at the implementation level of 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 – listen to messages from the server
send – send your messages to the server
That is, in the basic form everything is extremely simple, but if you want to go deeper into the topic, you can refer to MDN Web Docs and at the same time study libraries that implement their own layers on top of this API.
Why not have to be afraid to use WebSocket
The first thing that can scare away is browser support. To date, there is no such problem – WebSocket is supported almost completely both on the web and in the mobile segment.
The second point is the ease of implementation. Yes, at first it discourages.
The API is so simple that at first glance it can be difficult to understand how to work with such a modest number of methods, because all of them except for one report either errors or a connection, and only one of them – onmessage – for what Web sites are used, i.e. to retrieve data from the server.
In this case, the problem is that the server usually sends different servers, therefore, we need several different onmessage? Or do you need to create your own connection for each data model?
So, the task: you need to take from the server a model of the user and a model of the latest news, and maybe even something else.
I’ve come across such an “elegant” implementation:
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) => { // ... };
At first glance, everything is logical. But now imagine how it will look, if there are tens or hundreds of them. On one of the projects I worked on, there were about three hundred events.
Solve the problem.
All third-party libraries for working with webcasts allow you to subscribe to messages by the type addEventListener. It looks something like this:
ws.on("user", (userData) => { / .. })
As we know, we can operate on a single method – onmessage, which receives all the data within its connection, so this code looks somewhat unusual. This is implemented as follows: onmessage returns a MessageEvent that contains the data field. It is data that contains the information that the server sends us. This object looks like this:
{ "event": "user", "data": { "name": "John Doe", ... } }
Where the event is the key by which you can determine which information the server sent. Further on the front-end side, a bus is created, which filters information about the event and sends it to the right address:
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') { // ... } };
This provides the ability to receive different data within a single connection and subscribe to them using a syntax similar to the usual JS events.
WebSockets in Angular
Finally, we got to the most important thing – using WebSockets directly in Angular.
- Despite the ease of working with the native WebSocket API, in this article, we will use RxJs, which of course, because it’s about Angular.
The native WebSocket API can be used in applications on Angular, create an easy-to-use interface, RxJs Observable, subscribe to necessary messages, etc., but RxJs has already done the main job for you: WebSocketSubject is a reactive wrapper over the standard WebSocket API. It does not create an event bus or a reconnect processing. This is a common Subject, with which you can work with websites in a reactive style.
RxJs WebSocketSubject
So, WebSocketSubject expects WebSocketSubjectConfig and an optional destination, in which you can pass a link to your watched Subject, creates Observable, through which you can listen and send messages for Web sites.
Simply put, pass as an argument WebSocketSubject url connections and subscribe to the usual way for RxJs to the entire activity of the webcam. And if you want to send a message to the server, then use the same usual method webSocketSubject.next (data).
Making a service for working with WebSocket Angular
Briefly describe what we expect from the service:
- Single and concise interface;
- The possibility of configuration at the connection level of dependencies DI;
- The possibility of re-use;
- Typing;
- Possibility to subscribe to receive information on the key;
- Ability to abort subscription;
- Sending messages to the server;
- Reconnect.
The last point is worth paying special attention. Reconnect, or the organization of reconnection to the server, is a paramount factor when working with Web sites, because network breaks, server crashes, or other errors that cause the connection to break are capable of crashing the application.
It is important to take into account that attempts to reconnect should not be too frequent and should not continue indefinitely. this behavior can suspend the client.
Let’s get started.
First of all, we will create a service configuration interface and a module that will provide the ability to configure when connected.
I will, if possible, reduce the code, the full version you can see in the Angular on 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 }] }; } }
Next, we need to describe the websocket message interface:
export interface IWsMessage<T> { event: string; data: T; }
Where the event is the key, and data received by key is a typed model.
The public interface of the service looks like this:
export interface IWebsocketService { on<T>(event: string): Observable<T>; send(event: string, data: any): void; status: Observable<boolean>; }
The service has fields:
// WebSocketSubject configuration object private config: WebSocketSubjectConfig <IWsMessage <any >>; private websocketSub: SubscriptionLike; private statusSub: SubscriptionLike; // Observable for reconnect by interval private reconnection $: Observable <number>; private websocket $: WebSocketSubject <IWsMessage <any >>; // Tells when the connection occurs and reconnect private connection $: Observer <boolean>; // auxiliary Observable for working with subscriptions to messages private wsMessages $: Subject <IWsMessage <any >>; // pause between attempts to reconnect in milliseconds private reconnectInterval: number; // number of reconnect attempts private reconnectAttempts: number; // synchronous helper for connection status private isConnected: boolean; // connection status public status: Observable <boolean>;
In the constructor of the service class, we get the WebSocketConfig object specified when the module is connected:
constructor (@Inject (config) private wsConfig: WebSocketConfig) { this.wsMessages $ = new Subject <IWsMessage <any >> (); // look at the config, if it's empty, set the defaults for reconnect this.reconnectInterval = wsConfig.reconnectInterval || 5000; this.reconnectAttempts = wsConfig.reconnectAttempts || 10; // when the connection is minimized, we change the status of connection $ and the deaf websocket $ this.config = { url: wsConfig.url, closeObserver: { next: (event: CloseEvent) => { this.websocket $ = null; this.connection $ .next (false); } }, // at the connection we change the status of 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 ()); // start reconnect if there is no connection this.statusSub = this.status .subscribe ((isConnected) => { this.isConnected = isConnected; if (! this.reconnection $ && typeof (isConnected) === 'boolean' &&! isConnected) { this.reconnect (); } }); // say that something went wrong this.websocketSub = this.wsMessages $ .subscribe ( null, (error: ErrorEvent) => console.error ('WebSocket error!', error) ); // connect this.connect (); }
The method of connectivity is simple:
private connect (): void { this.websocket $ = new WebSocketSubject (this.config); // create // if there are messages, helmet them on, // if not, we expect // reconnect if we get an error this.websocket $ .subscribe ( (message) => this.wsMessages $ .next (message), (error: Event) => { if (! this.websocket $) { // run reconnect if errors this.reconnect (); } }); }
Reconnect is a bit more complicated:
private reconnect (): void { // Create an interval with a value from reconnectInterval this.reconnection $ = interval (this.reconnectInterval) .pipe (takeWhile ((v, index) => index <this.reconnectAttempts &&! this.websocket $)); // We try to connect yet we do not connect, or we will not restrain in restriction of attempts of connection this.reconnection $ .subscribe ( () => this.connect (), null, () => { // Subject complete if reconnect attemts ending this.reconnection $ = null; if (! this.websocket $) { this.wsMessages $ .complete (); this.connection $ .complete (); } }); }
The method on, it is also very simple, there’s nothing to comment on.
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) ); } }
The send method is even simpler:
public send (event: string, data: any = {}): void { if (event && this.isConnected) { // crutch with any because the "volume" end expects a string // more elegant crutch is not thought of :) this.websocket $ .next (<any> JSON.stringify ({event, data}))); } else { console.error ('Send error!'); } }
That’s the whole service. As you can see, the bulk of the code fell on the organization of reconnecting.
Let’s now see how to use it. We connect the module WebsocketModule:
imports: [ WebsocketModule.config ({ url: environment.ws // or just a link in the form 'ws: //www.example.com' }) ]
In the component’s constructor, the service is enigmatic and we subscribe to messages from ‘messages‘, send the text back to the server:
constructor(private wsService: WebsocketService) { this.wsService.on<IMessage[]>('messages') .subscribe((messages: IMessage[]) => { console.log(messages); this.wsService.send('text', 'Test Text!'); }); }
The name of events is more convenient to put in constants or enumeration. Create a file somewhere websocket.events.ts and write in it:
export const WS = { ON: { MESSAGES: 'messages' }, SEND: { TEXT: 'text' } };
We rewrite the subscription using the created WS object:
this.wsService.on<IMessage[]>(WS.ON.MESSAGES) .subscribe((messages: IMessage[]) => { console.log(messages); this.wsService.send(WS.SEND.TEXT, 'Test Text!'); });
In conclusion
Here, in fact, that’s all. This is the minimum required by the developer on Angular for WebSockets. I hope that this is clear enough. The full version of the service can be found on GitHub.