Как отправить веб-push-уведомления из приложений Django

_Автор выбрал Open Internet / Free Speech Fund для получения пожертвования в рамках https://do.co/w4do-cta [Писать для DOnations] программа.

Вступление

Сеть постоянно развивается, и теперь она может выполнять функции, которые ранее были доступны только на родных мобильных устройствах. Введение JavaScript service worker предоставило веб-пользователям новые возможности для таких задач, как фоновая синхронизация, автономное кэширование и отправка https: // developer. .mozilla.org / ru / docs / Web / API / Push_API [push-уведомления].

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

В этом руководстве вы настроите приложение Django в Ubuntu 18.04, которое отправляет push-уведомления всякий раз, когда есть действие, которое требует от пользователя посетить приложение. Для создания этих уведомлений вы будете использовать пакет Django-Webpush, а также настроить и зарегистрировать работника службы для отображения уведомлений клиенту. Рабочее приложение с уведомлениями будет выглядеть так:

изображение: https: //assets.digitalocean.com/articles/django_push_18_04/web_push_final.png [финал веб-публикации]

Предпосылки

Прежде чем начать это руководство, вам понадобится следующее:

Шаг 1 - Установка Django-Webpush и получение Vapid ключей

Django-Webpush - это пакет, который позволяет разработчикам интегрировать и отправлять веб-push-уведомления в приложения Django. Мы будем использовать этот пакет для запуска и отправки push-уведомлений из нашего приложения. На этом этапе вы установите Django-Webpush и получите ключи _VoluID Application Server Identification (VAPID) _, необходимые для идентификации вашего сервера и обеспечения уникальности каждого запроса.

Убедитесь, что вы находитесь в каталоге проекта + ~ / +, который вы создали в предварительных условиях:

cd ~/

Активируйте вашу виртуальную среду:

source /bin/activate

Обновите вашу версию + pip +, чтобы обеспечить ее актуальность:

pip install --upgrade pip

Установите Django-Webpush:

pip install django-webpush

После установки пакета добавьте его в список приложений в вашем файле + settings.py +. Сначала откройте + settings.py +:

nano ~/djangopush/djangopush/settings.py

Добавьте + webpush + в список + INSTALLED_APPS +:

~ / Djangopush / djangopush / settings.py

...

INSTALLED_APPS = [
   ...

]
...

Сохраните файл и выйдите из редактора.

Запустите migrations в приложении, чтобы применить внесенные изменения в схему базы данных:

python manage.py migrate

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

OutputOperations to perform:
 Apply all migrations: admin, auth, contenttypes, sessions, webpush
Running migrations:
 Applying webpush.0001_initial... OK

Следующим шагом в настройке push-уведомлений является получение VAPID-ключей. Эти ключи идентифицируют сервер приложений и могут использоваться для уменьшения секретности URL-адресов принудительной подписки, поскольку они ограничивают подписки для конкретного сервера.

Чтобы получить ключи VAPID, перейдите в веб-приложение wep-push-codelab. Здесь вам дадут автоматически сгенерированные ключи. Скопируйте закрытый и открытый ключи.

Затем создайте новую запись в + settings.py + для вашей информации VAPID. Сначала откройте файл:

nano ~/djangopush/djangopush/settings.py

Затем добавьте новую директиву + WEBPUSH_SETTINGS + с вашими открытыми и закрытыми ключами VAPID и адресом электронной почты ниже + AUTH_PASSWORD_VALIDATORS +:

~ / Djangopush / djangopush / settings.py

...

AUTH_PASSWORD_VALIDATORS = [
   ...
]







# Internationalization
# https://docs.djangoproject.com/en/2.0/topics/i18n/

...

Не забудьте заменить значения заполнителей `, ` и ++ своей собственной информацией. Ваш электронный адрес будет таким, каким вы будете уведомлены, если у сервера push возникнут какие-либо проблемы.

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

Шаг 2 - Настройка просмотров

На этом шаге мы настроим простой + home + view с помощью https://docs.djangoproject.com/en. /2.1/ref/request-response/#django.http.HttpRequest [+ HttpResponse + объект ответа] для нашей домашней страницы вместе с представлением + send_push +. Представления - это функции, которые возвращают объекты ответа из веб-запросов. Представление + send_push + будет использовать библиотеку Django-Webpush для отправки push-уведомлений, которые содержат данные, введенные пользователем на домашней странице.

Перейдите в папку + ~ / djangopush / djangopush +:

cd ~/djangopush/djangopush

Запуск + ls + внутри папки покажет вам основные файлы проекта:

Output/__init__.py
/settings.py
/urls.py
/wsgi.py

Файлы в этой папке автоматически генерируются утилитой + django-admin +, которую вы использовали для создания своего проекта в предварительных условиях. Файл + settings.py + содержит конфигурации всего проекта, такие как установленные приложения и статическая корневая папка. Файл + urls.py + содержит конфигурации URL для проекта. Здесь вы можете настроить маршруты в соответствии с вашими созданными видами.

Создайте новый файл в каталоге + ~ / djangopush / djangopush + с именем + views.py +, который будет содержать представления для вашего проекта:

nano ~/djangopush/djangopush/views.py

