JavaScript
HTML5
Chrome
firefox
ServiceWorker

Webでプッシュ通知するサービスを個人開発で作ってみた+ServiceWorkerPushAPIの実装方法まとめ

More than 1 year has passed since last update.

Webプッシュ通知

アプリの世界では当たり前になったプッシュ通知が、Webの世界にも来てますよ、と。まずはChromeから。そしてFirefoxもPushAPIにNightly版で対応をしています。正式対応したFirefoxは11月にリリースされる予定です(FirefoxはすでにSimplePushというFirefoxOSやForefoxMobileの機能があり、これをベースに開発をしているようです)。Webプッシュ通知は最近だとFacebookが対応をしており、Webでプッシュ通知を表示する流れは加速しそうですね。

というわけで、個人的に開発をしたサービスの紹介と、働きながら個人開発したプロセスの振り返り、その中で分かったServiceWorker PushAPIの実装方法をまとめてみました。

個人サービスを作ってみての振り返り

作ったサービス:Pushnate

Pushnate(プッシュネイト)というサービスを作りました。現時点ではまだベータ的な立ち位置で、単純なプッシュ通知しかできませんが近い将来アップデートをして細かい配信設定を出来るようにする予定です。無料なので是非試してみてください。

pushnate.png

開発サマリ

項目 内容
開発期間 約3ヶ月
開発人数 1人
コミット数 181
ステップ数 785
言語 Ruby / JavaScript
フレームワーク Ruby on Rails / Babel / Twitter Bootstrap / jQuery
テスト Rspec
Gitホスティング Bitbucket
サーバ さくらVPS
監視 new relic / SecureStar

酒と子どもと開発と

開発期間に約3ヶ月間かかっていますが、実際にコードを書いた(コミットした)のは約24日間。家に帰って夜中にやったり、週末にまとめてやったりしたので、個人サービスならこんなものかなと思います。

僕はお酒が好きでほぼ毎日家で飲んでいるのですが、ついつい飲み過ぎてコードを書くことが出来ないことがよくありました。こんなんでは個人開発であるかぎり、いつまで経ってもリリースできません。なので、事前に「今日はコードを最低3時間は書くぞ」と決めて、酒を飲む量をコントロールしました。

ポイントは、禁酒はしなかったというところです。僕は自分が意思の弱い人間(特にお酒に対して)で、禁酒なんて難易度の高いことに挑戦するよりも、ビールを1本飲みながら楽しく開発しよう!という方針のほうが継続できるということを知っているのです。という言い訳ですね。はい。

また、1歳半の子供もいるため週末の大半は家族と過ごします。なので集中可能な時間は子供が寝た後の20〜21時以降になります。このコアタイムを狙って、すぐにコードが書けるように夕飯の片付けなど家事を済ませておくとスムーズでした。頑張り過ぎると翌日以降に響くので、夜中の2〜3時位までを区切りとしました。

ある意味、メリハリが付いて週末の過ごし方も、より楽しくなりましたね。

  • :smile::thumbsup: コードを書く日は事前にちゃんと計画して実行する
  • :imp::thumbsdown: お酒は飲み過ぎない/開発もいいけど家族もね

フレームワークは慣れ親しんでるRails

また、今回の開発の中でキモとなるのが、ServiceWorkerを中心としたJavaScriptと、プッシュの大量配信インフラ部分だったので、サーバサイドはあまり気を使わなくて済むよう慣れ親しんでいるRailsを選択しました。

個人サービスを作る際には、技術全てを挑戦的なものにするのではなく、普段慣れ親しんでる技術を織り交ぜて開発するのが良いと思います。でないと、ただでさえ時間がとれなくて進捗ないのに、技術的にもつまって進捗なくて楽しいはずの個人サービス開発がツライ日々になるだけです。ただ、最低1つは今までやったことのない技術を放り込んでみるのが刺激的で良いです。今回だと、サービスそのもののServiceWorkerですね。

  • :smile::thumbsup: サービスを作る楽しみと、技術を知る楽しみを組み合わせる

