--- title: Использование Push API slug: conflicting/Web/API/Push_API translation_of: Web/API/Push_API translation_of_original: Web/API/Push_API/Using_the_Push_API original_slug: Web/API/Push_API/Using_the_Push_API ---

W3C Push API предоставляет некоторый захватывающий новый функционал для разработчиков для использования в web-приложениях: эта статья предлагает вводную информацию о том, как настроить Push-уведомления и управлять ими, с помощью простого демо.

Возможность посылать сообщения или уведомления от сервера клиенту в любое время — независимо от того, активно приложение или нет — было прерогативой нативных приложений некоторое время, и наконец пришло в Web! Поддерживается большинства возможностей Push сейчас возможна в браузерах Firefox 43+ и Chrome 42+ на настольных компьютерах, мобильные платформы, возможно, скоро присоединятся. {{domxref("PushMessageData")}} на данный момент экспериментально поддерживаются только в Firefox Nightly (44+), и реализация может меняться.

Примечание: Ранние версии Firefox OS использовали проприетарную версию этого API вызывая Simple Push. Считается устаревшим по стандартам Push API.

Демо: основы простого сервера чат-приложения

Демо, которые мы создали, представляет начальное описание простого чат-приложения. Оно представляет собой форму, в которую вводятся данные, и кнопку для подписки на push-сообщения . Как только кнопка будет нажата, вы подпишитесь на push-сообщения, ваши данные будут записаны на сервере, а отправленное push-сообщение сообщит всем текущим подписчикам, что кто-то подписался.

На данном этапе, имя нового подписчика появится в списке подписчиков, вместе с текстовым полем и кнопкой рассылки, чтобы позволить подписчику отправить сообщение.

Чтобы запустить демо, следуйте инструкциям на странице push-api-demo README. Заметьте, что серверная компонента все еще нуждается в небольшой доработке для запуска в Chrome и в общем запускается более разумным путем. Но аспекты Push все еще могут быть полностью понятны; мы углубимся в это после того, как просмотрим технологии в процессе.

Обзор технологии

Эта секция предоставляет описание того, какие технологии участвуют в примере.

Web Push-сообщения это часть семейства технологий сервис воркеров; в первую очередь, для получения push-сообщений сервис воркер должен быть активирован на странице. Сервис воркер получает push-сообщения, и затем вы сами решаете, как уведомить об этом страницу. Вы можете:

Обычно необходима комбинация этих двух решений; демо внизу включает пример обоих.

Примечание: Вам необходим некоторый код, запущенный на сервере, для управления конечной точкой/шифрованием данных и отправки запросов push-сообщений. В нашем демо мы собрали на скорую руку сервер, используя NodeJS.

Сервис воркер так же должен подписаться на сервис push-сообщений. Каждой сессии предоставляется собственная уникальная конечная точка, когда она подписывается на сервис push-сообщений. Эта конечная точка получается из свойства  ({{domxref("PushSubscription.endpoint")}}) объекта подписчика. Она может быть отправлена серверу и использоваться для пересылки сообщений активному сервис воркеру сессии. Каждый браузер имеет свой собственный сервер push-сообщений для  управления отправкой push-сообщений.

Шифрование

Примечание: Для интерактивного краткого обзора, попробуйте JR Conlin's Web Push Data Encryption Test Page.

Для отправки данных с помощью push-сообщений необходимо шифрование. Для этого необходим публичный ключ, созданный с использованием метода  {{domxref("PushSubscription.getKey()")}}, который основывается на некотором комплексе механизмов шифрования, которые выполняются на стороне сервера; читайте Message Encryption for Web Push. Со временем появятся библиотеки для управления генерацией ключей и шифрованием/дешифрованием push-сообщений; для этого демо мы используем Marco Castelluccio's NodeJS web-push library.

Примечание: Есть так же другая библиотека для управления шифрованием с помощью Node и Python, смотри encrypted-content-encoding.

