Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

de:code2018セッションフォローアップ「進化する Web ~ Progressive Web Apps の実装と応用 ~」

ひさびさにブログを書く時間が取れたので、今ごろですが de:code 2018 で私が担当した AD09「進化する Web ~ Progressive Web Apps の実装と応用 ~」のについてのフォローアップ記事を書きます。

二日目の最終セッションにもかかわらず、ご参加くださいました皆様、誠にありがとうございました。

アンケートの内容を見るに、多くの方にご満足いただけたようで非常にうれしく思います。

この記事では、同セッションの内容について記述していきます。

進化する Web ~ Progressive Web Apps の実装と応用 ~

進化する Web ~ Progressive Web Apps の実装と応用 ~ from Osamu Monoe

この記事では、昨今話題に上ることが多い PWA こと Progressive Web アプリケーションについて、実際の作り方を解説しながら、それがいったいどういったものであるかを詳(つまび)らかに紹介することを目的としています。

Web では、「ネイティブアプリと同じことができる」、「ネイティブアプリを置き換える」など、期待に胸を膨らませずにいられない浪漫に満ちた噂がありますが、それが本当かどうか記事をご覧いただくとご理解いただけると思います。

Progressive Web Apps のつくり方を紹介する前に、簡単に概要を紹介しましょう。

Progressive Web Apps とは?

Progressive Web Apps (以下 PWA と記述) は、一言でいうならば「ネイティブアプリのような UX を提供する Web アプリの概念」といったところでしょう。2015 年の 11 月に開催された Chrome Dev Summit 2015 のキーノートで発表されて話題となりました。

もともとは、その年の 10 月に Alex Russell 氏(Google) がブログ記事に投稿した、クライアントの性能に合わせて段階的に進歩する Web アプリケーションのコンセプトでした。

PWA というと、"ネイティブアプリのような体験" という特徴ばかりが注目されますが、大きな特徴がもうひとつあります。それは、PWA という名前にも含まれている Progressive というところです。

これは HTML5 が普及しはじめたころにあった、「HTML5 が解釈できるモダンブラウザーにはリッチな体験を、そうでないブラウザーには従来とおなじ体験を」というデザインの考え方 Progressive Enhancement に由来します。

PWA はこの思想を踏襲し、性能の低い Web ブラウザーを切り捨てることなく、クライアントの性能に合わせた機能提供を行います。

PWA は、性能の低い Web ブラウザーからアクセスがあった場合は従来と同じ Web ページとして動作し、PWA が動作する機能を備えた Web ブラウザーからアクセスがあった場合は、その機能を活かし、これまでの Web アプリには無かったネイティブアプリのような体験を提供します。

Progressive Web Apps を実現する機能

それでは、"これまでの Web アプリには無かったネイティブアプリのような体験" というのはどのようなものがあるでしょう?

代表的なものとしては以下の 4 つが挙げられます。

  1. オフラインサポート
  2. プッシュ通知
  3. バックアップグラウンド処理
  4. デバイスへのインストール(デバイスのホーム画面から起動できる)

これらはの機能はそれぞれ以下のような新しい API によって実現されます。

  1. オフラインサポート - Cache API
  2. プッシュ通知 - Push API
  3. バックアップグラウンド処理 - Background Sync
  4. デバイスへのインストール(OS のメニューから起動できる) - Web App Manifest

そして、これらの機能を提供しているのが 1 ~ 3 が Service Worker で、 4 が Web App Manifest です。

PWA の定義に明確なものはありませんが、上記の機能の中からオフラインサポートと、インストール(アイコンをデバイスのホーム画面に追加)機能をサポートしていれば、PWA を名乗ってもそう否定されることはないでしょう。

これらの機能により、デバイスのホーム画面から、ナビゲーションバーなどのブラウザー UI が無い状態でアプリケーションを起動し、オフラインの状態でも使用できる、という体験をユーザーに提供できます。

これらの体験は、これまでの Web アプリケーションには無かったネイティブアプリのならではのメリットと言えるでしょう。

それでは、逆に Web ならではのメリットとはなんでしょう?

Web のメリット