テストコードは必ず書く

それと今回は個人サービスであってもテストコードは、なるべく書くようにしました。今までは、どうでもいい個人サービスに関してはテストコードを省略することがほとんどだったんですが、実際にユーザに提供するようなサービスの場合はテストがないと厳しいと思ってそうしました。実際、テストがあったほうが楽ですね。すでに1回部分的なリファクタリングを行っていますがストレスなくやれました。

  • :smile::thumbsup: テストがあると一人でもアグレッシブに改善できる
  • :imp::thumbsdown: PrivateリポジトリだとCIサーバが有料でつらい…

BabelでES2015へ

ServiceWorker前提のサービスなので、動作環境もモダンなブラウザに限られます。なのでレガシーな環境をあまり気にすることなくBabelを使ってES2015を導入しました。ES2015といっても、PromiseとClass、ArrowFunctionとか程度ですが…。Gemにsprockets-es6というのがあり、それをいれれば勝手にビルドしてくれるので導入は超絶楽でした。

  • :smile::thumbsup: ES2015の書き方が楽/Promiseが便利
  • :imp::thumbsdown: 書き味だけならCoffeeでも良い気がする

デザインはTwitter Bootstrap頼み&フリー素材

僕はプログラマなのでデザイナではないです。つまり、コードを書くことは得意でもデザインは得意ではないのです。そうなると個人サービスを作っていて最も困るのはデザインです。あまり時間もかけられないので、ここは奇をてらわずTwitter Bootstrap先生頼みでいくことにしました。Twitter Bootstrapも極力カスタマイズせず、単にテーマを適用するくらいに留めて工数を削減&最低限それっぽい見た目に。

とはいえ、世に溢れているフラットデザインのテーマは飽きたので、今回はMaterial Design fot Bootstrapというテーマを使用しました。Googleのデザインガイドラインに沿った?テーマのようです。わりとサクッとマテリアルな雰囲気になるので結構良かったんですが、非デザイナが安易に使うと、単なるモタっとしたダサいデザインになってしまうのが難しいです…。もしかしたら将来的には別のテーマに変更するかもしれません。

素材について

また非デザイナなので、アイコンとかはフリー素材を活用することになります。今時なフラットデザインなアイコンが欲しいと検索していたら、なんと商用利用可能で、かつ著作権の表記なしという素晴らしいサイトを見つけました。クオリティもかなり高く、これが無料とは…とビックリしました。

このサイト、他にもいろいろなテイストの素材サイトをやられているようです。感謝カンゲキ雨嵐。

レスポンシブデザインについて

Pushnateはデスクトップもモバイルも両方ターゲットのサービスです。なので、スマホ対応も必要です。でも、両デバイス用にViewを作る余裕なんてない。というわけで、レスポンシブデザインを採用しました。シンプルなページ構成なので、とても簡単にスマホ対応できました。

ぶっちゃけ開発工数の中で一番デザインに割いた時間が多かったです…。

  • :smile::thumbsup: デザインは割りきってフレームワークや素材を活用する、凝りすぎない
  • :imp::thumbsdown: Materialなどのシンプル系デザインは、素人には扱いが難しい

ロゴ画像はSVG

Pushnateのロゴは画像ではなくSVGで描画しています。最初は普通に透過PNG画像にしようと思ったんですが、スクロールするタイミングで色が反転するような作りにしたため、画像を差し替えたりする必要があり面倒だったのでSVGにしてCSSで色変更してやれば楽やん!という安易な発想でSVGを採用しました。

