Tutorial

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

Published on January 24, 2020
Русский
Отправка push-уведомлений из приложений Django

Автор выбрал фонд Open Internet/Free Speech для получения пожертвования в рамках программы Write for DOnations.

Введение

Интернет постоянно меняется, и теперь он может получить функциональные возможности, которые ранее были доступны только непосредственно на мобильных устройствах. Появление в JavaScript инструмента service worker дает вебу такие новые возможности, как выполнение фоновой синхронизации, кеширование оффлайн и отправка push-уведомлений.

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

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

Итоговый вид push-уведомления

Предварительные требования

Для прохождения этого обучающего руководства вам потребуется следующее:

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

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

Вы обязательно должны находиться в директории проекта ~/djangopush, которая была создана на этапе выполнения предварительных требований:

  1. cd ~/djangopush

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

  1. source my_env/bin/activate

Обновите версию pip для гарантии ее актуальности:

  1. pip install --upgrade pip

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

  1. pip install django-webpush

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

  1. nano ~/djangopush/djangopush/settings.py

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

~/djangopush/djangopush/settings.py
...

INSTALLED_APPS = [
    ...,
    'webpush',
]
...

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

Запустите миграцию в приложении для применения изменений, которые вы внесли в схему базы данных:

  1. python manage.py migrate

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

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

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

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

Далее создайте новую запись в файле settings.py для ваших данных о VAPID. Откройте файл:

  1. nano ~/djangopush/djangopush/settings.py

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

~/djangopush/djangopush/settings.py
...

AUTH_PASSWORD_VALIDATORS = [
    ...
]

WEBPUSH_SETTINGS = {
   "VAPID_PUBLIC_KEY": "your_vapid_public_key",
   "VAPID_PRIVATE_KEY": "your_vapid_private_key",
   "VAPID_ADMIN_EMAIL": "admin@example.com"
}

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

...

Не забудьте заменить значения your_vapid_public_key, your_vapid_private_key и admin@example.com на ваши данные. Ваш адрес электронной почты будет использоваться для отправки вам уведомлений при наличии проблем на сервере push-уведомлений.

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

Шаг 2 — Настройка представлений

На этом шаге мы настроим базовое представление home с объектом-ответом HttpResponse для нашей домашней страницы, а также представление send_push. Представления — это функции, которые будут возвращать для веб-запросов. Представление send_push будет использовать библиотеку Django-Webpush для отправки push-уведомлений, которые будут содержать данные, введенные пользователем на домашней странице.

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

  1. cd ~/djangopush/djangopush

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

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

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

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

  1. 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, которое будет обрабатывать отправленные уведомления с помощью пакета django-webpush. Оно будет ограничено только запросами POST и будет выведено из-под защиты от *межсайтовой подделки запроса *(CSRF). Это позволит протестировать представление с помощью Postman или любой другой службы RESTful. Однако в реальном рабочем проекте вы должны удалить этот декоратор, чтобы защитить ваши представления от CSRF.

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

~/djangopush/djangopush/views.py
from django.http.response import JsonResponse, HttpResponse
from django.views.decorators.http import require_GET, require_POST
from django.shortcuts import get_object_or_404
from django.contrib.auth.models import User
from django.views.decorators.csrf import csrf_exempt
from webpush import send_user_notification
import json

Далее добавьте декоратор 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 запроса и с помощью пакета json десериализует документ JSON и получает объект Python, используя json.loads. json.loads получает структурированный документ JSON и преобразовывает его в объект Python.

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

  • head: заголовок push-уведомления.
  • body: тело уведомления.
  • id: id отправившего запрос пользователя.

Если какое-либо из требуемых свойств отсутствует, представление будет возвращать JSONResponse со статусом 404 “Not Found”. Если пользователь с данным основным ключом существует, представление будет возвращать user с соответствующим основным ключом, используя функцию get_object_or_404 из библиотеки django.shortcuts. Если пользователь не существует, функция будет возвращать ошибку 404.

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

  • User: получатель push-уведомления.
  • payload: информация уведомления, которая включает head и body уведомления.
  • ttl: максимальное время в секундах, в течение которого уведомление следует хранить, если пользователь находится оффлайн.

При отсутствии ошибок представление возвращает JSONResponse со статусом 200 “Success” и объектом данных. При возникновении ошибки KeyError представление будет возвращать статус 500 “Internal Server Error”. Ошибка KeyError возникает при отсутствии запрошенного ключа объекта.

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

