現在のHTML5ではvideoやaudioタグを使って動画、音楽などを再生できるようになっています。しかし、これらのデータはストリーミングで流したり、getUserMediaを使って取得することしかできませんでした。
そこで登場したのが Recording a media elementです。これは video/audioタグの内容をレコーディングし、ファイルとしてダウンロードもできるAPIになります。
使い方
まずHTMLを記述します。videoタグを使います。 #startButton を押すとWebカメラの映像をストリーミングで流します。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <div class="left"> | |
| <button id="startButton"> | |
| 開始 | |
| </button> | |
| <h2>プレビュー</h2> | |
| <video id="preview" width="160" height="120" autoplay muted></video> | |
| </div> |
さらに記録した内容を表示するDOMを用意します。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <div class="right"> | |
| <button id="stopButton"> | |
| 停止 | |
| </button> | |
| <h2>録画した内容</h2> | |
| <video id="recording" width="160" height="120" controls></video> | |
| <button id="downloadButton"> | |
| Download | |
| </button> | |
| </div> |
JavaScriptの処理
まず必要なDOMを変数化します。最後の recordingTimeMS は録画を行うタイミングで、今回は5秒ごとにしています。細かければ動画が滑らかになりますが、CPU負荷も大きくなります。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| let preview = document.getElementById("preview"); | |
| let recording = document.getElementById("recording"); | |
| let startButton = document.getElementById("startButton"); | |
| let stopButton = document.getElementById("stopButton"); | |
| let downloadButton = document.getElementById("downloadButton"); | |
| let logElement = document.getElementById("log"); | |
| let recordingTimeMS = 5000; |
開始した際の処理
まず開始時の処理について解説します。最初はgetUserMediaを使ってWebカメラにアクセスします。そして、映像を #preview に表示します。そして、 preview.captureStream を使って記録を開始します。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 開始ボタンを押した時の処理 | |
| startButton.addEventListener("click", function() { | |
| // Webカメラにアクセス | |
| navigator.mediaDevices.getUserMedia({ | |
| video: true, | |
| audio: true | |
| }).then(stream => { | |
| // アクセスが許可された場合 | |
| preview.srcObject = stream; // プレビューにWebカメラの映像を表示 | |
| downloadButton.href = stream; // ダウンロードボタンにも配置 | |
| // 表示内容をキャプチャする | |
| preview.captureStream = preview.captureStream || preview.mozCaptureStream; | |
| // プレビューが表示開始するタイミングをPromiseで取得 | |
| return new Promise(resolve => preview.onplaying = resolve); | |
| }) | |
| // 表示が開始したらレコーディングを開始 | |
| .then(() => startRecording(preview.captureStream(), recordingTimeMS)) | |
| // レコーディングが終了時の処理 | |
| .then (recordedChunks => { | |
| let recordedBlob = new Blob(recordedChunks, { type: "video/webm" }); | |
| recording.src = URL.createObjectURL(recordedBlob); | |
| downloadButton.href = recording.src; | |
| downloadButton.download = "RecordedVideo.webm"; | |
| }) | |
| .catch((err) => console.log(err)); | |
| }, false); |
レコーディング中は非同期処理になるので startRecording という関数の中で処理を行っています。 MediaRecorder のインスタンスにWebカメラの映像を指定し、 start メソッドを実行するとレコーディングが開始されます。レコーディング内容は ondataavailable メソッドで呼び出されるので、これを変数に残し続けます。
レコーディングが終わると onstop が呼ばれます。今回は lengthInMS で自動的に停止するようにしています。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| function startRecording(stream, lengthInMS) { | |
| let recorder = new MediaRecorder(stream); | |
| let data = []; | |
| recorder.ondataavailable = event => data.push(event.data); | |
| recorder.start(); | |
| let stopped = new Promise((resolve, reject) => { | |
| recorder.onstop = resolve; | |
| recorder.onerror = event => reject(event.name); | |
| }); | |
| let recorded = new Promise((resolve, reject) => { | |
| setTimeout(() => { | |
| recorder.state == "recording" && recorder.stop() | |
| resolve(); | |
| }, lengthInMS); | |
| }); | |
| return Promise.all([ | |
| stopped, | |
| recorded | |
| ]) | |
| .then(() => data); | |
| } |

