如何从Django应用程序发送Web推送通知
作为 Write for DOnations 计划的一部分,作者选择了 Open Internet/Free Speech Fund 来接受捐赠。
介绍
网络在不断发展,现在它可以实现以前只能在原生移动设备上使用的功能。 JavaScript 服务工作者 的引入为 Web 提供了新的能力来执行诸如后台同步、离线缓存和发送 推送通知 之类的事情。
推送通知允许用户选择接收移动和 Web 应用程序的更新。 它们还使用户能够使用定制的相关内容重新参与现有的应用程序。
在本教程中,您将在 Ubuntu 18.04 上设置一个 Django 应用程序,只要有需要用户访问该应用程序的活动,它就会发送推送通知。 要创建这些通知,您将使用 Django-Webpush 包并设置和注册服务工作者以向客户端显示通知。 带有通知的工作应用程序将如下所示:
先决条件
在开始本指南之前,您需要以下内容:
- 一台具有非 root 用户和活动防火墙的 Ubuntu 18.04 服务器。 您可以按照本 初始服务器设置指南 中的指南获取有关如何创建 Ubuntu 18.04 服务器的更多信息。
pip
和venv
按照这些 指南 安装。- 在您的主目录中创建了一个名为
djangopush
的 Django 项目,按照这些关于 在 Ubuntu 18.04 上创建示例 Django 项目的指南进行设置。 确保 将服务器的 IP 地址添加到settings.py
文件中的 ALLOWED_HOSTS 指令。
第 1 步 — 安装 Django-Webpush 并获取 Vapid 密钥
Django-Webpush 是一个包,它使开发人员能够在 Django 应用程序中集成和发送 Web 推送通知。 我们将使用这个包从我们的应用程序中触发和发送推送通知。 在这一步中,您将安装 Django-Webpush 并获取 自愿应用程序服务器标识 (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
列表中:
~/djangopush/djangopush/settings.py
... INSTALLED_APPS = [ ..., 'webpush', ] ...
保存文件并退出编辑器。
在应用程序上运行 migrations 以应用您对数据库架构所做的更改:
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 Web 应用程序。 在这里,您将获得自动生成的密钥。 复制私钥和公钥。
接下来,在 settings.py
中为您的 VAPID 信息创建一个新条目。 首先,打开文件:
nano ~/djangopush/djangopush/settings.py
接下来,使用您的 VAPID 公钥和私钥以及 AUTH_PASSWORD_VALIDATORS
下面的电子邮件添加一个名为 WEBPUSH_SETTINGS
的新指令:
~/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
。 如果推送服务器遇到任何问题,您的电子邮件地址会通知您。
接下来,我们将设置将显示应用程序主页并触发推送通知给订阅用户的视图。
第 2 步 — 设置视图
在这一步中,我们将为我们的主页设置一个基本的 home
view,其中包含 HttpResponse 响应对象 ,以及一个 send_push
视图。 视图是从 Web 请求返回响应对象的函数。 send_push
视图将使用 Django-Webpush 库发送推送通知,其中包含用户在主页上输入的数据。
导航到 ~/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
视图,它将显示用户可以发送推送通知的主页。 将以下代码添加到文件中:
~/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 响应并访问 webpush
库中的 send_user_notification
函数:
~/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
装饰器,它将使用用户发送的请求正文创建并触发推送通知:
~/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.loads 将 JSON 文档反序列化为 Python 对象。 json.loads
采用结构化 JSON 文档并将其转换为 Python 对象。
视图期望请求主体对象具有三个属性:
head
:推送通知的标题。body
:通知正文。id
:请求用户的id
。
如果缺少任何必需的属性,视图将返回带有 404“未找到”状态的 JSONResponse
。 如果具有给定主键的用户存在,则视图将使用 django.shortcuts
库中的 get_object_or_404 函数 返回具有匹配主键的 user
。 如果用户不存在,该函数将返回 404 错误。
该视图还使用了 webpush
库中的 send_user_notification
函数。 该函数接受三个参数:
User
:推送通知的接收者。payload
:通知信息,包括通知head
和body
。ttl
:如果用户离线,应该存储通知的最长时间(以秒为单位)。
如果没有发生错误,视图将返回一个 JSONResponse
和一个 200 “成功”状态和一个数据对象。 如果发生 KeyError
,视图将返回 500“Internal Server Error”状态。 当对象的请求键不存在时,会发生 KeyError
。
在下一步中,我们将创建相应的 URL 路由以匹配我们创建的视图。
第 3 步 — 将 URL 映射到视图
Django 可以使用称为 URLconf
的 Python 模块创建连接到视图的 URLs。 该模块将 URL 路径表达式映射到 Python 函数(您的视图)。 通常,在您创建项目时会自动生成 URL 配置文件。 在此步骤中,您将更新此文件以包含您在上一步中创建的视图的新路由,以及 django-webpush
应用程序的 URL,该应用程序将提供端点以订阅用户推送通知。
有关视图的更多信息,请参阅 如何创建 Django 视图。
打开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, 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
列表注册了 django-webpush
包的 URL,并将您的视图映射到 URL /send_push
和 /home
。
让我们测试 /home
视图以确保它按预期工作。 确保您位于项目的根目录中:
cd ~/djangopush
通过运行以下命令启动服务器:
python manage.py runserver your_server_ip:8000
导航到 http://your_server_ip:8000
。 您应该看到以下主页:
此时,您可以使用 CTRL+C
终止服务器,我们将继续使用 render
函数创建模板并在视图中呈现它们。
第 4 步 — 创建模板
Django 的模板引擎允许您使用模板定义应用程序的面向用户的层,这些模板类似于 HTML 文件。 在此步骤中,您将为 home
视图创建和渲染模板。
在项目的根目录中创建一个名为 templates
的文件夹:
mkdir ~/djangopush/templates
如果此时在项目的根文件夹中运行 ls
,输出将如下所示:
Output/djangopush /templates db.sqlite3 manage.py /my_env
在 templates
文件夹中创建一个名为 home.html
的文件:
nano ~/djangopush/templates/home.html
将以下代码添加到文件中以创建一个表单,用户可以在其中输入信息以创建推送通知:
{% 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 公钥和用户 ID。 这两个变量是注册用户并向他们发送推送通知所必需的。 此处需要用户 ID,因为您将向服务器发送 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': [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
文件的所有项目设置,以及来自 django.shortcuts
的 render
函数:
~/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
:从settings
配置中分配了WEBPUSH_SETTINGS
属性的值。vapid_key
:这会从webpush_settings
对象中获取VAPID_PUBLIC_KEY
值以发送到客户端。 将此公钥与私钥进行检查,以确保具有公钥的客户端被允许接收来自服务器的推送消息。user
:这个变量来自传入的请求。 每当用户向服务器发出请求时,该用户的详细信息都会存储在user
字段中。
渲染函数 将返回一个 HTML 文件和一个 上下文对象 ,其中包含当前用户和服务器的 vapid 公钥。 这里需要三个参数:request
、要渲染的 template
以及包含将在模板中使用的变量的对象。
创建模板并更新 home
视图后,我们可以继续配置 Django 以提供静态文件。
第 5 步 — 提供静态文件
Web 应用程序包括 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/' STATICFILES_DIRS = [ os.path.join(BASE_DIR, "static"), ]
您现在可以将 STATIC_URL
添加到 urls.py
文件中定义的路径列表中。
打开文件:
nano ~/djangopush/djangopush/urls.py
添加以下代码,将导入 static
url 配置并更新 urlpatterns
列表。 这里的辅助函数使用我们在 settings.py
文件中提供的 STATIC_URL
和 STATIC_ROOT
属性来服务项目的静态文件:
~/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
文件夹:
mkdir -p ~/djangopush/static/css
在 css
文件夹中打开一个名为 styles.css
的 css 文件:
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
文件:
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>
确保您位于主项目目录中并再次启动服务器以检查您的工作:
cd ~/djangopush python manage.py runserver your_server_ip:8000
当你访问 http://your_server_ip:8000
时,它应该是这样的:
同样,您可以使用 CTRL+C
终止服务器。
现在您已成功创建 home.html
页面并对其进行样式设置,您可以订阅用户在访问主页时推送通知。
第 7 步 — 注册 Service Worker 并为用户订阅推送通知
Web 推送通知可以在用户订阅的应用程序有更新时通知用户,或提示他们重新使用他们过去使用过的应用程序。 它们依赖于两种技术,push API 和 notifications API。 这两种技术都依赖于服务人员的存在。
当服务器向服务工作人员提供信息并且服务工作人员使用通知 API 显示此信息时,将调用推送。
我们将为我们的用户订阅推送,然后我们会将订阅中的信息发送到服务器以进行注册。
在 static
目录下,创建一个名为 js
的文件夹:
mkdir ~/djangopush/static/js
创建一个名为 registerSw.js
的文件:
nano ~/djangopush/static/js/registerSw.js
添加以下代码,在尝试注册服务工作人员之前检查用户浏览器是否支持服务工作人员:
~/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
函数在注册之前检查浏览器是否支持服务工作者。 注册后,使用注册数据调用initializeState
函数。 如果浏览器不支持 service worker,它会调用 showNotAllowed
函数。
接下来,在 registerSw
函数下方添加以下代码,以在尝试订阅之前检查用户是否有资格接收推送通知:
~/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
的值。 - 用户是否已授予应用程序显示通知的权限。
- 浏览器是否支持
PushManager
API。 如果这些检查中的任何一个失败,则调用showNotAllowed
函数并中止订阅。
showNotAllowed
功能会在按钮上显示一条消息,并在用户没有资格接收通知时将其禁用。 如果用户限制应用程序显示通知或浏览器不支持推送通知,它也会显示适当的消息。
一旦我们确保用户有资格接收推送通知,下一步就是使用 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
函数,并将订阅信息作为参数传入。
当不存在活动订阅时,使用 urlB64ToUint8Array
函数将 Base64 URL 安全编码的 VAPID 公钥转换为 Uint8Array。 然后使用 VAPID 公钥和 userVisible
值作为选项调用 pushManager.subscribe
。 您可以在此处 阅读有关可用选项 的更多信息。
用户订阅成功后,下一步就是将订阅数据发送到服务器。 数据将被发送到 django-webpush
包提供的 webpush/save_information
端点。 在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();
接下来,为 home.html
中的 registerSw.js
文件添加 script
标签。 打开文件:
nano ~/djangopush/templates/home.html
在 body
元素的结束标记之前添加 script
标记:
~/djangopush/templates/home.html
{% load static %} <!DOCTYPE html> <html lang="en"> <head> ... </head> <body> ... <script src="{% static '/js/registerSw.js' %}"></script> </body> </html>
因为服务工作者还不存在,如果您让应用程序继续运行或尝试重新启动它,您会看到一条错误消息。 让我们通过创建一个服务工作者来解决这个问题。
第 8 步——创建一个 Service Worker
要显示推送通知,您需要在应用程序的主页上安装一个活动的服务人员。 我们将创建一个服务工作者来监听 push
事件并在准备好时显示消息。
因为我们希望服务工作者的范围是整个域,所以我们需要将它安装在应用程序的根目录中。 您可以在这篇概述 如何注册服务工作者 的文章中阅读有关该过程的更多信息。 我们的方法是在 templates
文件夹中创建一个 sw.js
文件,然后我们将其注册为视图。
创建文件:
nano ~/djangopush/templates/sw.js
添加以下代码,告诉 service worker 监听推送事件:
~/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' }) ); });
服务工作者侦听推送事件。 在回调函数中,将 event
数据转换为文本。 如果事件数据没有,我们使用默认的 title
和 body
字符串。 showNotification
函数将通知标题、要显示的通知标题和 options 对象作为参数。 options 对象包含几个属性来配置通知的视觉选项。
为了让您的服务工作者为您的整个域工作,您需要将其安装在应用程序的根目录中。 我们将使用 TemplateView 来允许 service worker 访问整个域。
打开urls.py
文件:
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 作为模板和 application/x-javascript
作为模板的 content_type
来为 service worker 创建路径。
您现在已经创建了一个 service worker 并将其注册为一个路由。 接下来,您将在主页上设置表单以发送推送通知。
第 9 步 — 发送推送通知
使用主页上的表单,用户应该能够在您的服务器运行时发送推送通知。 您还可以使用 Postman 等任何 RESTful 服务发送推送通知。 当用户从首页的表单发送推送通知时,数据将包括一个head
和body
,以及接收用户的id
。 数据应按以下方式构建:
{ head: "Title of the notification", body: "Notification body", id: "User's id" }
为了监听表单的 submit
事件并将用户输入的数据发送到服务器,我们将在 ~/djangopush/static/js
目录下创建一个名为 site.js
的文件。
打开文件:
nano ~/djangopush/static/js/site.js
首先,向表单添加一个 submit
事件侦听器,使您能够获取表单输入的值和存储在模板的 meta
标记中的用户 ID:
~/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
属性中的用户id。 使用此信息,它可以向服务器上的 /send_push
端点发送 POST 请求。
要将请求发送到服务器,我们将使用本机 Fetch API。 我们在这里使用 Fetch 是因为大多数浏览器都支持它,并且不需要外部库来运行。 在您添加的代码下方,更新 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
,我们会发送请求并暂时禁用提交按钮。
完成的文件如下所示:
~/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
中:
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>
此时,如果您让应用程序继续运行或尝试重新启动它,您会看到一个错误,因为服务工作者只能在安全域或 localhost
上运行。 在下一步中,我们将使用 ngrok 创建通往我们的 Web 服务器的安全隧道。
第 10 步 — 创建安全隧道以测试应用程序
服务工作者需要安全连接才能在除 localhost
之外的任何站点上运行,因为它们可以允许连接被劫持,响应被过滤和伪造。 出于这个原因,我们将使用 ngrok 为我们的服务器创建一个安全隧道。
打开第二个终端窗口并确保您位于主目录中:
cd ~
如果您在先决条件中从干净的 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
您将看到以下输出,其中包括有关您的安全 ngrok URL 的信息:
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
。 您需要将其添加到 settings.py
文件中的 ALLOWED_HOSTS
列表中。
打开另一个终端窗口,导航到您的项目文件夹,然后激活您的虚拟环境:
cd ~/djangopush source my_env/bin/activate
打开settings.py
文件:
nano ~/djangopush/djangopush/settings.py
使用 ngrok 安全隧道更新 ALLOWED_HOSTS
列表:
~/djangopush/djangopush/settings.py
... ALLOWED_HOSTS = ['your_server_ip', 'ngrok_secure_url'] ...
导航到安全管理页面登录:https://ngrok_secure_url/admin/
。 您将看到如下所示的屏幕:
在此屏幕上输入您的 Django 管理员用户信息。 这应该与您在 先决条件步骤 中登录管理界面时输入的信息相同。 您现在可以发送推送通知了。
在浏览器中访问 https://ngrok_secure_url
。 您将看到一个提示,要求您获得显示通知的权限。 单击 Allow 按钮让您的浏览器显示推送通知:
提交填写的表格将显示类似于以下内容的通知:
注意: 在尝试发送通知之前,请确保您的服务器正在运行。
如果您收到通知,那么您的应用程序正在按预期工作。
您已经创建了一个 Web 应用程序,该应用程序在服务器上触发推送通知,并在服务人员的帮助下接收和显示通知。 您还完成了获取从应用程序服务器发送推送通知所需的 VAPID 密钥的步骤。
结论
在本教程中,您学习了如何使用通知 API 为用户订阅推送通知、安装 Service Worker 以及显示推送通知。
您可以通过配置通知以在单击时打开应用程序的特定区域来走得更远。 本教程的源代码可以在这里找到。