Pusna-RSにService Worker Pushの機能が実装されました!
伊藤 瑛
はじめに
こんにちは。
リクルートテクノロジーズAPソリューショングループの伊藤です。
前回はリクルートのスマホアプリで用いられているPush通知基盤、Pusna-RSについて、
こんな記事を書かせていただきました。
今回も前回に引き続き、Pusna-RSについての話題です。
Service Worker Push 日本初の商用利用開始!
さて、こちらのプレスリリースにもあります通り、2015年12月16日にSUUMO Android版のWebサイトにおいて、
Service Worker Pushが商用利用としては日本で初めて導入されました。
この裏側でPush通知を送っている仕組みこそ、何を隠そうPusna-RSです。
私はPusna-RS側の担当者としてこちらのプロジェクトに関わらせて頂きました。
今回はこのService Worker Pushの仕組みと開発の裏側を話したいと思います。
Service Workerとは?
Service Workerとは、ブラウザが表示しているWebページとは完全に独立して実行されるスクリプトです。
詳しくは、このページにまとまっていますが、
その機能の1つにPush通知をブラウザに対して送信するものがあります。
スマホのアプリ上のPush通知をユーザのブラウザに対して行うことができるようになる機能です。
これにより、ユーザが例えそのWebページを表示していなくても、バックグラウンドで通知を行うことが出来るようになり、
Push通知の優れた訴求効果をWebサイトにおいても得ることができるのではと期待されています。
Service Worker Pushのアーキテクチャ
Service Worker Pushは以下の図のような手順で利用できます。
1. Pushを飛ばしたいサイトのjsにService Workerを登録するコードを実装する。
2. Google Developer Consoleから、プロジェクトを作成してProject IDとkeyを取得。
3. Service WorkerのjavascriptにPush Event Listenerを実装する
3. GCMにPushリクエストを送信
4. ブラウザにPush通知が届く!
より詳しくはGoogle Developers Blogにまとまっていますので、見てみてください。
Pusna-RSではGCMから発行されるトークンの管理や、Pushリクエストの送信の機能を全社のモバイルアプリで使用できる基盤として提供しています。
この機能を提供することによりアプリ開発者はデータベースの運用やトークンのライフタイムの管理をすることなく、APIに数回リクエストを送るだけで簡単にPush通知を送ることができます。
Service Worker Pushの実装
クライアント側サンプルコード
せっかくなので、Service Worker Pushを使うためのクライアント側のサンプルコードを書いてみます。
上の手順でいうと、1と3あたりの話題です。
Google Developers Blogにも同様のコードは乗っていますが、Promiseがネストしていて少し分かり辛いと思ったので、
ES2015/2016の機能を使ってすっきりと書き直してみました。実際に用いる場合はbabelを使ってトランスパイルしてください。
(実際にサイトで動作しているコードとは異なるので注意してください!)
- Service Workerを登録するクラス (service_worker_controller.js)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
'use strict'; export default class ServiceWorkerController { /** ブラウザがService Worker Pushに対応しているかをチェックする。 */ isPossibleNotification() { if (navigator && ('serviceWorker' in navigator)) { throw new Error('Service Workerに対応していません') } if (!('showNotification' in ServiceWorkerRegistration.prototype) ) { throw new Error('Notificationの表示に対応していません'); } if (Notification && Notification.permission === 'denied') { throw new Error('Notificationがブロックされていますしています'); } if (!('PushManager' in window)) { throw new Error('Pushに対応していません'); } } /** Push通知に必要なSubscriptionを取得する。 */ async subscribe() { // Service Workerとして用いるjsを登録する await navigator.serviceWorker.register('./service_worker.js'); const serviceWorkerRegistration = await navigator.serviceWorker.ready; // 過去に登録したsubscriptionが存在すればとってくる。なかったら新たに取得する。 const subscription = await serviceWorkerRegistration.pushManager.getSubscription() || await serviceWorkerRegistration.pushManager.subscribe({userVisibleOnly: true}); /* subscriptionはObject形式で返ってくる。 必要なのはendpointプロパティのURLの末尾のtokenだけなので、パースしてreturnする。 */ return subscription.endpoint.split('/').pop(); } } |
このように、非同期処理の結果によって条件が分岐するような場合、Promiseよりもasync/awaitを用いたほうがスッキリとコードが書けると思います。
先日のNode学園祭でも議論になりましたが、async/awaitの仕様はまだ正式なものではなく、将来的に仕様が変わる可能性があることに注意してください。
また、上記のコードの中ではsubscribe(tokenを発行するところ)だけですが、本来ならばunsubscribe(発行したtokenを無効にする)処理の実装も必要です。
こちらの処理も非同期処理が複雑に絡むので、async / awaitを使ってスッキリ書くのがオススメです。
- フロントエンドのjs (index.js)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
'use strict'; import ServiceWorkerController from './service_worker_controller'; const serviceWorkerController = new ServiceWorkerController; try { serviceWorkerController.isPossibleNotification(); } catch (e) { console.log('Push通知に対応していません'); console.log(e); } // babelを用いればasync functionもPromiseで解決できる serviceWorkerController.subscribe().then((subscription) => { // 本来は取得してきたsubscriptionをサーバに送信するなどの処理をいれる console.log(subscription); }).catch((error) => { // エラー処理をする console.log(error); }); |
最近流行りのWebPackなどを用いる場合、フロントエンドでservice_worker_controllerを呼び出すコードはこういった書き方になるかと思います。
async functionでライブラリを作っておけばPromiseベースの書き方でも非同期処理を解決できます。
Promise / Generator / async・awaitと最近非同期処理の書き方が様々出てきていますが、それぞれ適材適所で使っていきたいですね。
- Service WorkerにPushイベントのリスナーを登録するコード (service_worker.js)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
'use strict'; self.addEventListener('push', (evt) => { /* 現在のServiceWorkerのPush通知の仕様では任意のメッセージを表示させることができない。 任意のメッセージを表示させたい場合、Service WorkerのPushイベントハンドラの中で外部のAPIサーバに問い合わせを行う必要がある。 また、XMLHttpRequestはService Worker内では使えないため、fetch APIを使う必要がある。 fetch('api host url').then((res) => { // ...... }); */ self.registration.showNotification(notification.title, notification.body); }); self.addEventListener('notificationclick', (evt) => { evt.notification.close(); }); |
このコードは実際にService Workerとして、フロントのWebページとは独立して実行されます。
ここではPushイベントのハンドラを登録しています。
1点注意しなければいけないのは、現在のGCMのService Worker Pushの仕様だと、任意のメッセージをPush通知で表示させることができません。
つまり、あらかじめソースコードに埋め込んだ固定のメッセージしか表示できません。
どうしても、場合に応じて任意のメッセージを表示させたい場合は、サンプルに書いたように、外部APIサーバに問い合わせを行い、メッセージを取得してくることで可能になります。
これでブラウザにPush通知を送る準備が整いました。
以下のコマンドを実行してブラウザにPush通知を送ってみましょう。
1 2 3 4 5 6 7 |
$ curl -k --header 'Authorization: key=<GCMで取得したAuthorization Key>' \ --header 'Content-Type:"application/json"' \ https://android.googleapis.com/gcm/send -d "{ \"registration_ids\":[ \"<subscribeで取得したtoken>\" ] }" |
Authorization KeyにはGCMで発行したAPI Keyを、registration_idsには上記のServiceWorkerController.subscribe()の返り値をいれましょう。
これで以下の画像のようなPush通知がブラウザに届くはずです。
Pusna-RS側の実装
Pusna-RSがService Worker Pushの中で担った役割は、ブラウザのSubscription Tokenの管理と、
実際にPushリクエストをGCMに送信する部分です。
上記のコードの中でいうと、index.jsの中の~本来なら~とかいてある部分以降の処理と、curlの部分の処理です。
こう書いてしまうとあっさりしていますが、実はサービスの規模が大きくなってくると、Tokenの管理やリアルタイムにPush配信を行うのはかなり難しく、それなりに煩雑になってきます。
この処理をPusna-RSは全て受け持っていて、Push通知を飛ばしたい事業側としては、配信対象のデバイスデータを管理することなく、APIを数回たたくだけで高速にPush通知を配信することができます。
Pusna-RSでは配信登録にNode.jsの中心的な機能であるStream APIを活用しています。
当然、Service Worker Pushの配信にもStreamを使っています。
下の図をご覧ください。これがPusna-RSでPush配信を行っているStreamの全体構成になっています。
この図からも分かる通り複数のStreamをpipe()でつなげて、
対象となるデバイスの取得から配信登録、配信結果の処理まで、一連の処理を複数のStreamで非同期に行っています。
今回のService WorkerのPush配信についてもこのStreamの中に組み込む形で実装しました。
今回のような比較的大規模な機能追加でも、データの形式を変換するTransfer Streamを挟んでやれば、既存コードの修正を最小限に抑えることができ、簡単に実装することが出来ました。
Stream APIはよく「データの流れを綺麗にかける」や「I/O性能を最大限に引き出せる」といった説明がされますが、個人的にはこの拡張性と柔軟性も魅力の一つだと思っています。
また、Stream3になり、以前ではなかなか実装が難しかった複雑な処理も簡単に実装できるようになりました。
手前味噌で恐縮ですが、このあたりの話題は自分の以前の記事を御覧になってください。
ちなみに今回新たに組み込まれたServiceWorkerNotificationStreamはnpmパッケージのreadable-streamを使ってNode.js v0.8系を用いつつも、Node.js v4系へのアップグレードに備え、Stream3の記法と機能を用いて実装しています。
まとめとこれからの展開
今回はService Worker Pushの仕組みと、Pusna側の実装について説明しました。
今の所、Service Worker PushはSUUMOにしか提供を行っていませんが、今後、様々な事業に拡大をしていく予定になっております。
自分は新卒のエンジニアとして今年の7月に今の部署に配属され、主にサーバーサイドの開発運用を担っていますが、今後も今回のように必要に応じて様々な技術をこのブログ上で共有していきます!
それでは今年もよろしくお願い致します!