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

Unlocking new capabilities for PWA

Takepepe
February 01, 2020

Unlocking new capabilities for PWA

Takepepe

February 01, 2020
Tweet

More Decks by Takepepe

Other Decks in Technology

Transcript

  1. About Me ▪ Takefumi Yoshii / @Takepepe ▪ DeNA /

    DeSC Healthcare ▪ Frontend Developer ▪ TypeScript Meetup JP member 2
  2. Agenda ▪ 1. PWA や Web App で利用可能な、実験的機能について ▪ 2.

    とある実験的機能 を利用してアプリを作った話 ▪ 3. とある実験的機能 の API 概要 ▪ 4. API 取り扱いポイント・React Redux に統合する話 3
  3. 1-1. Bridging the NativeApp Gap 1. Web Experimental Features その快適なレスポンスから、

    Web アプリケーション開発技術で、 Native アプリと同等機能を提供することに、 多くの期待が寄せられています。
  4. 1-1. Bridging the NativeApp Gap 1. Web Experimental Features しかし、Web

    ブラウザに許容されている 機能は限定的です。端末固有の機能に アクセスする Low-Level API の多くは、 JavaScript には解放されていません。
  5. 1-1. Bridging the NativeApp Gap 1. Web Experimental Features その状況を前進するべく、

    Google Chrome では Low-Level API への橋渡しを 積極的に行なっています。 Experimental Features(実験的機能) には、そんな機能が多数控えています。
  6. 1-2. Origin Trial 1. Web Experimental Features chrome://flags で実験的機能を有効にすれば、 その機能を試すことができます。

    そして、実験的機能を期限付き・ドメイン制限で 提供する「Origin Trial」もあります。 https://developers.chrome.com/origintrials
  7. 1-2. Origin Trial 1. Web Experimental Features 昨年 5月に開催された「Google I/O」

    昨年 11月に開催された「Chrome Dev Summit」で、 この「Origin Trial」について紹介がありました。 可能性の広がりに、期待が高まりますね!
  8. 2-1. Web NFC Media MEMO デモアプリは配信もしていますので、 NFC 機能付き Android をお持ちの方は

    試していただけると嬉しいです。 github にコードも公開しています。 https://webnfc-media-memo.netlify.com/ https://github.com/takefumi-yoshii/webnfc-media-memo 2. Devlop Trial
  9. 2-1. Web NFC Media MEMO このアプリを起動している時、 端末で検出された NFC Tag の処理は

    フォアグラウンドのブラウザに委ねられます。 2. Devlop Trial
  10. 2-1. Web NFC Media MEMO 2. Devlop Trial テキストメモ機能を利用して NFC

    Tag に書き込んだテキストは、 NFC Tag に永続化されます。
  11. 2-1. Web NFC Media MEMO 2. Devlop Trial 音声メモ機能は、 録音した後に

    NFC Tag にタッチすると、 保存することができます。
  12. 2-2. Prepare 2. Devlop Trial 一番早く API を試すことが出来る方法を紹介します。 NFC 機能が搭載された

    Android端末に、 Google Chrome Canary をインストール。 chrome://flags を URL バーに入力し、 Web-NFC を有効にします。 (端末の NFC機能を有効にすることも忘れずに)
  13. 2-2. Prepare 2. Devlop Trial localhost で挙動を確認するため、 この Android 端末を

    PC に接続します。 PC の Google Chrome を起動し、 More tools > Remote devices から、 接続している該当端末を選択。
  14. 2-2. Prepare 2. Devlop Trial Port forwarding に、開発サーバーの localhost を指定。

    モバイル端末の Developer Console にアクセスします。
  15. 2-2. Prepare Developer Console から、Web NFC が利用可能かを確認します。 定義が存在すれば、Web NFC を試す環境が整いました。

    2. Devlop Trial if ('NDEFReader' in window) { /* ... Scan NDEF Tags */ } if ('NDEFWriter' in window) { /* ... Write NDEF Tags */ }
  16. 3-1. About NFC Tag Origin Trials の description を引用します。 Low-level

    I/O operation などは、対応未定とのこと。 3. API Overview Low-level I/O operations (e.g. ISO-DEP, NFC-A/B, NFC-F) and Host-based Card Emulation (HCE) are not supported within the current scope.
  17. 3-1. About NFC Tag 現状試すことができる API は限定的ですが、 NFC Tag を選べば、デモアプリの様なことが可能です。

    3. API Overview Low-level I/O operations (e.g. ISO-DEP, NFC-A/B, NFC-F) and Host-based Card Emulation (HCE) are not supported within the current scope.
  18. 3-1. About NFC Tag NPX 社の「NTAG 215 チップ(NFC-A )」 が採用された

    NFC Tag は多く流通しており、 安価に入手することができます。 3. API Overview 例(カードタイプ / ¥600 / 12枚、シールタイプ / ¥790 / 11枚)
  19. 3-1. About NFC Tag 形だけでなく、容量も様々なものがあります。 そんなに大容量ではありません。 NTAG 213: 144Byte NTAG

    215: 504Byte NTAG 216: 888Byte 3. API Overview 例(カードタイプ / ¥600 / 12枚、シールタイプ / ¥790 / 11枚)
  20. 3-2. NDEFReader 簡単な読み込み例を確認していきましょう。 3. API Overview const reader = new

    NDEFReader() reader.scan().then(() => { console.log("Scan started successfully.") reader.onreading = event => { console.log(`NDEF message read.${event.serialNumber}`) } }).catch(error => { console.log(`Error! Scan failed to start: ${error}.`) })
  21. NDEFReader が NFC 読み込みに必要なインスタンスです。 scan 関数は Promise を返します。 3-2. NDEFReader

    3. API Overview const reader = new NDEFReader() reader.scan().then(() => { console.log("Scan started successfully.") reader.onreading = event => { console.log(`NDEF message read.${event.serialNumber}`) } }).catch(error => { console.log(`Error! Scan failed to start: ${error}.`) })
  22. 3-2. NDEFReader onreading ハンドラの callback は NDEFReadingEvent を受け取ります。 3. API

    Overview const reader = new NDEFReader() reader.scan().then(() => { console.log("Scan started successfully.") reader.onreading = event => { console.log(`NDEF message read.${event.serialNumber}`) } }).catch(error => { console.log(`Error! Scan failed to start: ${error}.`) })
  23. 3-2. NDEFReader serialNumber は、物理タグ出荷時に 付与されている一意の ID です。 3. API Overview

    const reader = new NDEFReader() reader.scan().then(() => { console.log("Scan started successfully.") reader.onreading = event => { console.log(`NDEF message read.${event.serialNumber}`) } }).catch(error => { console.log(`Error! Scan failed to start: ${error}.`) })
  24. 3-2. NDEFReader serialNumber を判別することで 「一意の NFC Tag を読み込んだ」ことを判別することができます。 3. API

    Overview const reader = new NDEFReader() reader.scan().then(() => { console.log("Scan started successfully.") reader.onreading = event => { console.log(`NDEF message read.${event.serialNumber}`) } }).catch(error => { console.log(`Error! Scan failed to start: ${error}.`) })
  25. 3-3. NDEFWriter 書き込みもいたって簡単です。 文字列を NFC Tag に書き込んでみます。 3. API Overview

    const writer = new NDEFWriter() writer.write("Hello World").then(() => { console.log("Message written.") }).catch(error => { console.log(`Write failed :-( try again: ${error}.`) })
  26. 3-3. NDEFWriter 書き込みには、 文字列を渡すだけの方法があります。 3. API Overview const writer =

    new NDEFWriter() writer.write("Hello World").then(() => { console.log("Message written.") }).catch(error => { console.log(`Write failed :-( try again: ${error}.`) })
  27. 3-3. NDEFWriter こちらの戻り値も Promise です。 NFC Tag が検出されたタイミングで、書き込みを試みます。 3. API

    Overview const writer = new NDEFWriter() writer.write("Hello World").then(() => { console.log("Message written.") }).catch(error => { console.log(`Write failed :-( try again: ${error}.`) })
  28. 3-3. NDEFWriter 書き込むのは文字列の他に、 NDEFMessage オブジェクトを指定する方法があります。 3. API Overview const message:

    NDEFMessage = { records: [{ recordType: "url", data: "https://w3c.github.io/web-nfc/" }] } const writer = new NDEFWriter() writer.write(message).then(() => { console.log("Message written.") }).catch(_ => { console.log("Write failed :-( try again.") })
  29. 3-3. NDEFWriter 書き込むのは文字列の他に、 NDEFMessage オブジェクトを指定する方法があります。 3. API Overview const message:

    NDEFMessage = { records: [{ recordType: "url", data: "https://w3c.github.io/web-nfc/" }] } const writer = new NDEFWriter() writer.write(message).then(() => { console.log("Message written.") }).catch(_ => { console.log("Write failed :-( try again.") })
  30. 3-4. NDEFMessage NFC Tag の読み込み時にも、NDEFMessage を受け取ります。 この内容を読み取ってみます。 3. API Overview

    function consoleNDEFRecords(message: NDEFMEssage) { if (message.records.length === 0) return message.records.map(record => { const decoder = new TextDecoder() console.log(`Text: ${decoder.decode(record.data)}`) }) }
  31. 3-4. NDEFMessage NFC Tag に書き込まれた文字列は、直接読み取ることができません。 TextDecoder インスタンスを利用し、デコードをする必要があります。 3. API Overview

    function consoleNDEFRecords(message: NDEFMEssage) { if (message.records.length === 0) return message.records.map(record => { const decoder = new TextDecoder() console.log(`Text: ${decoder.decode(record.data)}`) }) }
  32. 3-4. NDEFMessage 1byte文字列であれば、このままでも問題ありませんが、 2byte文字を含む文字列の場合、encoding の指定が必要です。 3. API Overview function consoleNDEFRecords(message:

    NDEFMEssage) { if (message.records.length === 0) return message.records.map(record => { const decoder = new TextDecoder(record.encoding) console.log(`Text: ${decoder.decode(record.data)}`) }) }
  33. 3-4. NDEFMessage この様に、NFC-A の NFC Tag であれば、 簡単に読み書き可能であることがわかります。 3. API

    Overview function consoleNDEFRecords(message: NDEFMEssage) { if (message.records.length === 0) return message.records.map(record => { const decoder = new TextDecoder(record.encoding) console.log(`Text: ${decoder.decode(record.data)}`) }) }
  34. 3-5. NDEFRecord NDEFRecord について、もう少し詳しくみていきましょう。 Web IDL interface はつぎの様に定義されています。 3. API

    Overview interface NDEFRecord { constructor(NDEFRecordInit recordInit) readonly attribute USVString recordType readonly attribute USVString? mediaType readonly attribute USVString? id readonly attribute DataView? data readonly attribute USVString? encoding readonly attribute USVString? lang sequence<NDEFRecord>? toRecords() }
  35. 3-5. NDEFRecord NDEFRecord のなかで必須プロパティであるのは、recordType です。 NDEFRecord の解析はこれを確認するところから始まります。 3. API Overview

    interface NDEFRecord { constructor(NDEFRecordInit recordInit) readonly attribute USVString recordType readonly attribute USVString? mediaType readonly attribute USVString? id readonly attribute DataView? data readonly attribute USVString? encoding readonly attribute USVString? lang sequence<NDEFRecord>? toRecords() }
  36. 3-5. NDEFRecord recordType: "url" の record を 保持した NFC Tag

    を検出した 端末は、何もアプリケーション を起動していない場合、 ブラウザの起動を促します。 3. API Overview
  37. 3-5. NDEFRecord そのため、Web NFC を利用し recordType: "url" を指定した NDEFRecord を書き込めば、

    任意の NFC Tag を ブラウザランチャーと することもできます。 3. API Overview
  38. 4-2. Permission Handling 4. Handling APIs in App アクセス権限状態の識別は重要です。 一度でも機能へのアクセスをブロックすると、

    それ以降、その機能にアクセスすることができません。 (設定解除への誘導が必要) function getUserMedia(constraints: MediaStreamConstraints) { return navigator.mediaDevices.getUserMedia(constraints) }
  39. 4-2. Permission Handling サンプルアプリでは、Permission API を利用し、 Origin の権限状態を取得しています。 ブロックされている状況などを把握するために、 Permission

    API は有効です。 4. Handling APIs in App const status = await navigator.permissions.query({ name: 'nfc' }) console.log(status.state) // "granted" | "denied" | "prompt" status.onchange = () => { // dosomething ex:) re render view }
  40. 4-2. Permission Handling サンプルアプリでは、Permission API を利用し、 Origin の権限状態を取得しています。 ブロックされている状況などを把握するために、 Permission

    API は有効です。 4. Handling APIs in App const status = await navigator.permissions.query({ name: 'nfc' }) console.log(status.state) // "granted" | "denied" | "prompt" status.onchange = () => { // dosomething ex:) re render view }
  41. 4-2. Permission Handling query 関数で状態を知る方法と同じ様に、 request 関数でパーミッション・プロンプトを表示することもできます。 いずれも、ブラウザによってサポート状況がまちまちなので 利用する前に確認しましょう。 4.

    Handling APIs in App const status = await navigator.permissions.query({ name: 'nfc' }) console.log(status.state) // "granted" | "denied" | "prompt" status.onchange = () => { // dosomething ex:) re render view }
  42. 4-2. Permission Handling 権限状況をユーザーに知らせる View があると親切です。 権限変更された時に発火されるハンドラも指定できます。 Native API の多くは、Permission

    を求めるものが多いです。 利用するアプリケーションでは、手厚くサポートしていきましょう。 4. Handling APIs in App const status = await navigator.permissions.query({ name: 'nfc' }) console.log(status.state) // "granted" | "denied" | "prompt" status.onchange = () => { // dosomething ex:) re render view }
  43. 4-3. Media Recorder 4. Handling APIs in App メディアの記録は、MediaRecorder を利用しています。

    録音・録画がとても簡単に実現できます。 const mediaRecorder = new MediaRecorder(stream, options) mediaRecorder.onstart = () => { console.log('start rec') } mediaRecorder.onstop = () => { console.log('stop rec') } mediaRecorder.ondataavailable = event => { console.log('data available') }
  44. 4-3. Media Recorder メディアの記録に必要な MediaStream を立ち上げた後に、 MediaRecorder インスタンスを生成します。 4. Handling

    APIs in App const mediaRecorder = new MediaRecorder(stream, options) mediaRecorder.onstart = () => { console.log('start rec') } mediaRecorder.onstop = () => { console.log('stop rec') } mediaRecorder.ondataavailable = event => { console.log('data available') }
  45. 4-3. Media Recorder 「ondataavailable」ハンドラでは、録画データを受け取ることができます。 この録画データを、任意の方法で保存します。 4. Handling APIs in App

    const mediaRecorder = new MediaRecorder(stream, options) mediaRecorder.onstart = () => { console.log('start rec') } mediaRecorder.onstop = () => { console.log('stop rec') } mediaRecorder.ondataavailable = event => { console.log('data available') }
  46. 4-4. SerialNumber & IndexedDB デモアプリでは、NFC Tag にメディアの記録をしているかの様に、 演出を施していました。 これは種明かしをすると、NFC Tag

    の「serialNumber」に紐付け、 データを永続化しているだけです。 4. Handling APIs in App localforageStore.getItem(id) localforageStore.setItem(id, blob)
  47. 4-4. SerialNumber & IndexedDB 4. Handling APIs in App その一意の

    ID を key に、IndexedDB に メディア SRC である Blob を書き込んでいます。 デモアプリでは、localforage を利用しています。 IndexedDB のラッパーライブラリです。 localforageStore.getItem(id) localforageStore.setItem(id, blob)
  48. 4-4. SerialNumber & IndexedDB IndexedDB の取り扱いはやや煩雑です。 localStorage と同じ感覚で使えるこちらのライブラリ。 Promise を返してくれるため、設計に盛り込みやすいです。

    (今回はスピード重視で選定しました) 4. Handling APIs in App localforageStore.getItem(id).then(...) localforageStore.setItem(id, blob).then(...)
  49. 4-5. Side Effect Handling さて、ここまでで紹介した複数の API。 コールバックハンドラや Promise がほとんどで、 非同期処理のサラダボウルですね。

    ひとつひとつの使い方が単純でも、順序や制御など、 アプリケーションへの統合に一工夫が必要になります。 4. Handling APIs in App
  50. 4-5. Side Effect Handling 4. Handling APIs in App 副作用が混在する

    Application は、 副作用に特化したライブラリが便利です。 私が手に馴染んでいる React では、 今回の様な アプリ色が濃いものの場合、 Redux と redux-saga を使っています。
  51. 4-5. Side Effect Handling 4. Handling APIs in App Store

    構成は「共有機能ドメイン」と「各ページドメイン」 に分けて、Reducer や Action をそれぞれ設けています。 これらのドメインをより集め、ひとつの Store とします。 各ページドメインは「共有機能ドメインの利用者」という位置付けです。 この構成により、各々の実装が単純になっています。
  52. 4-5. Side Effect Handling MediaRecorder を例に見てみましょう。 録画・録音の責務は、機能ドメインで一限管理します。 4. Handling APIs

    in App mediaRecorder.onstart = () => { store.dispatch(creators.onStartRecording()) } mediaRecorder.onstop = () => { store.dispatch(creators.onStopRecord()) } mediaRecorder.ondataavailable = event => { const blob = new Blob([event.data], { type: 'video/webm' }) store.dispatch(creators.onDataAvailable(blob)) }
  53. 4-5. Side Effect Handling 4. Handling APIs in App JavaScript

    Native API コールバックハンドラの中で、 Redux Store の dispatch を実行しています。 mediaRecorder.onstart = () => { store.dispatch(creators.onStartRecord()) } mediaRecorder.onstop = () => { store.dispatch(creators.onStopRecord()) } mediaRecorder.ondataavailable = event => { const blob = new Blob([event.data], { type: 'video/webm' }) store.dispatch(creators.onDataAvailable(blob)) }
  54. 4-5. Side Effect Handling 各ページは、共有機能ドメインの Action を購読したり、 共有機能ドメイン に Action

    を発行します。 4. Handling APIs in App switch (action.type) { case PermanentStorageTypes.ON_SUCCESS_PUT: return handleStateByMode(state, 'ready') case MediaRecorderTypes.ON_START_RECORDING: return handleStateByMode(state, 'recording') case MediaRecorderTypes.ON_DATA_AVAILABLE: return { ...state, blob: action.payload.blob } default: return state } Page Reducer Subscribe Actions
  55. 4-5. Side Effect Handling 各ページは、共有機能ドメインの Action を購読したり、 共有機能ドメイン に Action

    を発行します。 4. Handling APIs in App const handleClickIcon = React.useCallback(() => { switch (mode) { case 'ready': dispatch(startRecording({ audio: true, video: true })) break case 'recording': dispatch(stopRecording()) } }, [mode]) Page Component Dispatch Actions
  56. 4-5. Side Effect Handling 今回のデモアプリで redux-saga が最も役にたったシーンは、 メディアの録画開始時に表示されるカウントダウン機能です。 これは演出以外にも、重要な役割を担っています。 カメラの起動直後(MediaStream

    開始直後)暗い場所などでは 露出補正のため数秒間暗くなってしまう瞬間があります。 このタイミングで録画を始めてしまうと、良い画が撮れません。 4. Handling APIs in App
  57. 4-5. Side Effect Handling カウントダウンも一種の副作用であり、非同期処理といえます。 カウントダウン中に生じる別の副作用(画面遷移など)が問題になり得ます。 4. Handling APIs in

    App let count = 4 while (count) { const [normal, abnormal] = yield race([ call(countDownStep), call(countDownCancel) ]) if (abnormal !== undefined) break count -= normal yield put(creators.onCountDown(count)) }
  58. 4-5. Side Effect Handling この様な問題も saga-effect の組み合わせで克服することができます。 イレギュラーなケースもカバーしてくれる、心強い味方です。 4. Handling

    APIs in App let count = 4 while (count) { const [normal, abnormal] = yield race([ call(countDownStep), call(countDownCancel) ]) if (abnormal !== undefined) break count -= normal yield put(creators.onCountDown(count)) }
  59. まとめ Google Chrome の Experimental Features は、 面白いものが盛りだくさんです。 Native API

    にどんどんリーチできる様になっています。 アイディア次第で「便利・おもしろい」 アウトプットができそうです。