【PWA】Service workersとManifestでWebアプリをプログレッシブに

プログラミング初心者の勉強ブログ #123

PWA実装についてのまとめ。Service workersとManifestを使い、Androidであれば「ホーム画面に追加」のポップアップが出たり、単純にアプリの読み込みが早くなり、NativeアプリっぽいWebアプリにすることができる。1枚のhtmlとJSで作成した単調なアプリも使用感としてアプリっぽさがめちゃ上がる。仕組みをある程度知り、コードをコピペすれば簡単に実装できるのでそちらを紹介していく。

 

目次

[toc]

 

PWAとは

PWAとは、「Progressive Web Apps」の略称で、モバイル向けWebサイトをGooglePlayストアなどで見かけるスマートフォン向けアプリのように使える仕組みです。PWAはそれ自体が何か特殊な一つの技術、というわけではありません。レスポンシブデザイン、HTTPS化など、Googleが定める要素を備えたWebサイトであり、オフラインやプッシュ通知に対応するためのブラウザAPI(Service Workerなど)を利用しているWebサイトをPWAと呼びます。

PWAとは(Progressive Web Appsとは) | SEO用語集:意味/解説/SEO効果など [SEO HACKS]

検索すれば色々詳しく書いてくれているサイトはたくさんあるので、ここではPWAの説明は省く。

実際に僕が実装したアプリをみていく。

pwa1

上のスクショはAndroid端末でWebアプリを開いたときの画面だ。下部に「ホーム画面に追加」ボタンが表示される。このボタンを押すことでホーム画面にブックマークが保存され、ネイティブアプリのような使用感で直接アクセスできる。

 

※このアプリについては、

【JavaScript】Yahoo! JavaScriptマップAPIで地上絵を書くアプリを作る【後編】(完成コードとこだわりについて)

ここで詳細を書いている。

 

ホーム画面に追加後、アイコンをタップすると、

pwa2

見た目が変わる。具体的には上部のURL表示部が無くなる。後述するがPWAはキャッシュを利用しているため、読み込みの速さなどがネイティブアプリに近くなる。ぱっと見のPWAはこんな感じである。

 

仕組み

先ほどからPWA、PWAと言っているが、あくまでPWAという言葉はGoogleが推奨するプログレッシブなWebアプリケーションの機能全般の総称であり、今回のようなホーム画面に追加もPWAの要素の1つくらいの認識をする方が良い。レスポンシブデザイン対応などもPWAの要素の1つである。今回はこのPWAのとりわけ上のようななんちゃってネイティブアプリ化を構成するServce Workersの仕組みをまとめる。

 

Service Woekersとは

Service Workersを使うことで、Webサイトのキャッシュを意図してブラウザに保存させることができる。通常キャッシュは、画像など読み込みが重いデータを初回ロード時に一旦ブラウザが保存しておき、次回の同サイトアクセス時にキャッシュしたデータを参照して読み込み速度を向上させる。だから1回目よりも2回目に訪れたWebページの方が表示が早いわけで。Service Workersは指定したファイルデータ一式をキャッシュとして意図的に保存させていく。これを利用することでなんちゃってネイティブアプリの挙動をするのである。通信の必要のない静的サイトであればオフラインでもキャッシュを元にWebサイトを表示できるのがすごい。

Service Workersはディベロッパーツールで簡単に確認できる。

service worker

「Application」→「Service Workers」 をすると上のスクショのような画面が表示される。設定をかけていると、ここに情報が表示される。今回の場合、geopict.supunic.comに対し、「sw.js」というファイルで設定してますよ、ちゃんと動いていますよ、と表示されている。

 

保存されたキャッシュを確認する

実際にキャッシュされたファイルを確認する場合、ディベロッパーツールの「Application」→「Cache Storage」をみる。

 

Cache Storage下には、URLごとにディレクトリが作成される。そのディレクトリ内にファイルデータが保存されている。

cache storage

上の場合、html、css、image、jsなど計11ファイルをキャッシュとして保存している。これらは初回アクセス時にこのディレクトリに保存される。ここにキャッシュが保存されることで、次のアクセスが早くなる。

 

manifestについて

manifestは、Service Workersを実装するにあたっての様々な設定を行うものである。こちらもディベロッパーツール上で確認することができ、「Application」→「Manifest」から見ることができる。

manifest

 

具体的な設定内容としては、「ホーム画面に追加」の表示をはじめ、アプリ表示形式からテーマカラーや背景色、ホーム画面に表示させるアイコンなど。manifestとService Workersはセットで設定するべきもの。

 