Illustratorでロゴ画像を作成後、SVGファイルとして出力。それを<img>タグで表示させたんですが、なんとSVGファイルの場合、JavaScriptやCSSでの操作が不可能ということに後で気がつくという…。直接SVGをHTML上に書き込めば操作できるので、仕方なしにブチ込むことに。シンプルなロゴなので、まだ良いけどHTMLのソースは汚くなってしまった。

  • :smile::thumbsup: CSSで簡単に色変更など操作が可能で楽
  • :imp::thumbsdown: 埋め込みだとHTMLのソースがアレ

インフラ周りは安価なもので

業務ではAWSを使っていて簡単にスケーラブルな環境を用意できるのですが、個人にはちょっと費用の面で厳しい…なので、ひとまずサービス初期に関してはスケーラビリティは捨てて、さくらVPSで動かすようにしました。サービスが大きくなったらAWSに移行する予定です。Chefなどでインフラのレシピを作っているわけじゃないので、再度構築し直すコストがアレですが…。

また、リソース監視もZabbixを動かすほどでもないし動かすサーバもないので、new relicの無料サービスにしました。また、死活監視にはGMOのSecureStarというサービスを利用しています。無料で外部からの疎通確認をしてくれます。

あとは、サービスがある程度大きくなってきたら、AirbrakeやCircleCIなどの有料サービスを利用したいですね。個人で開発する場合、外部に移譲できるような機能は、積極的に採用していったほうが良い気がします。

  • :smile::thumbsup: 極力外部サービス利用して構築/管理コストを下げる
  • :imp::thumbsdown: 最初からChefなどで再現性のあるインフラ環境にしておく

ServiceWorker PushAPIの実装

ServiceWorkerを使ったPushAPIをサイトに実装する方法をまとめてみます。PushAPIはまだまだ各ブラウザで実装中の機能でバージョンにより仕様が変わったりします。2015年9月末時点での実装方法です。ServiceWorkerそのものについては、HTML5RockのServiceWorker紹介記事を見てみてください。

ライフサイクルはわりとシンプルで、インストール後、アクティベートされ、fetch/messageイベントで処理が行われ、終了すると勝手に終了します。

PushAPI対応状況

デスクトップ

ブラウザ 対応状況
Chrome 42〜
Firefox 42〜(Nightly)
IE 未対応
Edge 未対応
Opera 未対応(ServiceWorker自体には対応)
Safari 未対応

モバイル

ブラウザ 対応状況
Chrome 42〜
Firefox 未対応
Opera 未対応
Safari 未対応

基本的には、ChromeとNightly版FirefoxのみがPushAPIに対応をしています。ただWindows10のEdgeに関してはServiceWorkerの対応をするかもしれません(PushAPIかどうかは分からないけど)。

参考 : https://twitter.com/jacobrossi/status/608291251121618944

問題はSafariですがAppleからは特になんのアナウンスもないですね。iOS9でも特に動きはなかったので、対応されるとしたらiOS10以降でしょう。

Webプッシュ通知のサンプル

Webプッシュには2段階あります。ひとつ目はServiceWorkerのインストール。ふたつ目はプッシュ通知の配信許可です。当たり前ですが、ユーザに勝手にプッシュ通知を配信することは出来ません。「許可しますか?」のダイアログが表示されます。iPhoneのアプリと同様ですね。

ServiceWorkerのインストール

push.jsというファイル名のServiceWorkerをnavigator.serviceWorker.registerというメソッドでインストールします。第2引数のscopeプロパティで、ServiceWorkerの有効スコープを指定できます。ここではトップレベルディレクトリを指定しているので、ドメイン内のどのページでも有効になります。なお、インストールするServiceWorkerスクリプトより上の階層にはScopeは指定できません。

if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/push.js', {scope: '/'})
        .then(subscribe)
        .catch((error) => {
            console.log('インストールができませんでした');
        }
    );
} else {
    console.log('非対応ブラウザです');
}

