JavaScript
HTML5
HTML5Day 21

Progressive Web Appを作ってみる

More than 3 years have passed since last update.

Progressive Web Appとは

ServiceWorker、PushNotification、WebManifestをウェブアプリに導入することで、ネイティブアプリに近い操作性(パフォーマンスやユーザーエクスペリエンス)を提供することを可能にします。
また、これら機能に対応していないブラウザに対しては従来通りのウェブサイトとして提供されます。

環境構築

では、実際にProgressiveWebApp(以下、PWA)に対応した、サイト(シングルページレイアウト)を作っていきましょう。

ディレクトリ構成は以下のとおり

├── images
│   ├── bg.png
│   ├── building.jpg
│   ├── lake.jpg
│   ├── sky.jpg
│   └── tree.jpg
├── index.html
├── manifest.json
├── serviceWorker.js
├── scripts
│   └── index.js
└── styles
    ├── index.css
    └── normalize.css

Web Starter Kitで環境構築(オプション)

Web Starter Kitとは、ウェブアプリプロジェクトの雛形を自動で構築してくれるものです。Web Starter KitではServiceWorkerの構築まで行ってくれるのでおすすめです。
https://developers.google.com/web/tools/starter-kit/

Firebase(オプション)

Firebaseとは、Key/Valueストレージのバックエンドサービスなのですが、ファイルをデプロイするとウェブ環境も提供してくれます。また、SSLも同時に提供されますのでおすすめです
https://www.firebase.com

Service Worker

それでは、Service Worker(以下、sw)を設置してリソースをキャッシュし、オフラインでもページを表示したり、キャッシュをコントロールしてウェブアプリのパフォーマンスを上げてみましょう。

index.htmlにJSを読み込ませます

index.html
...
<script src="scripts/index.js"></script>
</body>

読み込ませた、index.jsを編集していきます。
リクエストがhttpsまたは、localhostであるかの判定(swは、この環境でしか動かないので)

scripts/index.js
    var isLocalhost = Boolean(
        window.location.hostname === 'localhost' ||
        window.location.hostname === '[::1]'     ||
        window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
    );

swがサポートされている場合、serviceWorker.jsを登録します。
ここで登録されたserviceWorker.jsは、リクエストがきた時に、ブラウザのswが実行するものになります。

scripts/index.js
    if (
        'serviceWorker' in navigator &&
        (window.location.protocol === 'https:' || isLocalhost)
    ) {
        navigator.serviceWorker.register('serviceWorker.js')
            .then(
            function (registration) {
                if (typeof registration.update == 'function') {
                    registration.update();
                }
            })
            .catch(function (error) {
                           });
    }

serviceWorker.jsにリクエストされたページをキャッシュする処理を書きます。
ブラウザのswが、serviceWorker.jsを読み込んだ際に、installイベントが発火します。その際に指定したリソースをキャッシュさせます。

urlsToCacheの配列にキャッシュしたいリソースをフルパスで登録します。

serviceWorker.js
var CACHE_NAME  = "my-portfolio-cache-v1";
var urlsToCache = [
    "/",
    "/styles/index.css",
    "/scripts/index.js",
    "https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css",
    "/images/bg.png",
    "/images/building.jpg",
    "/images/lake.jpg",
    "/images/sky.jpg",
    "/images/tree.jpg"
];

installイベント時に指定されたリソースをキャッシュします

serviceWorker.js
self.addEventListener('install', function(event) {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(
            function(cache){
                return cache.addAll(urlsToCache);
            })
    );
});

次にリクエスがあった場合に発火されるfetchイベントにキャッシュを返す処理を書きます。

serviceWorker.js
self.addEventListener('fetch', function(event) {
    event.respondWidth(
        caches.match(event.request)
            .then(
            function (response) {

                if (response) {
                    return response;
                }

                return fetch(event.request);
            })
    );
});

responseをreturnするとキャッシュされたリソースを。event.requestをreturnするとリクエストされたリクエストを返します(キャッシュは使わない)
つまり、上記の例では、キャッシュがあった場合にキャッシュを返し、無かったらリソースを取りに行くという感じになります。

上記を少し変更して、イメージの場合はキャッシュ優先、その他の場合はネットワーク優先にしてみます。

serviceWorker.js
self.addEventListener('fetch', function(event) {
    if (isImage(event.request.url)) {
        event.respondWith(
            caches.match(event.request).then(function (response) {
                return response || fetch(event.request);
            })
        );
    }
    else {
        event.respondWith(
            fetch(event.request).catch(function() {
                return caches.match(event.request);
            })
        );
    }
});

function isImage (url) {
    return Boolean(url.match(/(\.png)|(\.jpg)$/));
}

その他にいろいろな形でキャッシュをコントロールすることが出来ます。
https://jakearchibald.com/2014/offline-cookbook/#serving-suggestions-responding-to-requests

まとめ

Service Workerを使ってキャッシュをコントロールすることで、これまで画像の読み込みやJS/CSSの読み込みでページロードが発生したものを改善することが出来ます。某賃貸サイトではService Workerの活用により、ページのロード時間が 0.8 秒から 0.2 秒に短縮されたそうです。

このようにキャッシュを利用するだけでも、ウェブアプリがネイティブアプリのようなパフォーマンスで動かすことが可能となります。次回は見た目とプッシュ通知を導入するところをやっていきたいと思います。