Создайте сервер push-уведомлений

В этой лаборатории кода вы создадите сервер push-уведомлений. Сервер будет управлять списком push-подписок и отправлять им уведомления.

Клиентский код уже готов — в этой лаборатории кода вы будете работать над функциональностью на стороне сервера.

Сделайте ремикс примера приложения и просмотрите его на новой вкладке.

Уведомления автоматически блокируются из встроенного приложения Glitch, поэтому вы не сможете просмотреть приложение на этой странице. Вместо этого вот что делать:

  1. Нажмите Remix to Edit , чтобы сделать проект доступным для редактирования.
  2. Чтобы просмотреть сайт, нажмите «Просмотреть приложение» . Затем нажмите Полноэкранный режим полноэкранный .

Живое приложение откроется в новой вкладке Chrome. Во встроенном Glitch нажмите «Просмотреть исходный код» , чтобы снова отобразить код.

Работая над этой лабораторией кода, вносите изменения в код встроенного Glitch на этой странице. Обновите новую вкладку с вашим действующим приложением, чтобы увидеть изменения.

Ознакомьтесь со стартовым приложением и его кодом.

Начните с рассмотрения клиентского пользовательского интерфейса приложения.

На новой вкладке Chrome:

  1. Нажмите «Control+Shift+J» (или «Command+Option+J» на Mac), чтобы открыть DevTools. Откройте вкладку Консоль .

  2. Попробуйте нажимать кнопки в пользовательском интерфейсе (проверьте консоль разработчика Chrome на наличие вывода).

    • Регистрация сервисного работника регистрирует сервисного работника в пределах URL-адреса вашего проекта Glitch. Отменить регистрацию сервисного работника удаляет сервисного работника. Если к нему прикреплена принудительная подписка, она также будет деактивирована.

    • Подписаться на push- уведомление создает push-подписку. Он доступен только в том случае, если сервис-воркер зарегистрирован и в коде клиента присутствует константа VAPID_PUBLIC_KEY (подробнее об этом позже), поэтому пока вы не можете щелкнуть по нему.

    • Если у вас есть активная принудительная подписка, функция «Уведомить текущую подписку» запрашивает , чтобы сервер отправил уведомление на свою конечную точку.

    • Уведомить все подписки указывает серверу отправить уведомление всем конечным точкам подписки в его базе данных.

      Обратите внимание, что некоторые из этих конечных точек могут быть неактивными. Всегда возможно, что подписка исчезнет к тому моменту, когда сервер отправит ей уведомление.

Давайте посмотрим, что происходит на стороне сервера. Чтобы просмотреть сообщения из кода сервера, просмотрите журнал Node.js в интерфейсе Glitch.

  • В приложении Glitch нажмите «Инструменты» -> «Журналы» .

    Вероятно, вы увидите сообщение типа Listening on port 3000 .

    Если вы попытались нажать «Уведомить о текущей подписке» или «Уведомить все подписки» в пользовательском интерфейсе живого приложения, вы также увидите следующее сообщение:

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

Теперь давайте посмотрим на код.

  • public/index.js содержит готовый клиентский код. Он выполняет обнаружение функций, регистрирует и отменяет регистрацию сервисного работника, а также контролирует подписку пользователя на push-уведомления. Он также отправляет на сервер информацию о новых и удаленных подписках.

    Поскольку вы собираетесь работать только над функциональностью сервера, вы не будете редактировать этот файл (кроме заполнения константы VAPID_PUBLIC_KEY ).

  • public/service-worker.js — это простой сервис-воркер, который фиксирует push-события и отображает уведомления.

  • /views/index.html содержит пользовательский интерфейс приложения.

  • .env содержит переменные среды, которые Glitch загружает на ваш сервер приложений при его запуске. Вы заполните .env данными аутентификации для отправки уведомлений.

  • server.js — это файл, с которым вы будете выполнять большую часть своей работы во время этой лабораторной работы.

    Начальный код создает простой веб-сервер Express . Для вас есть четыре элемента TODO, отмеченные в комментариях к коду значком TODO: . Вам нужно:

    В этой лаборатории кода вы будете работать с этими элементами TODO по одному.

Создайте и загрузите данные VAPID

Ваш первый элемент TODO — сгенерировать данные VAPID, добавить их в переменные среды Node.js и обновить код клиента и сервера новыми значениями.

Фон

Когда пользователи подписываются на уведомления, им необходимо доверять идентичности приложения и его сервера. Пользователи также должны быть уверены, что они получают уведомление от того же приложения, которое настроило подписку. Им также необходимо быть уверенными, что никто другой не сможет прочитать содержимое уведомления.

Протокол, который делает push-уведомления безопасными и конфиденциальными, называется добровольной идентификацией сервера приложений для Web Push (VAPID). VAPID использует криптографию с открытым ключом для проверки личности приложений, серверов и конечных точек подписки, а также для шифрования содержимого уведомлений.

В этом приложении вы будете использовать пакет npm web-push для генерации ключей VAPID, а также для шифрования и отправки уведомлений.

Выполнение

