Как подружить Angular 2 и Socket.IO с помощью RxJS

3662
views

Angular 2 and socket.io

Работая над проектом, в котором как вы догадались, используются Angular 2 и Socket.io, я захотел поделиться способом создания провайдера, который будет содержать методы для подписки и вещания событий, возвращающих Observable переменные. Функционала написаного провайдера хватит на реализацию фактически любого веб-приложения использующего одно socket подключение.

Под одним socket подключением, имеется ввиду архитектура, в которой пользователь зайдя на сайт подключается к серверу и в этом единственном подключении подписывается/отписывается на различные каналы и вещает в них события получая в ответ статус запроса и какие-либо дополнительные данные с помощью библиотеки RxJS.

Туториал подразумевает, что используется Node.js, Angular 2 на TypeScript, а в качестве пакетного менеджера — npm.

Установка

Для разворачивания Angular 2 достаточно несколько консольных команд:

//Установим angular cli, если еще не установлен
npm install -g angular-cli

//Создадим стартовый проект
ng new PROJECT_NAME

//Для работы с проектом
cd PROJECT_NAME

ng serve // данная команда запустит сервер с livereload при изменении typescript файлов

Подробнее с документацией Angular cli можно ознакомиться в репо на github.

Для установки клиента Socket.IO необходимо ввести:

npm i socket.io-client --save

Создание сервиса

Теперь приступим к созданию самого Angular 2 сервиса, который будет связующим звеном между компонентами приложения и библиотекой Socket.IO.

Для начала с помощью angular-cli создадим файл сервиса. Для этого находясь в корне проекта пропишем в консоли:

ng g service socket

После этого в папке /src/app сгенерируются два новых файла: socket.service.ts и его спек для тестирования. Для удобства создадим папку services, в ней создадим папку socket и в нее поместим два сгенерированных файла.

Теперь созданный сервис-провайдер необходимо подключить в app.module.ts .

import {SocketService} from './services/socket/socket.service.ts';

