DjangoアプリケーションからWebプッシュ通知を送信する方法

提供:Dev Guides
移動先:案内検索

著者は、 Open Internet / Free Speech Fund を選択して、 Write forDOnationsプログラムの一環として寄付を受け取りました。

序章

Webは絶えず進化しており、以前はネイティブモバイルデバイスでしか利用できなかった機能を実現できるようになりました。 JavaScript サービスワーカーの導入により、バックグラウンド同期、オフラインキャッシュ、プッシュ通知の送信などを行うための新しい機能がWebに提供されました。

プッシュ通知を使用すると、ユーザーはオプトインしてモバイルおよびWebアプリケーションの更新を受信できます。 また、ユーザーは、カスタマイズされた関連コンテンツを使用して、既存のアプリケーションを再利用できます。

このチュートリアルでは、Ubuntu 18.04でDjangoアプリケーションをセットアップします。このアプリケーションは、ユーザーがアプリケーションにアクセスする必要があるアクティビティがあるたびにプッシュ通知を送信します。 これらの通知を作成するには、 Django-Webpush パッケージを使用し、サービスワーカーを設定して登録し、クライアントに通知を表示します。 通知付きの動作中のアプリケーションは次のようになります。

前提条件

このガイドを開始する前に、次のものが必要です。

  • 非rootユーザーとアクティブなファイアウォールを備えた1つのUbuntu18.04サーバー。 Ubuntu 18.04サーバーの作成方法の詳細については、この初期サーバーセットアップガイドのガイドラインに従うことができます。
  • pipおよびvenvは、これらのガイドラインに従ってインストールされます。
  • ホームディレクトリに作成されたdjangopushというDjangoプロジェクトは、 Ubuntu18.04でサンプルのDjangoプロジェクトを作成する際のガイドラインに従って設定します。 サーバーのIPアドレスをsettings.pyファイルのALLOWED_HOSTSディレクティブに必ず追加してください。

ステップ1—Django-WebpushのインストールとVapidキーの取得

Django-Webpushは、開発者がWebプッシュ通知をDjangoアプリケーションに統合して送信できるようにするパッケージです。 このパッケージを使用して、アプリケーションからプッシュ通知をトリガーして送信します。 このステップでは、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

webpushINSTALLED_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

Webプッシュ通知を設定する次のステップは、VAPIDキーを取得することです。 これらのキーはアプリケーションサーバーを識別し、サブスクリプションを特定のサーバーに制限するため、プッシュサブスクリプションURLの機密性を減らすために使用できます。

VAPIDキーを取得するには、 wep-push-codelabWebアプリケーションに移動します。 ここでは、自動的に生成されたキーが提供されます。 秘密鍵と公開鍵をコピーします。

次に、settings.pyにVAPID情報の新しいエントリを作成します。 まず、ファイルを開きます。

nano ~/djangopush/djangopush/settings.py

次に、WEBPUSH_SETTINGSという新しいディレクティブを、VAPIDの公開鍵と秘密鍵、およびAUTH_PASSWORD_VALIDATORSの下にある電子メールとともに追加します。

〜/ djangopush / djangopush / settings.py

...

AUTH_PASSWORD_VALIDATORS = [
    ...
]

WEBPUSH_SETTINGS = {
   "VAPID_PUBLIC_KEY": "your_vapid_public_key",
   "VAPID_PRIVATE_KEY": "your_vapid_private_key",
   "VAPID_ADMIN_EMAIL": "[email protected]"
}

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

...

プレースホルダー値your_vapid_public_keyyour_vapid_private_key、および[email protected]を独自の情報に置き換えることを忘れないでください。 あなたのメールアドレスは、プッシュサーバーで問題が発生した場合に通知される方法です。

次に、アプリケーションのホームページを表示し、サブスクライブしたユーザーへのプッシュ通知をトリガーするビューを設定します。

ステップ2—ビューを設定する

このステップでは、基本的なhome ビューを、send_pushビューとともにホームページのHttpResponse応答オブジェクトとともにセットアップします。 ビューは、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ビューには2つのデコレータを使用しています。ビューをPOSTリクエストのみに制限するrequire_POSTデコレータと、ビューをCSRF保護から除外するcsrf_exemptデコレータです。 。

