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 Slide

  2. 原 一成 Hara Kazunari
    Web Developer
    @herablog

    View Slide

  3. View Slide

  4. View Slide

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

    View Slide

  6. 本人の”声”でコンテンツ価値向上
    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 Slide

  7. View Slide

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

    View Slide

  9. View Slide

  10. View Slide

  11. View Slide

  12. View Slide

  13. View Slide

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

    View Slide

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

    View Slide

  16. are user experiences.”

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  21. Server DB
    Browser CDN

    View Slide

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

    View Slide

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

    View Slide

  24. 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 Slide

  25. イベント駆動パージ

    View Slide

  26. # 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 Slide

  27. # ブラウザに配信する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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  32. Service Worker (Cache API)

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

    View Slide

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

    View Slide

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

    View Slide

  35. index.html

    View Slide

  36. onload
    Service Worker

    View Slide

  37. Pre-cache assets

    View Slide

  38. Reload
    Activate Service Worker

    View Slide

  39. No Network Connection

    View Slide

  40. Update Service Worker
    New Version App

    View Slide

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

    View Slide

  42. No Network Connection

    View Slide

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

    View Slide

  44. View Slide

  45. UIパーツ

    View Slide

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

    View Slide




  47. View Slide

  48. 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 Slide

  49. 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 Slide

  50. 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 Slide

  51. 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 Slide

  52. 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 Slide

  53. 166 KB (gzip, style込み)

    View Slide

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

    View Slide

  55. 下書き保存
    with IndexedDB

    View Slide

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

    View Slide

  57. Offline Recording
    with Service Worker

    View Slide

  58. 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 Slide

  59. Offline Notification
    with navigator.onLine

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  63. {
    "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 Slide

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

    View Slide

  65. 1025pxからDesktop版

    View Slide

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

    View Slide

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

    View Slide

  68. 追加設定なしで
    Desktop PWA

    View Slide

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

    View Slide

  70. 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 Slide

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

    View Slide

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

    View Slide

  73. 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  78. Vibrate Notification
    with Vibration API

    View Slide

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

    View Slide

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

    View Slide

  81. マルチメディア

    View Slide

  82. Audio Recording

    View Slide

  83. Mic Web Worker Browser
    Blob
    Stream
    Messaging Messaging

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  87. 録音した音声を操作
    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 Slide

  88. Read Photo

    View Slide

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

    View Slide

  90. 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 Slide

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

    View Slide

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

    View Slide

  93. 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 Slide

  94. are user experiences.”

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

    View Slide