chevron-left chevron-right

[JS][PHP] Jak zaimplementować powiadomienia typu push w aplikacji JavaScript?

Spróbuj sobie wyobrazić korzystanie z Twittera, Facebooka czy też GitHuba bez otrzymywania powiadomień o tym co się właśnie wydarzyło. Brak informacji, że pojawiły się nowe wpisy na Twitterze; brak powiadomień, że ktoś do nas napisał na Facebooku czy chociażby brak powiadomień o tym, że ktoś utworzył pull requesta do naszego kodu na Githubie byłby bardzo uciążliwy. W niektórych przypadkach użyteczność takiej aplikacji czy strony znacząco uległaby zmniejszeniu.

Na szczęście, JavaScript API udostępniane przez HTML5 znacząco ułatwia nam implementację tego typu rozwiązań na stronach internetowych i w aplikacjach internetowych. W przypadku powiadomień typu push idealnym rozwiązaniem wydaje się być SSE - Server Sent Events.

Czym jest SSE?

Jest to mechanizm jednostronnej komunikacji między serwerem a przeglądarką, która odbywa się za pomocą protokołu HTTP. Połączenie jest ustanawiane raz i jest utrzymywane przez serwer aż do momentu w którym twórca aplikacji zdecyduje się zamknąć połączenie. SSE ma wbudowaną funkcjonalność automatycznego wznawiania połączenia w momencie gdy z jakiegoś powodu połączenie zostało zerwane.

Z racji tego, że komunikacja odbywa się po protokole HTTP to wiadomości przesyłane z serwera zawsze będą w postaci tekstu. Czyli danych binarnych nie będziemy już w stanie przesłać, tak jak to ma miejsce w przypadku korzystania WebSockets.

Ponadto, za pomocą SSE jesteśmy w stanie samodzielnie zdefiniować jakie eventy/zdarzenia po stronie serwera będą udostępniane, oprócz standardowych eventów:

  • open,
  • message,
  • error.

Dodatkowo, możemy przesłać ID eventu, które może się okazać przy wznawianiu zerwanego połączenia. W takim przypadku, ID eventu ostatniego otrzymanego, przez przeglądarkę, eventu będzie przesłane w nagłówku żądania (requestu) HTTP: Last-Event-ID.

Wsparcie SSE w przeglądarkach

Na chwilę obecną wsparcie przeglądarek dla tej funkcjonalności jest bardzo dobre wyłączając przeglądarkę Internet Explorer oraz system Android poniżej wersji 4.4. Jeśli zależy nam na wsparciu brakujących systemów to wystarczy do aplikacji dołączyć odpowiednią bibliotekę dodającą brakującą funkcjonalność, np. to rozwiązanie.

Implementacja SSE po stronie przeglądarki

Aby zacząć korzystać z SSE po stronie przeglądarki wystarczy, że zastosujemy następujący zapis:

1
var sse = new EventSource('sse-server.php');

Oczywiście zamiast wpisywać adres sse-server.php możemy wpisać dowolny docleowy adres URL. Jeśli korzystamy ze źródeł danych znajdujących się pod inną domeną, to musimy zastosować odpowiednią politykę CORS na serwerze. W przypadku, gdy backend naszej aplikacji jest napisany w PHP, to wystarczy dodać następujące nagłówki:

1
2
3
4
<?php
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST');
header('Access-Control-Allow-Headers: X-Requested-With');

Dzięki nim będziemy w stanie się połączyć z serwerem z dowolnej przeglądarki, z aplikacji znajdującej się pod innym adresem internetowym niż serwer z danymi.

Wracając do naszego kodu JavaScript, to aby obsłużyć zdarzenia z serwera, musimy zacząć nasłuchiwać je w następujący sposób:

1
2
3
var messageCallback = function (event) { console.log('sse:message', event.data); };
 
sse.addEventListener('message', messageCallback, false);

I to jest wszystko co właściwie trzeba zrobić po stronie strony internetowej lub aplikacji internetowej aby móc korzystać z SSE. W przypadku, gdybyśmy chcieli nasłuchiwać innych eventów wysyłanych przez serwer (niestandardowych eventów), to wystarczy dodać następujący kawałek kodu:

1
2
3
var postUpdatedCallback = function (event) { console.log('post:updated', event);
 
sse.addEventListener('postupdated', postUpdatedCallback, false);

Implementacja mechanizmu SSE po stronie serwera z wykorzystaniem PHP

Teraz pora na kawałek kodu, który na tym blogu od dawna nie pojawiał. Aplikacja na serwerze została napisana w prostym PHP, dzięki czemu można szybko sprawdzić działanie SSE w przeglądarce:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST');
header('Access-Control-Allow-Headers: X-Requested-With');
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
 
$counter = rand(1, 10);
$events = ['update', 'delete', 'create'];
$ids = [1, 12, 34, 65, 234, 18, 5];
 
while (1) {
    $counter--;
 
    if (!$counter) {
        echo 'event: ' . $events[array_rand($events)] . "\n";
        echo 'data: {"id": "' . $ids[array_rand($ids)] . '"}' . "\n\n";
 
        $counter = rand(1, 10);
    } else {
        echo 'event: ping' . "\n";
        echo 'data: {"time": "' . date(DATE_ISO8601) . '"}' . "\n\n";
    }
 
    ob_flush();
    flush();
    sleep(1);
}

Powyższy kod ma na celu losowe generowanie eventów z tablicy 3-ch elementów: update, create, delete z losowo dobranymi ID jakiejkolwiek treści (umówmy się, że będą to ID artykułów w bazie danych). Powyższy kod domyślnie wysyła event ping z danymi dotyczącymi czasu. W momencie gdy upłynie określony czas - zmienna $counter, to wysyła jeden z eventów o których wspomniałem wcześniej. Proste i łatwe do testowania.

Oczywiście, powyższe rozwiązanie ma na celu pokazanie jak SSE działa po stronie serwera. Kodu w postaci jak wyżej raczej nie będziesz pisać, aby obsłużyć swoją aplikację webową. Ze swojej strony mogę podpowiedzieć, że do nasłuchiwania zmiany stanu treści (artykułów, postów na blogu, stron) warto wykorzystać mechanizmy kolejek, np. RabbitMQ.

Postać danych przesyłanych z serwera za pomocą SSE

Jak wspomniałem wcześniej, dane są przesyłane w postaci tekstu i mogą przyjąć następujący format:

id: 1234\n
event: update\n
retry: 18000\n
data: Jakieś dane\n
data: {"title":"Tytuł artykułu","author":"Piotr Nalepa"}\n\n

Wyjaśnię, co oznaczają kolejne przedrostki:

id
ID eventu, który może być wykorzystany przy wznawianiu połączenia.
event
Nazwa eventu który jest wysyłany przez serwer.
retry
Odpowiedzialny za ustawienie opóźnienia wznawiania połączenia. Domyślnie jego wartość wynosi 3 sekundy. Stosujemy zapis w milisekundach.
data
Dane jakie chcemy przesłać do przeglądarki. Należy pamiętać aby dane przesyłać w postaci tekstu, czyli obiekty, tablice, klasy. Przykład sformatowanego JSONa został przedstawiony powyżej.

Podsumowanie

Mam nadzieję, że udało mi się w sposób wyczerpujący wyjaśnić czym jest SSE i jak z tego korzystać. Dzięki SSE można stworzyć wiele ciekawych funkcjonalności na stronach internetowych czy też wewnątrz aplikacji internetowych. Dzięki SSE można w łatwy sposób stworzyć aplikacje powiadamiające o jakichkolwiek zdarzeniach, np. o nowych wpisach na Twitterze czy też przesłać informacje o zmianie wyniku meczu piłkarskiego. Generalnie, to w jaki sposób można wykorzystać zaprezentowaną funkcjonalność zależy od pomysłu jaki ma dana osoba, np. Ty.

O budowaniu aplikacji javascriptowych z wykorzystaniem SSE, systemu CMS eZ Platform i tworzeniu natywnych aplikacji Windows wykorzystujących JavaScript, SSE i eZ Platform mówiłem na konferencji InfoShare 2015 w Gdańsku.

  • A tak teraz przeglądam se prezentację i miałbym małe pytanie: czy jest jakiś powód, dla którego polecasz Electrona, a nie nw.js? Dotąd bawiłem się tym drugim i prawdę mówiąc mam wrażenie, że jest takie… bardziej low-levelowe 😉 Jedyna rzecz, która podoba mi się w Electronie to to, że trzonem aplikacji jest moduł node’a, a nie okno chromium, jak w nw.js

  • Dorzuciłem metodę POST być może nadmiarowo. Akurat zdawałem sobie sprawę z tego, że SSE leci tylko GET-em.
    Co do RabbitMQ, to pomyślę o tym. Nie jestem specjalistą od konfigurowania systemów kolejkowych, więc musiałbym się bliżej temu rozwiązaniu przyjrzeć.

  • Electrona polecałem z jednego prostego powodu. Nie znałem nw.js dopóki Ty mi o tym nie napisałeś tutaj. Wydaje mi się, że Electron był w zupełności wystarczający do tego co chciałem osiągnąć.

  • THE GUGS

    Panie Piotrze mam pytanie zupełnie nie związane z tematem, ale nie wiem do kogo się z nim zwrócić. Właśnie projektuję bloga i mam taki problem, że gdy dodaję jakiś nowy wpis i link „czytaj więcej” to muszę dodać nowy plik html i w nim zapisać treść strony po wejściu w czytaj więcej. Wiem, że tak się nie robi, bo strona ważyła by zbyt wiele i jest to w ogóle niepraktyczne, dlatego proszę pana o radę jak mam rozwiązać ten problem, czyli jak stworzyć oddzielną stronę do której użytkownik wejdzie po kliknięciu „czytaj więcej” bez tworzenia oddzielnego pliku html.

  • Jeśli buduje Pan statyczne strony internetowe, tj. bez użycia żadnego CMS typu Joomla!, WordPress czy eZ Platform, to owszem trzeba za każdym razem tworzyć osobne pliki HTML z kodem strony. To można sobie uprościć na kilka sposobów. Pierwszy sposób, to wykorzystanie kodu PHP za którego pomocą można podzielić stronę na części takie jak np.: nagłówek, stopka, pasek boczny. Tym samym, nie trzeba będzie powtarzać większości kodu HTML. Inne podejście natomiast, zakłada ładowanie treści na sposób AJAX, tym samym strona nie będzie się przeładowywała, ale optymalizacja strony pod kątem wyszukiwarek nie będzie bardzo trudna. Na początek, zalecam wykorzystanie CMS dla ułatwienia sprawy.
    Co do wagi strony, to nie liczy się waga wszystkich plików jakie zawiera projekt strony, tylko waga plików ładowanych na konkretnej stronie. Tak więc tworzenie nowych plików HTML, nie zwiększa wagi strony.

  • THE GUGS

    Dziękuję. Bardzo mi Pan pomógł.