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


# Webプッシュ通知

アプリの世界では当たり前になったプッシュ通知が、Webの世界にも来てますよ、と。まずはChromeから。そしてFirefoxもPushAPIに[Nightly版](https://nightly.mozilla.org/)で対応をしています。正式対応したFirefoxは11月にリリースされる予定です（Firefoxはすでに[SimplePush](https://developer.mozilla.org/ja/docs/Web/API/Simple_Push_API)というFirefoxOSやForefoxMobileの機能があり、これをベースに開発をしているようです）。Webプッシュ通知は最近だと[Facebookが対応](http://jp.techcrunch.com/2015/09/15/20150914facechrome/)をしており、Webでプッシュ通知を表示する流れは加速しそうですね。

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

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

## 作ったサービス：Pushnate

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

![pushnate.png](https://qiita-image-store.s3.amazonaws.com/0/2152/3b4279ad-31e4-eeac-ad44-a605b338f496.png "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::+1: コードを書く日は事前にちゃんと計画して実行する**
- **:imp::-1: お酒は飲み過ぎない／開発もいいけど家族もね**

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

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

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

- **:smile::+1: サービスを作る楽しみと、技術を知る楽しみを組み合わせる**

### テストコードは必ず書く

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

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

### BabelでES2015へ

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

- **:smile::+1: ES2015の書き方が楽／Promiseが便利**
- **:imp::-1: 書き味だけならCoffeeでも良い気がする**

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

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

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

#### 素材について

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

- [FLAT ICON DESIGN](http://flat-icon-design.com/)

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

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

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

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

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

### ロゴ画像はSVG

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

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

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

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

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

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

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

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

# ServiceWorker PushAPIの実装

ServiceWorkerを使ったPushAPIをサイトに実装する方法をまとめてみます。PushAPIはまだまだ各ブラウザで実装中の機能でバージョンにより仕様が変わったりします。2015年9月末時点での実装方法です。ServiceWorkerそのものについては、[HTML5RockのServiceWorker紹介記事](http://www.html5rocks.com/ja/tutorials/service-worker/introduction/)を見てみてください。

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

![](https://mdn.mozillademos.org/files/8241/flowchart-production-version.png)

- 引用 : https://developer.mozilla.org/ja/docs/Web/API/ServiceWorker_API/Using_Service_Workers


## 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は指定できません。

```JavaScript
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}`を指定して上げる必要があります。ユーザが許可をするとプッシュのエンドポイントが取得できます。

```JavaScript
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](https://qiita-image-store.s3.amazonaws.com/0/2152/3d689937-b451-7c3b-b335-040182ebdb6f.png "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に実装されているようですが、[バグがあるよう](https://bugzilla.mozilla.org/show_bug.cgi?id=1203324)で正常に指定の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のフォーマットはなんでもいいのですが、今回の例だと以下のようになります。

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

### manifest.jsonを作る

配信するためには`manifest.json`というファイルを用意する必要があります。特に注意点はないですが、GCMの送信IDは事前に[Google DeveloperConsole](https://console.developers.google.com/project)で作っておく必要があります。

```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

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

### FirefoxはSimplePush

```bash
curl -X PUT https://updates.push.services.mozilla.com/push/APA91...（略）
```

![firefox_push.png](https://qiita-image-store.s3.amazonaws.com/0/2152/f7cf137d-bd84-f614-0446-0673f88d7e96.png "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](https://qiita-image-store.s3.amazonaws.com/0/2152/73a8eabb-12f6-bab2-9288-3b7b610ec41b.png "firefox_debugging.png")


# 最後に

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

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

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

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

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

[Webプッシュ通知はどうあるべき？サイトやプッシュの特性、マイクロモーメントから考えてみました](https://pushnate.com/labs/webpush/google-push-chrome-update)という記事を開発チームメンバーの[@yukikazu](https://twitter.com/yukikazu)が書いたのでWebプッシュに興味のある人は読んでみてください。