Первый вид, который мы создадим, - это вид + home +, который отобразит домашнюю страницу, на которую пользователи могут отправлять push-уведомления. Добавьте следующий код в файл:

~ / Djangopush / djangopush / views.py

from django.http.response import HttpResponse
from django.views.decorators.http import require_GET

@require_GET
def home(request):
   return HttpResponse('<h1>Home Page<h1>')

Представление + home + оформляется декоратором + require_GET +, который ограничивает представление только запросами GET. Представление обычно возвращает ответ на каждый сделанный ему запрос. Это представление возвращает простой тег HTML в качестве ответа.

Следующее представление, которое мы создадим, это + send_push +, которое будет обрабатывать отправленные push-уведомления, используя пакет + django-webpush +. Он будет ограничен только запросами POST и будет освобожден от защиты Cross Site Request Forgery (CSRF). Это позволит вам проверить представление, используя Postman или любую другую службу RESTful. В производстве, однако, вы должны удалить этот декоратор, чтобы не оставлять ваши представления уязвимыми для CSRF.

Чтобы создать представление + send_push +, сначала добавьте следующий импорт, чтобы включить ответы JSON, и получите доступ к функции + send_user_notification + в библиотеке + webpush +:

~ / Djangopush / djangopush / views.py

from django.http.response import , HttpResponse
from django.views.decorators.http import require_GET,

Затем добавьте декоратор + require_POST +, который будет использовать тело запроса, отправленное пользователем, для создания и запуска push-уведомления:

~ / Djangopush / djangopush / views.py

@require_GET
def home(request):
   ...


@require_POST
@csrf_exempt
def send_push(request):
   try:
       body = request.body
       data = json.loads(body)

       if 'head' not in data or 'body' not in data or 'id' not in data:
           return JsonResponse(status=400, data={"message": "Invalid data format"})

       user_id = data['id']
       user = get_object_or_404(User, pk=user_id)
       payload = {'head': data['head'], 'body': data['body']}
       send_user_notification(user=user, payload=payload, ttl=1000)

       return JsonResponse(status=200, data={"message": "Web push successful"})
   except TypeError:
       return JsonResponse(status=500, data={"message": "An error occurred"})

Мы используем два декоратора для представления + send_push +: декоратор + require_POST +, который ограничивает представление только запросами POST, и декоратор + csrf_exempt +, который освобождает представление от защиты CSRF.

Это представление ожидает данные POST и выполняет следующие действия: получает «+ body » запроса и, используя пакет https://docs.python.org/3/library/json.html[json], десериализует документ JSON. к объекту Python, используя https://docs.python.org/3.6/library/json.html [` json.loads `]. ` json.loads +` берет структурированный документ JSON и преобразует его в объект Python.

Представление ожидает, что объект тела запроса будет иметь три свойства:

  • + head +: заголовок push-уведомления.

  • + body +: тело уведомления.

  • + id +: + id + пользователя запроса.

Если какое-либо из обязательных свойств отсутствует, представление вернет + JSONResponse + со статусом 404 «Не найдено». Если пользователь с указанным первичным ключом существует, представление вернет + user + с соответствующим первичным ключом, используя https://docs.djangoproject.com/en/2.1/topics/http/shortcuts/#get-object -или-404 [+ get_object_or_404 + function] из библиотеки + django.shortcuts +. Если пользователь не существует, функция вернет ошибку 404.

Представление также использует функцию + send_user_notification + из библиотеки + webpush +. Эта функция принимает три параметра:

  • + Пользователь +: получатель push-уведомления.

  • + payload +: информация уведомления, которая включает в себя уведомление + head + и + body +.

  • + ttl +: максимальное время в секундах, в течение которого уведомление должно храниться, если пользователь находится в автономном режиме.

Если ошибок не возникает, представление возвращает «+ JSONResponse » со статусом «200» и объектом данных. Если происходит ` KeyError `, представление вернет статус 500 «Внутренняя ошибка сервера». ` KeyError +` возникает, когда запрошенный ключ объекта не существует.

На следующем шаге мы создадим соответствующие URL-маршруты, соответствующие представлениям, которые мы создали.

Шаг 3 - Сопоставление URL-адресов с представлениями

Django позволяет создавать URLs, которые подключаются к представлениям с помощью модуля Python, называемого + URLconf +. Этот модуль сопоставляет выражения пути URL с функциями Python (ваши представления). Обычно файл конфигурации URL генерируется автоматически при создании проекта. На этом этапе вы обновите этот файл, добавив в него новые маршруты для представлений, созданных на предыдущем шаге, а также URL-адреса для приложения + django-webpush +, которое предоставит конечные точки для подписки пользователей на push-уведомления.

Для получения дополнительной информации о представлениях см. Https://www.digitalocean.com/community/tutorials/how-to-create-django-views[How To Create Django Views].

Откройте + urls.py +:

nano ~/djangopush/djangopush/urls.py

Файл будет выглядеть так:

~ / Djangopush / djangopush / urls.py

