Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

原 一成 Hara Kazunari Web Developer @herablog

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

喋るだけで ブログになる

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

No content

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

are user experiences.” “ https://developers.google.com/web/progressive-web-apps/

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

Server DB Browser CDN

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

イベント駆動パージ

Slide 26

Slide 26 text

# 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; }

Slide 27

Slide 27 text

# ブラウザに配信する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'..." }

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

Service Worker (Cache API) で オリジン毎にキャッシュコ ントロール

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

index.html

Slide 36

Slide 36 text

onload Service Worker

Slide 37

Slide 37 text

Pre-cache assets

Slide 38

Slide 38 text

Reload Activate Service Worker

Slide 39

Slide 39 text

No Network Connection

Slide 40

Slide 40 text

Update Service Worker New Version App

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

No Network Connection

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

No content

Slide 45

Slide 45 text

UIパーツ

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

166 KB (gzip, style込み)

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

下書き保存 with IndexedDB

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

Offline Recording with Service Worker

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

Offline Notification with navigator.onLine

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

{ "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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

1025pxからDesktop版

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

Responsive Images

Slide 68

Slide 68 text

追加設定なしで Desktop PWA

Slide 69

Slide 69 text

ネットワーク状況 Network Information API

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

Vibrate Notification with Vibration API

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

マルチメディア

Slide 82

Slide 82 text

Audio Recording

Slide 83

Slide 83 text

Mic Web Worker Browser Blob Stream Messaging Messaging

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

Read Photo

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

are user experiences.” “ https://developers.google.com/web/progressive-web-apps/