2017年8月3日: WEBの革命児? ServiceWorkerについて

【ソリューション事業部 ヤマモト】
佐用町のひまわりキレイですね!
シーズン終わりにしかいったことないのですが、シーズン中はこんなにキレイなんですね。
さて、今回はソリューションらしく技術的な事を書いてみようと思います。
ここ最近一番驚いたServiceWorkerについて。
いままでWEBと言えば基本的にユーザがリクエストを送ってそのタイミングでレスポンスを返す。
(非同期もありますが、まぁ基本的にはページを開いた時に発火するので)
という単純な仕組みだったのですがServiceWorkerという革命児が生まれてしまいました。
まだまだ新しい技術なのでなりを潜めていますが
google mapがきっかけでそれまで見向きもされてなかったajaxが注目された時のようなインパクトのある仕様だと思ってます。
少しでも紹介できれば。


簡単な仕様は下記のようになります。
https://developers.google.com/web/fundamentals/getting-started/primers/service-workers?hl=ja
基本的にはこのようにブラウザにWEB製作者が書いたコードがインストールされる仕組みです。
インストールとはいってもユーザは実際には行動することなく、許可するボタンを押下するだけなので
ユーザの負担は限りなく低いと思います。
身近な下記の例をどう実現するのか?実例を交えて説明していきたいと思います。
・WEBサイトのプッシュ通知
スマホのアプリなどでよくある通知です。アプリを開いてなくても通知きますよね?
これがブラウザでも出来ちゃうっていう所が革命なんです。
WEBのトリガーの常識はユーザがリクエストを送ってから。という常識だったので!
処理のフロー的には下記のようになります。
1、ドメイン独自のServiceWorkerをブラウザにインストール
2、サブスクリプションを発行
3、2で発行したサブスクリプションをアプリ製作者のサーバーなどに送付
4、アプリ制作者は受け取ったサブスクリプションを管理する。
5、通知を流したい時にサブスクリプションを使用してfirebase宛に送信
6、通知される側のServiceWorkerはfirebaseに常時接続しているので
命令を受け取る。
7、命令に従ってインストールされているServiceWorkerの「push」ハンドリングを起動
8、pushハンドリングに通知内容を書いておくとそれが実行される。
かなり大まかに言えば上記のようなフローです。
では上記をどのように実現するのか簡単に書いてみます。
firebaseへの登録
まずfirebaseに登録します。
https://firebase.google.com/
にログインし、プロジェクトを作成します。
作成後、「設定」に移動します。(歯車のリンク)
まず「全般」タブより
「ウェブアプリに Firebase を追加」というリンクがあると思うので表示します。
jsが表示されるのでこの内容をコピーしておきます。
コピー後、更に「クラウドメッセージング」タブに移動します。
「Server key」
もメモしておきましょう。
デバッグ
ServiceWorkerのデバッグはchromeの開発者ツール「Application」タグの
ServiceWorkerで可能です。
実例(クライアント側)
ここでの例はindex.htmlにボタンを設置し、その押下アクションを拾ってます。
表示時などにイベントを発火する事も出来ます。よしなに。
はじめのfirebaseからのスクリプトはfirebaseで生成したjsになります。
任意の物を貼り付けてください。(例では全てhogehogehogehogeに置き換えてます)
/index.html
このページを開いて「通知登録して!!!!」リンクを押下すると通知を受け取る事が出来るようになります。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="manifest" href="./manifest.json">
<script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>
<script src="https://www.gstatic.com/firebasejs/3.7.0/firebase.js"></script>
<script>
// Initialize Firebase
var config = {
apiKey: "hogehogehogehoge",
authDomain: "hogehogehogehoge",
databaseURL: "hogehogehogehoge",
storageBucket: "hogehogehogehoge",
messagingSenderId: "hogehogehogehoge"
};
firebase.initializeApp(config);
</script>
<script src="./index.js"></script>
</head>
<body>
<h1>Web Push Test</h1>
<a href="#" id="push_regist"  style="display:none">通知登録して</a><br>
<a href="#" id="push_delete"  style="display:none">通知登録消して</a><br>
</body>
</html>

index.js
次はServiceWorkerのインストールやサブスクリプションIDをサーバーに送ったりする処理です。