次にPushManager.subscribeを使って、プッシュ通知の許可ダイアログを表示します。Chrome44以前ではgcm_user_visible_onlyで指定していましたが、45以降 + Firefoxでは{userVisibleOnly: true}を指定して上げる必要があります。ユーザが許可をするとプッシュのエンドポイントが取得できます。

function subscribe() {
    if(Notification.permission == 'denied') {
        console.log('プッシュ通知が有効ではありません');
        return;
    }

    navigator.serviceWorker.ready.then((sw) => {
        Notification.requestPermission((permission) => {
            if(permission !== 'denied') {
                sw.pushManager.subscribe({userVisibleOnly: true}).then((s) => {
                    // ここでPushのエンドポイントが取得できる
                    console.log(s.endpoint);
                });
            }
        });
    });
}

firefox.png

Firefoxだと、こんな感じの許可ダイアログが表示されます。

Chromeのエンドポイント例

https://android.googleapis.com/gcm/send/APA91...(略)

Firefoxのエンドポイント例

https://updates.push.services.mozilla.com/push/APA91...(略)

ServiceWorkerスクリプト

ブラウザにインストールするServiceWorkerのスクリプトを用意します。今回はpush.jsというファイル名にします。まずは、プッシュを受け取った際にポップアップで表示するようにします。今はまだリモートからのデータ送信に対応していないため、基本的には、予めServiceWorkerスクリプトに記載されたメッセージしか表示できません。またヴァイブレーションは、Android限定で対応しています。

push.js
self.addEventListener("push", function(event) {
    self.registration.showNotification("タイトル", {
        body: "本文",
        tag: "タグ",
        icon: "http://example.com/icon.png",
        vibrate: [200, 100, 200, 100, 200, 100, 200],
        data: "http://example.com/", // これは独自データ
    });
});

次に、通知メッセージをタップもしくはクリックした際に特定のURLへ飛ばすイベントリスナを設定します。先ほど設定した独自のdataプロパティにURLをセットしておくと、クリック時に取得することが可能です。なお、clients.openWindowはFirefox Nightly版42に実装されているようですが、バグがあるようで正常に指定のURLが開けないです。

push.js
self.addEventListener("notificationclick", function(event) {
    var uri = event.notification.data;
    event.notification.close();
    event.waitUntil(clients.matchAll({
        type: "window"
    }).then(function(clientLists) {
        if (clients.openWindow) return clients.openWindow(uri);
    }))
});

独自のメッセージを通知する

ちょっとした小技を使うと今でも独自のメッセージを通知するとが出来ます。まぁ、ChromeやFirefoxが正式対応したら、こんなテクニックは必要なくなりますが。仕組みは簡単で、サーバ上にJSON形式でデータを置いておいて、それをServiceWorkerからfetchするだけです。

先ほどのpush.jsのpushイベントリスナーを以下のように改造します。

push.js
self.addEventListener("push", function(event) {
    fetch("message").then(function(response) {
        if (200 !== response.status) throw Error();
        response.json().then(function(json) {
            if (json.error || !json.notification) throw Error();
            var data = json.notification;
            self.registration.showNotification(data.subject, {
                body: data.body,
                tag: data.tag,
                icon: data.icon,
                data: data.uri,
            })
        })
    });
});

これは、https://インストールドメイン/messageというURLをfetchします。fetch後、中身のJSONをパースしているだけです。簡単ですね。JSONのフォーマットはなんでもいいのですが、今回の例だと以下のようになります。

{
  "notification": {
    "subject": "タイトル",
    "body": "本文",
    "tag": "タグ",
    "icon": "http://example.com/icon.png",
    "uri": "http://example.com/",
  }
}

manifest.jsonを作る

配信するためにはmanifest.jsonというファイルを用意する必要があります。特に注意点はないですが、GCMの送信IDは事前にGoogle DeveloperConsoleで作っておく必要があります。

