Help us understand the problem. What is going on with this article?

俺的PWAの振り返り

はじめに

PWAに手を出したものの、手を出してからほったらかして1年近く。
ちょっと復習しておこうと思います。
理解が充分でないところはこれから深めていきます。
もし間違っていることがあればどんどん突っ込んでくださいw

拙作アプリの「LTタイマー」を題材に進めていこうかと思います。
LTタイマーを作ったのが1年以上前なのでもしかしたらやり方が古いかもしれないので、気づいた時点でアップデートしていきたい。
参考サイトのソースをベースとして、少し手を加えています。

参考

PWA: ServiceWorkerを使って、キャッシュをコントロールする(オフラインハンドリング)
MDN ウェブアプリマニフェスト
僕の考えた最強のService Workerキャッシュ戦略で爆速サービスを作った

PWAとは

PWAとはProgressive Web Appsの略です。
ネイティブアプリのような感じのWebアプリを作成できます。
あくまでネイティブアプリのような感じなので、ネイティブアプリにはできるけどPWAではできないこともあります。

PWAでアプリを作成・動作させるためには、PWAに対応したWebブラウザが必要になります。
PWAに対応していないブラウザでは、ただのWebサイトとして扱われるので心配はいりません。
また、localhostまたはHTTPSでないと、PWAは利用できません。
GitHubを使うと、GitHub PageでHTTPSで公開できるので、Webサーバーを自前で用意しなくてもPWAを試すことができます。
(PWAの勉強会で知った。アウトプットすると予想外のインプットがあるのでアウトプット大事!)

PWA自体は、ウェブアプリマニフェスト(manifest.json) + ServiceWorker + Casch API の組み合わせで成り立っています。

ServiceWorker

Service Workerはブラウザにインストールされ、バックグランドで常駐します。
まだやったことはありませんが、Service Workerでプッシュ通知を扱うことができるとのこと。
あと、ServiceWorkerはDOMにアクセスできません。

manifest.json

manifest.jsonはPWAの設定をJSON形式で記載したものです。
アプリの名前、表示方法、アイコンなどなどが設定できます。

Casch API

Casch APIをつかって、ローカルストレージに読み込んだコンテンツを保存したり、読み込んだりします。
ここではローカルストレージをメインで紹介していますが、実はローカルストレージのほかにSession Strage、IndexedDB、WebSQLなども扱えるとのことです、この辺も、調べないとね。

LTタイマーの構成

GitHubを見てもらうとわかりますが、こんな感じです。

image.png

ちなみに、GitHubの内容は古い場合がありますので注意してください。

manifest.jsonがマニフェスト
sw.jsがサービスワーカーの実装になります。

処理の概要

まずはServiceWorkerの登録処理になります。
登録処理はindex.htmlでおこなっています

index.html
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('./sw.js')
    .then(function(registration){
        console.log('Service Worker install success.');

        registration.onupdatefound = function() {
            // 更新があると呼び出される
            console.log('Update : ServiceWorker');
            registration.update();
        }
    })
    .catch((error) => {
        // registration failed
        console.log('register faild : ', error);
    });
}

まず、ServiceWorkerに対応しているかどうかのチェックが必要になります。
それを最初のif文で行っています。
次に、ServiceWorkerで動かすJavaScriptファイルを登録します。

index.html
    navigator.serviceWorker.register('./sw.js')

ServiceWorkerに変更があった場合の対応として、registration.onupdatefoundメソッドを実装しています。
この中で、update()メソッドを呼び出します。
※気になることが出てきたので、調べて更新する予定

index.html
    registration.onupdatefound = function() {
        // 更新があると呼び出される
        console.log('Update : ServiceWorker');
        registration.update();
    }

sw.jsの内容です。
まず、ServiceWorkerではファイルのキャッシュを行います。
キャッシュするファイルの一覧を配列で宣言しています。
CACHE_NAMEとVERSIONを結合してるのは、ServiceWorkerの更新が発生した際に、キャッシュの更新を行うことを目的としているためです。その辺は、後で説明します。

sw.js
var CACHE_NAME = 'lttimer';
var VERSION="1.0.1"
var CACHE_FILE = [
     './index.html'
    ,'./css/DSEG7Classic-Regular.woff'
    ,'./css/PixelMplus10-Regular.woff'
    ,'./js/jquery-1.9.1.min.js'
    ,'./sound/Zihou01-mp3/Zihou01-1.mp3'
    ,'./sound/Zihou01-mp3/Zihou01-1.ogg'
    ,'./sound/silent.mp3'
    ,'./sound/silent.ogg'
];
const CACHE_KEYS = [
  CACHE_NAME + VERSION
];

次にServiceWorkerのインストールイベントでキャッシュへの登録処理を行います。
installイベントは1回しか呼び出されません。

sw.js
self.addEventListener('install', function(e) {
    e.waitUntil(
        caches.open(CACHE_NAME.then(function(cache) {
            return cache.addAll(CACHE_FILE);
        })
    );
});

installイベントが終わるとactivateイベントが呼び出されます。
acticateの中では、キャッシュのキーと一致しないキャッシュの削除処理をおこなっています。

sw.js
self.addEventListener('activate', event => {
    event.waitUntil(
        caches.keys().then(keys => {
            return Promise.all(
                keys.filter(key => {
                    return !CACHE_KEYS.includes(key);
                }).map(key => {
                    // 不要なキャッシュを削除
                    if(key.indexOf(CACHE_NAME) == 0){
                        console.log("ServiceWorker : " + key+ " remove");
                        return caches.delete(key);
                    }else{
                        console.log("ServiceWorker : " + key+ "no remove");
                        return true;
                    }
                })
            );
        })
    );
});

次にfetchイベントでリクエストを処理します。
ここでリクエストをも元にキャッシュから取り出したり、キャッシュにないファイルを登録します。
場合によっては古いキャッシュを削除し、新しいファイルをキャッシュします。
(ここもほぼコピペであまり理解していない・・・^^;)

fetchイベントの中はオンライン、オフラインで処理を分けます。
オンラインオフラインの切り分けは navigator.onLine でできるとのこと。

sw.js
self.addEventListener('fetch', function(event) {
  //ブラウザが回線に接続しているかをboolで返してくれる
  var online = navigator.onLine;
  if(online){
    //オンラインのときの制御
  }else{
    //オフラインのときの制御
  }
});

オンラインの処理はこんな感じ
まだ、処理を完全に理解しきっていないのですが、たぶんこのコメント通りでいいはず。

sw.js
    event.respondWith(
        caches.match(event.request).then(
            function (response) {
                if (response) {
                    // キャッシュを返す
                    return response;
                }
                return fetch(event.request).then(function(response){
                    // キャッシュにないので追加
                    // Responseはストリームなのでキャッシュなので複製
                    cloneResponse = response.clone();
                    if(!response || response.status != 200){
                        //正常に取得できなかったときにハンドリングしてもよい
                        console.log("ServiceWorker : request faild " + response.status);
                    }else{
                        //現行のキャッシュに追加
                        caches.open(CACHE_NAME + VERSION).then(function(cache){
                            cache.put(event.request, cloneResponse).then(function(){
                                //正常にキャッシュ追加できたときの処理(必要であれば)
                                console.log("casshed");
                            });
                        });
                    }
                    return response;
                }).catch(function(error) {
                    //デバッグ用
                    return console.log(error);
                });
            }
        )
    );

event.respondWith()の中で、matchメソッドでキャッシュの検索結果を処理します。
一致すればそれを返却し、一致しなければ、キャッシュに登録します。

オフラインの時はこんな感じ。

sw.js
    event.respondWith(
        caches.match(event.request).then(
            function(response) {
                // キャッシュがあったのでそのレスポンスを返す
                if (response) {
                    return response;
                }
                //オフラインでキャッシュもなかったパターン
                return caches.match("offline.html").then(function(responseNodata){
                    //適当な変数にオフラインのときに渡すリソースを入れて返却
                    //今回はoffline.htmlを返しています
                    return responseNodata;
                });
            }
        )
    );

offline.htmlを返却するようにしているけど、offline.htmlを用意していない・・・w
基本的にはオンラインの時と同じでmatchの結果で処理を行っています。

ここまでで最低限と思われる処理になります。

manifest

manifestファイルには、アプリケーションのアイコンや、名称などの情報を設定します。
設定項目が多くあるので、MDNのウェブアプリマニフェストのページを参考にするとよいと思います。

manifest.json
{
    "name": "LT Timer",
    "orientation": "landscape",
    "display": "standalone",
    "start_url": "./",
    "short_name": "LT Timer",
    "description": "LT Timer",
    "background_color": "#000020",
    "theme_color": "#000020",
    "icons": [
            {
                "src": "./img/icon_48.png",
                "type": "image/png",
                "sizes": "48x48"
            },
        {
            "src": "./img/icon_96.png",
            "type": "image/png",
            "sizes": "96x96"
        },
        {
            "src": "./img/icon_192.png",
            "type": "image/png",
            "sizes": "192x192"
        }
    ]
}

とりあえず、使っている設定の概要を

設定名 概要
name アプリケーション名
orientation アプリケーションの向き
display standalone
start_url アプリケーションの開始URL
short_name アプリケーションの短縮名
description アプリケーションの説明
background_color 背景色
theme_color アプリケーションのテーマ色
icons アプリケーションのアイコン

デバッグ

デバッグはChromeのデベロッパーツールを使うのが良いと思います。
SafariとかFirefoxとかは使わないのでよくわからないけど。

デベロッパーツールをですが、通常のWebの開発で利用する機能は当然ですが、PWAに関するところで重要なのが以下の2点。

  • 保存したコンテンツを削除
  • Service Workerの登録解除
  • オンライン・オフラインの切り替え

これができないとデバッグが大変。というか無理でしょう。
上記の内容は、デベロッパーツールのApplicationタブで確認できます。

また、console.logでログを出力ができます。
というか、デベロッパーツールでApplicationタブのサービスワーカーを見るとエラーがカウントされているけど、確認ができません(自分だけ?)
そのため、ログに出さないとサービスワーカーでエラーが発生しても、エラーがでいるかの把握が困難になります。

最後に

この内容はYoutubeのチャンネルや日本Androidの会 浜松支部の2月の定例会でも扱いたいなぁと思っています。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした