Posted at

15分PWAクッキング -オフラインで動くページを作ってみよう-

恒例のジーズアカデミーアドベントカレンダー、今年も書きます。

さて、今年はエンジニアとして働き始めて2年目、色々とチャレンジした年でした。

勉強会に始まり、会社の先輩がやめたことで1人で2つのサービスを運用することになったり、技術書展5に出展したり。

その中で、コツコツと情報を集めて、Progressive Web Appsをテーマにした本を書きました(宣伝)。

ということで、PWAって騒がれてたけどなんなんだろう?どうやって実装するの?といった疑問に応えるべく記事を書きます。

テーマはズバリ「15分でPWAの概要を掴んで、オフラインでも動くページを作ろう!」です。

先日つらつらと書いた本の一部を公開しつつ、最後に私的なPWAのまとめも簡単に行いたいと思います。

(あくまでも私的な意見なので、参考程度に)

それではいってみましょう!


PWAとは

Progressive Web Appsの略で、まあ簡単に言うと、Webアプリケーションにネイティブアプリのいいところを突っ込んで、Webアプリをよりよくしていこう!というGoogle先生が提唱するWebアプリの新しい概念のようなものです。

Progressive Web Apps自体は技術ではなく、「アプリをPWA化する」のように使うマーケ用語のようなものだと僕は認識しています。


アプリをPWA化して、何ができるの?


  • Webアプリにプッシュ通知が実装できます。

  • キャッシュを返すことでWebアプリがオフラインでも動作するようになります。

  • 「ホーム画面に追加」がとても簡単になります。

  • 小額決済もできるようになります。

  • 開発工数が少なく、Webアプリをネイティブアプリのような動作にできます。(Androidに限る)

  • (Androidなら)ネイティブアプリだと認識されます。

一時期、ネイティブアプリ要らなくなる説もささやかれましたが、現時点ではネイティブアプリの方がやれることは多いし、完全代替!とはいかなさそうです。


PWAの技術的要素


  • Promise

  • Service Worker

  • Cache API

  • Push API

  • Payment Request API

  • manifest.json

などがあります。

今回はService Woerker,Promise,CacheAPIについて主に触れていきます。


Service Workerとは?

一行で書くと

Webページのバックグラウンドで動く、もう一つのJavaScript環境です。

ちょっとわかりづらいかもしれないので、こちらの図を見ながら理解していきましょう。

まずは通常のWebから。

スクリーンショット 2018-12-10 22.57.31.png

リクエストを投げて、レスポンスが返ってくる。はい、当たり前ですね。笑

ではService Workerが利用できるとどのようになるのでしょうか?

スクリーンショット 2018-12-10 23.05.26.png

上図のように、通常のWebの機能も残しながら、Service Workerが間に入り、ネットワークのリクエストとレスポンスに対してアクションを取れるようになります。

Service Workerがネットワークリクエストを傍受して、ある特定の処理を返すことができるようになるということです。

例えば、オンラインの時にはネットワークからレスポンスを返してもらい、オフラインの時にはキャッシュを返してあげる。

といったことですね。

この仕組みを使って、オフラインでのページの表示を後ほど実装しますが、なんとなく、「Service Workerってプロキシみたいに通信の間に割って入れるんだなあ」くらいに思っておけば良いです。

ちなみに、Service Workerのブラウザの対応状況はこんな感じです。

IEでは使えず、Safariも使える機能が限定されています。デバッグなどはChromeで進めた方がいいでしょう。

なお、この先出てくる開発についてですが、通常のJavascriptがある程度理解できていれば、簡単なサンプルを作る程度であれば、そんなに難しくはありません。

ただし、ES6のPromiseを多用することになるので、その辺りは自力で理解しておく必要があります。


Service Workerの注意点

https通信、あるいはlocalhostでないと動きません。

簡易的にやるならば、Github Pagesにアップしたり、Pythonのサーバーを立てたり、Chromeのサーバープラグインを使うと良いでしょう。


Promiseとは?

非同期処理を抽象化したオブジェクトと、それを操作する仕組みのことです。


参照:Javascript Promiseの本


????

という方もいるかもしれませんので、簡単にコードを書いて解説します。

PromiseはES6 Promisesに定義されている仕様の一つです。

コンストラクタ関数のPromiseを利用して、インスタンスとなるPromiseオブジェクトを利用します。


var first_promise = new Promise(function(resolve, reject) {
// 非同期の処理をここに書きます。
// 処理が終わったら、その結果によりresolve または rejectを呼びます。
});

resolve, rejectはそれぞれ処理が成功した場合、失敗した場合で呼び出されます。

そしてそれをthen()メソッドでチェーンをしていき

非同期処理Aが終わったらBへ、Bが終わったらCへ・・・ということを非常に簡潔に見やすくかける、というのが大きな利点です。

ここで抑えておくべきは、then()メソッドには引数が二つ取れるということ。先ほどのfirst_promiseを使うならば


first_promise.then(onFullfilled, onRejected);

のように書くことができます。

ちなみにthen()メソッドに二つ処理を書いてもいいですし、下記のようにcatchメソッドで例外処理を捕まえるようにしても問題ありません。


first_promise
.then(function onFullfilled(){
//成功した時の処理
console.log('OK');
})
.catch(function onRejected(){
//失敗した時の処理
console.log('NG)
});

先ほどの

「Aが終わったらB、Bが終わったらC」といった処理は


first_promise
.then(onFullfilledA, onRejectedA)
.then(onFullfilledB, onRejectedB)
.then(onFullfilledC, onRejectedC);

のように書くことができます。

これと同じ仕組みがコールバック関数ですが、コールバック関数を5個でも書こうものなら、入れ子地獄にハマり、抜け出せなくなることでしょう。

5個でも10個でもすっきりとかけるのがPromiseのいいところです。


Promiseの状態


  • pending: 初期状態。成功も失敗もしていません。

  • fulfilled: 処理が成功して完了したことを意味します。

  • rejected: 処理が失敗したことを意味します。

つまり、Promiseは

pending→fulfilled

pending→ rejected

のどちらかの道しか辿りません。

シンプルですね。


初めてのPromiseとService Worker

さて、ざっくりとPromiseに触れてみたので、次は実際にService Workerを使うとどんなコードになるのかみてみましょう。


if('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/sw.js')
.then(function() { console.log("Service Worker Registered"); })
.catch(function() { console.log("Service Worker Not Registered"); });
}

上から順に見ていきましょう。

1. if文でService Workerが使えるブラウザであるかを確認

2. その条件に入っていれば処理を開始する

3. registerメソッドでService Workerをブラウザに登録する

4. 成功した場合はconsole.log()でService Worker Registeredという文字が表示される

5. 失敗した場合はconsole.log()でService Worker Not Registeredという文字が表示される

という流れになっています。

さて、ここでまた新しいものが出てきました。

registerメソッドです。一つ一つの説明は色々な所に書かれているので割愛しますが、Service Workerを使うステップについて簡単に触れておきます。

Service Workerを利用するステップ(一例)としては

1. ブラウザで使えるようにダウンロードする(register)

2. それをインストールする(install)

3. 必要ならばアクティベートして古いキャッシュを削除(activate)

4. レスポンスを返す(fetch)

となります。(今回はこれの流れに沿った簡易版のサンプルを作ります)

ブラウザにインストールされて、それが問題なければ、Service Workerが使えるようになるということですね。

シンプルです。

もしダウンロードやインストールに失敗すると、そこで処理が終わるので、アプリケーション内部の、他のJavascriptにも大きな影響は与えません。

さて、簡単にService Workerのことを知ったところで、早速実装をしてみましょう。

今回は適当なページを作り、それがオフラインになっても表示される(!)ということを実現してみます。

必要なのは

* index.html

* style.css(別になくてもいいですが)

* sw.js(service workerのjsだからそういう名前にしているだけです)

* 実装するんだ!という気持ち

以上です。

まずは、index.htmlから作っていきましょう。


index.html

<!DOCTYPE html>

<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="./css/normalize.css">
<link rel="stylesheet" href="./css/main.css">
<title>chap02_01</title>
</head>
<body>
<header class="page-header" role="banner">
<h1>サンプル</h1>
</header>
<div class="page-main" role="main">
<div id="typo">
<div class="inner">PWA Sample</div>
</div>
</div>
<script type="text/javascript">
if('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/sw.js')
.then(function() { console.log("Service Worker Registered"); })
.catch(function() { console.log("Service Worker Not Registered"); });
}
</script>
</body>
</html>


ただのHTMLなので、そこの説明はしません。

一番下の部分に


<script type="text/javascript">
if('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/sw.js')
.then(function() { console.log("Service Worker Registered"); })
.catch(function() { console.log("Service Worker Not Registered"); });
}
</script>

という記述がありますね。

これによって

・Service Worker使えるなら登録するよ

・sw.jsを使えるようにするよ

・それが成功したら、consoleに「Service Worker Registered」って出してね

・それが失敗したら、consoleに「Service Worker Not Registered」って出してね

という事前準備をしています。


sw.js


//一旦は空でOK

sw.jsは一旦空で構いません。

そして、sw.jsの配置はindex.htmlと同じ場所(ルート)においてください。

jsフォルダの中に入れたりする場合は、別途registerの後の引数をご自身で変更をお願いします。

ブラウザでindex.htmlを開いてみましょう。

スクリーンショット 2018-12-09 11.26.37.png

右上に

Service Worker Not Registered

と出ていますね。。無念。

なぜかというと、Service Workerは先ほど少し触れた通り、https通信か、localhostでしか動かないからなんですね。。

では、簡易的にサーバーを立ててやってみましょう。

こちらを使います。

スクリーンショット 2018-12-09 11.39.38.png

できました!(faviconないぞとおこられているけど無視w)

意外と簡単に実装までたどり着けたのではないでしょうか。


オフラインで動作するページを実装してみよう

先ほど少し触れたように、Service Workerはinstall, fetchというイベントを経てキャッシュを返すことになります。

なので、ここからsw.jsを書いて、データをキャッシュさせて、表示させる

ということを書いていく必要があります。

参考はこちら

少し難しいので、とりあえずコピペでも動かせるようにしておきました。


sw.js

const STATIC_DATA = [

'index.html',
'/css/normalize.css',
'/css/main.css',
'/js/main.js'
];

self.addEventListener('install', function(e) {
e.waitUntil(
caches.open('cache_v1').then(function(cache) {
return cache.addAll(STATIC_DATA);
})
);
});

self.addEventListener('fetch', function(event) {
console.log(event.request.url);

event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
);
});


上から順にざっくりと説明をしていきます。

・まずSTATIC_DATAという箱に各ファイルのパスを指定して格納しておきます(今回はhtmlとcssとjsをキャッシュしたいので、それを入れました。)

・次にinstallされた時の処理を書きます


self.addEventListener('install', function(e) {
e.waitUntil(
caches.open('cache_v1').then(function(cache) {
return cache.addAll(STATIC_DATA);
})
);
});

ここですね。

caches,open('cache_v1')でキャッシュを開き

cache.addAll(STATIC_DATA);でキャッシュを登録する

というようなイメージです。

ちなみにこちらにも書いてありますが、

waitUntil()の内部のコードが実行され成功するまでService Workerはinstallされません。

ちなみに、ファイルがすべてキャッシュされた場合、Service Worker のインストールが完了します。

ただ、裏を返すと、渡したファイルのうちどれかひとつでもダウンロードに失敗した場合、インストールは失敗します。

ファイルの数が多すぎると失敗した時に処理が止まってしまうので、ある程度ファイルを絞ってコードを書くほうが良さそうです。


self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
);
});

そして最後の箇所です。

Service Workerがinstallされている状態の時に、他のページヘ移動したりページを更新したりすると、Service Workerはfetchイベントを受け取ります。

また、event.respondWith() の中では caches.match() の Promise を渡しています。caches.match() はリクエストを見て、Service Workerが生成したキャッシュの中に該当するものがあるかを探します。

該当するものがある場合はその値を返し、そうでない場合はコールした結果をfetch()に渡します。

これによりネットワークリクエストが発生し、結果が得られたらそれを返すような仕組みになっています。

もし新しいリクエストを逐次キャッシュさせたい場合は、fetch()のレスポンスを処理し、キャッシュに追加するような形になります。

また、Service Worker自体を更新したいケースもあると思いますが、それについてはこちらをご参照ください。


補足:PWAのデメリット


  • iOSでホーム画面に追加、を実装すると、「戻る」ボタンが消えます。(戻るボタンが使えなくなります。)

  • iOSだとPush通知ができません。

  • iOSだと他にもやれないことがたくさんあります。泣

  • つまり、iOSの端末では、PWAあまり使い物になりません。

  • iOSヘの対応があまりできていないため、日本市場における影響は限定的です。

  • 開発のドキュメントは、まだ日本語が少ないです。


補足:デスクトップPWA

最近、GoogleがDesktop PWAをしきりに推しています。

読んで字のごとく、Desktop上で動作するアプリも提供できるよ、ということですね。

PCのデスクトップに、誰しもフォルダがあり、そこからみたい情報にアクセスすると思いますが、その感覚でアプリを使えるようになるということです。Uberなんかは対応していて、PC上からタクシーを呼べるみたいですね。

僕個人としては、PCをいじっているときに、スマホを出してアプリを立ち上げて、というのが仕事中はめんどくさいので、デスクトップPWAというのは、その一手間を省くものとしては良さそうな気がしています。(Chromeのシェアも高いし、日本でも有効そう)

次回はこの辺りについて深掘りをしてみたいと思っています!

ということで、少し長くなりましたが、PWAの私的まとめでした。