ネイティブアプリにはない Web ならではのメリットを、Google さんのイベントではよく以下の SLICEという言葉で表しています。

  • Secure (安全)
  • Linkable (リンク可能)
  • Indexable (インデックス可能)
  • Composable (再構成可能)
  • Ephemeral (一時的な利用)

これら従来の Web とネイティブアプリの体験上のメリットを合わせたものが PWA のメリットとなります。

Progressive Web Apps が提供する価値

PWA の提供する価値について、Google さんのイベントではよく FIRE という言葉で紹介されていますが、

  • Fast: パフォーマンスの良い、軽快な動作
  • Integrated: OS と統合されたユーザー体験
  • Reliable: オフラインでも動作する利便性と信頼性
  • Engaging: Web サイトの価値向上

この記事ではもう少しかみ砕いて紹介しましょう。

発見性

PWA であれば、アプリを公開するのにアプリ ストアにわざわざお金を払ってアプリを提出したり、審査を通すための不自由なルールに縛られる必要はありません。

これまでの Web アプリと同じようにインターネットに公開しておけば、検索エンジンがクロールして見つけてくれます。

いままでの SEO のスキルがそのまま使えるうえ、Web は、アプリ ストアとは比べものにならない数のユーザーが、比べものにならないくらいの回数、常に検索を行っています。

この圧倒的なオポチュニティ(機会)の違いは、自分自身が今日何回 Web を検索したか、アプリストアを検索したか、比べてみればよくわかるでしょう。

インストール可能

PWA はさまざまなデバイスにインストール可能ですが、そのためにプラットフォームごとに違う開発言語で書き直したりパッケージンクしなおしたりする必要はありません。

PWA をサポートしている Web ブラウザーであれば、同じアプリケーションをその全部にインストールして使うことができます。

また、"インストールしなくても使える" 点もメリットです。

もともとが Web ページなので、ちょっとだけ使ってみるということが可能です。

サービスを試用してみようと思ったときに、"アプリのインストールが障壁になって利用を中断してしまった"、という経験は誰にでもあることでしょう

また、スマートフォンでコンテンツを閲覧中に、突然、アプリのストア画面が表示され、アプリのインストールを促されて不快な思いをしたこともあるかと思いますが、そういったことを避けられます。

PWA であれば提供者もユーザーもアプリのインストールについてのネガティブな点を回避することができます。

再エンゲージ可能

従来の Web ページであれば、ユーザーがコンテンツから離脱したあとは、再び訪問してくれることを祈るくらいしかできませんでしたが PWA はサーバー側から通知を Push することができます。

これによりユーザーに即時的な価値を提供できます。例えば、EC ショップであればタイムセールの開始であるとか、オークションサイトであればオークションの開始や、最高落札額の更新などです。通知機能を適切に利用することで、ユーザー側の機会獲得を増やし、再エンゲージを促すことができます。逆にどうでも良いことを通知しすぎるとユーザーの心象を害するので注意が必要です。

とくに、初めてページを訪問したユーザーに「プッシュ通知を許可しますか?」というメッセージを表示するのは避けたいところです。

ネットワーク非依存

PWA はオフラインでの使用が可能ですので、ネットワークの状態に左右されないように作ることが可能です。また、常に使用されるアセットをローカルにキャッシュすることで表示のスピードアップや、回線の使用料を減らすことにも貢献します。

プログレッシブとレスポンシブ

PWA はきちんとその思想を理解してつくれば、低機能なブラウザーや PWA をサポートしないデバイスにもサービスを提供できます。また、さまざまな画面サイズに対応するためのレスポンシブな機能については、Web には Media Queries 等、そのためのナレッジもリソースも豊富に存在するため、既存のスキルを活かして機能を実装できます。

安全

PWA は Web コンテンツと同じ Web ブラウザーの強力なサンドボックス内で動作するため、ネイティブアプリのようにユーザーの強い権限で動作して誤動作や悪意のあるコードによってシステムに深刻なダメージを与えることはありません。また https でしか動作しないためサーバーとのやりとりを安全に行うことでできます。

リンク可能

PWA はインターネット上でユニークな URL をもっており、ハイパーリンクのあるさまざまなところからサービスに接続することができます。PWA を使用するのにアプリ ストアは必要なく、面倒なインストールプロセスも必要ありません。

PWA のメリットは、Web とネイティブ アプリののメリットを合わせたということだけでなく、それらを状況に合わせて取捨選択できることです。

Progressive Web Apps を実現する API

ここからは PWA を実現するための API と、それらをどのように使ってアプリケーションを構築していくかについて紹介します。

Service Worker

PWA を実現するうえで中心となる機能を提供するのが Service Worker です。

Service Worker とはなにか?、端的に言うと、「バックグラウンドで動作する "プログラミング可能な"ネットワークプロキシ」です。

Service Worker は Web Worker の一つで、Web ページのスクリプトとは独立して動作しており、DOM にアクセスしたりすることはできませんが、Web ページのネットワーク リクエストすべてをインターセプトできます。

つまり、Web ページからのリクエストを横取りしてキャッシュしたものを返したり、改ざんしたりといったことができます。そのため localhost、127.0.0.1 接続以外は https の使用が必須となります。

Service Worker が提供する機能

Service Workerが提供する機能は主に以下の四つです。

  • キャッシュ
  • リクエストのハンドリング
  • Push 通知
  • バックグラウンド同期

これらの機能を図で紹介します。

キャッシュとリクエストのハンドリング

Service Worker は Web ページからのリクエストの中に、自分のキャッシュリストに含まれるアセットを見つけると、レスポンスからこれを取得してキャッシュします。

それ以降、Web ページからリクエストされたアセットがキャッシュ内に存在する場合は、そのキャッシュされたアセットを返します。ネットワークから取得しないので、アセットの取得は高速に完了し、通信コストは発生しません。

Push 通知

関連付けられた Web ページがアクティブでなくても、サーバーからの Push を受け取り Notification API 等を使用してユーザーに通知を行えます。

バックグラウンド同期

オフライン中にユーザーが行った操作をキャッシュし、

オンライン時にその内容を同期することができます。

このように Service Worker はプログラミング可能なネットワークプロキシとして、これまでにないさまざまな機能を提供します。

これらの機能の 2018 年 6 月時点のサポート状況は以下のとおりです。

ここからは Service Worker の具体的使い方について解説していきます。

Service Worker を使う準備

Service Worker を使った開発を行う際に用意しなければならないものがあります。

Web アプリケーション、もしくは Web コンテンツが必要です。これはけして SPA (Single Page Application) のようなアプリケーション然としたものである必要はなく、既存の一般的な Web ページであってもかまいません。

この Web ページ、もしくは Web アプリケーションは、html や css、js、や画像に代表されるメディアといった複数のファイルで構成されていると思いますが、それとはべつに Service Worker 用の JavaScript ファイルをひとつ用意します。ここでは便宜上 sw.js と名付けます。

この Service Worker 用の JavaScript ファイルは Web コンテンツ側の JavaScript コードから登録され稼働を開始します。

Service Worker は、自身のファイルが配置された以下のディレクトリに対しスコープを持つので、コンテンツ/アプリケーション全体をキャッシュするなど管理下におきたい場合は Web サイトのルートに配置します。

Service Worker の登録

Service Worker の登録は Web コンテンツの JavaScript から行います。

登録に必要なコードは非常にシンプルで、最低限、以下のコードで問題を発生させることなく Service Worker の登録が行えます。

Web コンテンツ側

//Service Worker がサポートされているかチェック
if (navigator.serviceWorker) {
   //Service Worker を登録
   navigator.serviceWorker.register('/sw.js');
}

Web コンテンツ側のコードから navigator.serviceWorker.register メソッドにより Service Worker の登録が行われると、Service Worker 用の JavaScript コード (ここでは sw.js) では install イベントが発生します。

sw.js 側

//キャッシュするアセットのリスト
var urlsToCache = [
   '/',
   '/index.html',
   '/css/index.css',
   '/script/index.js'
];
//install イベントのハンドラ
Self.addEventListener('install', function(event) {
   event.waitUntil(
      //キャッシュを開く
      caches.open('キャッシュの名前')
         .then(function(cache) {
            //アセットのリストをキャッシュに登録
            return cache.addAll(urlsToCache);
   }));
});

install イベントハンドラ内では、キャッシュを開き

//キャッシュするアセットのリスト
var urlsToCache = [
   '/',
   '/index.html',
   '/css/index.css',
   '/script/index.js'

];
//install イベントのハンドラ
Self.addEventListener('install', function(event) {
   event.waitUntil(
      //キャッシュを開く
      caches.open('キャッシュの名前')
         .then(function(cache) {
            //アセットのリストをキャッシュに登録
            return cache.addAll(urlsToCache);
   }));
});

キャッシュにアセットのリストを登録します。

//キャッシュするアセットのリスト
var urlsToCache = [
   '/',
   '/index.html',
   '/css/index.css',
   '/script/index.js'

];
//install イベントのハンドラ
Self.addEventListener('install', function(event) {
   event.waitUntil(
      //キャッシュを開く
      caches.open('キャッシュの名前')
         .then(function(cache) {
            //アセットのリストをキャッシュに登録
            return cache.addAll(urlsToCache);
   }));
});

Service Worker が登録され、install イベント内の処理が無事に完了すると、次に activate イベントが発生します。(もし install イベント内の処理が失敗した際には error イベントが発生します。)

self.addEventListener('activate', function(e) {
  e.waitUntil(
    caches.keys().then(function(keyList) {
      return Promise.all(keyList.map(function(key) {
        if (key !== 'キャッシュの名前') {
          return caches.delete(key);
        }
      }));
    })
  );
});

install イベント内で"しなければいけない処理"というものはとくにないのですが、一般的に古いキャッシュを削除するのに使用されます。このサンプルコードではキャッシュからキーの一覧を取り出し、

self.addEventListener('activate', function(e) {
  e.waitUntil(
    caches.keys().then(function(keyList) {
      return Promise.all(keyList.map(function(key) {
        if (key !== 'キャッシュの名前') {
          return caches.delete(key);
        }
      }));
    })
  );

});

キャッシュの名前 (新しく指定された) と比較し、異なるものをすべて削除しています。

self.addEventListener('activate', function(e) {
  e.waitUntil(
    caches.keys().then(function(keyList) {
      return Promise.all(keyList.map(function(key{
        if (key !== 'キャッシュの名前') {
          return caches.delete(
key)
        }
      }));
;
    })
  );
});

上記サンプルコードの、"キーの一覧と新しく登録されたキャッシュの名前を比較して異なるものをすべて削除してしまう"、というやり方は少々乱暴かもしれませんので、実際に実装を行う際にはどのようにキャッシュの削除を行うかは充分に考慮してください。

なぜ activate イベントでキャッシュを削除するのか?

Service Worker 自体は、Service Worker 用の JS ファイルを更新することで再登録されますが、キャッシュが削除されるわけではありません。よって、いずれどこかのタイミングで用済みとなった古いキャッシュを削除する必要が出てきます。

たとえば、キャッシュの対象となっている index.html に更新があった場合は、更新前の index.html を保持しているキャッシュを削除しないかぎりユーザーにはいつまでも更新前の index.html が表示されることになります。

Service Worker 用の JS ファイルが更新されたあと、新しい Service Worker はバックグラウンドでインストールされますがまだ動作はしません。新しい Service Worker に制御が移るタイミングは古い Service Worker が制御しているすべてのページが閉じた後からになります。そのため install イベントで古いキャッシュを削除してしまうと、まだ動作している古い Service Worker はキャッシュを使用できなくなってしまいます。

いっぽう activate イベントではページの制御が新しい Service Worker に移っているので、古い Service Worker が使用していたキャッシュを削除することができます。

なにをキャッシュさせるか?

キャッシュさせるべきは App Shell です。App Shell はアプリケーションの UI が機能するために必要な最小限のリソースです。

App Shell をローカルにキャッシュすることで、アプリケーションの UI 表示と使用可能となるまでの時間を短縮することができ、オフラインでの使用も可能になります。

また要件に応じて、キャッシュが有効となるアセット類を指定しても良いでしょう。

しかし、なんでもキャッシュさせれば良いというものでもありません。

たとえば、WebGL ベースのゲームは、一般的に使用するアセット類のサイズが大きく、これを毎度起動する際にサーバーからダウンロードするのは、時間もかかり通信コストもかさみます。

たしかに、これらをキャッシュさせておけば、通信コストを下げ、ゲーム開始までの時間も短縮できます。

しかし、キャッシュストレージの容量には制限があり、また、cache オブジェクトの代わりに容量の大きい IndexedDB を使用したとしてもクライアントのストレージを占有することになります。

PC などのストレージ容量の大きいデバイスではあまり問題にならないかもしれませんが、スマートフォンのようなモバイル デバイスではストレージ容量を圧迫することにつながります。

よって、なにをキャッシュさせるかは充分に考慮する必要があります。

リクエストのハンドリング

Service Worker は有効化(activate)された後、アイドル状態となり Web ページからのリクエストやサーバーからの Push を待ちます。

アイドル状態のとき、制御下にある Web ページでリンクをクリックするなどしてネットワークリクエストが発生すると Service Worker では fetch イベントが発生します。

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

fetch イベントハンドラの引数として渡されるオブジェクトに、発生したリクエストが含まれるので、同リクエストがキャッシュ内に存在するのか調べ、

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

キャッシュ内にリクエストが存在すればそれを返し、存在しなければ fetch メソッドを使用してネットワークにリクエストを投げます。

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

それぞれの処理結果をリクエスト元の Web コンテンツ側に返します。

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

この動作により Web コンテンツ側では Service Worker やキャッシュを意識することなく、これまで通りの方法でページの制作を行うことができます。

また既存の Web コンテンツに Service Worker の機能を追加する場合も、Service Worker を登録するためコードを追加する以外の作業は、基本的に必要ありません。

de:code 2018 のセッション動画で Service Worker を追加するデモを行っていますので、ぜひ実際の動作をご覧ください。

Web App Manifest

Web App Manifest は、デバイスのブラウザーによって [ホーム画面に追加] される際のアイコンや、ホーム画面から起動した際のスプラッシュアイコンや背景色、アプリケーションが動作するウィンドウのスタイルを定義します。

manifest は、json 形式で記述し、以下のような link タグを Web コンテンツ/アプリケーション側に追加して参照させます。

link rel="manifest" href="/manifest.json">

Web App Manifest については、詳しい解説が MDN にあるのでそちらを参照することをお勧めしますが、以下に簡単な説明を兼ねたサンプルを掲示します。

{
   "lang": "ja", ← 言語
    "name": "The enemy of galaxy", ← アプリケーションの名前
    "short_name": "T.E.O.G", ← アプリケーションのショートネーム
    "start_url": "/?utm_source=pwd", ← 開始するときの URL (※1)
    "display": "standalone", ← ウィンドウのスタイル
    "background_color": "black", ← 背景色
    "description": "銀河に平和を取り戻すためのゲームです。", ← 説明文
    "orientation": "portrait" ← 画面の向き
    "icons": [{ (※2)
        "src": "images/homescreen48.png", ← 画像ファイルのパス
        "sizes": "48x48", ← サイズ
        "type": "image/png" ← 画像の種類
      }, {
      "src": "images/homescreen72.png",
      "sizes": "72x72",
      "type": "image/png"
   }]
}

(※1)Google Analytics 等を使用しているときはクエリーストリングを使用することで PWA として起動されたのか、ブラウザからページにアクセスしたのか判断が可能

(※2)iPhone や iPad でアイコンが反映されない場合は apple-touch-icon を使用

de:code 2018 のセッション動画で Web App Manifest を追加するデモを行ってぜひ実際の動作をご覧ください。

現在の Windows 10 バージョン 1803 の Progressive Web Apps の動作

なお、2018 年 6 月現在の Windows 10 バージョン 1803 (OS ビルド 17134.112) では残念ながら Web App Manifest によるウィンドウの制御は有効になりません。

たとえば Windows 10 の Microsoft Edge で Progressive Web Apps のページをタスクバーにピン

Share the post

de:code2018セッションフォローアップ「進化する Web ~ Progressive Web Apps の実装と応用 ~」

×

Subscribe to Msdn Blogs | Get The Latest Information, Insights, Announcements, And News From Microsoft Experts And Developers In The Msdn Blogs.

Get updates delivered right to your inbox!

Thank you for your subscription

×