このビューはPOSTデータを想定し、次のことを行います。リクエストのbodyを取得し、 json パッケージを使用して、json.loadsを使用してJSONドキュメントをPythonオブジェクトに逆シリアル化します。json.loadsは、構造化されたJSONドキュメントを取得し、それをPythonオブジェクトに変換します。

ビューは、リクエスト本文オブジェクトに次の3つのプロパティがあることを想定しています。

  • head:プッシュ通知のタイトル。
  • body:通知の本文。
  • id:リクエストユーザーのid

必要なプロパティのいずれかが欠落している場合、ビューは404「見つかりません」ステータスのJSONResponseを返します。 指定された主キーを持つユーザーが存在する場合、ビューはdjango.shortcutsライブラリのget_object_or_404関数を使用して、一致する主キーを持つuserを返します。 ユーザーが存在しない場合、関数は404エラーを返します。

このビューでは、webpushライブラリのsend_user_notification関数も使用されます。 この関数は3つのパラメーターを取ります。

  • User:プッシュ通知の受信者。
  • payload:通知headおよびbodyを含む通知情報。
  • ttl:ユーザーがオフラインの場合に通知を保存する最大時間(秒単位)。

エラーが発生しない場合、ビューは200の「成功」ステータスとデータオブジェクトを持つJSONResponseを返します。 KeyErrorが発生した場合、ビューは500の「内部サーバーエラー」ステータスを返します。 KeyErrorは、オブジェクトの要求されたキーが存在しない場合に発生します。

次のステップでは、作成したビューに一致する対応するURLルートを作成します。

ステップ3—URLをビューにマッピングする

Djangoを使用すると、URLconfと呼ばれるPythonモジュールを使用してビューに接続するURLを作成できます。 このモジュールは、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には、2つのフィールドを持つフォームが含まれています。input要素は通知の先頭/タイトルを保持し、textarea要素は通知本文を保持します。

ファイルのheadセクションには、VAPID公開鍵とユーザーのIDを保持する2つのmetaタグがあります。 これらの2つの変数は、ユーザーを登録してプッシュ通知を送信するために必要です。 サーバーにAJAXリクエストを送信し、idを使用してユーザーを識別するため、ここではユーザーのIDが必要です。 現在のユーザーが登録ユーザーの場合、テンプレートはidをコンテンツとしてmetaタグを作成します。

次のステップは、テンプレートの場所を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.pyファイルのプロジェクトのすべての設定を含むsettings構成や、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、およびテンプレートで使用される変数を含むオブジェクトの3つのパラメーターを取ります。

テンプレートを作成し、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/'

次に、Djangoが静的ファイルを検索するSTATICFILES_DIRSというディレクトリのリストを追加します。

〜/ djangopush / djangopush / settings.py

...
STATIC_URL = '/static/'
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, "static"),
]

これで、urls.pyファイルで定義されたパスのリストにSTATIC_URLを追加できます。

ファイルを開きます。

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—サービスワーカーの登録とプッシュ通知へのユーザーのサブスクライブ

Webプッシュ通知は、サブスクライブしているアプリケーションに更新がある場合にユーザーに通知したり、過去に使用したアプリケーションに再度アクセスするようにユーザーに促したりできます。 これらは、 pushAPIとnotificationsAPIの2つのテクノロジーに依存しています。 どちらのテクノロジーも、サービスワーカーの存在に依存しています。

サーバーがServiceWorkerに情報を提供し、ServiceWorkerが通知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関数を呼び出します。 ブラウザでサービスワーカーがサポートされていない場合は、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の値を使用して、ユーザーが通知を有効にしているかどうか。
  • ユーザーが通知を表示するためのアプリケーション権限を付与したかどうか。
  • ブラウザがPushManagerAPIをサポートしているかどうか。 これらのチェックのいずれかが失敗した場合、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関数は、パラメーターとして渡されたサブスクリプション情報を使用して呼び出されます。

アクティブなサブスクリプションが存在しない場合、Base64 URLセーフでエンコードされたVAPID公開鍵は、urlB64ToUint8Array関数を使用してUint8Arrayに変換されます。 次に、pushManager.subscribeは、VAPID公開鍵とuserVisible値をオプションとして使用して呼び出されます。 利用可能なオプションの詳細については、こちらをご覧ください。

