Upgrade to Pro — share decks privately, control downloads, hide ads and more …

こえのブログでのPWA ~ PWA編 ~ / PWA Night Vol.4

こえのブログでのPWA ~ PWA編 ~ / PWA Night Vol.4

PWA Night vol.4 ~PWAのミライや活用方法をみんなで考えよう~の資料です。
https://pwanight.connpass.com/event/128434/

Kazunari Hara

May 15, 2019
Tweet

More Decks by Kazunari Hara

Other Decks in Technology

Transcript

  1. こえのブログでのPWA
    ~ PWA編 ~
    2019年5月15日 @株式会社ウフル
    Kazunari Hara

    View full-size slide

  2. 原 一成 Hara Kazunari
    Web Developer
    @herablog

    View full-size slide

  3. 喋るだけで
    ブログになる

    View full-size slide

  4. 本人の”声”でコンテンツ価値向上
    https://voice.ameba.jp/emb
    ed/kobayashi-maya/rxxqHm
    6s4iAqYRP4mjK5
    https://voice.ameba.jp/e
    mbed/kose-sports/eaxzb
    5mP6vMlw3FWpqX9
    https://voice.ameba.jp/e
    mbed/toshl-official/9nzC7
    iAFn6IDKHPerj6P

    View full-size slide

  5. https://github.com/webmaxru/progressive-web-apps-logo

    View full-size slide

  6. クロスプラットフォーム
    小さくリリースできる
    ブラウザ機能の充実

    View full-size slide

  7. Lighthouseでhttps://voice.ameba.jp/をMobile、Simulated Fast 3G、4x CPU Slowdown、ローカル環境で測定。

    View full-size slide

  8. are user experiences.”

    https://developers.google.com/web/progressive-web-apps/

    View full-size slide

  9. キャッシュ UIパーツ マルチメディア

    View full-size slide

  10. サーバーサイド &
    クライアントサイド
    キャッシュ

    View full-size slide

  11. Network
    GET /
    GET /voice-app.js
    GET
    /api/entry.json

    View full-size slide

  12. Server DB
    Browser
    I/O
    計算量
    キャパシティ
    リダイレクト
    クエリ性能
    ネットワーク状況
    地理

    View full-size slide

  13. Server DB
    Browser CDN

    View full-size slide

  14. CDN利用:
    できる限りキャッシュ
    イベント駆動パージ
    エッジコンピューティング

    View full-size slide

  15. できる限りキャッシュ:
    Time To Live (TTL)
    Surrogate Key

    View full-size slide

  16. Method Path TTL Surrogate Key
    GET / max-age=2592000 web, web/release
    GET /src/components/voice-app.js max-age=2592000 web, web/release
    GET
    /assets/audios/stadard/$USER
    _ID/$ENTRY_ID.mp3
    max-age=2592000
    api,
    entry/$ENTRY_ID,
    blogger/$USER_ID
    GET
    /api/entries/$USER_ID/$ENTR
    Y_ID/
    max-age=2592000
    api,
    entry/$ENTRY_ID,
    blogger/$USER_ID
    GET
    /api/playcounts/$USER_ID/$EN
    TRY_ID/
    max-age=30,
    stale-while-revali
    date=120
    api,
    entry/$ENTRY_ID,
    blogger/$USER_ID

    View full-size slide

  17. イベント駆動パージ

    View full-size slide

  18. # Surrogate Keyを操作
    sub vcl_fetch {
    declare local var.SurrogateKey STRING;
    If (req.http.x-url ~ "/audios/standard/([a-z0-9-]{3,24})/([a-zA-Z0-9]+)") {
    set var.SurrogateKey =
    var.SurrogateKey + " blogger/" + re.group.1 + " entry/" + re.group.2 +
    " audio/" + re.group.2; // e.g. "blogger/abcde", "entry/12345"
    }
    set beresp.http.Surrogate-Key = var.SurrogateKey;
    }

    View full-size slide

  19. # ブラウザに配信するHTTPレスポンスヘッダーを追加
    sub vcl_deliver {
    add resp.http.Server-Timing =
    fastly_info.state {", fastly;desc="Edge time";dur="} time.elapsed.msec;
    set resp.http.Referrer-Policy = "origin-when-cross-origin";
    set resp.http.X-Content-Type-Options = "nosniff";
    add resp.http.Content-Security-Policy = "default-src 'self'; script-src 'self'..."
    }

    View full-size slide

  20. CDN詳細は、
    WEB+DB PRESS vol.109

    View full-size slide

  21. クライアントキャッシュ:
    HTTP Headers
    Service Worker (Cache API)
    キャッシュ

    View full-size slide

  22. HTTP Headerでの
    キャッシュ Cache-Control: maxage=3600

    View full-size slide

  23. “25.5% of all logged requests were
    missing the cache.”
    https://code.fb.com/web/web-performance-cache-efficiency-exercise/

    View full-size slide

  24. Service Worker (Cache API)

    オリジン毎にキャッシュコ
    ントロール

    View full-size slide

  25. プリキャッシュ:
    アプリの雛形となる
    ファイル(HTML, Image, JS)
    全て
    Index.html
    (Entrypoint)
    Voice-app.js
    (App shell)
    voice-home.js voice-editor.js
    lazy-resources.js
    PRPLパターン
    (Fragment)

    View full-size slide

  26. プリキャッシュ:
    各ファイルの変更毎に
    入れ替え
    workbox.precaching.precacheAndRou
    te([{
    url: "index.html",
    revision: "999s0cnacavav"
    },
    …]);

    View full-size slide

  27. onload
    Service Worker

    View full-size slide

  28. Pre-cache assets

    View full-size slide

  29. Reload
    Activate Service Worker

    View full-size slide

  30. No Network Connection

    View full-size slide

  31. Update Service Worker
    New Version App

    View full-size slide

  32. 変更があるファイル
    だけ更新

    View full-size slide

  33. No Network Connection

    View full-size slide

  34. ランタイムキャッシュ:
    オフラインや次回訪問に
    備えてAPIデータや
    アセットをキャッシュ
    Cache First
    アートワーク画像
    Network First
    変更が多いAPIデータ
    Stale While Revalidate
    変更が少ないAPIデータ

    View full-size slide

  35. Web Components (LitElement)
    CSRのWebアプリ
    モバイルター
    ゲット
    コンポーネント
    Web標準
    Web Components

    View full-size slide

  36. class VoiceMic extends LitElement {
    _handleMicClick() {
    this.dispatchEvent(new CustomEvent('mic-click'));
    }
    render() {
    const { disabled, recording } = this;
    const buttonLabel = recording ? '停止' : '開始';
    return html`

    aria-live="assertive"
    ?disabled=${disabled}
    type="button"
    @click=${this._handleMicClick}
    >
    `;
    }
    }
    customElements.define('voice-mic', VoiceMic);

    View full-size slide

  37. class VoiceMic extends LitElement {
    _handleMicClick() {
    this.dispatchEvent(new CustomEvent('mic-click'));
    }
    render() {
    const { disabled, recording } = this;
    const buttonLabel = recording ? '停止' : '開始';
    return html`

    aria-live="assertive"
    ?disabled=${disabled}
    type="button"
    @click=${this._handleMicClick}
    >
    `;
    }
    }
    customElements.define('voice-mic', VoiceMic);

    View full-size slide

  38. class VoiceMic extends LitElement {
    _handleMicClick() {
    this.dispatchEvent(new CustomEvent('mic-click'));
    }
    render() {
    const { disabled, recording } = this;
    const buttonLabel = recording ? '停止' : '開始';
    return html`

    aria-live="assertive"
    ?disabled=${disabled}
    type="button"
    @click=${this._handleMicClick}
    >
    `;
    }
    }
    customElements.define('voice-mic', VoiceMic);

    View full-size slide

  39. class VoiceMic extends LitElement {
    _handleMicClick() {
    this.dispatchEvent(new CustomEvent('mic-click'));
    }
    render() {
    const { disabled, recording } = this;
    const buttonLabel = recording ? '停止' : '開始';
    return html`

    aria-live="assertive"
    ?disabled=${disabled}
    type="button"
    @click=${this._handleMicClick}
    >
    `;
    }
    }
    customElements.define('voice-mic', VoiceMic);

    View full-size slide

  40. class VoiceMic extends LitElement {
    _handleMicClick() {
    this.dispatchEvent(new CustomEvent('mic-click'));
    }
    render() {
    const { disabled, recording } = this;
    const buttonLabel = recording ? '停止' : '開始';
    return html`

    aria-live="assertive"
    ?disabled=${disabled}
    type="button"
    @click=${this._handleMicClick}
    >
    `;
    }
    }
    customElements.define('voice-mic', VoiceMic);

    View full-size slide

  41. 166 KB (gzip, style込み)

    View full-size slide

  42. オフライン対応:
    IndexedDB
    Service Worker (Cache API)
    navigator.onLine

    View full-size slide

  43. 下書き保存
    with IndexedDB

    View full-size slide

  44. 記事データが
    更新されると
    Indexed DBに
    保存

    View full-size slide

  45. Offline Recording
    with Service Worker

    View full-size slide

  46. Index.html
    (Entrypoint)
    Voice-app.js
    (App shell)
    voice-home.js voice-editor.js
    lazy-resources.js
    PRPLパターン
    (Fragment)
    Offline Recording
    with Service Worker

    View full-size slide

  47. Offline Notification
    with navigator.onLine

    View full-size slide

  48. function watchOffline(callback) {
    window.addEventListener(
    ’online’,
    () => callback(false),
    );
    window.addEventListener(
    ‘offline’,
    () => callback(true),
    );
    callback(
    navigator.onLine === false
    );
    }

    View full-size slide

  49. watchOffline((offline) => {
    If (offline) {
    // Display snack bar
    }
    });

    View full-size slide

  50. Web App:
    Web App Manifest
    Media Queries
    Responsive Images
    Desktop PWA

    View full-size slide

  51. {
    "short_name": "こえ",
    "name": "こえのブログ by Ameba",
    "description": "「こえのブログ」は、...",
    "lang": "ja-JP",
    "icons": [],
    "background_color": "#fff",
    "theme_color": "#fff",
    "start_url": "/?source=homescreen",
    "scope": "/",
    "display": "standalone"
    }
    manifest.json

    View full-size slide

  52. https://twitter.com/Nkzn/status/1110369084166692864

    View full-size slide

  53. 1025pxからDesktop版

    View full-size slide

  54. @media screen and (min-width: 1025px) {
    :root {
    --app-header-height: 60px;
    --app-page-background-color: var(--clr-whitesmoke);
    --app-drawer-width: 400px;
    ...
    }
    }

    View full-size slide

  55. Responsive Images src="toshi.jpg?size=80"
    srcset="
    toshi.jpg?size=160 2x,
    toshi.jpg?size=240 3x,
    "
    />

    View full-size slide

  56. 追加設定なしで
    Desktop PWA

    View full-size slide

  57. ネットワーク状況
    Network Information API

    View full-size slide

  58. function watchNetwork (callback) {
    const connection = navigator.connection;
    if (connection) {
    callback(connection);
    connection.addEventListener(
    'change',
    () => callback(connection)
    );
    }
    }
    watchNetwork(({ effectiveType } => {
    if (effectiveType.includes('2g')) {
    // Display notification
    }
    });

    View full-size slide

  59. Lazy-loading:
    Intersection Observer
    Native lazy-loading
    (onscroll)

    View full-size slide

  60. Lazy-loading:
    Intersection Observer
    Native lazy-loading (Beta)

    View full-size slide

  61. class LazyloadImage extends LitElement {
    firstUpdated() {
    If ('loading' in HTMLImageElement.prototype) {
    this.shadowRoot.querySelector(‘img’).src =
    this.src;
    } else { // Use Intersection Observer }
    }
    render() {
    return html`
    loading="lazy" height=${height}
    srcset=${srcset} width=${width} />
    `;
    }
    }

    View full-size slide

  62. こえのブログをシェア
    with Web Share API

    View full-size slide

  63. if (navigator.share) {
    navigator.share({
    title: ‘こえのブログ by Ameba’,
    text: ‘こえのブログは...’,
    url: ‘https://voice.ameba.jp/’,
    });
    } else {
    // Open custom dialog
    }

    View full-size slide

  64. こえのブログを貼り付け
    with Clipboard API

    View full-size slide

  65. const text = ‘text to copy’;
    if (navigator.clipboard) {
    navigator.clipboard.writeText(
    Text
    );
    } else {
    // document.execCommand('copy');
    }

    View full-size slide

  66. Vibrate Notification
    with Vibration API

    View full-size slide

  67. ブッ ブブ ブゥ
    録音開始 残り10秒 録音終了

    View full-size slide

  68. function notifyRecordingStart() {
    navigator.vibrate(30);
    }
    function notifyTimeToFinish() {
    navigator.vibrate([30, 100, 30]);
    }
    function notifyRecordingEnd() {
    navigator.vibrate(100);
    }
    ブブ

    View full-size slide

  69. マルチメディア

    View full-size slide

  70. Audio Recording

    View full-size slide

  71. Mic Web Worker Browser
    Blob
    Stream
    Messaging Messaging

    View full-size slide

  72. 端末のマイクに
    アクセス
    navigator.mediaDevices
    .getUserMedia({
    audio: {
    autoGainControl: false,
    channelCount: 1,
    echoCancellation: true,
    noiseSuppression: true,
    },
    })
    .then((stream) => {
    // use the stream
    })
    .catch((err) => {
    // NotAllowedError or
    // NotFoundError
    });

    View full-size slide

  73. 録音中の音声圧縮
    WebAssembly &
    Web Worker
    WAV MP3

    View full-size slide

  74. https://github.com/Kagami/vmsg
    Kagami/vmsg

    View full-size slide

  75. 録音した音声を操作
    with Blob (Binary Large OBject)
    // Save to IndexedDB
    const transaction =
    db.transaction(
    ['voice'], 'readwrite');
    const objectStore =
    transaction.objectStore('voice');
    const objectStoreRequest =
    objectStore.put({ audio: blob });
    // Create URL to play audio
    URL.createObjectURL(blob);
    blob:https://voice.ameba.jp/76ee6fef-c126-4
    f83-8a46-8fb00db57808

    View full-size slide

  76. Camera/Photo Browser Server
    Blob
    ArrayBuffer
    File API Resize/Upload

    View full-size slide

  77. type="file"
    accept="image/jpeg"
    />
    function onFileChange(event) {
    const el = event.target;
    if (el.files && el.files[0]) {
    const reader = new FileReader();
    reader.onload = e => {
    const buffer = e.target.result;
    const type = el.files[0].type;
    const blob =
    new Blob([buffer], { type });
    const fileName = el.files[0].name;
    };
    }
    }
    端末画像を読み込み
    with File API

    View full-size slide

  78. window.loadImage(
    blob,
    canvas = >
    canvas.toBlob(
    resizedBlob => {
    // Display resized image
    }
    ),
    {
    canvas: true,
    maxHeight: 1024px,
    maxWidth: 1024px,
    },
    );
    アップロード前に
    リサイズ

    View full-size slide

  79. https://github.com/blueimp/JavaScript-Load-Image
    blueimp/
    JavaScript-Load-Image
    (将来的に変更する可能性あり)

    View full-size slide

  80. https://developers.cyberagent.co.jp/blog/archives/20506/
    詳細は・・・
    CDN/PWA/Speech
    Recognition/WASM/Web
    Components/Service
    Worker/Performance
    Budget etc...
    https://speakerdeck.com/herablog/koe-no-blog-pwa
    こえのブログでのPWA

    View full-size slide

  81. are user experiences.”

    https://developers.google.com/web/progressive-web-apps/

    View full-size slide