function initialiseState() {
if (!("showNotification" in ServiceWorkerRegistration.prototype)) {
console.warn("プッシュ通知が対応されておりません");
return;
}
if (Notification.permission === "denied") {
console.warn("通知をブロックしております");
return;
}
if (!("PushManager" in window)) {
console.warn("プッシュ通知が対応されておりません");
return;
}
//既に過去に登録されている場合は登録するボタンではなく、削除ボタンを表示します
navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {
serviceWorkerRegistration.pushManager.getSubscription().then(
function (subscription) {
console.log(subscription);
$("#push_regist").hide();
$("#push_delete").hide();
if (!subscription) {
$("#push_regist").show();
return;
}
$("#push_delete").show();
}).catch(function(err){
console.warn("Error during getSubscription()", err);
});
});
}
$(document).ready(function(){
if ("serviceWorker" in navigator &&
(window.location.protocol === "https:" || isLocalhost)) {
navigator.serviceWorker.register("./sw.js").then(
function (registration) {
if (typeof registration.update == "function") {
registration.update();
}
initialiseState();
}).catch(function (error) {
console.error("Service Worker registration failed: ", error);
});
}
//サブスクリプションを発行します
$("#push_regist").on("click", function(){
Notification.requestPermission(function(permission) {
if(permission !== "denied") {
subscribe();
} else {
alert ("プッシュ通知を有効にできません。ブラウザの設定を確認して下さい。");
}
});
});
//サブスクリプションをサーバ、ブラウザ共に削除します
$("#push_delete").on("click", function(){
unsubscribled();
});
function subscribe() {
navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {
serviceWorkerRegistration.pushManager.subscribe({ userVisibleOnly: true }).then(
function(subscription) {
$("#push_regist").hide();
$("#push_delete").show();
return sendSubscriptionToServer(subscription);
}
).catch(function (e) {
if (Notification.permission == "denied") {
console.warn("Permission for Notifications was denied");
} else {
console.error("Unable to subscribe to push.", e);
window.alert(e);
}
})
});
}
function unsubscribled() {
navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {
serviceWorkerRegistration.pushManager.getSubscription().then(
function(subscription) {
if (!subscription ) {
$("#push_regist").show();
$("#push_delete").hide();
return;
}
sendSubscriptionToServerForDelete(subscription);
subscription.unsubscribe().then(function(successful) {
$("#push_regist").show();
$("#push_delete").hide();
}).catch(function(e) {
console.error("Unsubscription error: ", e);
$("#push_regist").show();
$("#push_delete").hide();
});
}
).catch(
function(e) {
console.error("Error thrown while unsubscribing from push messaging.", e);
}
)
});
}
function sendSubscriptionToServer(subscription) {
//発行したサブスクリプションをサーバー側に送信します。
//ここではサブスクリプションを/recieve.phpに送信しています。
console.log('sending to server for regist:',subscription);
var data = {"subscription" : subscription.endpoint};
$.ajax({
type: "POST",
url: "/recieve.php",
dataType: "json",
cache: false,
data: data
});
}
function sendSubscriptionToServerForDelete(subscrption) {
console.log('sending to server for delete:', subscrption);
}
});

/sw.js
ServiceWorkerはjsが配置されているディレクトリ配下の操作が出来るようになっています。
なのでjsディレクトリに本体を入れてしまうとjsディレクトリ以下しか操作できません。
その回避策です。
ただ本体を更新してもこちら変化しないので
キャッシュが効いてしまう。?日付のようにクエリ入れるのも面倒くさい。
いっその事/sw.jsを呼び出し先にしてしまって良いかもしれません。

importScripts("./js/sw.js");

/js/sw.js
ServiceWorker本体です。
ご覧のように現在はpushハンドラ内にてサーバーへメッセージを取得しにいっている。
メッセージを暗号化するとメッセージ込みで送信する事も可能らしいです。

//ServiceWorkerにインストールされるスクリプト
//プッシュ通知が行われると「push」イベントが起動する
self.addEventListener("install", function(event) {
self.skipWaiting();
console.log("Installed", event);
});
self.addEventListener("activate", function(event) {
console.log("Activated", event);
});
self.addEventListener("push", function(event) {
console.log("Push message received", event);
event.waitUntil(getEndpoint().then(function(endpoint) {
//通知内容をサーバに取得しに行きます。
return fetch("/notifications.php?endpoint=" + endpoint);
}).then(function(response) {
if (response.status === 200) {
return response.json();
}
throw new Error("notification api response error")
}).then(function(response) {
//TODO デザインやボタンの有無などの調整が必要
return self.registration.showNotification(response.title, {
icon: response.icon,
body: response.body,
tag: "push-test",
actions: [{
action: "act1",
title: "ボタン1"
}, {
action: "act2",
title: "ボタン2"
}],
vibrate: [200, 100, 200, 100, 200, 100, 200],
data: {
url: response.url
}
})
})
);
});
//押したaction名はnotificationclickのevent.actionで取得できます。
self.addEventListener("notificationclick", function(event) {
console.log("notification clicked:" + event);
console.log("action:" + event.action);
event.notification.close();
var url = "/";
if (event.notification.data.url) {
url = event.notification.data.url
}
event.waitUntil(
clients.matchAll({type: "window"}).then(function() {
if(clients.openWindow) {
return clients.openWindow(url)
}
})
);
});
function getEndpoint() {
return self.registration.pushManager.getSubscription().then(function(subscription) {
if (subscription) {
return subscription.endpoint;
}
throw new Error("User not subscribed");
});
}

manifest.json
プッシュを許可しますか?という定型メッセージのデザインです。
アイコンや色などを変更する事ができます。

{
"name": "Web Push Test",
"short_name": "WebPush",
"icons": [{
"src": "/image/icon.png",
"sizes": "256x256",
"type": "image/png"
}],
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000040",
"gcm_sender_id": "hogehogehoge"
}

実例(サーバーサイド側)
テスト用の超簡易版です。チェックも何もしてないです。
本来ならサブスクリプションをユーザと紐づけてDB管理したりするべきだと思います。
サーバーサイド側はfirebaseにリクエストを投げる処理、
firebaseのトリガによって起動されたServiceWorkerが問い合わせてくるリクエストに対するレスポンスを実装します。
駆け足になりましたがどうでしょうか。
これだけで通知が実現できます。
ご覧の通り、今回はユーザがリクエストを起動したタイミングで処理が走るのではなく、
firebase経由でトリガーが走り処理が走るという事が実感できたと思います。
この他にもServiceWorkerはcache apiなど様々なapiにも対応しているようです。
今後もきっと増えていくでしょう。
更にはServiceWorkerはリエクストを奪ってプロキシのように振る舞うことができるので
例えば、商品カタログを全てcacheさせ、ServiceWorker経由で全てcache apiからレスポンスを返すといった事も可能です。
それによって通信速度やパケットを気にすることなく分厚い商品カタログを読めるような機能を
アプリ開発、またインストールさせることなく実現できたり。と本当に様々なことに応用できそうです。
しかし、、、新しい技術は出て来る度にワクワクしますが、
習得していかないと技術者としては廃れていくのでストレスにもなったり。複雑な気分です(^^;

コメント