На этом этапе сгенерируйте пару ключей VAPID для вашего приложения и добавьте их в переменные среды. Загрузите переменные среды на сервер и добавьте открытый ключ как константу в клиентский код.

  1. Используйте функцию generateVAPIDKeys библиотеки web-push , чтобы создать пару ключей VAPID.

    В server.js удалите комментарии в следующих строках кода:

    server.js

    // Generate VAPID keys (only do this once).
    /*
     * const vapidKeys = webpush.generateVAPIDKeys();
     * console.log(vapidKeys);
     */
    const vapidKeys = webpush.generateVAPIDKeys();
    console.log(vapidKeys);
    
  2. После того, как Glitch перезапустит ваше приложение, оно выводит сгенерированные ключи в журнал Node.js в интерфейсе Glitch ( а не в консоль Chrome). Чтобы увидеть ключи VAPID, выберите «Инструменты» -> «Журналы» в интерфейсе Glitch.

    Обязательно скопируйте открытый и закрытый ключи из одной и той же пары ключей!

    Glitch перезапускает ваше приложение каждый раз, когда вы редактируете свой код, поэтому первая пара сгенерированных вами ключей может исчезнуть из поля зрения по мере последующего вывода.

  3. В .env скопируйте и вставьте ключи VAPID. Заключите ключи в двойные кавычки ( "..." ).

    Для VAPID_SUBJECT вы можете ввести "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. В server.js еще раз закомментируйте эти две строки кода, поскольку вам нужно сгенерировать ключи VAPID только один раз.

    server.js

    // Generate VAPID keys (only do this once).
    /*
    const vapidKeys = webpush.generateVAPIDKeys();
    console.log(vapidKeys);
    */
    const vapidKeys = webpush.generateVAPIDKeys();
    console.log(vapidKeys);
    
  5. В server.js загрузите данные VAPID из переменных среды.

    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. Скопируйте и вставьте открытый ключ в клиентский код.

    В public/index.js введите то же значение для VAPID_PUBLIC_KEY , которое вы скопировали в файл .env:

    public/index.js

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

Реализовать функционал для отправки уведомлений

Фон

В этом приложении вы будете использовать пакет npm web-push для отправки уведомлений.

Этот пакет автоматически шифрует уведомления при вызове webpush.sendNotification() , поэтому вам не нужно об этом беспокоиться.

web-push принимает несколько вариантов уведомлений — например, вы можете прикрепить к сообщению заголовки и указать кодировку содержимого.

В этой лаборатории кода вы будете использовать только два параметра, определенные следующими строками кода:

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

Параметр TTL (время жизни) устанавливает тайм-аут истечения срока действия уведомления. Это способ сервера избежать отправки уведомления пользователю после того, как оно больше не актуально.

Параметр vapidDetails содержит ключи VAPID, которые вы загрузили из переменных среды.

Выполнение

В server.js измените функцию sendNotifications следующим образом:

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

Поскольку webpush.sendNotification() возвращает обещание, вы можете легко добавить обработку ошибок.

В server.js снова измените функцию 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} `);
    });
  });
}

Обработка новых подписок

Фон

Вот что происходит, когда пользователь подписывается на push-уведомления:

  1. Пользователь нажимает «Подписаться», чтобы отправить .

  2. Клиент использует константу VAPID_PUBLIC_KEY (открытый ключ VAPID сервера) для создания уникального объекта subscription , специфичного для сервера. Объект subscription выглядит следующим образом:

       {
         "endpoint": "https://fcm.googleapis.com/fcm/send/cpqAgzGzkzQ:APA9...",
         "expirationTime": null,
         "keys":
         {
           "p256dh": "BNYDjQL9d5PSoeBurHy2e4d4GY0sGJXBN...",
           "auth": "0IyyvUGNJ9RxJc83poo3bA"
         }
       }
    
  3. Клиент отправляет запрос POST на URL-адрес /add-subscription , включая подписку в виде строкового JSON в теле.

  4. Сервер извлекает строковую subscription из тела запроса POST, анализирует ее обратно в JSON и добавляет в базу данных подписок.

    В базе данных хранятся подписки, используя в качестве ключа собственные конечные точки:

    {
      "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: { ... }
      },
    }

Теперь новая подписка доступна серверу для отправки уведомлений.

Выполнение

Запросы на новые подписки поступают по маршруту /add-subscription , который представляет собой URL-адрес POST. Вы увидите обработчик маршрута-заглушки в server.js :

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

В вашей реализации этот обработчик должен:

  • Получите новую подписку из тела запроса.
  • Доступ к базе активных подписок.
  • Добавьте новую подписку в список активных подписок.

Для обработки новых подписок:

  • В server.js измените обработчик маршрута для /add-subscription следующим образом:

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

Обработка отмены подписки

Фон

Сервер не всегда будет знать, когда подписка становится неактивной — например, подписка может быть удалена, когда браузер выключает Service Worker.

Однако сервер может узнать об отмененных подписках через пользовательский интерфейс приложения. На этом этапе вы реализуете функцию удаления подписки из базы данных.

Таким образом, сервер избегает отправки множества уведомлений на несуществующие конечные точки. Очевидно, что это не имеет особого значения для простого тестового приложения, но становится важным в более широком масштабе.

Выполнение

Запросы на отмену подписки приходят на POST URL /remove-subscription .

Обработчик маршрута-заглушки в server.js выглядит следующим образом:

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

В вашей реализации этот обработчик должен:

  • Получите конечную точку отмененной подписки из тела запроса.
  • Доступ к базе активных подписок.
  • Удалите отмененную подписку из списка активных подписок.

Тело POST-запроса от клиента содержит конечную точку, которую необходимо удалить:

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

Для обработки отмены подписки:

  • В server.js измените обработчик маршрута для /remove-subscription следующим образом:

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