Автор выбрал фонд Open Internet/Free Speech для получения пожертвования в рамках программы Write for DOnations.
Интернет постоянно меняется, и теперь он может получить функциональные возможности, которые ранее были доступны только непосредственно на мобильных устройствах. Появление в JavaScript инструмента service worker дает вебу такие новые возможности, как выполнение фоновой синхронизации, кеширование оффлайн и отправка push-уведомлений.
Push-уведомления позволяют пользователям принимать новости от мобильных и веб-приложений. Также они позволяют пользователям поддерживать взаимодействие с существующими приложениями, используя персонализированные и релевантный контент.
В этом обучающем руководстве вы настроите приложение Django в Ubuntu 18.04, которое отправляет push-уведомления в случае любой активности, которая требует от пользователя посещения приложения. Для создания этих уведомлений вы будете использовать пакет Django-Webpush и должны будете настроить и зарегистрировать service worker для отображения уведомлений для клиента. Работающее приложение с уведомлениями будет выглядеть следующим образом:
Для прохождения этого обучающего руководства вам потребуется следующее:
pip
и venv
, установленные в соответствии с данными указаниями.djangopush
, созданный в домашней директории и настроенный в соответствии с указаниями руководства по созданию примера проекта Django в Ubuntu 18.04. Обязательно добавьте IP-адрес вашего сервера в директиву ALLOWED_HOSTS
файла settings.py
.Django-Webpush — это пакет, позволяющий разработчикам интегрировать и отправлять push-уведомления в приложения Django. Мы будем использовать этот пакет для запуска и отправки push-уведомлений из нашего приложения. На этом шаге вы установите Django-Webpush и получите ключи добровольной идентификации сервера приложения (Voluntary Application Server Identification, VAPID), которые необходимы для идентификации вашего сервера и обеспечения уникальности каждого запроса.
Вы обязательно должны находиться в директории проекта ~/djangopush
, которая была создана на этапе выполнения предварительных требований:
- cd ~/djangopush
Активируйте вашу виртуальную среду:
- source my_env/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
:
...
INSTALLED_APPS = [
...,
'webpush',
]
...
Сохраните файл и закройте редактор.
Запустите миграцию в приложении для применения изменений, которые вы внесли в схему базы данных:
- python manage.py migrate
Вывод будет выглядеть следующим образом при условии успешного выполнения миграции:
OutputOperations 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. Откройте файл:
- nano ~/djangopush/djangopush/settings.py
Далее добавьте новую директиву с именем WEBPUSH_SETTINGS
с публичными и частными VAPID ключами и ваш адрес электронной почты под AUTH_PASSWORD_VALIDATORS:
...
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-уведомлений подписавшимся пользователям.
На этом шаге мы настроим базовое представление home
с объектом-ответом 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-уведомления. Добавьте в файл следующий код:
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
:
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-уведомления:
@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 для представлений, которые мы создали.
Django позволяет создавать URL-адреса, которые будут подключаться к представлениям с помощью модуля Python с именем URLconf
. Этот модуль размечает выражения маршрута URL для функций Python (ваших представлений). Обычно файл конфигурации URL генерируется автоматически при создании проекта. На этом шаге вы будете обновлять этот файл для включения новых маршрутов для представлений, созданных на предыдущем шаге, а также URL-адресов для приложения django-webpush
, которые будут предоставлять конечные точки для подписанных пользователей для push-уведомлений.
Дополнительную информацию о представлениях см. в руководстве Создание представлений Django.
Откройте urls.py
:
- nano ~/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 будут добавляться в ваш проект:
"""webpushdjango URL Configuration
...
"""
from django.contrib import admin
from django.urls import path, include
Далее импортируйте представления, которые вы создали на последнем шаге, и обновите список urlpatterns
для разметки представлений:
"""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
, чтобы убедиться, что оно работает надлежащим образом. Убедитесь, что вы находитесь в корневой директории проекта:
- cd ~/djangopush
Запустите ваш сервер с помощью следующей команды:
- python manage.py runserver your_server_ip:8000
Перейдите по адресу http://your_server_ip:8000
. Вы должны увидеть следующую домашнюю страницу:
В данный момент вы можете остановить сервер с помощью кнопок CTRL+C
, потому что мы переходим к созданию шаблонов и их отображению в наших представлениях с помощью функции render
.
Движок шаблонов 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
для указания пути к директории шаблонов:
...
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
. Откройте файл:
- nano ~/djangpush/djangopush/views.py
Во-первых, добавьте ряд импортов, включая конфигурацию settings
, которая содержит все параметры проекта из файла settings.py
и функцию render
из django.shortcuts
:
...
from django.shortcuts import render, get_object_or_404
...
import json
from django.conf import settings
...
Далее удалите первоначальный код, добавленный в представление home
, и добавьте следующие данные, указывающие, как созданный вами шаблон будет отображаться:
...
@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 для обслуживания статичных файлов.
Веб-приложения, включая CSS, JavaScript и другие файлы образа, которые Django воспринимает в качестве “статичных файлов”. Django позволяет собирать все статичные файлы из каждого приложения в вашем проекте в одном месте, из которого они будут обслуживаться. Это решение называется django.contrib.staticfiles
. На этом шаге мы обновим наши настройки, чтобы указать Django, где наши статичные файлы будут храниться.
Откройте файл settings.py
:
- nano ~/djangopush/djangopush/settings.py
В файле settings.py
нужно убедиться, что значение STATIC_URL
было определено:
...
STATIC_URL = '/static/'
Далее добавьте список директорий с названием STATICFILES_DIRS
, где Django будет искать статичные файлы:
...
STATIC_URL = '/static/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static"),
]
Теперь вы можете добавить STATIC_URL
в список путей, определенных в файле urls.py
.
Откройте файл:
- nano ~/djangopush/djangopush/urls.py
Добавьте следующий код, который будет импортировать конфигурацию static
URL-адресов и обновлять список urlpatterns
. Вспомогательная функция здесь использует свойства STATIC_URL
и STATIC_ROOT
, которые мы предоставили в файле settings.py
для обслуживания статичных файлов проекта:
...
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
...
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
После настройки параметров статичных файлов мы можем перейти к определению стиля домашней страницы приложения.
После настройки вашего приложения для обслуживания статичных файлов вы можете создать внешнюю таблицу стилей и привязать ее к файлу home.html
для определения стиля домашней страницы. Все ваши статичные файлы будут храниться в директории static
корневой папки вашего проекта.
Создайте папку static
, а внутри папки static
создайте папку css
:
- mkdir -p ~/djangopush/static/css
Откройте файл css с именем styles.css
в папке css
:
- nano ~/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
:
- nano ~/djangopush/templates/home.html
Обновите раздел head
для включения в него ссылки на внешнюю таблицу стилей:
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
...
<link href="{% static '/css/styles.css' %}" rel="stylesheet">
</head>
<body>
...
</body>
</html>
Убедитесь, что вы находитесь в директории основного проекта, и снова запустите ваш сервер для проверки работы:
- cd ~/djangopush
- python manage.py runserver your_server_ip:8000
При посещении http://your_server_ip:8080
страница должна выглядеть следующим образом:
Снова используйте сочетание клавиш CTRL+C
для остановки сервера.
Теперь, когда вы успешно создали страницу home.html
и добавили для нее таблицу стилей, вы можете оформить для пользователей подписку на push-уведомления, когда бы они ни посетили домашнюю страницу.
Push-уведомления в веб могут уведомлять пользователей о наличии обновлений для приложений, на которые они подписаны, или для напоминания о возможности вспомнить приложение, которое они использовали в прошлом. Они опираются на две технологии, API push и API notifications. Обеим технологиям необходимо наличие service worker.
Push-уведомление отправляется, когда сервер предоставляет информацию для service worker, а service worker использует API уведомлений для отображения этой информации.
Мы будем подписывать наших пользователей на push-уведомления, а затем будем отправлять информацию из подписки на сервер для их регистрации.
В директории static
создайте папку с именем js
:
- mkdir ~/djangopush/static/js
Создайте файл с именем registerSw.js
:
- nano ~/djangopush/static/js/registerSw.js
Добавьте следующий код, который проверяет, поддерживает ли service worker’ы в браузере пользователя, прежде чем пытаться регистрировать service worker:
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-уведомления, прежде чем пытаться подписать их:
...
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
.PushManager
или нет. Если какая-либо из этих проверок не будет пройдена, функция showNotAllowed
вызывается, а подписка отменяется.Функция showNotAllowed
отображает сообщение на кнопке и отключает его, если пользователь не имеет права принимать уведомления. Также она отображает соответствующие сообщения, если пользователь ограничил для приложения отображение уведомлений, либо если браузер не поддерживает push-уведомления.
После того как мы убедимся, что пользователь может получать push-уведомления, следующим шагом будет оформления подписки на уведомления с помощью команды pushManager
. Добавьте следующий код под функцией showNotAllowed
:
...
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:
...
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()
для запуска процесса подписки пользователя.
Завершенный файл выглядит следующим образом:
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
. Откройте файл:
- nano ~/djangopush/templates/home.html
Добавьте тег script
перед закрывающим тегом элемента body
:
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
...
</head>
<body>
...
<script src="{% static '/js/registerSw.js' %}"></script>
</body>
</html>
Поскольку service worker еще не существует, если бы вы оставили приложение запущенным или попытались запустить его снова, то увидели бы сообщение об ошибке. Давайте устраним эту проблему с помощью service worker.
Для отображения push-уведомления вам потребуется активный service worker, установленный на домашней странице приложения. Мы создадим service worker, который прослушивает события push
и отображает сообщения при готовности.
Поскольку мы хотим, чтобы service worker покрывал весь домен, нам нужно установить его в корневой директории приложения. В статье о регистрации service worker вы можете подробнее ознакомиться с процессом. Согласно нашему подходу будет создан файл sw.js
в папке templates
, который мы позднее зарегистрируем в качестве представления.
Создайте файл:
- nano ~/djangopush/templates/sw.js
Добавьте следующий код, который указывает service worker на необходимость прослушивания push событий:
// 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
:
- nano ~/djangopush/djangopush/urls.py
Добавьте новое объявление импорта и путь в список urlpatterns
для создания представления на основе классов:
...
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-уведомлений.
Используя форму на домашней странице, пользователи смогут отправлять 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
вашего шаблона:
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:
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.
Завершенный файл выглядит следующим образом:
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
:
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
...
</head>
<body>
...
<script src="{% static '/js/site.js' %}"></script>
</body>
</html>
В данный момент, если вы оставили приложение запущенным или попытались запустить его снова, то получите ошибку, поскольку service workers может функционировать только на защищенных доменах или в localhost
: На следующем шаге мы будем использовать ngrok для создания безопасного туннеля на нашем веб-сервере.
Service worker’ы требуют наличия защищенных соединений для работы на любом сайте, кроме localhost
, поскольку они не защищены от взлома для последующей фильтрации и подмены ответов. По этой причине мы создадим защищенный туннель для нашего сервера с помощью ngrok.
Откройте второе окно командной строки и убедитесь, что вы находитесь в домашней директории:
- cd ~
Если вы начали работу с чистым сервером на базе Ubuntu 18.04, вам потребуется выполнить установку unzip
:
- sudo apt update && sudo apt install unzip
Загрузите ngrok:
- 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 your_server_ip:8000
Вы должны были сделать это, прежде чем создать защищенный туннель для вашего приложения.
Во втором окне командной строки перейдите к папке проекта и активируйте виртуальную среду:
- cd ~/djangopush
- source my_env/bin/activate
Создайте защищенный туннель для вашего приложения:
- ngrok http your_server_ip: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://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
.
Откройте другое окно командной строки перейдите в папку проекта и активируйте виртуальную среду:
- cd ~/djangopush
- source my_env/bin/activate
Откройте файл settings.py
:
- nano ~/djangopush/djangopush/settings.py
Обновите список ALLOWED_HOSTS
с защищенным туннелем ngrok:
...
ALLOWED_HOSTS = ['your_server_ip', 'ngrok_secure_url']
...
Перейдите на защищенную страницу администратора для входа: https://ngrok_secure_url/admin/
. Вы увидите экран, который будет выглядеть примерно так:
Введите данные пользователя Django с правами администратора на этом экране. Эта информация должна повторять информацию, которую вы вводили при входе в интерфейс администратора на этапе предварительной подготовки. Теперь вы готовы к отправке push-уведомлений.
Введите https://ngrok_secure_url
в адресной строке браузера. Вы увидите запрос разрешения на отображение уведомлений. Нажмите кнопку Allow (Разрешить), чтобы разрешить отображение 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.
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!
Sign up for Infrastructure as a Newsletter.
Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.
e