Обобщение рабочего процесса Push

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

  1. Запрос на разрешение web-уведомлений или что-то другое, что вы используете и для чего необходимо разрешение.
  2. Регистрация сервис воркера для контроля над страницей с помощью вызова {{domxref("ServiceWorkerContainer.register()")}}.
  3. Подписка на сервис push-уведомлений с помощью {{domxref("PushManager.subscribe()")}}.
  4. Запрашивание конечной точки, соответствующей подписчику, и генерация публичного ключа клиента ({{domxref("PushSubscription.endpoint")}} и {{domxref("PushSubscription.getKey()")}}. Заметьте, что getKey() на данный момент экспериментальная технологий и доступна только в Firefox.)
  5. Отправка данных на сервер, чтобы тот мог присылать push-сообщения, когда необходимо. Это демо использует {{domxref("XMLHttpRequest")}}, но вы можете использовать Fetch.
  6. Если вы используете Channel Messaging API для связи с сервис воркером, установите новый канал связи ({{domxref("MessageChannel.MessageChannel()")}}) и отправьте port2 сервис воркеру с помощью вызова {{domxref("Worker.postMessage()")}} для того, чтобы открыть канал связи. Вы так же должны настроить слушателя для ответов на сообщения, которые будут отправляться обратно с сервис воркера.
  7. На стороне сервера сохраните конечную точку и все остальные необходимые данные, чтобы они были доступны, когда будет необходимо отправить push-сообщение добавленному подписчику (мы используем простой текстовый файл, но вы можете использовать базу данных или все что угодно на ваш вкус). В приложении на продакшене убедитесь, что скрываете эти данные, так что злоумышленники не смогут украсть конечную точку и разослать спам подписчикам в push-сообщениях.
  8. Для отправки push-сообщений необходимо отослать HTTP POST конечному URL. Запрос должен включать TTL заголовок, который ограничивает время пребывания сообщения в очереди, если пользователь не в сети. Для добавления полезной информации в запросе, необходимо зашифровать ее (что включает публичный ключ клиента). В нашем примере мы используем web-push модуль, который управляет всей тяжелой работой.
  9. Поверх в сервис воркере настройте обработчик событий push для ответов на полученные push-сообщения.
    1. Если вы хотите отвечать отправкой сообщения канала обратно основному контексту (смотри шаг 6), необходимо сначала получить ссылку на port2, который был отправлен контексту сервис воркера ({{domxref("MessagePort")}}). Это доступно в объекте  {{domxref("MessageEvent")}}, передаваемого обработчику onmessage ({{domxref("ServiceWorkerGlobalScope.onmessage")}}). Конкретнее, он находится в свойстве ports, индекс 0. Когда это сделано, вы можете отправить сообщение обратно port1, используя {{domxref("MessagePort.postMessage()")}}.
    2. Если вы хотите ответить запуском системного уведомления, вы можете сделать это, вызвав {{domxref("ServiceWorkerRegistration.showNotification()")}}. Заметьте, что в нашем коде мы вызываем его внутри метода {{domxref("ExtendableEvent.waitUntil()")}} — это растягивает время жизни события, пока уведомление не будет запущено, так что мы можем убедиться, что все, что мы хотели, чтобы произошло, действительно произошло.

Сборка демо

Давайте пройдемся по коду для демо, чтобы понять, как все работает.

HTML и CSS

Нет ничего примечательного в HTML и CSS демо; HTML содержит простую форму для ввода данных для входа в чат, кнопку для подписки на push-уведомления и двух списков, в которых перечислены подписчики и сообщения чата. После подписки появляются дополнительные средства для того, чтобы пользователь мог ввести сообщение в чат.

CSS был оставлен очень минимальным, чтобы не отвлекать от объяснения того, как функционируют Push API.

Основной файл JavaScript

 JavaScript очевидно намного более существенный. Давайте взглянем на основной файл JavaScript.

Переменные и начальные настройки

Для начала определим некоторые переменные, которые будем использовать в нашем приложении:

var isPushEnabled = false;
var useNotifications = false;

var subBtn = document.querySelector('.subscribe');
var sendBtn;
var sendInput;

var controlsBlock = document.querySelector('.controls');
var subscribersList = document.querySelector('.subscribers ul');
var messagesList = document.querySelector('.messages ul');

var nameForm = document.querySelector('#form');
var nameInput = document.querySelector('#name-input');
nameForm.onsubmit = function(e) {
  e.preventDefault()
};
nameInput.value = 'Bob';

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

Далее, мы перехватываем ссылку на {{htmlelement("button")}} подписки/отписки и задаем переменные для сохранения ссылок на наши кнопку отправки сообщения/ввода (который создастся только после успешной подписки).

Следующие переменные перехватывают ссылки на три основные {{htmlelement("div")}} элемента, так что мы можем включить в них элементы (к примеру, когда появится кнопка Отправки Сообщения Чата или сообщение появится с писке Сообщений).

