Tworzenie serwera powiadomień push

Z tego ćwiczenia w programie utworzysz serwer powiadomień push. Serwer będzie zarządzać listą subskrypcji push i wysyłać do nich powiadomienia.

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

Zremiksuj przykładową aplikację i wyświetl ją w nowej karcie

W umieszczonej aplikacji Glitch powiadomienia są automatycznie blokowane, więc na tej stronie nie będzie można wyświetlić podglądu aplikacji. Zamiast tego wykonaj te czynności:

  1. Aby umożliwić edytowanie projektu, kliknij Zremiksuj do edycji.
  2. Aby wyświetlić podgląd strony, kliknij Wyświetl aplikację, a potem Pełny ekran pełny ekran.

Aktywna aplikacja otworzy się w nowej karcie Chrome. W umieszczonym w nim błędzie kliknij Wyświetl źródło, aby ponownie wyświetlić kod.

Podczas wykonywania ćwiczeń z programowania wprowadzaj zmiany w kodzie zawartym w umieszczonym na tej stronie glitchu. Odśwież nową kartę, używając aktywnej aplikacji, aby zobaczyć zmiany.

Zapoznaj się z aplikacją początkową i jej kodem

Zacznij od przyjrzenia się interfejsowi klienta aplikacji.

Na nowej karcie Chrome:

  1. Naciśnij „Control + Shift + J” (lub „Command + Option + J” na Macu), aby otworzyć Narzędzia deweloperskie. Kliknij kartę Konsola.

  2. Kliknij przyciski w interfejsie (sprawdź dane w konsoli programisty Chrome).

    • Zarejestruj skrypt service worker, by zarejestrować skrypt service worker w zakresie adresu URL projektu Glitch. Wyrejestruj skrypt service worker, usuwając go. Jeśli jest do niej połączona subskrypcja push, ona też zostanie dezaktywowana.

    • Subskrybuj, aby przesłać push, tworzy subskrypcję push. Jest on dostępny tylko wtedy, gdy skrypt service worker został zarejestrowany, a kod klienta zawiera stałą VAPID_PUBLIC_KEY (więcej informacji na ten temat znajdziesz później), więc nie możesz jeszcze jej kliknąć.

    • Gdy masz aktywną subskrypcję push, funkcja Powiadom bieżącą subskrypcję żąda, aby serwer wysłał powiadomienie do swojego punktu końcowego.

    • Powiadamiaj wszystkich o subskrypcjach informuje serwer, że ma wysł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. Subskrypcja może zawsze zniknąć, gdy serwer wyśle do niej powiadomienie.

Zobaczmy, co dzieje się po stronie serwera. Aby zobaczyć komunikaty z kodu serwera, zajrzyj do dziennika Node.js w interfejsie Glitch.

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

    Prawdopodobnie zobaczysz komunikat taki jak Listening on port 3000.

    Jeśli w interfejsie opublikowanej aplikacji klikniesz Powiadom bieżącą subskrypcję lub Powiadom wszystkie subskrypcje, pojawi się też ten komunikat:

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

