如何从Django应用程序发送Web推送通知

来自菜鸟教程
跳转至:导航、​搜索

作为 Write for DOnations 计划的一部分,作者选择了 Open Internet/Free Speech Fund 来接受捐赠。

介绍

网络在不断发展,现在它可以实现以前只能在原生移动设备上使用的功能。 JavaScript 服务工作者 的引入为 Web 提供了新的能力来执行诸如后台同步、离线缓存和发送 推送通知 之类的事情。

推送通知允许用户选择接收移动和 Web 应用程序的更新。 它们还使用户能够使用定制的相关内容重新参与现有的应用程序。

在本教程中,您将在 Ubuntu 18.04 上设置一个 Django 应用程序,只要有需要用户访问该应用程序的活动,它就会发送推送通知。 要创建这些通知,您将使用 Django-Webpush 包并设置和注册服务工作者以向客户端显示通知。 带有通知的工作应用程序将如下所示:

先决条件

在开始本指南之前,您需要以下内容:

第 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_keyyour_vapid_private_keyadmin@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:通知信息,包括通知headbody
  • 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.shortcutsrender 函数:

~/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_URLSTATIC_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 端点需要有关订阅状态(subscribeunsubscribe)、订阅数据和浏览器的信息。 最后,我们调用 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 数据转换为文本。 如果事件数据没有,我们使用默认的 titlebody 字符串。 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 服务发送推送通知。 当用户从首页的表单发送推送通知时,数据将包括一个headbody,以及接收用户的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 函数获取表单内的 inputtextareabutton。 它还从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;
    }
});

如果存在三个必需的参数 headbodyid,我们会发送请求并暂时禁用提交按钮。

完成的文件如下所示:

~/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 以及显示推送通知。

您可以通过配置通知以在单击时打开应用程序的特定区域来走得更远。 本教程的源代码可以在这里找到。