"""untitled URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
   https://docs.djangoproject.com/en/2.1/topics/http/urls/
Examples:
Function views
   1. Add an import:  from my_app import views
   2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
   1. Add an import:  from other_app.views import Home
   2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
   1. Import the include() function: from django.urls import include, path
   2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path

urlpatterns = [
   path('admin/', admin.site.urls),
]

Следующим шагом является сопоставление созданных представлений с URL-адресами Во-первых, добавьте импорт + include +, чтобы гарантировать, что все маршруты для библиотеки Django-Webpush будут добавлены в ваш проект:

~ / Djangopush / djangopush / urls.py

"""webpushdjango URL Configuration
...
"""
from django.contrib import admin
from django.urls import path,

Затем импортируйте представления, созданные вами на последнем шаге, и обновите список + urlpatterns +, чтобы отобразить ваши представления:

~ / Djangopush / djangopush / urls.py

"""webpushdjango URL Configuration
...
"""
from django.contrib import admin
from django.urls import path,



urlpatterns = [
                 path('admin/', admin.site.urls),



             ]

Здесь список + urlpatterns + регистрирует URL-адреса пакета + django-webpush + и отображает ваши взгляды на URL-адреса + / send_push + и + / home +.

Давайте проверим представление «+ / home +», чтобы убедиться, что оно работает так, как задумано. Убедитесь, что вы находитесь в корневом каталоге проекта:

cd ~/djangopush

Запустите свой сервер, выполнив следующую команду:

python manage.py runserver :8000

Перейдите к + http: //: 8000 +. Вы должны увидеть следующую домашнюю страницу:

изображение: https: //assets.digitalocean.com/articles/django_push_18_04/django_push_home.png [Начальный вид домашней страницы]

На этом этапе вы можете уничтожить сервер с помощью + CTRL + C +, и мы перейдем к созданию шаблонов и их отображению в наших представлениях с использованием функции + render +.

Шаг 4 - Создание шаблонов

Шаблонный движок Django позволяет вам определять пользовательские слои вашего приложения с помощью шаблонов, которые похожи на HTML-файлы. На этом шаге вы создадите и отобразите шаблон для представления + home +.

Создайте папку с именем + templates + в корневом каталоге вашего проекта:

mkdir ~/djangopush/templates

Если вы запустите + ls + в корневой папке вашего проекта на этом этапе, результат будет выглядеть следующим образом:

Output/djangopush
/templates
db.sqlite3
manage.py
/my_env

Создайте файл с именем + home.html в папке` + templates`:

nano ~/djangopush/templates/home.html

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

{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <meta http-equiv="X-UA-Compatible" content="ie=edge">
   <meta name="vapid-key" content="{{ vapid_key }}">
   {% if user.id %}
       <meta name="user_id" content="{{ user.id }}">
   {% endif %}
   <title>Web Push</title>
   <link href="https://fonts.googleapis.com/css?family=PT+Sans:400,700" rel="stylesheet">
</head>

<body>
<div>
   <form id="send-push__form">
       <h3 class="header">Send a push notification</h3>
       <p class="error"></p>
       <input type="text" name="head" placeholder="Header: Your favorite airline 😍">
       <textarea name="body" id="" cols="30" rows="10" placeholder="Body: Your flight has been cancelled 😱😱😱"></textarea>
       <button>Send Me</button>
   </form>
</div>
</body>
</html>

+ Body + файла включает в себя форму с двумя полями: элемент + input будет содержать заголовок / заголовок уведомления, а элемент` + textarea` будет содержать тело уведомления.

В разделе + head файла есть два тега` + meta`, которые будут содержать открытый ключ VAPID и идентификатор пользователя. Эти две переменные необходимы для регистрации пользователя и отправки им push-уведомлений. Идентификатор пользователя здесь необходим, потому что вы будете отправлять запросы AJAX на сервер, а + id + будет использоваться для идентификации пользователя. Если текущий пользователь является зарегистрированным пользователем, то шаблон создаст тег + meta со своим` + id` в качестве содержимого.

Следующим шагом будет сообщить Django, где найти ваши шаблоны. Для этого вы отредактируете + settings.py + и обновите список + TEMPLATES +.

Откройте файл + settings.py +:

nano ~/djangopush/djangopush/settings.py

Добавьте следующее в список + DIRS +, чтобы указать путь к каталогу шаблонов:

~ / Djangopush / djangopush / settings.py

...
TEMPLATES = [
   {
       'BACKEND': 'django.template.backends.django.DjangoTemplates',
       'DIRS': [],
       'APP_DIRS': True,
       'OPTIONS': {
           'context_processors': [
               ...
           ],
       },
   },
]
...

Затем в файле + views.py обновите представление` + home + , чтобы отобразить шаблон + home.html`. Откройте файл:

nano ~/djangpush/djangopush/views.py

Сначала добавьте несколько дополнительных импортов, в том числе конфигурацию + settings +, которая содержит все настройки проекта из файла + settings.py +, и функцию + render + из + django.shortcuts +:

~ / Djangopush / djangopush / views.py

...
from django.shortcuts import , get_object_or_404
...
import json


...

Затем удалите исходный код, который вы добавили в представление + home +, и добавьте следующее, которое указывает, как будет отображаться только что созданный шаблон:

~ / Djangopush / djangopush / views.py

...

@require_GET
def home(request):

Код назначает следующие переменные:

  • + webpush_settings +: ему присваивается значение атрибута + WEBPUSH_SETTINGS + из конфигурации + settings +.

  • + vapid_key +: получает значение + VAPID_PUBLIC_KEY + из объекта + webpush_settings + для отправки клиенту. Этот открытый ключ сверяется с закрытым ключом, чтобы гарантировать, что клиент с открытым ключом может получать push-сообщения от сервера.

  • + user +: эта переменная приходит из входящего запроса. Всякий раз, когда пользователь отправляет запрос на сервер, сведения об этом пользователе хранятся в поле «+ пользователь +».

Https://docs.djangoproject.com/en/2.1/topics/http/shortcuts/#render [+ render + function] вернет файл HTML и https://docs.djangoproject.com/en/2.1 /ref/templates/api/#django.template.Context[context object], содержащий текущего пользователя и vapid открытый ключ сервера. Здесь требуется три параметра: + request +, + template + для визуализации и объект, который содержит переменные, которые будут использоваться в шаблоне.

Создав наш шаблон и обновив представление + home +, мы можем перейти к настройке Django для обслуживания наших статических файлов.

Шаг 5 - Обслуживание статических файлов

Веб-приложения включают в себя CSS, JavaScript и другие файлы изображений, которые Django называет «статическими файлами». Django позволяет вам собирать все статические файлы из каждого приложения в вашем проекте в одном месте, откуда они обслуживаются. Это решение называется + django.contrib.staticfiles +. На этом этапе мы обновим наши настройки, чтобы сообщить Django, где будут храниться наши статические файлы.

Откройте + settings.py +:

nano ~/djangopush/djangopush/settings.py

В + settings.py +, сначала убедитесь, что + STATIC_URL + был определен:

~ / Djangopush / djangopush / settings.py

...
STATIC_URL = '/static/'

Затем добавьте список каталогов с именем + STATICFILES_DIRS +, где Django будет искать статические файлы:

~ / Djangopush / djangopush / settings.py

...
STATIC_URL = '/static/'

Теперь вы можете добавить + STATIC_URL + в список путей, определенных в вашем файле + urls.py +.

Откройте файл:

nano ~/djangopush/djangopush/urls.py

Добавьте следующий код, который импортирует конфигурацию URL + static + и обновит список + urlpatterns +. Вспомогательная функция здесь использует свойства + STATIC_URL + и + STATIC_ROOT +, которые мы указали в файле + settings.py +, для обслуживания статических файлов проекта:

~ / Djangopush / djangopush / urls.py

...



urlpatterns = [
   ...
]

С нашими настройками статических файлов мы можем перейти к стилизации домашней страницы приложения.

Шаг 6 - стилизация домашней страницы

После настройки приложения для обслуживания статических файлов вы можете создать внешнюю таблицу стилей и связать ее с файлом + home.html + для стилизации домашней страницы. Все ваши статические файлы будут храниться в каталоге + static + в корневой папке вашего проекта.

Создайте папку + static + и папку + css + в папке + static +:

mkdir -p ~/djangopush/static/css

Откройте файл css с именем + styles.css + внутри папки + css +:

nano ~/djangopush/static/css/styles.css

Добавьте следующие стили для домашней страницы:

~ / Djangopush / статический / CSS / styles.css

body {
   height: 100%;
   background: rgba(0, 0, 0, 0.87);
   font-family: 'PT Sans', sans-serif;
}

div {
   height: 100%;
   display: flex;
   align-items: center;
   justify-content: center;
}

form {
   display: flex;
   flex-direction: column;
   align-items: center;
   justify-content: center;
   width: 35%;
   margin: 10% auto;
}

form > h3 {
   font-size: 17px;
   font-weight: bold;
   margin: 15px 0;
   color: orangered;
   text-transform: uppercase;
}

form > .error {
   margin: 0;
   font-size: 15px;
   font-weight: normal;
   color: orange;
   opacity: 0.7;
}

form > input, form > textarea {
   border: 3px solid orangered;
   box-shadow: unset;
   padding: 13px 12px;
   margin: 12px auto;
   width: 80%;
   font-size: 13px;
   font-weight: 500;
}

form > input:focus, form > textarea:focus {
   border: 3px solid orangered;
   box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.2);
   outline: unset;
}

form > button {
   justify-self: center;
   padding: 12px 25px;
   border-radius: 0;
   text-transform: uppercase;
   font-weight: 600;
   background: orangered;
   color: white;
   border: none;
   font-size: 14px;
   letter-spacing: -0.1px;
   cursor: pointer;
}

form > button:disabled {
   background: dimgrey;
   cursor: not-allowed;
}

Создав таблицу стилей, вы можете связать ее с файлом + home.html + с помощью static tags тегов , Откройте файл + home.html +:

nano ~/djangopush/templates/home.html

Обновите раздел + head +, добавив ссылку на внешнюю таблицу стилей:

~ / Djangopush / шаблоны / home.html

{% load static %}
<!DOCTYPE html>
<html lang="en">

<head>
   ...

</head>
<body>
   ...
</body>
</html>

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

cd ~/djangopush
python manage.py runserver :8000

Когда вы посещаете + http: //: 8000 +, это должно выглядеть так:

image: https: //assets.digitalocean.com/articles/django_push_18_04/push_styled_home.png [просмотр домашней страницы] + Опять же, вы можете убить сервер с помощью + CTRL + C +.

Теперь, когда вы успешно создали страницу + home.html и стилизовали ее, вы можете подписать пользователей на push-уведомления, когда они посещают домашнюю страницу.

Шаг 7 - Регистрация работника сервиса и подписка пользователей на push-уведомления

Push-уведомления через Интернет могут уведомлять пользователей об обновлениях приложений, на которые они подписаны, или предлагать им повторно подключиться к приложениям, которые они использовали в прошлом. Они используют две технологии: push API и https://developer.mozilla.org/en-US/docs/. Web / API / Notifications_API [уведомления] API. Обе технологии основаны на присутствии сервисного работника.

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

Мы подпишем наших пользователей на push, а затем отправим информацию из подписки на сервер, чтобы зарегистрировать их.

В каталоге + static + создайте папку с именем + js +:

mkdir ~/djangopush/static/js

Создайте файл с именем + registerSw.js +:

nano ~/djangopush/static/js/registerSw.js

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

~ / djangopush / статический / JS / registerSw.js

const registerSw = async () => {
   if ('serviceWorker' in navigator) {
       const reg = await navigator.serviceWorker.register('sw.js');
       initialiseState(reg)

   } else {
       showNotAllowed("You can't send push notifications ☹️😢")
   }
};

Во-первых, перед регистрацией их функция + registerSw + проверяет, поддерживает ли браузер обслуживающий персонал. После регистрации он вызывает функцию + initializeState + с регистрационными данными. Если сервисные работники не поддерживаются в браузере, он вызывает функцию + showNotAllowed +.

Затем добавьте следующий код ниже функции + registerSw +, чтобы проверить, имеет ли пользователь право на получение push-уведомлений, прежде чем пытаться подписаться на них:

~ / djangopush / статический / JS / registerSw.js

...

const initialiseState = (reg) => {
   if (!reg.showNotification) {
       showNotAllowed('Showing notifications isn\'t supported ☹️😢');
       return
   }
   if (Notification.permission === 'denied') {
       showNotAllowed('You prevented us from showing notifications ☹️🤔');
       return
   }
   if (!'PushManager' in window) {
       showNotAllowed("Push isn't allowed in your browser 🤔");
       return
   }
   subscribe(reg);
}

const showNotAllowed = (message) => {
   const button = document.querySelector('form>button');
   button.innerHTML = `${message}`;
   button.setAttribute('disabled', 'true');
};

Функция + initializeState + проверяет следующее:

  • Независимо от того, активировал ли пользователь уведомления, используя значение + reg.showNotification +.

  • Предоставил ли пользователь разрешение приложения на отображение уведомлений.

  • Независимо от того, поддерживает ли браузер API + PushManager +. Если любая из этих проверок не удалась, вызывается функция + showNotAllowed + и подписка прерывается.

Функция + showNotAllowed + отображает сообщение на кнопке и отключает его, если пользователь не имеет права получать уведомления. Также отображаются соответствующие сообщения, если пользователь запретил приложению отображать уведомления или если браузер не поддерживает push-уведомления.

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

~ / djangopush / статический / JS / registerSw.js

...

function urlB64ToUint8Array(base64String) {
   const padding = '='.repeat((4 - base64String.length % 4) % 4);
   const base64 = (base64String + padding)
       .replace(/\-/g, '+')
       .replace(/_/g, '/');

   const rawData = window.atob(base64);
   const outputArray = new Uint8Array(rawData.length);
   const outputData = outputArray.map((output, index) => rawData.charCodeAt(index));

   return outputData;
}

const subscribe = async (reg) => {
   const subscription = await reg.pushManager.getSubscription();
   if (subscription) {
       sendSubData(subscription);
       return;
   }

   const vapidMeta = document.querySelector('meta[name="vapid-key"]');
   const key = vapidMeta.content;
   const options = {
       userVisibleOnly: true,
       // if key exists, create applicationServerKey property
       ...(key && {applicationServerKey: urlB64ToUint8Array(key)})
   };

   const sub = await reg.pushManager.subscribe(options);
   sendSubData(sub)
};

Вызов функции + pushManager.getSubscription + возвращает данные для активной подписки. Когда активная подписка существует, вызывается функция + sendSubData + с информацией о подписке, передаваемой в качестве параметра.

Когда активной подписки не существует, открытый ключ VAPID, кодированный URL-адресом Base64, преобразуется в массив Uint8Array с помощью функции + urlB64ToUint8Array +. + pushManager.subscribe + затем вызывается с открытым ключом VAPID и значением + userVisible + в качестве параметров. Вы можете узнать больше о доступных опциях here.

После успешной подписки пользователя следующим шагом будет отправка данных подписки на сервер. Данные будут отправлены в конечную точку + webpush / save_information +, предоставленную пакетом + django-webpush +. Добавьте следующий код ниже функции + subscribe +:

~ / djangopush / статический / JS / registerSw.js

...

const sendSubData = async (subscription) => {
   const browser = navigator.userAgent.match(/(firefox|msie|chrome|safari|trident)/ig)[0].toLowerCase();
   const data = {
       status_type: 'subscribe',
       subscription: subscription.toJSON(),
       browser: browser,
   };

   const res = await fetch('/webpush/save_information', {
       method: 'POST',
       body: JSON.stringify(data),
       headers: {
           'content-type': 'application/json'
       },
       credentials: "include"
   });

   handleResponse(res);
};

const handleResponse = (res) => {
   console.log(res.status);
};

registerSw();

Конечной точке + save_information + требуется информация о статусе подписки (+ subscribe + и + unsubscribe +), данных о подписке и браузере. Наконец, мы вызываем функцию + registerSw () +, чтобы начать процесс подписки пользователя.

Готовый файл выглядит так:

~ / djangopush / статический / JS / registerSw.js

const registerSw = async () => {
   if ('serviceWorker' in navigator) {
       const reg = await navigator.serviceWorker.register('sw.js');
       initialiseState(reg)

   } else {
       showNotAllowed("You can't send push notifications ☹️😢")
   }
};

const initialiseState = (reg) => {
   if (!reg.showNotification) {
       showNotAllowed('Showing notifications isn\'t supported ☹️😢');
       return
   }
   if (Notification.permission === 'denied') {
       showNotAllowed('You prevented us from showing notifications ☹️🤔');
       return
   }
   if (!'PushManager' in window) {
       showNotAllowed("Push isn't allowed in your browser 🤔");
       return
   }
   subscribe(reg);
}

const showNotAllowed = (message) => {
   const button = document.querySelector('form>button');
   button.innerHTML = `${message}`;
   button.setAttribute('disabled', 'true');
};

function urlB64ToUint8Array(base64String) {
   const padding = '='.repeat((4 - base64String.length % 4) % 4);
   const base64 = (base64String + padding)
       .replace(/\-/g, '+')
       .replace(/_/g, '/');

   const rawData = window.atob(base64);
   const outputArray = new Uint8Array(rawData.length);
   const outputData = outputArray.map((output, index) => rawData.charCodeAt(index));

   return outputData;
}

const subscribe = async (reg) => {
   const subscription = await reg.pushManager.getSubscription();
   if (subscription) {
       sendSubData(subscription);
       return;
   }

   const vapidMeta = document.querySelector('meta[name="vapid-key"]');
   const key = vapidMeta.content;
   const options = {
       userVisibleOnly: true,
       // if key exists, create applicationServerKey property
       ...(key && {applicationServerKey: urlB64ToUint8Array(key)})
   };

   const sub = await reg.pushManager.subscribe(options);
   sendSubData(sub)
};

const sendSubData = async (subscription) => {
   const browser = navigator.userAgent.match(/(firefox|msie|chrome|safari|trident)/ig)[0].toLowerCase();
   const data = {
       status_type: 'subscribe',
       subscription: subscription.toJSON(),
       browser: browser,
   };

   const res = await fetch('/webpush/save_information', {
       method: 'POST',
       body: JSON.stringify(data),
       headers: {
           'content-type': 'application/json'
       },
       credentials: "include"
   });

   handleResponse(res);
};

const handleResponse = (res) => {
   console.log(res.status);
};

registerSw();

Затем добавьте тег + script для файла` + register.js` в + home.html +. Откройте файл:

nano ~/djangopush/templates/home.html

Добавьте тег + script перед закрывающим тегом элемента` + body`:

~ / Djangopush / шаблоны / home.html

{% load static %}
<!DOCTYPE html>
<html lang="en">

<head>
  ...
</head>
<body>
  ...

</body>
</html>

Поскольку работника службы еще не существует, если вы оставите приложение запущенным или попытаетесь запустить его снова, вы увидите сообщение об ошибке. Давайте исправим это, создав сервисного работника.

Шаг 8 - Создание сервисного работника

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

Поскольку мы хотим, чтобы область обслуживания работника составляла весь домен, нам нужно будет установить его в корне приложения. Вы можете прочитать больше о процессе в этой статье, описывающей how для регистрации работника службы. Наш подход заключается в создании файла + sw.js в папке` + templates`, который мы затем зарегистрируем как представление.

Создайте файл:

nano ~/djangopush/templates/sw.js

Добавьте следующий код, который сообщает работнику службы прослушивания событий push:

~ / djangopush / шаблоны / sw.js

// Register event listener for the 'push' event.
self.addEventListener('push', function (event) {
   // Retrieve the textual payload from event.data (a PushMessageData object).
   // Other formats are supported (ArrayBuffer, Blob, JSON), check out the documentation
   // on https://developer.mozilla.org/en-US/docs/Web/API/PushMessageData.
   const eventInfo = event.data.text();
   const data = JSON.parse(eventInfo);
   const head = data.head || 'New Notification 🕺🕺';
   const body = data.body || 'This is default content. Your notification didn\'t have one 🙄🙄';

   // Keep the service worker alive until the notification is created.
   event.waitUntil(
       self.registration.showNotification(head, {
           body: body,
           icon: 'https://i.imgur.com/MZM3K5w.png'
       })
   );
});

Сервисный работник слушает событие push. В функции обратного вызова данные + event + преобразуются в текст. Мы используем строки + title + и + body + по умолчанию, если в данных события их нет. Функция + showNotification + получает заголовок уведомления, заголовок отображаемого уведомления и options Объект в качестве параметров. Объект параметров содержит несколько свойств для настройки визуальных параметров уведомления.

Чтобы работники службы работали для всего домена, вам необходимо установить его в корне приложения. Мы будем использовать https://docs.djangoproject.com/en/2.1/topics/class-based-views/ [+ TemplateView +], чтобы предоставить работнику сервиса доступ ко всему домену.

Откройте файл + urls.py +:

nano ~/djangopush/djangopush/urls.py

Добавьте новый оператор импорта и путь в список + urlpatterns +, чтобы создать представление на основе классов:

~ / Djangopush / djangopush / urls.py

...


urlpatterns = [
                 ...

             ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

Представления на основе классов, такие как + TemplateView +, позволяют создавать гибкие многоразовые представления. В этом случае метод + TemplateView.as_view + создает путь для работника службы, передавая недавно созданного работника службы в качестве шаблона и + application / x-javascript + в качестве + content_type + шаблона.

Вы создали сервисного работника и зарегистрировали его как маршрут. Далее вы настроите форму на главной странице для отправки push-уведомлений.

Шаг 9 - Отправка push-уведомлений

Используя форму на домашней странице, пользователи должны иметь возможность отправлять push-уведомления во время работы вашего сервера. Вы также можете отправлять push-уведомления, используя любой сервис RESTful, например Postman. Когда пользователь отправляет push-уведомления из формы на домашней странице, данные будут содержать + head + и + body +, а также + id + принимающего пользователя. Данные должны быть структурированы следующим образом:

{
   head: "Title of the notification",
   body: "Notification body",
   id: "User's id"
}

Чтобы прослушать событие + submit + формы и отправить данные, введенные пользователем на сервер, мы создадим файл с именем + site.js + в каталоге + ~ / djangopush / static / js +.

Откройте файл:

nano ~/djangopush/static/js/site.js

Во-первых, добавьте слушатель события + submit + в форму, который позволит вам получить значения входных данных формы и идентификатор пользователя, хранящийся в теге + meta + вашего шаблона:

~ / djangopush / статический / JS / site.js

const pushForm = document.getElementById('send-push__form');
const errorMsg = document.querySelector('.error');

pushForm.addEventListener('submit', async function (e) {
   e.preventDefault();
   const input = this[0];
   const textarea = this[1];
   const button = this[2];
   errorMsg.innerText = '';

   const head = input.value;
   const body = textarea.value;
   const meta = document.querySelector('meta[name="user_id"]');
   const id = meta ? meta.content : null;
   ...
   // TODO: make an AJAX request to send notification
});

Функция + pushForm + возвращает внутри формы + input,` + textarea` и + button. Он также получает информацию из тега + meta +, включая атрибут name + user_id + и идентификатор пользователя, хранящийся в атрибуте + content + тега. С помощью этой информации он может отправить POST-запрос конечной точке + / send_push + на сервере.

Для отправки запросов на сервер мы будем использовать собственный API-интерфейс Fetch. Мы используем Fetch здесь, потому что он поддерживается большинством браузеров и не требует внешних библиотек для работы. Ниже добавленного кода обновите функцию + pushForm +, включив в нее код для отправки запросов AJAX:

~ / djangopush / статический / JS / site.js

const pushForm = document.getElementById('send-push__form');
const errorMsg = document.querySelector('.error');

pushForm.addEventListener('submit', async function (e) {
    ...
   const id = meta ? meta.content : null;

    if (head && body && id) {
       button.innerText = 'Sending...';
       button.disabled = true;

       const res = await fetch('/send_push', {
           method: 'POST',
           body: JSON.stringify({head, body, id}),
           headers: {
               'content-type': 'application/json'
           }
       });
       if (res.status === 200) {
           button.innerText = 'Send another 😃!';
           button.disabled = false;
           input.value = '';
           textarea.value = '';
       } else {
           errorMsg.innerText = res.message;
           button.innerText = 'Something broke 😢..  Try again?';
           button.disabled = false;
       }
   }
   else {
       let error;
       if (!head || !body){
           error = 'Please ensure you complete the form 🙏🏾'
       }
       else if (!id){
           error = "Are you sure you're logged in? 🤔. Make sure! 👍🏼"
       }
       errorMsg.innerText = error;
   }
});

Если присутствуют три обязательных параметра + head +, + body + и + id +, мы отправляем запрос и временно отключаем кнопку отправки.

Готовый файл выглядит так:

~ / djangopush / статический / JS / site.js

const pushForm = document.getElementById('send-push__form');
const errorMsg = document.querySelector('.error');

pushForm.addEventListener('submit', async function (e) {
   e.preventDefault();
   const input = this[0];
   const textarea = this[1];
   const button = this[2];
   errorMsg.innerText = '';

   const head = input.value;
   const body = textarea.value;
   const meta = document.querySelector('meta[name="user_id"]');
   const id = meta ? meta.content : null;

   if (head && body && id) {
       button.innerText = 'Sending...';
       button.disabled = true;

       const res = await fetch('/send_push', {
           method: 'POST',
           body: JSON.stringify({head, body, id}),
           headers: {
               'content-type': 'application/json'
           }
       });
       if (res.status === 200) {
           button.innerText = 'Send another 😃!';
           button.disabled = false;
           input.value = '';
           textarea.value = '';
       } else {
           errorMsg.innerText = res.message;
           button.innerText = 'Something broke 😢..  Try again?';
           button.disabled = false;
       }
   }
   else {
       let error;
       if (!head || !body){
           error = 'Please ensure you complete the form 🙏🏾'
       }
       else if (!id){
           error = "Are you sure you're logged in? 🤔. Make sure! 👍🏼"
       }
       errorMsg.innerText = error;
   }
});

Наконец, добавьте файл + site.js в` + home.html`:

nano ~/djangopush/templates/home.html

Добавьте тег + script:

~ / Djangopush / шаблоны / home.html

{% load static %}
<!DOCTYPE html>
<html lang="en">

<head>
  ...
</head>
<body>
  ...

</body>
</html>

В этот момент, если вы оставите ваше приложение работающим или попытаетесь запустить его снова, вы увидите ошибку, так как работники службы могут работать только в защищенных доменах или на + localhost +. На следующем шаге мы будем использовать ngrok для создания безопасного туннеля к нашему веб-серверу.

Шаг 10 - Создание безопасного туннеля для тестирования приложения

Работникам сервисов требуются безопасные соединения для работы на любом сайте, кроме + localhost +, поскольку они могут разрешать захват соединений, а также фильтрацию и изготовление ответов. По этой причине мы создадим безопасный туннель для нашего сервера с https://ngrok.com [ngrok].

Откройте второе окно терминала и убедитесь, что вы находитесь в своем домашнем каталоге:

cd ~

Если вы начали с чистого сервера 18.04 в предварительных условиях, то вам нужно будет установить + unzip +:

sudo apt update && sudo apt install unzip

Скачать нгрок:

wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip
unzip ngrok-stable-linux-amd64.zip

Переместите + ngrok + в + / usr / local / bin +, чтобы у вас был доступ к команде + ngrok + из терминала:

sudo mv ngrok /usr/local/bin

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

cd ~/djangopush
python manage.py runserver :8000

Вам нужно будет сделать это перед созданием безопасного туннеля для вашего приложения.

Во втором окне терминала перейдите в папку вашего проекта и активируйте виртуальную среду:

cd ~/djangopush
source my_env/bin/activate

Создайте безопасный туннель для вашего приложения:

ngrok http :8000

Вы увидите следующий вывод, который включает информацию о вашем безопасном URL-адресе ngrok:

Outputngrok by @inconshreveable                                                                                                                       (Ctrl+C to quit)

Session Status                online
Session Expires               7 hours, 59 minutes
Version                       2.2.8
Region                        United States (us)
Web Interface                 http://127.0.0.1:4040
Forwarding                    http:// -> 203.0.113.0:8000
Forwarding                    https:// -> 203.0.113.0:8000

Connections                   ttl     opn     rt1     rt5     p50     p90
                             0       0       0.00    0.00    0.00    0.00

Скопируйте ++ из вывода консоли. Вам нужно будет добавить его в список + ALLOWED_HOSTS в вашем файле` + settings.py`.

Откройте другое окно терминала, перейдите в папку вашего проекта и активируйте виртуальную среду:

cd ~/djangopush
source my_env/bin/activate

Откройте файл + settings.py +:

nano ~/djangopush/djangopush/settings.py

Обновите список + ALLOWED_HOSTS + с помощью защищенного туннеля ngrok:

~ / Djangopush / djangopush / settings.py

...

ALLOWED_HOSTS = ['your_server_ip', '']
...

Перейдите на защищенную страницу администратора, чтобы войти: + https: /// admin / +. Вы увидите экран, который выглядит так:

изображение: https: //assets.digitalocean.com/articles/django_push_18_04/ngrok_login.png [ngrok admin login]

Введите информацию о вашем администраторе Django на этом экране. Это должна быть та же информация, которую вы ввели при входе в интерфейс администратора в https://www.digitalocean.com/community/tutorials/how-to-install-the-django-web-framework-on-ubuntu-18 -04 # test-the-development-server [обязательные шаги]. Теперь вы готовы отправлять push-уведомления.

Посетите + https: // + в вашем браузере. Вы увидите приглашение с просьбой разрешить отображение уведомлений. Нажмите кнопку * Разрешить *, чтобы позволить вашему браузеру отображать push-уведомления:

изображение: https: //assets.digitalocean.com/articles/django_push_18_04/allow_push_two.png [запрос push-уведомлений]

Отправка заполненной формы будет отображать уведомление, подобное этому:

изображение: https: //assets.digitalocean.com/articles/django_push_18_04/web_push_final.png [скриншот уведомления]

Если вы получили уведомления, значит ваше приложение работает должным образом.

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

Заключение

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

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

Related