...
@NgModule({
    declarations: [...],
    imports: [...],
    providers: [
         SocketService
    ],
    ...
}

И вот у нас все готово для написания сервиса.

Написание самого сервиса

На данный момент файл socket.service.ts должен выглядеть таким образом:

import { Injectable } from '@angular/core';

@Injectable()
export class SocketService {

  constructor() { }

}

Декоратор @Injectable необходим для того, чтобы сервис мог сам внедрять зависимости. Рекомендуется такой декоратор добавлять в любые сервисы.

Так как мы хотим, чтобы методы возвращали Observable переменные, то необходимо импортировать класс Observable из библиотеки rxjs:

import {Observable} from 'rxjs/Observable';

Также сразу подключим установленную библиотеку Socket.IO. Так как библиотека написана не на TypeScript, то необходимо задать имя подключаемого компонента, назовем его io.

import * as io from "socket.io-client";

Теперь объявим переменные, которые будут использоваться в сервисе:

export class SocketService {
    //предположим, сервер сокетов доступен на 3100 порту локалхоста.
    private host: string = "http://localhost:3100";
    private socket: any;

...

Как говорилось ранее, мы собираемся использовать всего одно сокет-подключение и удобнее всего инициализировать объект socket.io при первом создании инстанса сервиса. А следовательно — объявить его необходимо в конструкторе класса:

constructor() {
        this.socket = io(this.host);
        this.socket.on("connect", () => this.connected());
        this.socket.on("disconnect", () => this.disconnected());
        this.socket.on("error", (error: string) => {
            console.log(`ERROR: "${error}" (${this.host})`);
        });
    }

Коллбэки, которые вызываются по событиям подключения/отключения, будут объявлены ниже. Необходимы они для отправки авторизационных данных или наоборот удаления их при разлогинивании.

Теперь добавим два метода необходимые для принудительного отключения и подключения к сокетам (при повторной авторизации).

connect () {
        this.socket.connect();
}

disconnect () {
        this.socket.disconnect();
}

И вот мы дошли до основного функционала, а именно вещание событий и подписки на них.

Вещание событий

Базовый функционал Socket.io не позволяет получать статус отправленного события.

//Так выглядит стандартный метод вещания событий
socket.emit('канал', data);

А хотелось бы иметь подобно $http запросам различные респонсы, чтобы знать, нужно ли выводить сообщение об успехе или уведомить об ошибке. Для решения этой задачи добавим третим атрибутом callback функцию, в которую на серверной стороне будем отдавать статус и прочие данные.

socket.emit(chanel, message, function (data) {
       if (data.success) {
             // Успех
       } else {
             // Что-то пошло не так
       }
});

Добавив усовершенствованную функцию и обернув её в Observable переменную получим финальный код:

emit(chanel:string, message:any) {
        return new Observable<any>(observer => {
            this.socket.emit(chanel, message, function (data) {
                if (data.success) {
                    // Успех
                    observer.next(data.msg);
                } else {
                    // Что-то пошло не так
                    observer.error(data.msg);
                }
                observer.complete();
            });
        });
    }

Вызов этого метода из другого компонента будет таким:

import { SocketService } from '../services/socket/socket.service';

...

constructor(private socket: SocketService) {
     ...
     this.socket.emit('event', data).subscribe(
            (data) => {
                console.log('Success',data);
            },
            (error) => {
                console.log('Error',error);
            },
            () => {
                console.log('complete');
            }
        );
}

А на серверной стороне код будет выглядеть следующим образом:

socket.on('event', function (data, res) {
    ...
    if (success) {
        res({success: true, msg: 'Success.'});
    } else {
        res({success: false, msg: 'Something went wrong.'});
    }
});

Подписка на события

Слушать события еще проще, так как не требуется кастомизация на серверной стороне.

on(event_name) {
        return new Observable<any>(observer => {
            this.socket.off(event_name); //Если такое событие уже существует
            this.socket.on(event_name, (data) => {
                observer.next(data);
            });
        });
}

Теперь осталось объявить функции, которые упоминались ранее, и вызываются при подключении или отключении от сокета:

// Вызывается при открытии соединения
private connected() {

    // Любая логика...например отправка jwt токена

    let currentUser = JSON.parse(localStorage.getItem('currentUser'));
    this.emit("user_token", currentUser.token).subscribe(
        (data) => {
            console.log('Success',data);
        },
        (error) => {
            console.log('Error',error);
        },
        () => {
            console.log('complete');
        }
    );

}

// Вызывается при закрытии соединения
private disconnected() {
    console.log('Disconnected');
}

Ну вот и все! Сумарно код выглядит таким образом:

import {Injectable} from "@angular/core";
import {Observable} from 'rxjs/Observable';
import * as io from "socket.io-client";

@Injectable()
export class SocketService {
    private host: string = window.location.protocol + "//" + window.location.hostname + ":" + 3100;
    private socket: any;

    constructor() {
        this.socket = io(this.host);
        this.socket.on("connect", () => this.connected());
        this.socket.on("disconnect", () => this.disconnected());
        this.socket.on("error", (error: string) => {
            console.log(`ERROR: "${error}" (${this.host})`);
        });
    }

    connect () {
        this.socket.connect();
    }

    disconnect () {
        this.socket.disconnect();
    }

    emit(chanel, message) {
        return new Observable<any>(observer => {
            console.log(`emit to ${chanel}:`,message);
            this.socket.emit(chanel, message, function (data) {
                if (data.success) {
                    observer.next(data.msg);
                } else {
                    observer.error(data.msg);
                }
                observer.complete();
            });
        });
    }

    on(event_name) {
        console.log(`listen to ${event_name}:`);
        return new Observable<any>(observer => {
            this.socket.off(event_name);
            this.socket.on(event_name, (data) => {
                observer.next(data);
            });
        });
    }

    private connected() {
        console.log('Connected');
    }

    private disconnected() {
        console.log('Disconnected');
    }
}

Подведем итог

Полученный сервис позволяет легко связать библиотеку Socket.io с компонентами Angular 2. Вещание события возвращает Observable переменную, что позволяет устанавливать лоадеры при отправке или выводить уведомления об ошибке/успехе. А слушать события становится еще проще, чем даже использовать промисы.

Если вам полезен данный материал, подписывайтесь на мой блог, чтобы получать уведомления о новых публикациях на темы: верстки, javascript и разработки кроссплатформенных мобильных приложений.

Подписаться на блог по эл. почте