実践PWA:光る電卓「Llumino PWA」の開発と技術解説

2018-05-09

前回は、PWAの基礎知識について説明しました。今回は実際に制作してみたPWAに触れてもらいつつ、その内側・技術的側面について解説していきたいと思います。

今回の題材:光る電卓「Llumino(ルミノ)」

2013年にiOSネイティブ版をお披露目したLluminoは、「計算をもっと楽しく」がコンセプトの電卓アプリ。当時は世界中で話題になりました。(もう5年前…!) 当時の制作過程については連載としてまとめていますので、興味がありましたらどうぞ。(👉 連載:Lluminoができるまで

今回のチャレンジは、そんなLluminoを最新のWeb技術で再現・PWA化してみようというものです。

まずは実物を触ってみてください

制作したWebアプリは https://pwa.llumino.app/ に配置しました。まずは実物を触って体感してみてください! (昨日解禁になったばかりの「.app」ドメインだぞ!!)

Lluminoのコンセプトを継承して、触って楽しくなるエフェクトを再現しました。テーマ機能も搭載し、好みの配色に切り替えられます。

iOS/Androidをお持ちの方は、ホーム画面に追加してみてください。また違った体験が得られるはず。

触り心地はいかがでしょうか?ユーザー視点の考察はいったん置いておいて、今回は技術的な側面に着目してみます。

開発方針

今回のテーマは、「PWAは本当にネイティブアプリを置き換えるものではないのか?」を確かめること。先日の記事でも触れたPWAの位置づけから考えると、結論は「置き換えるものではない!」ですが、やってみないと何も言えませんよね。

そんな攻めのテーマですので、Webページというよりもアプリに近い操作感を目指してみました。ホーム画面に追加することを前提に、コンテンツは基本的に一画面に収め、スクロールするのは画面の一部のみ。

技術解説

ソースコードはすべて公開しちゃいます!!ドン!!MITライセンスです。

全体構成は Flow + React + Redux で、最近では割と一般的なものではないでしょうか。

アニメーションはCSSで実現

触り心地を大切にしたいLluminoですが、体験のコアとなるディスプレイ・ボタンのエフェクトはCSS animation/transitionのみで実現されています。古めの端末ではやや動作がもたつくかもしれませんが、少なくとも最近のデバイスにおいては、この程度のエフェクトであれば十分動いてくれるようです。パワフルな時代になったもんだ…。

本気でパフォーマンスを追求するなら、WebGLをフル活用するなどしてより低レイヤーの描画処理を組むことになるでしょう。

オフライン環境での動作の要「Service Worker」

「Service Worker」は、Webページとは別にバックグラウンドで実行することのできるスクリプト。 navigator.serviceWorker.register で外部のJavaScriptファイルを登録できます。

<script>
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('service-worker.js').then(function(reg) {
    }).catch(function(err) {
      console.error('Registration failed:', error);
    });
  }
</script>

https://github.com/cocopon/llumino-pwa/blob/master/public/index.html

Lluminoでは、Service Workerの登録処理はHTMLに直書きしています。続いて、こちらがService Workerとして登録するスクリプトの一部。

self.addEventListener('install', (ev) => {
  // キャッシュを追加
  ev.waitUntil(updateCache());
});

self.addEventListener('fetch', (ev) => {
  // キャッシュがあればそれを返し、なければ新たに取得
  ev.respondWith(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.match(ev.request).then((res) => {
        return res || fetch(ev.request);
      });
    })
  );
});

https://github.com/cocopon/llumino-pwa/blob/master/src/js/service-worker.js

Service Workerは、状況に応じて様々なイベントが発生します。 install は初回登録のとき、 fetch は何らかのリクエストが発生したとき。

初回登録時にHTML/CSS/JSなどすべてのリソースをキャッシュし、以後のリクエストではそのキャッシュを返すようにすれば、オフライン環境でも動作するようになる…といった仕組みです。

データの保存は「Local Storage」

データの保存には「Local Storage」を利用します。今回はデータフローの設計にReduxを採用しているので、「Redux Persist」を利用して、アプリ全体の状態を司るStateを適宜Local Storageに保存・起動時に復元してあげればOKです。

https://github.com/cocopon/llumino-pwa/blob/master/src/js/store.js

画面の設定(iOS/Android共通)

ネイティブアプリの操作感に近づけるためには、画面に対していくつか設定が必要になります。画面幅をデバイス幅に合わせたり、拡大縮小を禁止したり。これらはHTMLのmetaタグで設定できます。

<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, viewport-fit=cover">

https://github.com/cocopon/llumino-pwa/blob/master/public/index.html

拡大縮小の禁止は minimum-scale および maximun-scale を等倍の 1 に、また user-scalableno にすることで実現できます。が、これらを設定するには相応の覚悟が必要です。

というのも、通常のWebページでは多少文字や画像が小さくとも、ユーザーのピンチ操作で拡大表示することができます。「ユーザーが拡大操作する自由」をわざわざ奪うということは、そのぶん等倍での閲覧性・操作性をしっかり確保しなければならないということ。そうでなければ、単に使いづらいだけのアプリになってしまいます。

アプリ化用のタグ(iOS)

iOSのSafariでホームアイコンに追加する場合、アイコンとタイトルはHTMLのmetaタグ・linkタグで設定します。

<meta name="apple-mobile-web-app-title" content="Llumino PWA">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="apple-touch-icon" href="assets/img/icon/ios/180x180.png">
<link rel="apple-touch-startup-image" href="assets/img/launch.png">

https://github.com/cocopon/llumino-pwa/blob/master/public/index.html

アプリのアイコンが並んでいるホーム画面は、ユーザーのファッション的な側面を持ち合わせています。アイコンのデザインを気にするユーザーも多く、何も設定していなかったり、設定されていてもダサいアイコンだったりするだけで、ユーザーの愛着はぐっと下がってしまうでしょう。気合を入れて準備しておきましょう。

ちなみに、iOSは勝手にアイコンを角丸に切り抜いてくれるので、切り抜き前の四角い画像を用意します。

他にも、起動時の画像(いわゆるlaunch image)を設定する apple-touch-startup-image や、ブラウザーのナビゲーションやツールバーを隠す apple-mobile-app-capable の設定も忘れずに。

このあたりは、Appleの公式資料「Configuring Web Applications」に書かれています。…が、最終更新が2016年と久しく更新されておらず、Appleのやる気のなさが伝わってくるようです。

アプリ情報「Web App Manifest」(主にAndroid)

一方Androidは、「Web App Manifest」というファイルによって、ホーム画面に追加するアプリの情報を定義します。HTMLのlinkタグでマニフェストへのパスを指定します。

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

https://github.com/cocopon/llumino-pwa/blob/master/public/index.html

マニフェストはJSON形式で、背景色やテーマカラー、アプリのアイコンなどの情報を指定します。

{
  "background_color": "#000000",
  "display": "standalone",
  "icons": [{
    "sizes": "180x180",
    "src": "assets/img/icon/android/180x180.png",
    "type": "image/png"
  }, {
    "sizes": "192x192",
    "src": "assets/img/icon/android/192x192.png",
    "type": "image/png"
  }, {
    "sizes": "512x512",
    "src": "assets/img/icon/android/512x512.png",
    "type": "image/png"
  }],
  "name": "Llumino PWA",
  "short_name": "Llumino",
  "start_url": "./",
  "theme_color": "#000000"
}

https://github.com/cocopon/llumino-pwa/blob/master/public/manifest.json

起動時の画像は、上記の色やアイコン、タイトルから自動的に生成され、現時点では変更することができません。(ちょっとダサくて悲しい…)

ちなみに、AndroidのアイコンはiOSと異なり、OS側で切り抜きしてくれないどころか、必須であるはずのドロップシャドウすら追加してくれないので注意してください。すべて自前で加工して用意する必要があるということですね。

サイズやスタイルについては、Googleがガイドを用意してくれているので、これを参照しながらOSに馴染んだアイコンを準備しましょう。

テキスト選択の抑制

Webアプリでやってしまいがちな悲しいこととして、タップ長押しでブラウザー標準の文字選択UIが出てしまうというのがあります。

これはCSSで抑制できますので、忘れずに指定しておきましょう。テキスト入力系まで効かせてしまうと、文字が入力できなくなってしまうので :not で除外します。

:not(input, textarea) {
  -webkit-user-select: none;
}

https://github.com/cocopon/llumino-pwa/blob/master/src/sass/common/_base.scss

ホーム画面への追加を促す (Android)

せっかくネイティブアプリの代替を目指して仕上げたので、製作者としてはアプリのようにホーム画面に追加してフルパワーを発揮してもらいたいもの。

AndroidのChromeでは、一定の条件を満たすと自動的にバナーを表示してくれます。

  • マニフェストファイルが存在し、アイコンや名前など必要な情報が設定されている
  • Service Workerが登録されている
  • アクセス頻度が多い

詳細な条件はGoogleの公式資料「ウェブアプリのインストール バナー」を参照しましよう。

一方iOSについては特に用意されておらず、類似の仕組みを自前で準備する必要があります。今回Lluminoには載せていませんが、やるならばUser agentをみてiOSであることを確認後、 navigator.standalone のフラグを見てまだホーム画面に登録されていないことを確認、適宜メッセージを出す…といった感じでしょうか。

タップ時のハイライトの制御

iOSでは、デフォルトでタップ時のハイライト(黒くて薄い角丸のマスク)が付加されます。

ユーザー操作に対するインタラクションがあること自体はよいことですが、このままだとWebっぽいというか、きちんとUIに調和するよう制御してあげたいところです。まずは -webkit-tap-highlight-color で透明色を指定して無効化してあげます。

button {
  -webkit-tap-highlight-color: transparent;
}

https://github.com/cocopon/llumino-pwa/blob/master/src/sass/common/_reset.scss

このままではユーザー操作に対する反応が皆無になり、押したかどうかわかりづらく気持ち悪さを感じてしまうので、 :active で調和した外観を定義しましょう。

.common-display:active {
  background-color: rgba(white, 0.05);
}

この際気をつけなければならないのは、iOSでは :active のつく要素に ontouchstart ハンドラーを追加しないとアクティブ時のスタイルの指定が効かないということ。この挙動に対するAppleの公式資料は見あたらず本来の意図を知る術はありませんが、そういうものとして対処していくしかなさそうです。タップ時に実行したい処理が特にない場合は、単に空の関数 () => {} を設定するのでも大丈夫です。

画面の固定・スクロールバウンスの抑制

通常、Webページのコンテンツは縦横に伸び、画面には収まりきらないのでスクロールして閲覧します。この挙動はアプリらしくみせるには邪魔なので、 position: fixed; で固定しつつサイズも指定して抑制します。

html, body {
  height: 100%;
  left: 0;
  overflow: hidden;
  position: fixed;
  top: 0;
  width: 100%;
}

https://github.com/cocopon/llumino-pwa/blob/master/src/sass/common/_base.scss

とはいえ、Webアプリ内ではリストコンポーネントなどスクロールさせたいコンテンツも出てくるので、その場合は overflow-scrolling: touch; を設定しましょう。ネイティブアプリのような挙動でスクロールしてくれるようになります。

.common-tab_pageLayout {
  -webkit-overflow-scrolling: touch;
  overflow-scrolling: touch;
}

タップ反応の改善(Android)

Androidにおいて、タップのハンドリングに click を使っていると、わずかに :active の反映速度が遅れ(100〜200ms程度?)気持ち悪く感じます。 touchstart / touchend を使うと遅れなく反応してくれるので、電卓のキーパッドなど反応速度が求められるシビアな場面では後者を使っていきましょう。

さらに、moveを伴うタッチイベントが無視されてしまうという現象にも遭遇しました。こちらも touchstart / touchend でハンドリングすることで改善できます。

https://github.com/cocopon/llumino-pwa/blob/master/src/js/view/calc/button-grid.jsx

まだまだあります

Lluminoには、ここに書ききれない細かなチューニングがまだまだたくさん詰まっています。詳細は実際のコードや、GitHubのissueあたりを見ていただくのがよいでしょう。とにかく、Webアプリをネイティブの挙動に近づけるには多くの労力と工夫が必要になり、完全に補えるものではない、ということを頭の片隅に置いておくとよいと思います。

今回のまとめ

…といった感じで、「PWAは本当にネイティブアプリを置き換えるものではないのか?」の検証をテーマに、拙作の光る電卓Lluminoを再現してみました。ある程度作り込んだものに触れてみることで、どんな手触りになるかを実感いただけたかと思います。「意外といけるじゃん?」という意見も「やっぱりネイティブには及ばないな?」という意見もあるでしょう。

次回はこの制作を通して得られた知見と、PWAの現状や可能性について触れていきたいと思います。

おまけ:参考資料

まだ盛り上がりはじめたばかり技術なので、みんな手探りです。比較的信頼性の高い公式資料を中心に参照していくのがよいと思います。

  • Configuring Web Applications
    Apple提供。iOSでホーム画面に追加するWebアプリを作るためのもの。なのだが…最終更新が2016年で、Appleのやる気のなさを感じる。
  • Caching Files with Service Worker
    Google提供。Service Workerでファイルをキャッシュするための基本的な流れとキャッシュ戦略について。
  • ServiceWorker Cookbook
    Mozilla提供。Service Workerの用途に応じたコード例を丁寧に解説してくれている。
  • The Web App Manifest
    Google提供。Web App Manifestについて図入りで詳しく解説。
  • Service Worker の調査
    Google提供。意図せぬキャッシュなどでどハマりしやすいService Workerですが、Chromeによるデバッグ方法とコツについて解説。とても役に立ちます。

書いている人:cocopon

Developer/Designer. Web/iOSなどのフロントエンドを主軸に、UIデザインから開発全般まで手がける。

趣味が高じて、ドット絵やジェネラティブアートが仕事になりつつある。