「このファイルいれたら大丈夫!」って言われて入れたけど全然大丈夫じゃなかった小話

ふわっしょい(@fluflufluffy)
PWA nights:2回目/LT:はじめて

PWAをはじめて触ったときの思い出

  • サイトのとあるコンテンツをPWA対応を導入することに
    • A2HS
    • スプラッシュスクリーン
    • オフライン対応
    • リソースのキャッシュ
  • このコンテンツの運用担当は自分
  • 「PWA対応しといたからこのファイルいれといて」とPWA実装に言われた(運用担当だったので)
    • ちなみにサイトを作ったのは別の人、PWAを実装した人はさらに別の人
  • 当時の知識レベル:
    • PWAは技術総称。以下の技術で構成されてる
      • Service Worker
      • manifest.json
      • HTTPS
  • いちおうふわっと基礎知識は知ってた
  • 基本的な動作はきちんとファイルに書かれてた
  • ので脳直でいれた

ページちょっとこわれた

\(^o^)/

PWA思った通りに動かなかったポイント

動かなかったポイントその1:動画

  • サイトのトップページに動画がある
  • 動画がSafariで再生されなくなりました
  • Safariで起きる
    • ネットワーク経由から動画を取得する場合は問題ない
    • キャッシュから動画を取得すると再生されない

原因

  • Safariでは、mp4のようなサイズが大きくなると思われるファイルをHTTPリクエストする場合にHTTP Range Requestを利用する
  • SafariではHTTP Rangeリクエストに対してレスポンスコード200を受け取るとそれ以降そのファイルを読み込まなくなる

解決方法

  • Service WorkerのFetchイベント時、Request.destinationでリクエストの種類を取得
  • Request.destinationvideoのときにはステータスコード206 Partial Contentでレスポンスを返却するように設定する
Request.destination
  • Request.destinationはRequestインターフェイスのプロパティ
  • リクエストしているコンテンツの種類を文字列で返す
    • Service Workerのfetchイベント時に用いると、リソースの種類別にキャッシュ戦略を切り替えることが可能

A request has an associated destination, which is the empty string,
"audio", "audioworklet", "document", "embed", "font", "image",
"manifest", "object", "paintworklet", "report", "script",
"serviceworker", "sharedworker", "style", "track", "video",
"worker", or "xslt". Unless stated otherwise it is the empty string.

  self.addEventListener('fetch', (fetchEvent) => {
    const destination = fetchEvent.request.destination;
    const requestURL = new URL(fetchEvent.request.url);

    if (requestURL.origin === location.origin) {
      switch (destination) {
        case 'video':{
            // ステータスコード206 Partial Contentでレスポンスを返却する処理
            return;
        }
        default:{
            fetchEvent.respondWith(
                cacheFallingBackToNetwork(fetchEvent)
            );
            return;
        }
      }
    }
  });
レスポンスを返却する処理
const rangeHeader = fetchEvent.request.headers.get('range');
const rangeMatch = rangeHeader.match(/^bytes\=(\d+)\-(\d+)?/)
const position = Number(rangeMatch[1]);
let position2 = rangeMatch[2];

if (position2) {
    position2 = Number(position2); 
}

if (!rangeHeader) {
    cacheFirst(fetchEvent);
    return;
}

fetchEvent.respondWith(
    caches.open(CACHE_VERSION)
    .then((cache) => {
    return cache.match(fetchEvent.request.url);
    })
    .then((response) => {
    if (!response) {
        fetch(fetchEvent.request.url).then((res) => {
        return res.arrayBuffer();
        })
    }

    return response.arrayBuffer();
    })

...
...
   .then((arrayBuffer) => {
    let responseHeaders = {
        status: 206,
        statusText: 'Partial Content',
        headers: [
        ['Content-Type', 'video/mp4'],
        ['Content-Range', 'bytes ' + position + '-' + 
        (position2 || (arrayBuffer.byteLength - 1)) + '/' +
        arrayBuffer.byteLength]]
    };

    let arrayBufferSliced = {};

    if (position2 > 0) {
        arrayBufferSliced = arrayBuffer.slice(position, position2 + 1);
    } else {
        arrayBufferSliced = arrayBuffer.slice(position);
    }

    return new Response(
        arrayBufferSliced, responseHeaders
    );
    })
)

動かなかったポイントその2:iOSのスプラッシュスクリーン

  • Androidはmanifest.jsonに以下の項目を設定するとスプラッシュスクリーンを自動的に生成してくれる
    • name
    • background_color
    • icons(少なくとも512px x 512pxで.png
  • iOSはしてくれない

解決方法

対象デバイスごとに端末のサイズや、画像解像度、縦向き、横向きをlink要素のmedia属性で設定する

<link href="/path/to/iphonex_splash.png"
    media="(device-width: 375px) and (device-height: 812px) 
    and (-webkit-device-pixel-ratio: 3) 
    and (orientation: portrait)" 
    rel="apple-touch-startup-image">

<link href="/path/to/iphonex_splash_landscape.png"
    media="(device-width: 375px) and (device-height: 812px) 
    and (-webkit-device-pixel-ratio: 3) 
    and (orientation: landscape)" 
    rel="apple-touch-startup-image">

おまけ:iOSのステータスバー

  • Androidはmanifest.jsonのステータスバーの色をtheme_colorで変更可能
  • iOSはできない

解決方法

Metaタグを設定する

<meta name="apple-mobile-web-app-status-bar-style" content="black">
  • default:黒文字に白背景
  • black:白文字に黒背景
  • black-translucent:白文字に透明

width: 90%

この件から学習したこと

  • あとからPWAを導入する際は、既存コンテンツに影響ないか充分に注意しよう

  • AndroidとiOSの実装方法の違いを把握する

  • 実機でデバッグをちゃんとしよう(当たり前)