Spójrzmy teraz na kod.

  • public/index.js zawiera kompletny kod klienta. Wykonuje wykrywanie funkcji, rejestruje i wyrejestrowuje skrypt service worker oraz kontroluje subskrypcję użytkownika na powiadomienia push. Wysyła też na serwer informacje o nowych i usuniętych subskrypcjach.

    Ponieważ będziesz pracować tylko nad funkcją serwera, nie będziesz edytować tego pliku (oprócz wypełnienia 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 podasz dane uwierzytelniające na potrzeby wysyłania powiadomień.

  • server.js to plik, w którym będziesz wykonywać większość pracy w trakcie tych ćwiczeń z programowania.

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

    W tym ćwiczeniu w programie będziesz osobno wykonywać zadania z tych zadań.

Generowanie i wczytywanie szczegółów VAPID

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

Wprowadzenie

Gdy użytkownicy subskrybują powiadomienia, muszą zaufać tożsamości aplikacji i jej serwera. Użytkownicy muszą też mieć pewność, że gdy otrzymają powiadomienie, pochodzi z tej samej aplikacji, w której skonfigurowano subskrypcję. Muszą też mieć pewność, że nikt inny nie będzie mógł odczytać ich treści.

Protokół, który zapewnia bezpieczeństwo i prywatność powiadomień push, nosi nazwę 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 szyfrowania treści powiadomień.

W tej aplikacji będziesz używać pakietu npm web-push do generowania kluczy VAPID oraz szyfrowania i wysyłania powiadomień.

Implementacja

W tym kroku wygeneruj dla swojej aplikacji parę kluczy VAPID 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 w bibliotece web-push, aby utworzyć parę kluczy VAPID.

    W kodzie server.js usuń komentarze z tych wierszy 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. Po ponownym uruchomieniu aplikacji Glitch przesyła wygenerowane klucze do dziennika Node.js w interfejsie Glitch (nie do konsoli Chrome). Aby zobaczyć klucze VAPID, w interfejsie Glitch wybierz Narzędzia -> Logi.

    Pamiętaj, aby skopiować klucze publiczne i prywatne z tej samej pary kluczy.

    Zakłócenie powoduje ponowne uruchomienie aplikacji za każdym razem, gdy edytujesz kod, więc pierwsza para kluczy może zniknąć, gdy pojawi się więcej danych wyjściowych.

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

    Jako 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 umieść jeszcze raz komentarz w tych 2 wierszach kodu, ponieważ klucze VAPID wystarczy 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 szczegóły 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 klucz publiczny i wklej go do kodu klienta.

    W pliku public/index.js wpisz tę samą wartość dla VAPID_PUBLIC_KEY, która została skopiowana do pliku .env:

    public/index.js

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

Wdrażanie funkcji wysyłania powiadomień

Wprowadzenie

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

Ten pakiet automatycznie szyfruje powiadomienia, gdy pojawia się webpush.sendNotification(), więc nie musisz się tym przejmować.

Web-push akceptuje różne opcje powiadomień, na przykład możesz dołączać nagłówki do wiadomości i określić kodowanie treści.

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

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

Opcja TTL (czas życia) ustawia czas wygaśnięcia powiadomienia. Dzięki temu serwer uniknie wysyłania do użytkownika powiadomień, które przestały być istotne.

Opcja vapidDetails zawiera klucze VAPID wczytane ze 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ługuj nowe subskrypcje

Wprowadzenie

Co się stanie, gdy użytkownik zasubskrybuje powiadomienia push:

  1. Użytkownik klika Subskrybuj, aby przesłać dane push.

  2. Klient używa stałej VAPID_PUBLIC_KEY (publicznego klucza VAPID serwera) do wygenerowania unikalnego, dotyczącego serwera obiektu subscription. 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 na adres URL /add-subscription żądanie POST, uwzględniając subskrypcję w postaci ciągu tekstowego JSON w treści.

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

    Baza danych przechowuje subskrypcje za pomocą 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: { ... }
      },
    }

Teraz nowa subskrypcja jest dostępna dla serwera do wysyłania powiadomień.

Implementacja

Żądania nowych subskrypcji są kierowane na trasę /add-subscription, która jest adresem URL POST. W pliku server.js zobaczysz moduł obsługi wycinka 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ł obsługi musi:

  • Pobierz nową subskrypcję z treści żądania.
  • Dostęp do bazy danych aktywnych subskrypcji.
  • Dodaj nową subskrypcję do listy aktywnych subskrypcji.

Aby obsługiwać nowe subskrypcje:

  • W pliku server.js zmodyfikuj moduł obsługi trasy /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

Wprowadzenie

Serwer nie zawsze wie, że subskrypcja staje się nieaktywna – na przykład subskrypcja może zostać wyczyszczona, gdy przeglądarka wyłączy skrypt service worker.

Serwer może jednak dowiedzieć się o subskrypcjach, które zostały anulowane za pomocą interfejsu aplikacji. W tym kroku wdrożysz funkcję usuwania subskrypcji z bazy danych.

Dzięki temu serwer uniknie wysyłania mnóstwa powiadomień do nieistniejących punktów końcowych. Oczywiście nie ma to znaczenia w przypadku prostej aplikacji testowej, ale staje się to ważne na większą skalę.

Implementacja

Prośby o anulowanie subskrypcji są wysyłane na adres URL POST /remove-subscription.

Moduł obsługi wycinka kodu 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ł obsługi musi:

  • Pobierz punkt końcowy anulowanej subskrypcji z treści żądania.
  • Dostęp do bazy danych aktywnych subskrypcji.
  • Usuń anulowaną subskrypcję z listy aktywnych.

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

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

Co zrobić w przypadku anulowania subskrypcji:

  • 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);
  });