manifest.json
{
  "name": "Pushnate",
  "short_name": "Pushnate",
  "icons": [{
    "src": "icon.png",
    "sizes": "400x400",
    "type": "image/png"
  }],
  "start_url": "/",
  "display": "standalone",
  "gcm_sender_id": "GCMの送信ID",
  // "gcm_user_visible_only": true 古い方法で今は必要ない
}

ServiceWorkerをインストールするHTMLから指定してあげます。

index.html
<link rel="manifest" href="manifest.json">

以上がプッシュ通知に必要な最低限なソースコードになります。

プッシュ通知の配信

配信方法は現在ではChromはGCMを、FirefoxはSimplePushのサーバを利用します。取得したエンドポイントを利用して配信します。例としてcurlを使ったコマンドを記載します。GCMは、事前に作成したAPIキーを指定する必要があります。

ChromeはGCM

curl --header "Authorization: key=GCMのAPIキー" --header Content-Type:"application/json" https://android.googleapis.com/gcm/send -d "{\"registration_ids\":[\"APA91...(略)\"]}"

FirefoxはSimplePush

curl -X PUT https://updates.push.services.mozilla.com/push/APA91...(略)

firefox_push.png

デスクトップ版のFirefoxでは、こんな感じで表示されます。

GCMの場合、1度の送信で1000デバイスまで同時に指定できます。FirefoxのSimplePushでは1デバイスでの送信方法しか見つかりませんでした。大量に送信する場合は、なんかしら工夫が必要そうです。また、FirefoxのSimplePushの場合、Headerにversionを指定してインクリメントする必要があるという記載がありましたが、WebPushAPIの場合は必要なく送信することが出来ました。

ServiceWorkerのデバッグ方法

Chrome

Chromeの場合、chrome://inspect/#service-workersにアクセスすると稼働しているServiceWorkerの一覧が見られます。そこからServiceWorkerのインスペクタが起動できるので、ここでスクリプトのデバッグが可能です。

Firefox

about:serviceworkersからインストール済みServiceWorkerのUpdate / Unregisterが行えます。また、コンソールはBrowserConsoleからServerのタブを開くとログが出力されるようになります。開発者ツールの設定で、下記画像の4つにチェックを付けておくと色々と捗ると思います。

firefox_debugging.png

最後に

仕事以外でこれくらいの規模のサービスを個人開発したのは久しぶりで、なかなかに刺激的でした。個人開発の敵は自分の中にある欲望で、これにさえ勝てればあとは普通にできると思います。僕は自宅のMacは開発するか、写真の現像をするかしか使ってないんので、最初で最後で最大の難関が開発Macの前に座ること。これだけ。それに打ち勝つためにいくつかの対策をたてています。

  • 宣言駆動開発
    • とにかく嫁に「今日の夜はコード書くから」と言っておく。ダラダラしてると嫁から言われる
  • 写真駆動開発
    • 撮った写真の現像をするために、まずMacの前に座るようにする。ついソファでダラダラしないように
  • 興味駆動開発
    • ES2015やServiceWorkerなど比較的新しい技術を試しながら、サービス開発へ移る

逆にあまりオススメできないのは、人参駆動開発で、これを実装したらビールが飲める!とかの人参をぶら下げる形はその瞬間はいいんですが、継続性がないです。そんなビールばっかり飲んでられないし。

個人サービスの開発は誰かからお金がもらえるわけでもなく、サービスがヒットする保証もなく、運用するコストだって馬鹿にならず、サーバ代がかかってむしろマイナスだけど、自分でユーザの想像をし、サービスの仕様を考え、インフラからコード、デザインまで手を動かせるのは、単純にモノヅクリスキーとしては楽しかったです。

後はもうほんとこれ。 運用コスト これ。ここだけ解消できるように工夫しなくちゃなー。

Webプッシュ通知はどうあるべき?サイトやプッシュの特性、マイクロモーメントから考えてみましたという記事を開発チームメンバーの@yukikazuが書いたのでWebプッシュに興味のある人は読んでみてください。