ユーザーを正常にサブスクライブした後、次のステップはサブスクリプションデータをサーバーに送信することです。 データは、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.htmlregisterSw.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>

Service Workerはまだ存在しないため、アプリケーションを実行したままにするか、アプリケーションを再起動しようとすると、エラーメッセージが表示されます。 サービスワーカーを作成してこれを修正しましょう。

ステップ8—サービスワーカーの作成

プッシュ通知を表示するには、アプリケーションのホームページにアクティブなServiceWorkerがインストールされている必要があります。 pushイベントをリッスンし、準備ができたらメッセージを表示するServiceWorkerを作成します。

Service Workerのスコープをドメイン全体にする必要があるため、アプリケーションのルートにインストールする必要があります。 このプロセスの詳細については、サービスワーカーの登録方法の概要を説明しています。 私たちのアプローチは、templatesフォルダーにsw.jsファイルを作成し、それをビューとして登録することです。

ファイルを作成します。

nano ~/djangopush/templates/sw.js

次のコードを追加します。これは、サービスワーカーにプッシュイベントをリッスンするように指示します。

〜/ 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オブジェクトには、通知の視覚的オプションを構成するためのいくつかのプロパティが含まれています。

Service Workerがドメイン全体で機能するには、アプリケーションのルートにサービスワーカーをインストールする必要があります。 TemplateView を使用して、ServiceWorkerがドメイン全体にアクセスできるようにします。

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メソッドは、最近作成されたサービスワーカーをテンプレートとして渡し、application/x-javascriptをテンプレートのcontent_typeとして渡すことにより、サービスワーカーのパスを作成します。

これで、サービスワーカーが作成され、ルートとして登録されました。 次に、プッシュ通知を送信するようにホームページにフォームを設定します。

ステップ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関数は、フォーム内にinputtextarea、およびbuttonを取得します。 また、metaタグから、名前属性user_idや、タグのcontent属性に格納されているユーザーのIDなどの情報を取得します。 この情報を使用して、サーバー上の/send_pushエンドポイントにPOST要求を送信できます。

サーバーにリクエストを送信するには、ネイティブの FetchAPIを使用します。 ここでは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;
    }
});

3つの必須パラメーターheadbody、および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を使用してサーバー用の安全なトンネルを作成します。

2番目のターミナルウィンドウを開き、ホームディレクトリにいることを確認します。

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

アプリケーションの安全なトンネルを作成する前に、これを行う必要があります。

2番目のターミナルウィンドウで、プロジェクトフォルダーに移動し、仮想環境をアクティブ化します。

cd ~/djangopush
source my_env/bin/activate

アプリケーションへの安全なトンネルを作成します。

ngrok http your_server_ip:8000

次の出力が表示されます。これには、安全なngrokURLに関する情報が含まれています。

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

ALLOWED_HOSTSのリストをngrokセキュアトンネルで更新します。

〜/ djangopush / djangopush / settings.py

...

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

安全な管理ページに移動してログインします:https://ngrok_secure_url/admin/。 次のような画面が表示されます。

この画面にDjango管理者ユーザー情報を入力します。 これは、前提条件の手順で管理インターフェースにログインしたときに入力した情報と同じである必要があります。 これで、プッシュ通知を送信する準備が整いました。

ブラウザでhttps://ngrok_secure_urlにアクセスします。 通知を表示する許可を求めるプロンプトが表示されます。 許可ボタンをクリックして、ブラウザにプッシュ通知を表示させます。

記入済みのフォームを送信すると、次のような通知が表示されます。

注:通知を送信する前に、サーバーが実行されていることを確認してください。


通知を受け取った場合、アプリケーションは期待どおりに機能しています。

サーバー上でプッシュ通知をトリガーし、サービスワーカーの助けを借りて、通知を受信して表示するWebアプリケーションを作成しました。 また、アプリケーションサーバーからプッシュ通知を送信するために必要なVAPIDキーを取得する手順も実行しました。

結論

このチュートリアルでは、通知APIを使用して、ユーザーをサブスクライブしてプッシュ通知をサブスクライブし、サービスワーカーをインストールし、プッシュ通知を表示する方法を学習しました。

クリックしたときにアプリケーションの特定の領域を開くように通知を構成することで、さらに先に進むことができます。 このチュートリアルのソースコードはここにあります。