Finally we grab references to our name selection form and {{htmlelement("input")}} element, give the input a default value, and use preventDefault() to stop the form submitting when the form is submitted by pressing return.

Next, we request permission to send web notifications, using {{domxref("Notification.requestPermission","requestPermission()")}}:

Notification.requestPermission();

Now we run a section of code when onload is fired, to start up the process of inialising the app when it is first loaded. First of all we add a click event listener to the subscribe/unsubscribe button that runs our unsubscribe() function if we are already subscribed (isPushEnabled is true), and subscribe() otherwise:

window.addEventListener('load', function() {
  subBtn.addEventListener('click', function() {
    if (isPushEnabled) {
      unsubscribe();
    } else {
      subscribe();
    }
  });

Next we check to see if service workers are supported. If so, we register a service worker using {{domxref("ServiceWorkerContainer.register()")}}, and run our initialiseState() function. If not, we deliver an error message to the console.

  // Check that service workers are supported, if so, progressively
  // enhance and add push messaging support, otherwise continue without it.
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('sw.js').then(function(reg) {
      if(reg.installing) {
        console.log('Service worker installing');
      } else if(reg.waiting) {
        console.log('Service worker installed');
      } else if(reg.active) {
        console.log('Service worker active');
      }

      initialiseState(reg);
    });
  } else {
    console.log('Service workers aren\'t supported in this browser.');
  }
});