また、プレビューを停止する場合には次のように実装することもできます。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| stopButton.addEventListener("click", function() { | |
| preview.srcObject.getTracks().forEach(track => track.stop()); | |
| }, false); |
デモ
ここまでの実装をJSFiddleにてデモできます。なお、執筆時点ではChromeとMozillaでしか動作しません。
動画リソースはWebカメラに限らず使えるでしょう。そうした時に特定の場所だけキャプチャするといった用途でも使えそうです。動画を編集するという目的にしてはCPU負荷が大きそうですが、動画コンテンツをより扱いやすくしてくれそうです。
PWAは幾つかの技術要素で構成されますが、その基本とも言えるのがService Workerです。Service Workerがなければオフライン対応もアプリとしてのインストール、プッシュ通知もできません。なお、Service WorkerはWebブラウザとは別なプロセスのJavaScriptとして実行されます。
今回はそんなService Workerとフロントエンド側とでメッセージを送受信する方法を紹介します。
WebブラウザからService Workerを呼び出し
Service WorkerはWebサイトにアクセス時にインストールされます。次に訪問した際に更新されているかチェックするのですが、1日程度はキャッシュされるようです。そのためキャッシュする内容を逐次変えたいと言った時にハードコーディングされていると不便です。
そこでWebブラウザからメッセージを送る方法を覚えておきましょう。port2に送るのがコツです。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // メッセージ送信 | |
| const channel = new MessageChannel(); | |
| navigator.serviceWorker.controller | |
| .postMessage('Hello, world', [channel.port2]); | |
| }); |
Service Worker側では message イベントとして呼び出されます。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| self.addEventListener('message', e => { | |
| console.log(`I got a message from browser. ${e.data}`); | |
| }); |
Service WorkerからWebブラウザを呼び出し
プッシュ通知を受け取った際など、Webブラウザ側にメッセージを送ることで表示を変えると言った効果が考えられます。
まず Service Worker側でメッセージを送りますが、Service Workerの場合はイベントが発生しないと実行できません。例えば push イベント時にメッセージを送る場合です。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| self.addEventListener('push', e => { | |
| e.ports[0].postMessage({ | |
| msg: "Hey I just got a push from you!", | |
| data: e.data | |
| }); | |
| }); |
そしてWebブラウザ側では port1 にメッセージが届きます。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const channel = new MessageChannel(); | |
| channel.port1.onmessage = e => { | |
| return e.data.error ? | |
| console.log(e.data.error) : | |
| console.log(`I got a message from ws. ${JSON.stringify(e.data)}`); | |
| }; |
WebブラウザからService Workerに対してメッセージを送る場合には port2 、逆の場合は port1 に来ると覚えておくと良いでしょう。Service Workerをよりダイナミックに活用したい時に覚えておくと良さそうです。
Webブラウザにプッシュ通知を送れるPush APIは魅力的な機能ですが、各ブラウザによって実装が異なっていたり、Firebaseでプロジェクトを登録したりするのが手間でした。それを共通化し、さらにプロジェクト登録不要で使えるようにする仕組みがVAPIDになります。
今回はこのVAPIDを使ったPush APIの使い方です。
必要なもの
今回はVAPIDに対応したライブラリ、web-pushを使います。今回はNode.jsのライブラリです。zaru/webpushというRuby向けのライブラリもあります。
初期設定
まずサーバ側で秘密鍵と公開鍵を用意します。これは以下のコードで可能です。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const webpush = require("web-push"); | |
| JSON.stringify(webpush.generateVAPIDKeys()); | |
| // '{"publicKey":"BDC..tP4","privateKey":"Kne…Km8"}' |
この内容を application-server-keys.json などとしてサーバに保存しておきます。
JavaScriptの準備
Service Worker
VAPIDを使った場合の魅力として、ペイロードが送れるという点が挙げられます。メッセージ付きで送れるので、クライアントサイドで処理判別ができます。そのための処理をService Worker用のJavaScriptに入れておきます。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| self.addEventListener('install', () => { | |
| console.log('ServiceWorker がインストールされました'); | |
| }); | |
| self.addEventListener('push', ev => { | |
| // payloadの取得 | |
| const {title, msg, icon} = ev.data.json(); | |
| self.registration.showNotification(title, { | |
| icon: icon, | |
| body: msg | |
| }); | |
| self.registration.pushManager.getSubscription().then(subscription => { | |
| console.log(subscription); | |
| }, err => console.log(err)); | |
| }); |
クライアントサイド
次にクライアントサイドですが、まずService Workerを読み込みます。そして、その後プッシュ通知の登録状態を確認しています。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| if (!('serviceWorker' in navigator)) { | |
| // Service Worker非対応 | |
| } | |
| navigator.serviceWorker.register('./serviceworker.js') | |
| .then(() => { | |
| if (!('showNotification' in ServiceWorkerRegistration.prototype)) { | |
| // プッシュ通知がサポートされていない場合 | |
| return; | |
| } | |
| if (Notification.permission === 'denied') { | |
| // プッシュ通知を拒否された場合 | |
| return; | |
| } | |
| if (!('PushManager' in window)) { | |
| // PushManagerが存在しない場合 | |
| return; | |
| } | |
| return navigator.serviceWorker.ready; | |
| }) | |
| .then(serviceWorkerRegistration => { | |
| return serviceWorkerRegistration.pushManager.getSubscription(); | |
| }) | |
| .then(subscription => { | |
| if (!subscription) { | |
| // すでに購読中 | |
| } else { | |
| // 未購読 | |
| console.log(subscription.toJSON()); | |
| } | |
| }) |
購読を開始する際のイベントは次の通りです。大事なポイントとして、 applicationServerKey を追加しています。ここには公開鍵をunit8にしたものを適用します(後述)。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| navigator.serviceWorker.ready | |
| .then(serviceWorkerRegistration => { | |
| return serviceWorkerRegistration.pushManager.subscribe({ | |
| userVisibleOnly: true, | |
| applicationServerKey: convertedVapidKey | |
| }); | |
| }) | |
| .then(subscription => { | |
| // 購読開始 | |
| console.log(subscription.toJSON()); | |
| }) |
公開鍵の設定です。 vapidPublicKey には web-push で生成した公開鍵を指定します。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const vapidPublicKey = 'BOO…YEk'; | |
| const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey); | |
| function urlBase64ToUint8Array(base64String) { | |
| const padding = '='.repeat((4 – base64String.length % 4) % 4); | |
| const base64 = (base64String + padding) | |
| .replace(/\-/g, '+') | |
| .replace(/_/g, '/'); | |
| const rawData = window.atob(base64); | |
| const outputArray = new Uint8Array(rawData.length); | |
| for (let i = 0; i < rawData.length; ++i) { | |
| outputArray[i] = rawData.charCodeAt(i); | |
| } | |
| return outputArray; | |
| } |
Web Pushの購読データ(subscription)をJSON出力すると、次のような内容になっています。このデータをすべて保存しておきます(サーバなど)。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "endpoint":"https://fcm.googleapis.com/fcm/send/e7K…Hag", | |
| "expirationTime":null, | |
| "keys":{ | |
| "p256dh":"BK7…e5U", | |
| "auth":"DGt…D8g" | |
| } | |
| } |
Web Pushを送る
ではサーバからプッシュ通知を送ってみましょう。コードは以下のようになります。まず鍵の設定と受信者を指定します。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const webpush = require("web-push"); | |
| const keys = require("./application-server-keys.json"); | |
| webpush.setVapidDetails( | |
| "mailto:admin@moongift.jp", | |
| keys.publicKey, | |
| keys.privateKey | |
| ); | |
| const subscribers = [ | |
| { | |
| "endpoint":"https://fcm.googleapis.com/fcm/send/e7K…Hag", | |
| "expirationTime":null, | |
| "keys":{ | |
| "p256dh":"BK7…e5U", | |
| "auth":"DGt…D8g" | |
| } | |
| } | |
| ]; |
そしてプッシュ通知を送ります。 icon で表示するアイコンを指定しています。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const icon = `app.png`; | |
| const params = { | |
| title: "プッシュ通知です!", | |
| msg: `これはサーバから送っています. 今は ${new Date().toLocaleString()} です。 メッセージとアイコンも送っています `, | |
| icon: icon | |
| }; | |
| Promise.all(subscribers.map(subscription => { | |
| return webpush.sendNotification(subscription, JSON.stringify(params), {}); | |
| })) | |
| .then(res => console.log(res)) | |
| .catch(err => console.log('ERROR', err)); |
そしてGoogle ChromeやFirefoxに対して通知が送れます。

Firefoxの場合です。

VAPIDを使った場合の利点としては、Firebaseでプロジェクトを作る手間がなく、Webブラウザの共通仕様の基で開発できるということです。なお、Safariはこの仕様に則っていない(そもそも独自仕様)ので、対応できません。
PWAを構成する技術要素の一つにプッシュ通知があります。Push APIがそうで、該当URLを開いていなくても通知を受けられる仕組みです。
Push APIには幾つかの実装方法がありますが、今回はFCM/GCMを使ったGoogle Chrome向けの実装を紹介します。
Firebaseでプロジェクトを作成する
FCMを使うので、まず最初にFirebaseでプロジェクトを作成します。

作成した後、ウェブアプリにFirebaseを追加を選択します。

そして表示されるモーダルの中に messagingSenderId という項目があります。これを覚えておきます。

さらにプロジェクトの設定を選び、クラウドメッセージングを選びます。

その中に表示される、サーバーキーも覚えておきます。

必要な技術
Push APIを使う際に必要なのはService Workerになります。
manifest.jsonの作成
manifest.jsonを作成します。内容は例えば以下のようになります。 YOUR_SENDER_ID となっているところは先ほどの messagingSenderId の値になります。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "name": "Push Notification Demo", | |
| "short_name": "Push Notification Demo", | |
| "gcm_sender_id": "YOUR_SENDER_ID" | |
| } |
serviceworker.js の作成
Service Worker用のJavaScriptファイルを作成します。 serviceworker.js とします。今回は簡単な内容にしています。最低限必要なのは install と push イベントになります。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| self.addEventListener('install', () => { | |
| console.log('ServiceWorker がインストールされました'); | |
| }); | |
| self.addEventListener('push', ev => { | |
| self.registration.showNotification('メッセージが届きました', { | |
| body: '新しいメッセージです' | |
| }); | |
| }); |
HTMLの記述
HTMLではmanifest.jsonを読み込みます。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <link rel="manifest" href="manifest.json"> |
JavaScriptの記述
初期化
まずPush通知が使えるかどうか、さらに購読中かどうかを判別します。以下のようなコードで順番に確認できます。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| function onLoad() { | |
| if (!('serviceWorker' in navigator)) { | |
| // Service Worker非対応 | |
| } | |
| navigator.serviceWorker.register('./serviceworker.js') | |
| .then(() => { | |
| if (!('showNotification' in ServiceWorkerRegistration.prototype)) { | |
| // プッシュ通知がサポートされていない場合 | |
| return; | |
| } | |
| if (Notification.permission === 'denied') { | |
| // プッシュ通知を拒否された場合 | |
| return; | |
| } | |
| if (!('PushManager' in window)) { | |
| // PushManagerが存在しない場合 | |
| return; | |
| } | |
| return navigator.serviceWorker.ready; | |
| }) | |
| .then(serviceWorkerRegistration => { | |
| return serviceWorkerRegistration.pushManager.getSubscription(); | |
| }) | |
| .then(subscription => { | |
| if (!subscription) { | |
| // 未購読 | |
| } else { | |
| // すでに購読中 | |
| } | |
| }) | |
| } |
購読処理
次に購読処理を書きます。これは何かのボタンを押したタイミングなどで実行すれば良いでしょう。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const SubscribePushNotification = () => { | |
| navigator.serviceWorker.ready | |
| .then(serviceWorkerRegistration => { | |
| const subscribe = serviceWorkerRegistration | |
| .pushManager | |
| .subscribe({ | |
| userVisibleOnly: true | |
| }); | |
| return subscribe; | |
| }) | |
| .then(subscription => { | |
| // 購読開始 | |
| }) | |
| } |
プッシュ通知の送り方
上記のコードで取得できる subscription には以下のような内容が入っています。
- endpoint
- expirationTime
- options
- options.applicationServerKey
- options.userVisibleOnly
今回のようなFCMから送信する場合に必要なのは endpoint になります。例えば以下のような値です。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| https://android.googleapis.com/gcm/send/eF7Jh_81aPc:APA9…lOUg |
そして、この対象に対してプッシュ通知を送るには以下のようなコマンドを実行します。 YOUR_API_KEY となっているのは Firebase で取得したサーバキーになります。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| curl –header "Authorization: key=YOUR_API_KEY" \ | |
| –header Content-Type:"application/json" \ | |
| https://android.googleapis.com/gcm/send \ | |
| -d "{\"registration_ids\":[\"eF7Jh_81aPc:APA9…lOUg\"]}" |
registration_idsのところに配列でendpointとして取得したトークンを指定すればOKです。
受信する
プッシュ通知を受信すると通知バナーが表示されます。このメッセージは Service Worker側で固定していますが、サーバにfetchで問い合わせてデータ取得することも可能です。登録IDが必要な場合は、以下のコードをService Worker内で実行します。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| self.registration.pushManager.getSubscription().then(subscription => { | |
| console.log(subscription.endpoint); // 登録IDが返ってきます | |
| }, err => console.log(err)); |

注意点
Push APIはタブを閉じていても(そのURLがアクティブでなくとも)表示されます。ただしGoogle Chromeを終了していたら届きませんので注意してください。Webブラウザを再開すると通知が表示されます。この方式の場合、表示させるメッセージをプッシュ通知側から送ることはできません。データが送れるのは VAPID という方式を使った場合になります。
この方法はGoogle Chrome独自の方式になります。手軽に実装できますが、FirefoxやEdgeなどでは使えませんのでご注意ください。
今回の方式の場合、サーバ側の用意は殆どいりません。取得した登録IDをサーバ側に入れておくだけで大丈夫です。保存する際に任意のユーザIDと結びつけておくことで、プッシュ通知で表示する内容をカスタマイズできるでしょう。
PWA(Progressive Web App)という単語がトレンドになっています。PWAは特定の技術を指す言葉ではなく、様々な技術が組み合わさったスマートフォンにおけるWebアプリのベストプラクティスと言えます。
今回はそんなPWAの基礎になる、Web App Manifestを作る時に便利なテクニックを紹介します。
Web App Manifestとは?
Web App ManifestはWebアプリをスマートフォンアプリのようにインストールできる技術です。アプリストアを経由せずに配布できるのが魅力です。
インストール数を知る
どれくらいのPWAアプリが使われているか、それは皆さんが知りたいことだと思います。この時使えるのはGoogleアナリティクスです。例えば、manifest.jsonのstart_urlを次のように指定します。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "start_url": "/?utm_source=homescreen" |
こうすることでGoogle Analyticsを使って、起動数を測定可能になります。
PWAの場合だけ表示を変える
Webブラウザから使った場合と、PWAとして立ち上げた場合で表示を変えたいこともあるでしょう。PWAではオフラインで使うことも想定してキャッシュされるので、サーバサイドでの表示出し分けはよくありません。そこでJavaScriptを使います。例えば manifest.json で次のように設定します。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "start_url": "index.html?launcher=true" |
こうすることで、JavaScript側でlancherというパラメータの有無によって表示を変えられるようになります。不要なフッター情報などを削除しても良いでしょう。
オンライン/オフライン判定
これはPWAに限りませんが、 navigator.onLine がtrue(オンライン)/false(オフライン)によって判定できます。ムダなアクセスをなくしたり、デフォルト画像を表示したりするのに使えます。
インストールプロンプトの抑止
サイトを閲覧中にインストールしませんか、というバナーを出してもなかなかOKされないでしょう。インストールしたくなるような文脈が必要なはずです。そこで、インストールプロンプトを抑止し、任意のタイミングで出せるようにします。
まず抑止は beforeinstallprompt イベントで行います。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| var deferredPrompt; | |
| window.addEventListener('beforeinstallprompt', function(e) { | |
| e.preventDefault(); | |
| // ここでプロンプトを変数に入れておく | |
| deferredPrompt = e; | |
| return false; | |
| }); |
そして、任意のタイミングで deferredPrompt.prompt(); を実行します。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| btnSave.addEventListener('click', function() { | |
| if(deferredPrompt !== undefined) { | |
| // インストールプロンプト表示 | |
| deferredPrompt.prompt(); | |
| deferredPrompt.userChoice | |
| .then(function(choiceResult) { | |
| // キャンセルされた場合 | |
| if(choiceResult.outcome == 'dismissed') { | |
| console.log('User cancelled home screen install'); | |
| } else { | |
| // インストールされた場合 | |
| console.log('User added to home screen'); | |
| } | |
| deferredPrompt = null; | |
| }); | |
| } | |
| }); |
ただし、現状では beforeinstallprompt がいつ実行されるかが分からず、かつ一度離脱した後、確実に次も呼ばれるかは分かりませんので注意してください。
インストール状況を調べる
PWAをインストール済みかどうかは event.userChoice で判別できます。 accepted であればインストール済み、dismissedであればキャンセル済みです。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| window.addEventListener('beforeinstallprompt', function(e) { | |
| e.userChoice.then(function(choiceResult) { | |
| if(choiceResult.outcome == 'dismissed') { | |
| console.log('User cancelled home screen install'); | |
| } | |
| else { | |
| console.log('User added to home screen'); | |
| } | |
| }); | |
| }); |
ネイティブアプリのインストールを進める
manifest.jsonに related_applications を指定することでネイティブアプリを進められるようになります。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "related_applications": [ | |
| { | |
| "platform": "play", | |
| "id": "com.google.samples.apps.iosched" | |
| } | |
| ] |
さらに preferrelatedapplications を true として指定するとPWAのインストールバナーは出さず、アプリのバナーのみになります。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "prefer_related_applications": true, | |
| "related_applications": [ | |
| { | |
| "platform": "play", | |
| "id": "com.google.samples.apps.iosched" | |
| } | |
| ] |
PWAはネイティブアプリとWebアプリの中間とも言える存在です。単純にWebアプリ + インストールできる程度に扱うのではなく、ちょっとした工夫で解析や、よりユーザビリティの高い仕組みが提供できます。
PWA(Progressive Web App)という単語がトレンドになっています。PWAは特定の技術を指す言葉ではなく、様々な技術が組み合わさったスマートフォンにおけるWebアプリのベストプラクティスと言えます。
今回はそんなPWAの基礎になる、Web App Manifestを作る時に便利なテクニックを紹介します。
Web App Manifestとは?
Web App ManifestはWebアプリをスマートフォンアプリのようにインストールできる技術です。アプリストアを経由せずに配布できるのが魅力です。
検証
Web App Manifestではmanifest.jsonというファイルを作成しますが、これが正しく作れているかどうかが確認しづらいという問題があります。そこで使ってみて欲しいのがGoogle Chromeの開発者ツールです。アプリケーションタブにManifestという項目が追加されています。これを見ると設定内容が正しく認識されているか一目で分かります。

また、Add to HomescreenというリンクをクリックしてChromeアプリにすることもできます。これはほぼブックマークですが、Chromeのブックマークバーよりアプリと言うリンクをクリックすると一覧で表示できます。

さらに詳しい検証
もっと詳しい情報を知ったり、 Web App Manifestに限らずPWAが正しく設定できているか確認するためにはLighthouseという機能拡張が便利です。PWAにしたいWebサイトを表示した状態で実行すると、レポート生成してくれます。

主なチェックポイントとしては次の通りです。
- Service Workerを登録しているか
- オフラインでも200のレスポンスを返すか
- HTTPSを使っているか
- スプラッシュスクリーンが使えるか
- アドレスバーの色設定が行われているか
- HTTPへのアクセスがHTTPSにリダイレクトされるか
- 3G回線でも十分に速いか
- メタタグでviewport設定を行っているか
Google Chromeの設定
開発中はインストールバナーが表示されるタイミングが非常に分かりづらく、何度も再読込を繰り返すことになるかと思います。そこでAndroidのGoogle Chromeにて、フラグの設定をします。 chroke://flags を表示して、Bypass user engagement checksを有効にします。これでバナーがすぐに出るようになります。

なお、アプリをインストールした後に削除し、再度バナーを出す場合にはアプリのデータをすべて削除し、再度Bypass user engagement checksを有効にすると表示されるようです。
Web App Manifestはまだ対応しているブラウザが少なく、利用されるとしてもAndroidがメインデバイスになるかと思います(Safariはまだ中途半端にしか対応していないため)。まだ機能的に不十分な感はありますが、幾つかのテクニックを駆使して効率的な開発に努めてください。
PWA(Progressive Web App)という単語がトレンドになっています。PWAは特定の技術を指す言葉ではなく、様々な技術が組み合わさったスマートフォンにおけるWebアプリのベストプラクティスと言えます。
今回はそんなPWAの基礎になる、Web App Manifestについて紹介します。
利用できるブラウザについて
Web App Manifestはまだ仕様が確定しておらず、一部のWebブラウザでしか使えません。Google Chromeはデスクトップ(アイコンのみ)、スマートフォンともに利用できます。また、iOSのSafariでは11.3からサポートを開始しています。デスクトップで利用できるのはEdgeくらいのようです。

要件
技術的には以下が必須になります。
- HTTPSサイトである
- Service Workerを使っている
- マニフェストファイルを配置している
Service Workerを使っている
Service Workerを使ってオンライン上のファイルをキャッシュします。まずWebブラウザ側のJavaScriptでService Worker用のJavaScriptを読み込みます。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| if ('serviceWorker' in navigator) { | |
| navigator.serviceWorker | |
| .register('./sw.js', { | |
| scope: './' | |
| }) | |
| .then((reg) => { | |
| if(reg.installing) { | |
| console.log('Service Worker をインストール中です'); | |
| } else if(reg.waiting) { | |
| console.log('Service Worker をインストールしました'); | |
| } else if(reg.active) { | |
| console.log('Service Worker は有効です'); | |
| } | |
| }) | |
| .catch((error) => { | |
| // registration failed | |
| console.log('登録失敗:', error); | |
| }); | |
| } |
Service Worker用のJavaScriptではキャッシュしたいファイルを読み込みます。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| self.addEventListener('install', e => { | |
| // インストール時に実行 | |
| e.waitUntil( | |
| caches.open('mismith').then(cache => { | |
| return cache.addAll([ | |
| '/', | |
| '/client.js', | |
| '/style.css' | |
| ]) | |
| .then(() => self.skipWaiting()); | |
| }) | |
| ); | |
| }); |
マニフェストファイルを設置している
マニフェストファイルは manifest.json という名前で作られることが多いようです。このファイルをHTMLのheadタグ内で読み込みます。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <link rel="manifest" href="/manifest.json"> |
内容は次のようになります。縦向きで、アプリのようなUIで表示されます。また、テーマカラーであったり、スプラッシュスクリーンの背景色なども指定できます。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "display": "standalone", | |
| "short_name": "hifiveTodo", | |
| "name": "hifiveTodo", | |
| "orientation": "portrait", | |
| "icons": [ | |
| { | |
| "src": "launcher-icon-1x.png", | |
| "type": "image/png", | |
| "sizes": "48×48" | |
| }, | |
| { | |
| "src": "launcher-icon-2x.png", | |
| "type": "image/png", | |
| "sizes": "96×96" | |
| }, | |
| { | |
| "src": "launcher-icon-3x.png", | |
| "type": "image/png", | |
| "sizes": "192×192" | |
| } | |
| ], | |
| "start_url": "./index.html", | |
| "background_color": "#e1efde", | |
| "theme_color": "#e1efde" | |
| } |
これをWebサイト上に配置します。
サイトを表示する
Google Chromeの場合は5分以上置いてアクセスするとインストールバナーが表示されるとあります。しかし体感としてはもっと空けるか、Webブラウザの設定を変更した方が良いのではないかと思います。
表示して少し経つとバナーが表示されます。

こういう小さいバナーの場合もあります。

バナーをタップすると、インストールの確認が出ます。

インストールするとホーム画面上にアイコンが表示されます。

アプリと同じなので、コンテクストメニューがブックマークとは異なります。

アイコンをタップするとアプリが表示されます。アプリのアイコンで192pxのものを用意しておくとスプラッシュスクリーンが表示されるようです。Service Workerを使っていますのでオフラインでも使えます。

Web App Manifestはまだブラックボックスな動作もあります(バナーが表示されるタイミングや再表示させる方法など)。アプリを一度アンインストールした後、再度インストールする方法も明文化されていません。
とは言え、Webアプリをネイティブアプリ化できるのは面白い技術で、通常のアプリストアを頼らない配布も便利です。業務アプリなどでも使えそうです。
PWA(Progressive Web App)という単語がトレンドになっています。PWAは特定の技術を指す言葉ではなく、様々な技術が組み合わさったスマートフォンにおけるWebアプリのベストプラクティスと言えます。
今回はそんなPWAの基礎になる、Service Workerについて紹介します。
利用できるWebブラウザについて
Can I useによると、モダンなブラウザの中でService Workerが使えないのはIEとOpera miniだけになっています。スマートフォンであれば安心して利用できるようになっています。

オンライン/オフラインは navigator.onLine で判定する
オンラインかオフラインの判定として使えるのが navigator.onLine です。trueの場合はオンライン、falseはオフラインです。
オフラインの時だけ表示を変える
ではオンラインの時だけユーザがそれと分かるように表示してあげましょう。ただしService WorkerはDOMが使えませんので、文字を出すことはできません。そこで、SVG(または画像)を使います。
二つのSVGをキャッシュする
まずオンライン、オフライン用のSVGを二つ読み込みます。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| self.addEventListener('install', e => { | |
| // インストール時に実行 | |
| e.waitUntil( | |
| caches.open('mismith').then(cache => { | |
| return cache.addAll([ | |
| '/', | |
| '/client.js', | |
| '/style.css', | |
| '/ok.svg', // オンライン用 | |
| '/fail.svg' // オフライン用 | |
| ]) | |
| .then(() => self.skipWaiting()); | |
| }) | |
| ); | |
| }); |
HTMLの修正
HTML側はオンライン時の表示としておきます。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <p> | |
| Network status: <img src="ok.svg" width="20px" /> | |
| </p> |
表示判定処理
そして、キャッシュを表示する fetch 処理にてオンライン、オフライン状態に応じてレスポンスを変えます。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| self.addEventListener('fetch', e => { | |
| // 外部リソース取得時 | |
| const url = new URL(e.request.url); | |
| // オフラインの時にはfail.svgを表示する | |
| if (url.origin == location.origin && url.pathname == '/ok.svg' && !navigator.onLine) { | |
| return e.respondWith(caches.match('/fail.svg')); | |
| } | |
| // それ以外の場合は同じ | |
| e.respondWith( | |
| caches.match(e.request, { | |
| ignoreSearch: true | |
| }) | |
| .then(response => { | |
| return response || fetch(e.request); | |
| }) | |
| ); | |
| }); |
こうするとオフライン時にはオフライン用のSVG画像を出せるようになります。

PWAではある程度のオフライン状態でも利用できますが、それでも最近のWebアプリとしてはネットワークがないと機能不足になってしまうでしょう。そうした時にJavaScript側でエラー処理を行うのはもちろんですが、ユーザの視覚にも反映してあげるとユーザビリティが高いと言えます。
PWA(Progressive Web App)という単語がトレンドになっています。PWAは特定の技術を指す言葉ではなく、様々な技術が組み合わさったスマートフォンにおけるWebアプリのベストプラクティスと言えます。
今回はそんなPWAの基礎になる、Service Workerの開発方法について紹介します。
Google Chromeの場合
Google ChromeはWebブラウザ側、Service Worker側の両方のスクリプトについて一つの開発者ツール内で扱えます。

また、オフライン時の確認をする際には開発者ツールのアプリケーションタブ、Service Workersの中にOfflineというチェックを有効にすることで動作検証できます。

Firefoxの場合
Firefoxはツールメニューのウェブ開発、Service Workerという項目を選択します。

現在、デバッガーがマルチプロセスコンテンツに対応していないのでオプトアウトしないとデバッグできません。

オプトアウト後、Service Workerが組み込まれているページを表示するとデバッグボタンが押せるようになります。これを押すと別ウィンドウでデバッガーが立ち上がります。

もちろんブレークポイントを入れることもできます。

オフライン時の検証はファイルメニューのオフライン作業を選んで有効にできます。

Safariの場合
Safariは開発メニューのService Workerを選んでデバッグします。

Firefoxと同レベルのデバッグができます。ただしオフライン作業にはできませんでした。

Edgeの場合
Edgeは開発者ツールのデバッガーの中にService Workersという項目があり、スクリプトを選んで検査というリンクをクリックすると専用のデバッガーが立ち上がります。


EdgeはService Workerが読み込まれていることを通知してくれます。これは他のブラウザにはない機能です。

なお、オフラインにする機能はないようです。
Google Chromeだけが通常のJavaScriptとService Workerを同じ開発者ツールの中でデバッグできます。他のブラウザは別ウィンドウでデバッグとなります。また、Google ChromeとFirefoxはオフライン時の挙動を素早く確認できます。
通常のJavaScriptと異なるので、デバッグ方法も異なります。各ブラウザでのやり方を覚えておくと開発がスムーズになるでしょう。
PWA(Progressive Web App)という単語がトレンドになっています。PWAは特定の技術を指す言葉ではなく、様々な技術が組み合わさったスマートフォンにおけるWebアプリのベストプラクティスと言えます。
今回はそんなPWAの基礎になる、Service Workerについて紹介します。
利用できるWebブラウザについて
Can I useによると、モダンなブラウザの中でService Workerが使えないのはIEとOpera miniだけになっています。スマートフォンであれば安心して利用できるようになっています。

使うための基本形
Service WorkerはフロントエンドのJavaScriptとは別プロセスで実行されます。まずWebブラウザ側のJavaScriptにてService Workerを登録します(今回は sw.js としています)。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| if ('serviceWorker' in navigator) { | |
| navigator.serviceWorker | |
| .register('./sw.js', { | |
| scope: './' | |
| }) | |
| .then((reg) => { | |
| if(reg.installing) { | |
| console.log('Service Worker をインストール中です'); | |
| } else if(reg.waiting) { | |
| console.log('Service Worker をインストールしました'); | |
| } else if(reg.active) { | |
| console.log('Service Worker は有効です'); | |
| } | |
| }) | |
| .catch((error) => { | |
| // registration failed | |
| console.log('登録失敗:', error); | |
| }); | |
| } |
これで登録できます。

Service Workerの内容
Service Workerではselfというオブジェクトが存在します。これはdocumentが使えないService Worker内で全体のイベントハンドリングを行う存在です。イベントハンドリングが基本機能になります。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| self.addEventListener('install', e => { | |
| // インストール時に実行 | |
| }); | |
| self.addEventListener('activate', e => { | |
| // アップデート時など | |
| }); | |
| self.addEventListener('fetch', e => { | |
| // 外部リソース取得時 | |
| }); |
インストール時の内容
インストール(install)イベントではキャッシュの設定をします。以下のコードではルートのHTMLだけキャッシュします。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| self.addEventListener('install', e => { | |
| // インストール時に実行 | |
| e.waitUntil( | |
| caches.open('mismith').then(cache => { | |
| return cache.addAll([ | |
| '/' | |
| ]) | |
| .then(() => self.skipWaiting()); | |
| }) | |
| ); | |
| }); |
各リソースへのリクエスト
画像に限らず、リソースへのアクセスがあるとこのfetchイベントが発生します。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| self.addEventListener('fetch', e => { | |
| // 外部リソース取得時 | |
| console.log('fetch', e.request.url); | |
| e.respondWith( | |
| caches.match(e.request, { | |
| ignoreSearch:true | |
| }) | |
| .then(response => { | |
| return response || fetch(e.request); | |
| }) | |
| ); | |
| }); |
ここではキャッシュの存在を確認し、キャッシュがなければfetchで改めてリクエストします。
例えばこの状態で一度Webページを表示し、オフラインして表示するとオフライン化はされますが、HTMLだけしか表示されません。

そこでキャッシュするリソースを追加します。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| self.addEventListener('install', e => { | |
| // インストール時に実行 | |
| e.waitUntil( | |
| caches.open('mismith').then(cache => { | |
| return cache.addAll([ | |
| '/', | |
| '/client.js', | |
| '/style.css' | |
| ]) | |
| .then(() => self.skipWaiting()); | |
| }) | |
| ); | |
| }); |
この状態で一度オンライン状態で読み込み、再度オフラインにして読み込み直すとスタイルシートもキャッシュされているのが確認できます。

Service Workerを使ったリソースのキャッシングの仕組みはとてもシンプルです。PWA化の第一歩として試してみてください。