通知の変更を通知する

Matt Gaunt 氏

まず、タイトルがおかしくてごめんなさいだけど、できなかった。

Chrome 44 では、Notfication.dataServiceWorkerRegistration.getNotifications() が追加されており、プッシュ メッセージを含む通知を扱う際の一般的なユースケースが開放または簡素化されています。

通知データ

Notification.data を使用すると、JavaScript オブジェクトを通知に関連付けることができます。

要するに、プッシュ メッセージを受信したとき、いくつかのデータを使用して通知を作成し、notificationclick イベントでクリックされた通知を取得し、そのデータを取得できます。

たとえば、次のようにデータ オブジェクトを作成して通知オプションに追加します。

self.addEventListener('push', function(event) {
    console.log('Received a push message', event);

    var title = 'Yay a message.';
    var body = 'We have received a push message.';
    var icon = '/images/icon-192x192.png';
    var tag = 'simple-push-demo-notification-tag';
    var data = {
    doge: {
        wow: 'such amaze notification data'
    }
    };

    event.waitUntil(
    self.registration.showNotification(title, {
        body: body,
        icon: icon,
        tag: tag,
        data: data
    })
    );
});

つまり、notificationclick イベントで情報を取得できます。

self.addEventListener('notificationclick', function(event) {
    var doge = event.notification.data.doge;
    console.log(doge.wow);
});

これまでは、IndexDB にデータを保管するか、アイコンの URL の末尾に何かを配置する必要がありました。

ServiceWorkerRegistration.getNotifications()

プッシュ通知に取り組んでいるデベロッパーからよく寄せられる要望の一つに、表示する通知をより細かく制御してほしいというものがあります。

ユースケースの一例として、ユーザーが複数のメッセージを送信し、受信者に複数の通知を表示するチャット アプリケーションがあります。ウェブアプリが、表示されていない複数の通知を認識し、1 つの通知に折りたたむようにすることが理想的です。

getNotifications() を使用しない場合、最善の策は以前の通知を最新のメッセージで置き換えることです。getNotifications() を使用すると、通知がすでに表示されている場合は通知を「折りたたむ」ことができるため、ユーザー エクスペリエンスが向上します。

通知をグループ化する例。

これを行うコードは比較的シンプルです。push イベント内で ServiceWorkerRegistration.getNotifications() を呼び出して現在の通知の配列を取得し、そこから適切な動作(すべての通知の折りたたみまたは Notification.tag の使用)を判断します。

function showNotification(title, body, icon, data) {
    var notificationOptions = {
    body: body,
    icon: icon ? icon : 'images/touch/chrome-touch-icon-192x192.png',
    tag: 'simple-push-demo-notification',
    data: data
    };

    self.registration.showNotification(title, notificationOptions);
    return;
}

self.addEventListener('push', function(event) {
    console.log('Received a push message', event);

    // Since this is no payload data with the first version
    // of Push notifications, here we'll grab some data from
    // an API and use it to populate a notification
    event.waitUntil(
    fetch(API_ENDPOINT).then(function(response) {
        if (response.status !== 200) {
        console.log('Looks like there was a problem. Status Code: ' +
            response.status);
        // Throw an error so the promise is rejected and catch() is executed
        throw new Error();
        }

        // Examine the text in the response
        return response.json().then(function(data) {
        var title = 'You have a new message';
        var message = data.message;
        var icon = 'images/notification-icon.png';
        var notificationTag = 'chat-message';

        var notificationFilter = {
            tag: notificationTag
        };
        return self.registration.getNotifications(notificationFilter)
            .then(function(notifications) {
            if (notifications && notifications.length > 0) {
                // Start with one to account for the new notification
                // we are adding
                var notificationCount = 1;
                for (var i = 0; i < notifications.length; i++) {
                var existingNotification = notifications[i];
                if (existingNotification.data &&
                    existingNotification.data.notificationCount) {
                    notificationCount +=
existingNotification.data.notificationCount;
                } else {
                    notificationCount++;
                }
                existingNotification.close();
                }
                message = 'You have ' + notificationCount +
                ' weather updates.';
                notificationData.notificationCount = notificationCount;
            }

            return showNotification(title, message, icon, notificationData);
            });
        });
    }).catch(function(err) {
        console.error('Unable to retrieve data', err);

        var title = 'An error occurred';
        var message = 'We were unable to get the information for this ' +
        'push message';

        return showNotification(title, message);
    })
    );
});

self.addEventListener('notificationclick', function(event) {
    console.log('On notification click: ', event);

    if (Notification.prototype.hasOwnProperty('data')) {
    console.log('Using Data');
    var url = event.notification.data.url;
    event.waitUntil(clients.openWindow(url));
    } else {
    event.waitUntil(getIdb().get(KEY_VALUE_STORE_NAME,
event.notification.tag).then(function(url) {
        // At the moment you cannot open third party URL's, a simple trick
        // is to redirect to the desired URL from a URL on your domain
        var redirectUrl = '/redirect.html?redirect=' +
        url;
        return clients.openWindow(redirectUrl);
    }));
    }
});

このコード スニペットでまず強調すべき点は、フィルタ オブジェクトを getNotifications() に渡して通知をフィルタすることです。これにより、特定のタグ(この例では特定の会話)の通知リストを取得できます。

var notificationFilter = {
    tag: notificationTag
};
return self.registration.getNotifications(notificationFilter)

次に、表示されている通知を調べて、その通知に関連付けられている通知数があるかどうかを確認し、それに基づいて増分します。このようにして、未読メッセージが 2 つあることを通知する 1 つの通知がある場合、新しい push の受信時に未読メッセージが 3 件あることを通知します。

var notificationCount = 1;
for (var i = 0; i < notifications.length; i++) {
    var existingNotification = notifications[i];
    if (existingNotification.data && existingNotification.data.notificationCount) {
    notificationCount += existingNotification.data.notificationCount;
    } else {
    notificationCount++;
    }
    existingNotification.close();
}

注意すべき点は、通知に対して close() を呼び出して、通知リストから通知を削除する必要があることです。同じタグが使用されているため、各通知が次の通知に置き換えられるため、これは Chrome のバグです。現時点では、getNotifications() から返された配列にこの置換は反映されていません。

これは getNotifications() の一例にすぎません。ご想像のとおり、この API により他のさまざまなユースケースが可能になります。

NotificationOptions.vibrate

Chrome 45 以降では、通知の作成時にバイブレーション パターンを指定できます。Vibration API をサポートするデバイス(現時点では Android 向け Chrome のみ)では、通知が表示されるときに使用するバイブレーション パターンをカスタマイズできます。

バイブレーション パターンは、数値の配列か、1 つの数値(1 つの数値の配列)のいずれかになります。配列内の値はミリ秒単位で表され、偶数のインデックス(0、2、4、...)は振動する時間、奇数の指標は次のバイブレーションまでの一時停止時間を示します。

self.registration.showNotification('Buzz!', {
    body: 'Bzzz bzzzz',
    vibrate: [300, 100, 400] // Vibrate 300ms, pause 100ms, then vibrate 400ms
});

その他の一般的な機能リクエスト

デベロッパーから寄せられる共通の機能リクエストとして残っているのは、一定期間後に通知を閉じる機能や、通知が表示されている場合にのみ閉じることを目的としてプッシュ通知を送信する機能です。

現時点では、このような操作を行う方法はなく、仕様の規定によってそれが可能になる方法はありませんが、Chrome のエンジニアリング チームはこのユースケースを認識しています。

Android の通知

パソコンでは、次のコードを使用して通知を作成できます。

new Notification('Hello', {body: 'Yay!'});

これはプラットフォームの制限により、Android ではサポートされていませんでした。具体的には Chrome では、通知オブジェクト(外の通知)でのコールバックはサポートしていませんでした。ただし、デスクトップでは、現在開いているウェブアプリの通知を表示するために使用されます。

もともと、以下のようなシンプルな機能検出がパソコンのサポートに役立ち、Android でエラーを発生しないことが唯一の理由として挙げられます。

if (!'Notification' in window) {
    // Notifications aren't supported
    return;
}

ただし、Chrome for Android のプッシュ通知のサポートにより、Service Worker から通知は作成できますが、ウェブページからは通知を作成できません。そのため、この機能検出は適切ではなくなりました。Chrome for Android で通知を作成しようとすると、次のエラー メッセージが表示されます。

_Uncaught TypeError: Failed to construct 'Notification': Illegal constructor.
Use ServiceWorkerRegistration.showNotification() instead_

Android とパソコンの機能検出の現時点での最善の方法は、以下を行うことです。

    function isNewNotificationSupported() {
        if (!window.Notification || !Notification.requestPermission)
            return false;
        if (Notification.permission == 'granted')
            throw new Error('You must only call this \*before\* calling
    Notification.requestPermission(), otherwise this feature detect would bug the
    user with an actual notification!');
        try {
            new Notification('');
        } catch (e) {
            if (e.name == 'TypeError')
                return false;
        }
        return true;
    }

これは次のように使用できます。

    if (window.Notification && Notification.permission == 'granted') {
        // We would only have prompted the user for permission if new
        // Notification was supported (see below), so assume it is supported.
        doStuffThatUsesNewNotification();
    } else if (isNewNotificationSupported()) {
        // new Notification is supported, so prompt the user for permission.
        showOptInUIForNotifications();
    }