コードとディレクトリ構成

PWAを実際に実装していくにあたりとりあえず、コードはもちろんディレクトリ構成も意識しなければならない。

先に今回の例で挙げている「geopict」のGitHubのリンクを載せる。わかる人はこっちの方がわかりやすいかもしれない。

https://github.com/supunic/geopict

 

ディレクトリ構成

directory

とにかくディレクトリ構成はこう。root直下に「manifest.json」と「sw.js」を配置することで、ここから下のファイル群をService Workersの対象として認識する。「index.html」と並列に置く。

 

実際のコード

sw.jsがService Workersの設定ファイル、manifest.jsonがManifestの設定ファイルである。

 

sw.jsテンプレート

以下はgeopictで設定したときのファイルなので、「// 修正」のコメントアウトの部分を任意に変えていけば解決する。

// configuration
"use strict";

const
  // 修正 (バージョン設定)
  version = '1.0.0',

  // 修正 (Cache Storageに生成されるディレクトリ名の設定)
  CACHE = version + '::geopict',

  // 修正 (オフライン時のroot。どこを表示させるか。)
  offlineURL = './',

  // 修正(必ずキャッシュさせたいファイルを書き込む)
  urlsToCache = [
    './manifest.json',
    './index.html',
    './css/reset.css',
    './css/main.css',
    './js/gp.js',
    './js/map.js',
    './js/main.js',
    './img/pin.png',
    './img/heroImg.png',
  ].concat(offlineURL),
  // 修正 (できればキャッシュしたいファイルを書き込む)
  urlsToCache2 = [
    './img/preloader.gif',
    './img/touch_icon.png',
  ];

//************************************************
//InstallEvent
//************************************************
  self.addEventListener('install', function(event) {
    // インストール処理
    event.waitUntil(
      caches.open(CACHE)
        .then(cache => {
          cache.addAll(urlsToCache2);
          return cache.addAll(urlsToCache);
      }).then(() => self.skipWaiting())
    );
  });

//************************************************
//FetchEvent
//************************************************
  self.addEventListener('fetch', function(event) {
    event.respondWith( // ページにレスポンスを返す(キャッシュがあれば)
      //cacheStrageを参照
      caches.open(CACHE).then(cache => {
           let url = event.request.url;
          //キャッシュファイルがあるかの確認
          return caches.match(event.request)
          .then(response => {
              // キャッシュがあったのでそのレスポンスを返す
              if (response) {
                //Cache
                console.log("Cache:"+url);
                return response;
              }
              //Network
              console.log("Network:"+url);
              return fetch(event.request).then(req => {
                //***Cacheに追加 [Cache固定の場合はコメント!] ***
                //if(req.ok) cache.put(event.request,req.clone());
                return req;
              });
          })
          //OFFLINE
          .catch(function(){
            return offlineAsset(url);
          })
      }) //then(cache=>{}
    );
  });

//*************************************************************************
//OFFLINE OR Image Replace
//*************************************************************************
// is image URL?
let iExt = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'].map(f => '.' + f);
function isImage(url) {
  return iExt.reduce((ret, ext) => ret || url.endsWith(ext), false);
}
//return Offline asset
function offlineAsset(url) {
  if (isImage(url)) {
    // return image
    return new Response(
      '<svg role="img" viewBox="0 0 400 300" xmlns="http://www.w3.org/2000/svg"><title>offline</title><path d="M0 0h400v300H0z" fill="#eee" /><text x="200" y="150" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif" font-size="50" fill="#ccc">offline</text></svg>',
      { headers: {
        'Content-Type': 'image/svg+xml',
        'Cache-Control': 'no-store'
      }}
    );
  } else {
    // return page
    return caches.match(offlineURL);
  }
}

//************************************************
// ActivatedEvent
//************************************************
self.addEventListener('activate', event => {
  console.log('service worker: activate');
  event.waitUntil(
    caches.keys().then(keylist => {
      return Promise.all(
        keylist
          .filter(key => key !== CACHE)
          .map(key => caches.delete(key))
      );
    }).then(() => self.clients.claim())
  );
});

 

 

mainfest.jsonテンプレート

こちらは基本的に全て自分の設定したいもので書き換える。詳細はググった方がわかりやすいのでそちらでお願いしたい。

