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

PWA サンプルを動かしてみた

More than 1 year has passed since last update.

やること

PWAのサンプルをいじって理解する。

入れる機能

更新がないコンテンツ(AppShell)はSWインストール時にデータをCacheに格納し、、それ以降はCacheのみをみる

更新が頻繁にあるコンテンツは、まずCacheのデータを使い、同時にリクエストを送信して帰ってきたレスポンスを使って再表示する。

ネットワークに繋がらなくてもCacheがないページは見栄えの良いエラーページを見せるようにする

完成形のデザイン

スクリーンショット 2018-12-02 16.07.05.png

左上のメニューを開いてHelpという文字をクリックすると別のページに飛ぶ

スクリーンショット 2018-12-02 16.08.21.png

実装

ずっと更新されないコンテンツと頻繁に更新されるコンテンツごとにキャッシュ戦略を練りたい。

入れる機能の部分でも紹介したが、上記写真の上にある写真はヘッダーなので、こいつは変える予定がない。

また、ヘッダーのデザインやメニューのデザインなど、いわゆるAppShellと呼ばれる外側の見栄えに関してはそうそう変えることない。

よって、SWをinstallした時点で全てCacheする。ただこのままでは、SWを更新した時やブラウザを開き直した時など、 SWをインストールするというタイミングで無駄に通信が発生してしまう。

よってfetchイベントの時に、更新されないコンテンツへのURLかを確認し、当てはまればfetchせずにcacheを返すことにする。またここで注意したいのは、このままだと一番最初にユーザーがページに訪れてSWをインストールしたとしても上の処理によっていつまでたってもCacheされなくなってしまう。

なので、更新されない静的コンテンツに関しては、Cacheにあればそれを返し、Cacheになければfetchしてくる。これによって初回のインストール時以外は、Cacheから読むことになる。

これがコードだ。

self.addEventListener("install", function (event) {
  console.log("[Service Worker] Installing Service Worker ...", event);
  event.waitUntil(caches.open(CACHE_STATIC_NAME).then(function (cache) {
    console.log("Service Worker  Precaching App shell")
    cache.addAll([
      "/",
      "/offline.html",
      "/index.html",
      "/src/js/app.js",
      "/src/js/feed.js",
      "/src/js/material.min.js",
      "/src/css/app.css",
      "/src/css/feed.css",
      "/src/images/main-image.jpg",
      "https://fonts.googleapis.com/css?family=Roboto:400,700",
      "https://cdnjs.cloudflare.com/ajax/libs/material-design-lite/1.3.0/material.indigo-pink.min.css",
      "https://fonts.googleapis.com/icon?family=Material+Icons"
    ])
  }));
});

ネットワークエラーの時に読み込む最後の手段としてのoffline.htmlもここで取りに行くのを忘れないようにされたい。

そしてfetchイベントの中身はこちら

function isInArray(url) {
  for (var item of staticFiles) {
    console.log(url.match(/*item*/))
    if (url.match(/*item*/)) {
      return true
    }
  }
  return false
}



function requestAfterCache (cacheType, event) {
  event.respondWith(
    caches.match(event.request).then(function (response) {
      // "https://httpbin.org/get"
      // 上のURL出なかった場合は、Cacheを見にいきなければfetchしてcacheするという戦略
      if (response) {
        console.log("there a cahce!!")
        return response
      } else {
        console.log("there are no cache")
        return fetch(event.request)
          .then(function (res) {
            return caches.open(cacheType).then(function (cache) {
              cache.put(event.request.url, res.clone())
              return res
            })
          })
          .catch(function (err) {
            return caches.open(CACHE_STATIC_NAME)
              .then(function (cache) {
                // 通信に失敗したのがhtmlファイルの要求だった場合のみoffline.htmlを表示
                if (event.request.headers.get("accept").includes("text/html")) {
                  return cache.match("/offline.html")
                }
              })
          })
      }
    })
  )
}


self.addEventListener("fetch", function (event) {
 if (isInArray(event.request.url)) {
    requestAfterCache(CACHE_STATIC_NAME, event)
  } else {
    requestAfterCache(CACHE_DYNAMIC_NAME, event)
  }
})

これで最初だけ通信が発生し、2回目以降はCacheをみに行ってくれているのがconsoleから分かります

最後は、動的な(頻繁に更新があるコンテンツ)です。パフォーマンスとアップデート性を両方兼ね備えたCacheThenNetworkというキャッシュ戦略をとります。

まずはCacheをみて、あればCacheのデータを表示して、同時並行で新しいコンテンツがないかをリクエストして、帰ってきたレスポンスを使用して更新するというものです。

上記の写真でいうと、下の写真が頻繁に更新されるやつです。

実際に写真を表示させて、その後コンテンツをアップデートするときちんと更新されているか確認してみましょう。

次の写真は、https://httpbin.org/getにjsで通信を飛ばして返り値を使ってUIを作っています。

スクリーンショット 2018-12-02 16.07.05.png

本来なら返ってくるjsonの値を変更したいところですが、jsonをCacheする方法は今回は省きたいので、便宜的にjsonが返ってきた後に挟むimgタグの中身を変えてみたいと思います。

まずhttps://httpbin.org/getへのリクエストのfetchコードは以下です。
動的に作るUIの作成に必要な画像もこのCache戦略を使用します。

self.addEventListener("fetch", function (event) {
  var url = "https://httpbin.org/get"
  var url2 = "/src/images/sf-boat.jpg"
  if (event.request.url.indexOf(url) > -1  ||  event.request.url.match(/*url2*/) ) {
    console.log("aaaaawgegwegagawg")
    event.respondWith(
      caches.open(CACHE_DYNAMIC_NAME).then(function (cache) {
        return fetch(event.request).then(function (res) {
          cache.put(event.request, res.clone())
          return res
        })
      })
    )

次にSW.jsではなく実際にUIを形成しているJSファイルのコードは以下です

var url = "https://httpbin.org/get"
fetch(url)
  .then(function(res) {
    return res.json();
  })
  .then(function(data) {
    console.log("From web", data)
    clearCards()
    createCard();
  });


var networkDataReceived = false

if ("caches" in window) {
  caches.match(url).then(function(response) {
    if (response) {
      return response.json()
    }
  }).then(function(data) {
    console.log("from cache", data)
    if (!networkDataReceived) {
      createCard()
    }
  })
}

createCard関数で画像を使ってカードのデザインを作り上げてます。
万が一ネットワークを介してデータを取ってきた後にCacheの読み込みが終わって上書されると困るので、リクエストが完了したかどうかの変数で処理を分けてます。

これによって2回目ページを読み込んだ際、サーバのデータが変わっていれば、それが反映されるようになりました。

ちなみにネットワークに繋がっていなければCacheをみに行き、それでもなければMimeTypeに応じて表示するページを変えるのが良いそうです。

本来ならBackgroundSyncやJsonデータのCacheなどまだまあSWのメリットはたくさんありますが。これだけでもPWAのすごさがお分り頂けたのではないでしょうか?

Why do not you register as a user and use Qiita more conveniently?
  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
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