The next thing in the source code is the initialiseState() function — for the full commented code, look at the initialiseState() source on Github (we are not repeating it here for brevity's sake.)

initialiseState() first checks whether notifications are supported on service workers, then sets the useNotifications variable to true if so. Next, it checks whether said notifications are permitted by the user, and if push messages are supported, and reacts accordingly to each.

Finally, it uses {{domxref("ServiceWorkerContainer.ready()")}} to wait until the service worker is active and ready to start doing things. Once its promise resolves, we retrieve our subscription to push messaging using the {{domxref("ServiceWorkerRegistration.pushManager")}} property, which returns a {{domxref("PushManager")}} object that we then call {{domxref("PushManager.getSubscription()")}} on. Once this second inner promise resolves, we enable the subscribe/unsubscribe button (subBtn.disabled = false;), and check that we have a subscription object to work with.

If we do, then we are already subscribed. This is possible when the app is not open in the browser; the service worker can still be active in the background. If we're subscribed, we update the UI to show that we are subscribed by updating the button label, then we set isPushEnabled to true, grab the subscription endpoint from {{domxref("PushSubscription.endpoint")}}, generate a public key using {{domxref("PushSubscription.getKey()")}}, and run our updateStatus() function, which as you'll see later communicates with the server.

As an added bonus, we set up a new {{domxref("MessageChannel")}} using the {{domxref("MessageChannel.MessageChannel()")}} constructor, grab a reference to the active service worker using {{domxref("ServiceworkerRegistration.active")}}, then set up a channel betweeen the main browser context and the service worker context using {{domxref("Worker.postMessage()")}}. The browser context receives messages on {{domxref("MessageChannel.port1")}}; whenever that happens, we run the handleChannelMessage() function to decide what to do with that data (see the {{anch("Handling channel messages sent from the service worker")}} section).

Subscribing and unsubscribing

Let's now turn our attention to the subscribe() and unsubscribe() functions used to subscribe/unsubscribe to the push notification service.

In the case of subscription, we again check that our service worker is active and ready by calling {{domxref("ServiceWorkerContainer.ready()")}}. When the promise resolves, we subscribe to the service using {{domxref("PushManager.subscribe()")}}. If the subscription is successful, we get a {{domxref("PushSubscription")}} object, extract the subscription endpoint from this and generate a public key (again, {{domxref("PushSubscription.endpoint")}} and {{domxref("PushSubscription.getKey()")}}), and pass them to our updateStatus() function along with the update type (subscribe) to send the necessary details to the server.

We also make the necessary updates to the app state (set isPushEnabled to true) and UI (enable the subscribe/unsubscribe button and set its label text to show that the next time it is pressed it will unsubscribe.)

The unsubscribe() function is pretty similar in structure, but it basically does the opposite; the most notable difference is that it gets the current subscription using {{domxref("PushManager.getSubscription()")}}, and when that promise resolves it unsubscribes using {{domxref("PushSubscription.unsubscribe()")}}.

Appropriate error handling is also provided in both functions.  

We only show the subscribe() code below, for brevity; see the full subscribe/unsubscribe code on Github.

function subscribe() {
  // Disable the button so it can't be changed while
  // we process the permission request

  subBtn.disabled = true;

  navigator.serviceWorker.ready.then(function(reg) {
    reg.pushManager.subscribe({userVisibleOnly: true})
      .then(function(subscription) {
        // The subscription was successful
        isPushEnabled = true;
        subBtn.textContent = 'Unsubscribe from Push Messaging';
        subBtn.disabled = false;

        // Update status to subscribe current user on server, and to let
        // other users know this user has subscribed
        var endpoint = subscription.endpoint;
        var key = subscription.getKey('p256dh');
        updateStatus(endpoint,key,'subscribe');
      })
      .catch(function(e) {
        if (Notification.permission === 'denied') {
          // The user denied the notification permission which
          // means we failed to subscribe and the user will need
          // to manually change the notification permission to
          // subscribe to push messages
          console.log('Permission for Notifications was denied');

        } else {
          // A problem occurred with the subscription, this can
          // often be down to an issue or lack of the gcm_sender_id
          // and / or gcm_user_visible_only
          console.log('Unable to subscribe to push.', e);
          subBtn.disabled = false;
          subBtn.textContent = 'Subscribe to Push Messaging';
        }
      });
  });
}

Updating the status in the app and server

The next function in our main JavaScript is updateStatus(), which updates the UI for sending chat messages when subscribing/unsubscribing and sends a request to update this information on the server.

The function does one of three different things, depending on the value of the statusType parameter passed into it:

Again, we have not included the entire function listing for brevity. Examine the full updateStatus() code on Github.

Handling channel messages sent from the service worker

As mentioned earlier, when a channel message is received from the service worker, our handleChannelMessage() function is called to handle it. This is done by our handler for the {{event("message")}} event, {{domxref("channel.port1.onmessage")}}:

channel.port1.onmessage = function(e) {
  handleChannelMessage(e.data);
}

This occurs when the service worker sends a channel message over.

The handleChannelMessage() function looks like this:

function handleChannelMessage(data) {
  if(data.action === 'subscribe' || data.action === 'init') {
    var listItem = document.createElement('li');
    listItem.textContent = data.name;
    subscribersList.appendChild(listItem);
  } else if(data.action === 'unsubscribe') {
    for(i = 0; i < subscribersList.children.length; i++) {
      if(subscribersList.children[i].textContent === data.name) {
        subscribersList.children[i].parentNode.removeChild(subscribersList.children[i]);
      }
    }
    nameInput.disabled = false;
  } else if(data.action === 'chatMsg') {
    var listItem = document.createElement('li');
    listItem.textContent = data.name + ": " + data.msg;
    messagesList.appendChild(listItem);
    sendInput.value = '';
  }
}

What happens here depends on what the action property on the data object is set to:

Note: We have to pass the data back to the main context before we do DOM updates because service workers don't have access to the DOM. You should be aware of the limitations of service workers before attemping to ue them. Read Using Service Workers for more details.

Sending chat messages

When the Send Chat Message button is clicked, the content of the associated text field is sent as a chat message. This is handled by the sendChatMessage() function (again, not shown in full for brevity). This works in a similar way to the different parts of the updateStatus() function (see {{anch("Updating the status in the app and server")}}) — we retrieve an endpoint and public key via a {{domxref("PushSubscription")}} object, which is itself retrieved via {{domxref("ServiceWorkerContainer.ready()")}} and {{domxref("PushManager.subscribe()")}}. These are sent to the server via {{domxref("XMLHttpRequest")}} in a message object, along with the name of the subscribed user, the chat message to send, and a statusType of chatMsg.

The server

As mentioned above, we need a server-side component in our app, to handle storing subscription details, and send out push messages when updates occur. We've hacked together a quick-and-dirty server using NodeJS (server.js), which handles the XHR requests sent by our client-side JavaScript code.

It uses a text file (endpoint.txt) to store subscription details; this file starts out empty. There are four different types of request, marked by the statusType property of the object sent over in the request; these are the same as those understood client-side, and perform the required server actions for that same situation. Here's what each means in the context of the server:

A couple more things to note:

The service worker

Now let's have a look at the service worker code (sw.js), which responds to the push messages, represented by {{Event("push")}} events. These are handled on the service worker's scope by the ({{domxref("ServiceWorkerGlobalScope.onpush")}}) event handler; its job is to work out what to do in response to each received message. We first convert the received message back into an object by calling {{domxref("PushMessageData.json()")}}. Next, we check what type of push message it is, by looking at the object's action property:

self.addEventListener('push', function(event) {
  var obj = event.data.json();

  if(obj.action === 'subscribe' || obj.action === 'unsubscribe') {
    fireNotification(obj, event);
    port.postMessage(obj);
  } else if(obj.action === 'init' || obj.action === 'chatMsg') {
    port.postMessage(obj);
  }
});

Next, let's look at the fireNotification() function (which is blissfully pretty simple).

function fireNotification(obj, event) {
  var title = 'Subscription change';
  var body = obj.name + ' has ' + obj.action + 'd.';
  var icon = 'push-icon.png';
  var tag = 'push';

  event.waitUntil(self.registration.showNotification(title, {
    body: body,
    icon: icon,
    tag: tag
  }));
}

Here we assemble the assets needed by the notification box: the title, body, and icon. Then we send a notification via the {{domxref("ServiceWorkerRegistration.showNotification()")}} method, providing that information as well as the tag "push", which we can use to identify this notification among any other notifications we might be using. When the notification is successfully sent, it manifests as a system notification dialog on the users computers/devices in whatever style system notifications look like on those systems (the following image shows a Mac OSX system notification.)

Note that we do this from inside an {{domxref("ExtendableEvent.waitUntil()")}} method; this is to make sure the service worker remains active until the notification has been sent. waitUntil() will extend the life cycle of the service worker until everything inside this method has completed.

Note: Web notifications from service workers were introduced around Firefox version 42, but are likely to be removed again while the surrounding functionality (such as Clients.openWindow()) is properly implemented (see {{bug(1203324)}} for more details.)

Handling premature subscription expiration

Sometimes push subscriptions expire prematurely, without {{domxref("PushSubscription.unsubscribe()")}} being called. This can happen when the server gets overloaded, or if you are offline for a long time, for example.  This is highly server-dependent, so the exact behavior is difficult to predict. In any case, you can handle this problem by watching for the {{Event("pushsubscriptionchange")}} event, which you can listen for by providing a {{domxref("ServiceWorkerGlobalScope.onpushsubscriptionchange")}} event handler; this event is fired only in this specific case.

self.addEventListener('pushsubscriptionchange', function() {
  // do something, usually resubscribe to push and
  // send the new subscription details back to the
  // server via XHR or Fetch
});

Note that we don't cover this case in our demo, as a subscription ending is not a big deal for a simple chat server. But for a more complex example you'd probably want to resubscribe the user.

Extra steps for Chrome support

To get the app working on Chrome, we need a few extra steps, as Chrome currently relies on Google's Cloud Messaging service to work.

Setting up Google Cloud Messaging

To get this set up, follow these steps:

  1. Navigate to the Google Developers Console  and set up a new project.
  2. Go to your project's homepage (ours is at https://console.developers.google.com/project/push-project-978, for example), then
    1. Select the Enable Google APIs for use in your apps option.
    2. In the next screen, click Cloud Messaging for Android under the Mobile APIs section.
    3. Click the Enable API button.
  3. Now you need to make a note of your project number and API key because you'll need them later. To find them:
    1. Project number: click Home on the left; the project number is clearly marked at the top of your project's home page.
    2. API key: click Credentials on the left hand menu; the API key can be found on that screen.

manifest.json

You need to include a Google app-style manifest.json file in your app, which references the project number you made a note of earlier in the gcm_sender_id parameter. Here is our simple example manifest.json:

{
  "name": "Push Demo",
  "short_name": "Push Demo",
  "icons": [{
        "src": "push-icon.png",
        "sizes": "111x111",
        "type": "image/png"
      }],
  "start_url": "/index.html",
  "display": "standalone",
  "gcm_sender_id": "224273183921"
}

You also need to reference your manifest using a {{HTMLElement("link")}} element in your HTML:

<link rel="manifest" href="manifest.json">

userVisibleOnly

Chrome requires you to set the userVisibleOnly parameter to true when subscribing to the push service, which indicates that we are promising to show a notification whenever a push is received. This can be seen in action in our subscribe() function.

See also

Note: Some of the client-side code in our Push demo is heavily influenced by Matt Gaunt's excellent examples in Push Notifications on the Open Web. Thanks for the awesome work, Matt!