{
  "name"              : "GeoPict", 
  "short_name"        : "GeoPict",
  "start_url"         : "./",
  "display"           : "standalone", // 表示形式の設定
  "orientation"       : "portrait", // スマホの縦横表示の設定
  "background_color"  : "#fff", // 背景色の設定
  "theme_color"       : "#2cb696",  // テーマカラーの設定
  // アイコンの設定
  "icons": [
    {
      "src"           : "./img/touch_icon.png",
      "sizes"         : "192x192",
      "type"          : "image/png"
    },
    {
      "src"           : "./img/touch_icon.png",
      "sizes"         : "512x512",
      "type"          : "image/png"
    }
  ]
}

 

index.htmlにコード追加

index.htmlのhead内に以下のコードを追加してmanifest.jsonとsw.jsを読み込ませる。

<!-- PWA -->
<meta name="theme-color" content="#F8F7F8">
<link rel="manifest" href="./manifest.json">
<script>
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('./sw.js');
  }
</script>
<!-- /PWA -->

 

 

まとめ

少し勉強が落ち着いたタイミングだったので、今週の土日は思い切って一切コードに触れず休んでみた。考えてみればG’s入ってからあまり休んでなかった気がする。毎日頑張っているのかというと、そんなに頑張っている感覚はないが、精神面は気づかないうちに疲れていることが多いのでたまには気分転換した方がいいかなと。とは言ってもやることはなく、とりあえずだいぶ部屋のホコリが溜まっていたので7割くらい掃除した。気づかないうちにミニマリストの思考に寄りはじめていたのか、部屋の要らないものを無性に捨てたくなった。なんかわからんけど思い出があって捨てられなかった類のものをあっさり捨てることができた。価値観は間違いなくここ半年で変化していると感じた。

遠出はしたくなかったが、家から出て気分転換を測るべく、また、買いたいものがあったので、ちょうど良い距離感にある池袋に行った。池袋の人混みのなかでやっと、今日が日曜日であることを理解し、別日にオフを作れば良かったと少し後悔した。色々お店を回りたかったので、サンシャインシティまで一人で行った。その日は沖縄祭みたいのをやっていたらしく、めちゃめちゃ混んでいた。

とりあえず3COINSで目当てのものを手に入れた。人多すぎもう帰りたい状態だったので、すぐサンシャインシティを出ようとした。しかし、途中でやばい人混みと遭遇した。人混みの中心で誰かが歌っている。まじで勘弁して欲しいわと思いながら、さっさと人混みを通り過ぎようとしたとき、なんか聞いたことのある曲だと気づいた。アーティストを見ると、HYが居た。HYのライブに偶然遭遇した訳である。

HYはめっちゃ好きだったとかそういう感じではなかったが、中学の昼飯の時間に校内放送でAM11:00が流れてくるくらいには世代なので、びっくりした。そりゃこんだけ人混みできるなと理解した。サンシャインシティの1階から4階?の吹き抜けのホールでライブは行われており、僕は1階に居たが、見上げると各階に人だらけだった。

エモさがすごかった。ニューアルバムリリースの宣伝と沖縄祭がゆえのキャスティングだったっぽく、計5曲のうち2,3曲はニューアルバム収録の曲だったが、AM11:00とホワイトビーチが流れたときは、なんか分からんけど懐かしさからくる泣けるエモさがあった。気づいたら最初から最後まで1時間ほど居た。HYのエンターテイメントに魅了されていた。

ひたすらWhy meを考えて、自分は何がやりたいのかを考えて考えて、Why meなんてものはそもそも無いんじゃないかという結論に至りつつある僕の中で、HYのライブにあらわれるWhy meに、それを否定された感じがした。アーティストは「やりたいことやってる」の典型で、そのHYの「やりたいことやってる」がサンシャインシティ内の多くの人を魅了していた事実と、そこにメソッドやロジックみたいなものは無く、純粋に「楽しい」でつき動くものであること。なんかもっとテキトーでいいんじゃね?って感じであり、もっと素直にならんとなって感じであり、まだまだ自分は子供だなと思った的な感じであり、多分そんな物事を深く考える必要はないのであり、要はライブは良いなと思ったわけである。木村カエラの15周年ライブチケット取っとけばよかったなと後悔した。

あと4ヶ月くらい自由に色々できる期間が残っている。ここで何するかで人生変わってくるんでねーの?というスタンスで大体僕は失敗してきた訳なので、あと4ヶ月あるしあれやりたいからやってみよう、的なラフなスタンスでいこうと思う。

以上ありがとうございました。

返信を残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA