Tworzenie serwera powiadomień push

W tym ćwiczeniu z programowania utworzysz serwer powiadomień push. Serwer będzie zarządzać listą subskrypcji push i wysyłać do nich powiadomienia.

Kod po stronie klienta jest już gotowy. W tym ćwiczeniu będziesz pracować nad funkcjami po stronie serwera.

Powiadomienia z umieszczonej aplikacji Glitch są automatycznie blokowane, więc nie możesz wyświetlić podglądu aplikacji na tej stronie. Zamiast tego:

  1. Kliknij Remix to Edit (Zmiksuj do edycji), aby umożliwić edycję projektu.
  2. Aby wyświetlić podgląd strony, kliknij Wyświetl aplikację. Następnie kliknij Pełny ekranpełny ekran.

Aplikacja na żywo otworzy się w nowej karcie Chrome. Aby ponownie wyświetlić kod, kliknij Wyświetl źródło w umieszczonej wersji błędu.

W trakcie wykonywania ćwiczeń w tym laboratorium kodu wprowadzaj zmiany w kodzie w osadzonym na tej stronie Glitchu. Odśwież nową kartę z aplikacją na żywo, aby zobaczyć zmiany.

Poznaj aplikację startową i jej kod

Zacznij od przejrzenia interfejsu użytkownika aplikacji.

W nowej karcie Chrome:

  1. Aby otworzyć Narzędzia dla programistów, naciśnij `Control+Shift+J` (lub `Command+Option+J` na Macu). Kliknij kartę Konsola.

  2. Spróbuj kliknąć przyciski w interfejsie (wynik znajdziesz w konsoli deweloperskiej Chrome).

    • Zarejestruj skrypt service worker – rejestruje skrypt service worker w zakresie adresu URL projektu Glitch. Wyrejestruj skrypt service worker powoduje usunięcie skryptu service worker. Jeśli jest do niego podłączona subskrypcja push, to taka subskrypcja również zostanie dezaktywowana.

    • Subskrybuj powiadomienia push tworzy subskrypcję push. Jest ona dostępna tylko wtedy, gdy skrypt service worker został zarejestrowany, a w kodzie klienta znajduje się stała VAPID_PUBLIC_KEY (więcej informacji na ten temat znajdziesz w dalszej części tego artykułu), dlatego nie można jeszcze jej kliknąć.

    • Gdy masz aktywną subskrypcję push, powiadomienie o bieżącej subskrypcji prosi serwer o wysłanie powiadomienia do punktu końcowego.

    • Powiadomij wszystkie subskrypcje powoduje, że serwer wysyła powiadomienie do wszystkich punktów końcowych subskrypcji w swojej bazie danych.

      Pamiętaj, że niektóre z tych punktów końcowych mogą być nieaktywne. Zawsze istnieje możliwość, że subskrypcja zniknie, zanim serwer wyśle powiadomienie.

Zobaczmy, co dzieje się po stronie serwera. Aby zobaczyć komunikaty z kodu serwera, sprawdź log Node.js w interfejsie Glitch.

  • W aplikacji Glitch kliknij Narzędzia -> Logi.

    Prawdopodobnie zobaczysz komunikat podobny do Listening on port 3000.

    Jeśli w interfejsie aplikacji na żywo klikniesz Powiadom obecną subskrypcję lub Powiadom wszystkie subskrypcje, zobaczysz też ten komunikat:

    TODO: Implement sendNotifications()
    Endpoints to send to:  []

Przyjrzyjmy się teraz kodom.

  • public/index.js zawiera kompletny kod klienta. Wykonuje wykrywanie funkcji, rejestruje i odrejestruje skrypt service worker oraz kontroluje subskrypcję powiadomień push przez użytkownika. Serwer wysyła też informacje o nowych i usuniętych subskrypcjach.

    Ponieważ będziesz pracować tylko nad funkcjami serwera, nie będziesz edytować tego pliku (poza wypełnianiem stałej VAPID_PUBLIC_KEY).

  • public/service-worker.js to prosty skrypt service worker, który rejestruje zdarzenia push i wyświetla powiadomienia.

  • /views/index.html zawiera interfejs aplikacji.

  • .env zawiera zmienne środowiskowe, które Glitch wczytuje na serwer aplikacji podczas uruchamiania. W polu .env wpisz dane uwierzytelniające na potrzeby wysyłania powiadomień.

  • server.js to plik, w którym będziesz wykonywać większość czynności podczas tego ćwiczenia.

    Kod początkowy tworzy prosty serwer WWW Express. Masz 4 elementy TODO oznaczone w komentarzach kodu symbolem TODO:. Czynności, które musisz wykonać:

    W tym ćwiczeniu będziesz kolejno rozwiązywać te zadania.

Generowanie i wczytywanie szczegółów VAPID

Pierwszym krokiem jest wygenerowanie szczegółów VAPID, dodanie ich do zmiennych środowiskowych Node.js i zaktualizowanie kodu klienta i serwera za pomocą nowych wartości.

Tło

Aby subskrybować powiadomienia, użytkownicy muszą ufać tożsamości aplikacji i jej serwera. Użytkownicy muszą też mieć pewność, że powiadomienie pochodzi z tej samej aplikacji, która skonfigurowała subskrypcję. Musi też mieć pewność, że nikt inny nie może odczytać treści powiadomienia.

Protokół, który zapewnia bezpieczeństwo i prywatność powiadomień push, nazywa się Voluntary Application Server Identification for Web Push (VAPID). VAPID używa kryptografii klucza publicznego do weryfikacji tożsamości aplikacji, serwerów i punktów końcowych subskrypcji oraz do szyfrowania treści powiadomień.

W tej aplikacji do generowania kluczy VAPID oraz szyfrowania i wysyłania powiadomień użyjesz pakietu npm web-push.

Implementacja

W tym kroku wygeneruj parę kluczy VAPID dla aplikacji i dodaj je do zmiennych środowiskowych. Wczytaj zmienne środowiskowe na serwerze i dodaj klucz publiczny jako stałą w kodzie klienta.

  1. Użyj funkcji generateVAPIDKeys biblioteki web-push, aby utworzyć parę kluczy VAPID.

    W pliku server.js usuń komentarze z podanych niżej linii kodu:

    server.js

    // Generate VAPID keys (only do this once).
    /*
     * const vapidKeys = webpush.generateVAPIDKeys();
     * console.log(vapidKeys);
     */

    const vapidKeys = webpush.generateVAPIDKeys();
    console
    .log(vapidKeys);
  2. Gdy Glitch uruchomi ponownie Twoją aplikację, wygenerowane klucze zostaną wyprowadzone do dziennika Node.js w interfejsie Glitch (nie do konsoli Chrome). Aby wyświetlić klucze VAPID, w interfejsie Glitch wybierz Narzędzia -> Dzienniki.

    Pamiętaj, aby skopiować klucz publiczny i prywatny z tej samej pary kluczy.

    Usterka uruchamia ponownie aplikację za każdym razem, gdy edytujesz kod, więc pierwsza para wygenerowanych kluczy może zniknąć z widoku w miarę pojawiania się kolejnych danych wyjściowych.

  3. W pliku .env skopiuj i wklej klucze VAPID. Umieść klucze w cudzysłowie ("...").

    W polu VAPID_SUBJECT możesz wpisać "mailto:test@test.test".

    .env

    # process.env.SECRET
    VAPID_PUBLIC_KEY
    =
    VAPID_PRIVATE_KEY
    =
    VAPID_SUBJECT
    =
    VAPID_PUBLIC_KEY
    ="BN3tWzHp3L3rBh03lGLlLlsq..."
    VAPID_PRIVATE_KEY
    ="I_lM7JMIXRhOk6HN..."
    VAPID_SUBJECT
    ="mailto:test@test.test"
  4. W pliku server.js ponownie wyłącz te 2 wiersze kodu, ponieważ klucze VAPID trzeba wygenerować tylko raz.

    server.js

    // Generate VAPID keys (only do this once).
    /*
    const vapidKeys = webpush.generateVAPIDKeys();
    console.log(vapidKeys);
    */

    const vapidKeys = webpush.generateVAPIDKeys();
    console
    .log(vapidKeys);
  5. W pliku server.js wczytaj informacje VAPID ze zmiennych środowiskowych.

    server.js

    const vapidDetails = {
     
    // TODO: Load VAPID details from environment variables.
      publicKey
    : process.env.VAPID_PUBLIC_KEY,
      privateKey
    : process.env.VAPID_PRIVATE_KEY,
      subject
    : process.env.VAPID_SUBJECT
    }
  6. Skopiuj i wklej też klucz public do kodu klienta.

    W pliku public/index.js wpisz tę samą wartość parametru VAPID_PUBLIC_KEY, którą skopiowano do pliku .env:

    public/index.js

    // Copy from .env
    const VAPID_PUBLIC_KEY = '';
    const VAPID_PUBLIC_KEY = 'BN3tWzHp3L3rBh03lGLlLlsq...';
    ````