Шаг 3 — Разметка URL-адресов для представлений

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

Дополнительную информацию о представлениях см. в руководстве Создание представлений Django.

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

  1. 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, include

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

~/djangopush/djangopush/urls.py

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

from .views import home, send_push

urlpatterns = [
                  path('admin/', admin.site.urls),
                  path('', home),
                  path('send_push', send_push),
                  path('webpush/', include('webpush.urls')),
              ]

Здесь список urlpatterns регистрирует URL-адреса для пакета django-webpush и сопоставляет представления с URL-адресами /send_push и /home.

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

  1. cd ~/djangopush

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

  1. python manage.py runserver your_server_ip:8000

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

Первоначальное представление домашней страницы

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

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

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

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

  1. mkdir ~/djangopush/templates

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

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

Создайте файл home.html в папке templates:

  1. 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:

  1. nano ~/djangopush/djangopush/settings.py

Добавьте следующие данные в список DIRS для указания пути к директории шаблонов:

~/djangopush/djangopush/settings.py
...
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                ...
            ],
        },
    },
]
...

Затем в файле views.py обновите представление home для отображения шаблона home.html. Откройте файл:

  1. nano ~/djangpush/djangopush/views.py

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

~/djangopush/djangopush/views.py
...
from django.shortcuts import render, get_object_or_404
...
import json
from django.conf import settings

...

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

~/djangopush/djangopush/views.py
...

@require_GET
def home(request):
   webpush_settings = getattr(settings, 'WEBPUSH_SETTINGS', {})
   vapid_key = webpush_settings.get('VAPID_PUBLIC_KEY')
   user = request.user
   return render(request, 'home.html', {user: user, 'vapid_key': vapid_key})

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

  • webpush_settings: данный параметр присваивает значение атрибута WEBPUSH_SETTINGS из конфигурации settings.
  • vapid_key: этот элемент получает значение VAPID_PUBLIC_KEY из объекта webpush_settings для отправки клиенту. Данный публичный ключ сравнивается с закрытым ключом, чтобы убедиться, что клиент с публичным ключом может получать push-сообщения от сервера.
  • user: эта переменка поступает из входящего запроса. Когда пользователь отправляет запрос на сервер, данные этого пользователя сохраняются в поле user.

Функция render будет возвращать файл HTML и объект context, содержащий текущего пользователя и публичный ключ VAPID. Здесь требуются три параметра: запрос, шаблон для отображения и объект, который содержит переменные, используемые в шаблоне.

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

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

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

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

  1. 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/'
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, "static"),
]

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

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

  1. nano ~/djangopush/djangopush/urls.py

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

~/djangopush/djangopush/urls.py

...
from django.conf import settings
from django.conf.urls.static import static

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

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

Шаг 6 — Определение стиля домашней страницы

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

Создайте папку static, а внутри папки static создайте папку css:

  1. mkdir -p ~/djangopush/static/css

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

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

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

~/djangopush/static/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, используя теги статичного шаблона. Откройте файл home.html:

  1. nano ~/djangopush/templates/home.html

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

~/djangopush/templates/home.html

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

<head>
    ...
    <link href="{% static '/css/styles.css' %}" rel="stylesheet">
</head>
<body>
    ...
</body>
</html>

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

  1. cd ~/djangopush
  2. python manage.py runserver your_server_ip:8000

При посещении http://your_server_ip:8080 страница должна выглядеть следующим образом:

Представление домашней страницы Снова используйте сочетание клавиш CTRL+C для остановки сервера.

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

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

Push-уведомления в веб могут уведомлять пользователей о наличии обновлений для приложений, на которые они подписаны, или для напоминания о возможности вспомнить приложение, которое они использовали в прошлом. Они опираются на две технологии, API push и API notifications. Обеим технологиям необходимо наличие service worker.

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

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

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

  1. mkdir ~/djangopush/static/js

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

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

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

~/djangopush/static/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 проверяет, поддерживает ли браузер service worker’ы, прежде чем регистрировать их. После регистрации она вызывает функцию initializeState с данными регистрации. Если service worker’ы не поддерживаются в браузере, вызывается функция showNotAllowed.

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

~/djangopush/static/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/static/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, который шифруется с помощью алгоритма Base64, преобразовывается в Uint8Array с помощью функции urlB64TUint8Array. Затем вызывается функция pushManager.subscribe с публичным ключом VAPID и значением userVisible в качестве опции. Вы можете ознакомиться с доступными опциями здесь.

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

~/djangopush/static/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/static/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 для файла registerSw.js в home.html. Откройте файл:

  1. nano ~/djangopush/templates/home.html

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

~/djangopush/templates/home.html

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

<head>
   ...
</head>
<body>
   ...
   <script src="{% static '/js/registerSw.js' %}"></script>
</body>
</html>

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

Шаг 8 — Создание service worker

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

Поскольку мы хотим, чтобы service worker покрывал весь домен, нам нужно установить его в корневой директории приложения. В статье о регистрации service worker вы можете подробнее ознакомиться с процессом. Согласно нашему подходу будет создан файл sw.js в папке templates, который мы позднее зарегистрируем в качестве представления.

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

  1. nano ~/djangopush/templates/sw.js

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

~/djangopush/templates/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'
        })
    );
});

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

Чтобы service worker мог работать для всего домена, вам потребуется выполнить его установки в корневой директории приложения. Мы будем использовать TemplateView для предоставления service worker доступа ко всему домену.

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

  1. nano ~/djangopush/djangopush/urls.py

Добавьте новое объявление импорта и путь в список urlpatterns для создания представления на основе классов:

~/djangopush/djangopush/urls.py
...
from django.views.generic import TemplateView

urlpatterns = [
                  ...,
                  path('sw.js', TemplateView.as_view(template_name='sw.js', content_type='application/x-javascript'))
              ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

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

Вы уже создали service worker и зарегистрировали его в качестве маршрута. Далее вы настроите форму на домашней странице для отправки 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.

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

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

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

~/djangopush/static/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, включая атрибут user_id и идентификатор пользователя, который хранится в атрибуте content тега. Получив эту информацию, она может отправить запрос POST на конечную точку /send_push на сервере.

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

~/djangopush/static/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 присутствуют, мы отправим запрос и временно отключим кнопку submit.

Завершенный файл выглядит следующим образом:

~/djangopush/static/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:

  1. nano ~/djangopush/templates/home.html

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

~/djangopush/templates/home.html

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

<head>
   ...
</head>
<body>
   ...
   <script src="{% static '/js/site.js' %}"></script>
</body>
</html>

В данный момент, если вы оставили приложение запущенным или попытались запустить его снова, то получите ошибку, поскольку service workers может функционировать только на защищенных доменах или в localhost: На следующем шаге мы будем использовать ngrok для создания безопасного туннеля на нашем веб-сервере.

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

Service worker’ы требуют наличия защищенных соединений для работы на любом сайте, кроме localhost, поскольку они не защищены от взлома для последующей фильтрации и подмены ответов. По этой причине мы создадим защищенный туннель для нашего сервера с помощью ngrok.

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

  1. cd ~

Если вы начали работу с чистым сервером на базе Ubuntu 18.04, вам потребуется выполнить установку unzip:

  1. sudo apt update && sudo apt install unzip

Загрузите ngrok:

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

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

  1. sudo mv ngrok /usr/local/bin

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

  1. cd ~/djangopush
  2. python manage.py runserver your_server_ip:8000

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

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

  1. cd ~/djangopush
  2. source my_env/bin/activate

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

  1. ngrok http your_server_ip:8000

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

Output
ngrok 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://ngrok_secure_url -> 203.0.113.0:8000 Forwarding https://ngrok_secure_url -> 203.0.113.0:8000 Connections ttl opn rt1 rt5 p50 p90 0 0 0.00 0.00 0.00 0.00

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

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

  1. cd ~/djangopush
  2. source my_env/bin/activate

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

  1. nano ~/djangopush/djangopush/settings.py

Обновите список ALLOWED_HOSTS с защищенным туннелем ngrok:

~/djangopush/djangopush/settings.py
...

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

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

Вход для администратора ngrok

Введите данные пользователя Django с правами администратора на этом экране. Эта информация должна повторять информацию, которую вы вводили при входе в интерфейс администратора на этапе предварительной подготовки. Теперь вы готовы к отправке push-уведомлений.

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

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

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

скриншот уведомления

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

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

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

Заключение

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

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

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the authors


Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
1 Comments


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Become a contributor for community

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

DigitalOcean Documentation

Full documentation for every DigitalOcean product.

Resources for startups and SMBs

The Wave has everything you need to know about building a business, from raising funding to marketing your product.

Get our newsletter

Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.

New accounts only. By submitting your email you agree to our Privacy Policy

The developer cloud

Scale up as you grow — whether you're running one virtual machine or ten thousand.

Get started for free

Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

*This promotional offer applies to new accounts only.