Wdrożenie funkcji wysyłania powiadomień

Tło

W tej aplikacji do wysyłania powiadomień będziesz używać pakietu npm web-push.

Ten pakiet automatycznie szyfruje powiadomienia, gdy zostanie wywołana funkcja webpush.sendNotification(), więc nie musisz się tym martwić.

Web-push akceptuje wiele opcji powiadomień – na przykład możesz dołączyć nagłówki do wiadomości i określić kodowanie treści.

W tym ćwiczeniu użyjesz tylko 2 opcji zdefiniowanych za pomocą tych linii kodu:

let options = {
  TTL
: 10000; // Time-to-live. Notifications expire after this.
  vapidDetails
: vapidDetails; // VAPID keys from .env
};

Opcja TTL (czas życia) ustawia limit czasu wygaśnięcia powiadomienia. Dzięki temu serwer nie będzie wysyłać powiadomienia do użytkownika, gdy nie będzie ono już aktualne.

Opcja vapidDetails zawiera klucze VAPID załadowane z zmiennych środowiskowych.

Implementacja

W pliku server.js zmodyfikuj funkcję sendNotifications w ten sposób:

server.js

function sendNotifications(database, endpoints) {
 
// TODO: Implement functionality to send notifications.
  console
.log('TODO: Implement sendNotifications()');
  console
.log('Endpoints to send to: ', endpoints);
  let notification
= JSON.stringify(createNotification());
  let options
= {
    TTL
: 10000, // Time-to-live. Notifications expire after this.
    vapidDetails
: vapidDetails // VAPID keys from .env
 
};
  endpoints
.map(endpoint => {
    let subscription
= database[endpoint];
    webpush
.sendNotification(subscription, notification, options);
 
});
}

Funkcja webpush.sendNotification() zwraca obietnicę, więc możesz łatwo dodać obsługę błędów.

W pliku server.js ponownie zmodyfikuj funkcję sendNotifications:

server.js

function sendNotifications(database, endpoints) {
  let notification
= JSON.stringify(createNotification());
  let options
= {
    TTL
: 10000; // Time-to-live. Notifications expire after this.
    vapidDetails
: vapidDetails; // VAPID keys from .env
 
};
  endpoints
.map(endpoint => {
    let subscription
= database[endpoint];
    webpush
.sendNotification(subscription, notification, options);
    let id
= endpoint.substr((endpoint.length - 8), endpoint.length);
    webpush
.sendNotification(subscription, notification, options)
   
.then(result => {
      console
.log(`Endpoint ID: ${id}`);
      console
.log(`Result: ${result.statusCode} `);
   
})
   
.catch(error => {
      console
.log(`Endpoint ID: ${id}`);
      console
.log(`Error: ${error.body} `);
   
});
 
});
}

Obsługa nowych subskrypcji

Tło

Oto, co się dzieje, gdy użytkownik subskrybuje powiadomienia push:

  1. Użytkownik klika Subskrybuj powiadomienia push.

  2. Klient używa stałej VAPID_PUBLIC_KEY (publicznego klucza VAPID serwera) do wygenerowania unikalnego obiektu subscription dla danego serwera. Obiekt subscription wygląda tak:

       {
         
    "endpoint": "https://fcm.googleapis.com/fcm/send/cpqAgzGzkzQ:APA9...",
         
    "expirationTime": null,
         
    "keys":
         
    {
           
    "p256dh": "BNYDjQL9d5PSoeBurHy2e4d4GY0sGJXBN...",
           
    "auth": "0IyyvUGNJ9RxJc83poo3bA"
         
    }
       
    }
  3. Klient wysyła żądanie POST do adresu URL /add-subscription, w tym subskrypcję w postaci ciągu znaków JSON w treści.

  4. Serwer pobiera z treści żądania POST ciąg znaków subscription, przetwarza go z powrotem na format JSON i dodaje do bazy danych subskrypcji.

    Baza danych przechowuje subskrypcje, używając ich własnych punktów końcowych jako klucza:

    {
     
"https://fcm...1234": {
        endpoint
: "https://fcm...1234",
        expirationTime
: ...,
        keys
: { ... }
     
},
     
"https://fcm...abcd": {
        endpoint
: "https://fcm...abcd",
        expirationTime
: ...,
        keys
: { ... }
     
},
     
"https://fcm...zxcv": {
        endpoint
: "https://fcm...zxcv",
        expirationTime
: ...,
        keys
: { ... }
     
},
   
}

Nowa subskrypcja jest teraz dostępna dla serwera do wysyłania powiadomień.

Implementacja

Żądania dotyczące nowych subskrypcji trafiają do ścieżki /add-subscription, która jest adresem URL POST. W pliku server.js zobaczysz fragment obsługi trasy:

server.js

app.post('/add-subscription', (request, response) => {
 
// TODO: implement handler for /add-subscription
  console
.log('TODO: Implement handler for /add-subscription');
  console
.log('Request body: ', request.body);
  response
.sendStatus(200);
});

W Twojej implementacji ten moduł musi:

  • Pobierz nową subskrypcję z treści żądania.
  • Uzyskać dostęp do bazy danych o aktywnych subskrypcjach.
  • Dodaj nową subskrypcję do listy aktywnych subskrypcji.

Aby obsługiwać nowe subskrypcje:

  • W pliku server.js zmodyfikuj przetwarzacz trasy dla /add-subscription w ten sposób:

    server.js

    app.post('/add-subscription', (request, response) => {
     
// TODO: implement handler for /add-subscription
      console
.log('TODO: Implement handler for /add-subscription');
      console
.log('Request body: ', request.body);
      let subscriptions
= Object.assign({}, request.session.subscriptions);
      subscriptions
[request.body.endpoint] = request.body;
      request
.session.subscriptions = subscriptions;
      response
.sendStatus(200);
   
});

Obsługa anulowania subskrypcji

Tło

Serwer nie zawsze wie, kiedy subskrypcja staje się nieaktywna. Na przykład subskrypcja może zostać usunięta, gdy przeglądarka wyłączy usługę.

Serwer może jednak wykryć subskrypcje anulowane w interfejsie aplikacji. W tym kroku wdrożysz funkcję usuwania subskrypcji z bazy danych.

Dzięki temu serwer nie wysyła dużej liczby powiadomień do nieistniejących punktów końcowych. Oczywiście w przypadku prostej aplikacji testowej nie ma to większego znaczenia, ale na większą skalę staje się to istotne.

Implementacja

Prośby o anulowanie subskrypcji trafiają na adres URL POST /remove-subscription.

Ignorowany moduł obsługi trasy w pliku server.js wygląda tak:

server.js

app.post('/remove-subscription', (request, response) => {
 
// TODO: implement handler for /remove-subscription
  console
.log('TODO: Implement handler for /remove-subscription');
  console
.log('Request body: ', request.body);
  response
.sendStatus(200);
});

W Twojej implementacji ten moduł musi:

  • Z treści żądania pobierz punkt końcowy anulowanej subskrypcji.
  • Uzyskać dostęp do bazy danych o aktywnych subskrypcjach.
  • Anulowana subskrypcja zostanie usunięta z listy aktywnych subskrypcji.

Treść żądania POST z klienta zawiera punkt końcowy, który należy usunąć:

{
 
"endpoint": "https://fcm.googleapis.com/fcm/send/cpqAgzGzkzQ:APA9..."
}

Aby anulować subskrypcję:

  • W pliku server.js zmodyfikuj moduł obsługi trasy /remove-subscription w ten sposób:

    server.js

  app.post('/remove-subscription', (request, response) => {
   
// TODO: implement handler for /remove-subscription
    console
.log('TODO: Implement handler for /remove-subscription');
    console
.log('Request body: ', request.body);
    let subscriptions
= Object.assign({}, request.session.subscriptions);
   
delete subscriptions[request.body.endpoint];
    request
.session.subscriptions = subscriptions;
    response
.sendStatus(200);
 
});