Share
Embed
Transcript
101 slides
-
About Me ▪ Takefumi Yoshii / @Takepepe ▪ DeNA /
DeSC Healthcare ▪ Frontend Developer ▪ TypeScript Meetup JP member 2 -
Agenda ▪ 1. PWA や Web App で利用可能な、実験的機能について ▪ 2.
とある実験的機能 を利用してアプリを作った話 ▪ 3. とある実験的機能 の API 概要 ▪ 4. API 取り扱いポイント・React Redux に統合する話 3 -
1-1. Bridging the NativeApp Gap ServiceWorker のキャッシュ戦略により、 PWA のみならず、Webブラウジングに 快適な
UX がもたらされています。 1. Web Experimental Features -
1-1. Bridging the NativeApp Gap 1. Web Experimental Features その快適なレスポンスから、
Web アプリケーション開発技術で、 Native アプリと同等機能を提供することに、 多くの期待が寄せられています。 -
1-1. Bridging the NativeApp Gap 1. Web Experimental Features しかし、Web
ブラウザに許容されている 機能は限定的です。端末固有の機能に アクセスする Low-Level API の多くは、 JavaScript には解放されていません。 -
1-1. Bridging the NativeApp Gap 1. Web Experimental Features その状況を前進するべく、
Google Chrome では Low-Level API への橋渡しを 積極的に行なっています。 Experimental Features(実験的機能) には、そんな機能が多数控えています。 -
1-2. Origin Trial 1. Web Experimental Features chrome://flags で実験的機能を有効にすれば、 その機能を試すことができます。
そして、実験的機能を期限付き・ドメイン制限で 提供する「Origin Trial」もあります。 https://developers.chrome.com/origintrials -
1-2. Origin Trial 1. Web Experimental Features 昨年 5月に開催された「Google I/O」
昨年 11月に開催された「Chrome Dev Summit」で、 この「Origin Trial」について紹介がありました。 可能性の広がりに、期待が高まりますね! -
1-2. Origin Trial 1. Web Experimental Features 今日ご紹介するのは、 現在の私たちの生活には欠かすことのできない あの技術についてです。
最も直近に解放された API でもあります。 -
2-1. Web NFC Media MEMO Web NFC のトライアルとして、 デモアプリを作成しました。 次の検証環境で動作確認をしています。
▪ Pixel 3a ▪ Android 9 ▪ Google Chrome Canary v81 2. Devlop Trial https://vimeo.com/388157513 -
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 -
2-1. Web NFC Media MEMO このアプリを起動している時、 端末で検出された NFC Tag の処理は
フォアグラウンドのブラウザに委ねられます。 2. Devlop Trial -
2-1. Web NFC Media MEMO 2. Devlop Trial テキストメモ機能を利用して NFC
Tag に書き込んだテキストは、 NFC Tag に永続化されます。 -
2-1. Web NFC Media MEMO 2. Devlop Trial 音声メモ機能は、 録音した後に
NFC Tag にタッチすると、 保存することができます。 -
2-2. Prepare 2. Devlop Trial 一番早く API を試すことが出来る方法を紹介します。 NFC 機能が搭載された
Android端末に、 Google Chrome Canary をインストール。 chrome://flags を URL バーに入力し、 Web-NFC を有効にします。 (端末の NFC機能を有効にすることも忘れずに) -
2-2. Prepare 2. Devlop Trial localhost で挙動を確認するため、 この Android 端末を
PC に接続します。 PC の Google Chrome を起動し、 More tools > Remote devices から、 接続している該当端末を選択。 -
2-2. Prepare 2. Devlop Trial Port forwarding に、開発サーバーの localhost を指定。
モバイル端末の Developer Console にアクセスします。 -
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 */ } -
3-1. About NFC Tag API 詳細の話の前に、 NFC Tag について少し紹介します。 NFC
Tag には様々な種類があります。 3. API Overview -
3-1. About NFC Tag Suica / Edy などに台頭する電子マネーは、 NFC-F(Type-F)にあたります。 データ管理やセキュリティ機能が
強化されているものです。 3. API Overview -
3-1. About NFC Tag NFC-F については、今回調べたなかでは、 Web NFC で読み書きはできませんでした。 (ただし
検出は可能) 3. API Overview -
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. -
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. -
3-1. About NFC Tag デモアプリで利用想定しているのは、 NFC-A(Type-A)のものです。 これであれば「Web NFC」でも 読み書きをすることが可能です。 カードタイプ・シールタイプなどがあります。
3. API Overview 例(カードタイプ / ¥600 / 12枚、シールタイプ / ¥790 / 11枚) -
3-1. About NFC Tag NPX 社の「NTAG 215 チップ(NFC-A )」 が採用された
NFC Tag は多く流通しており、 安価に入手することができます。 3. API Overview 例(カードタイプ / ¥600 / 12枚、シールタイプ / ¥790 / 11枚) -
3-1. About NFC Tag 形だけでなく、容量も様々なものがあります。 そんなに大容量ではありません。 NTAG 213: 144Byte NTAG
215: 504Byte NTAG 216: 888Byte 3. API Overview 例(カードタイプ / ¥600 / 12枚、シールタイプ / ¥790 / 11枚) -
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}.`) }) -
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}.`) }) -
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}.`) }) -
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}.`) }) -
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}.`) }) -
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}.`) }) -
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}.`) }) -
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}.`) }) -
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.") }) -
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.") }) -
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)}`) }) } -
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)}`) }) } -
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)}`) }) } -
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)}`) }) } -
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() } -
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() } -
3-5. NDEFRecord recordType は任意の文字列と することができますが、 Well-known type records として取り決められている のは次の3種類です。
▪ text ▪ url ▪ smart-poster 3. API Overview -
3-5. NDEFRecord recordType: "url" の record を 保持した NFC Tag
を検出した 端末は、何もアプリケーション を起動していない場合、 ブラウザの起動を促します。 3. API Overview -
3-5. NDEFRecord そのため、Web NFC を利用し recordType: "url" を指定した NDEFRecord を書き込めば、
任意の NFC Tag を ブラウザランチャーと することもできます。 3. API Overview -
4-1. Permission Request Web NFC を使うアプリは、 何よりもはじめにパーミッションを 得る必要があります。 4. Handling
APIs in App const reader = new NDEFReader() await reader.scan() -
4-1. Permission Request 注意しなければいけないのは、 ページロードと同時に API をコールしないことです。 4. Handling APIs
in App const reader = new NDEFReader() await reader.scan() -
4-1. Permission Request このタイミングでコールすると、 パーミッション・プロンプトが 表示されません。 4. Handling APIs in
App const reader = new NDEFReader() await reader.scan() -
4-1. Permission Request パーミッション・プロンプトを 表示させるため、押下コールバックで API をコールする設計にしましょう。 4. Handling APIs
in App const reader = new NDEFReader() await reader.scan() -
4-1. Permission Request そして、NDEFReader インスタンスを 解放しない留意が必要です。scan が実行 されていないと、別アプリが検出に反応し、 フォアグラウンドを奪われてしまいます。 4.
Handling APIs in App const reader = new NDEFReader() await reader.scan() -
4-1. Permission Request そもそも端末設定で、NFC 機能への アクセスを拒否している場合もあります。 それらの考慮と併せ、利用できないケースを ユーザーに明示しましょう。 4. Handling
APIs in App const reader = new NDEFReader() await reader.scan() -
4-2. Permission Handling サンプルアプリでは カメラ・マイクを利用しており、 こちらもパーミッションが必要になります。 下記関数を利用することで MediaStream が立ち上がりますが、 「カメラ・マイク」のパーミッションプロンプトも同時に表示されます。
4. Handling APIs in App function getUserMedia(constraints: MediaStreamConstraints) { return navigator.mediaDevices.getUserMedia(constraints) } -
4-2. Permission Handling 4. Handling APIs in App アクセス権限状態の識別は重要です。 一度でも機能へのアクセスをブロックすると、
それ以降、その機能にアクセスすることができません。 (設定解除への誘導が必要) function getUserMedia(constraints: MediaStreamConstraints) { return navigator.mediaDevices.getUserMedia(constraints) } -
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 } -
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 } -
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 } -
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 } -
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') } -
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') } -
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') } -
4-4. SerialNumber & IndexedDB デモアプリでは、NFC Tag にメディアの記録をしているかの様に、 演出を施していました。 これは種明かしをすると、NFC Tag
の「serialNumber」に紐付け、 データを永続化しているだけです。 4. Handling APIs in App localforageStore.getItem(id) localforageStore.setItem(id, blob) -
4-4. SerialNumber & IndexedDB 4. Handling APIs in App その一意の
ID を key に、IndexedDB に メディア SRC である Blob を書き込んでいます。 デモアプリでは、localforage を利用しています。 IndexedDB のラッパーライブラリです。 localforageStore.getItem(id) localforageStore.setItem(id, blob) -
4-4. SerialNumber & IndexedDB IndexedDB の取り扱いはやや煩雑です。 localStorage と同じ感覚で使えるこちらのライブラリ。 Promise を返してくれるため、設計に盛り込みやすいです。
(今回はスピード重視で選定しました) 4. Handling APIs in App localforageStore.getItem(id).then(...) localforageStore.setItem(id, blob).then(...) -
4-5. Side Effect Handling さて、ここまでで紹介した複数の API。 コールバックハンドラや Promise がほとんどで、 非同期処理のサラダボウルですね。
ひとつひとつの使い方が単純でも、順序や制御など、 アプリケーションへの統合に一工夫が必要になります。 4. Handling APIs in App -
4-5. Side Effect Handling 4. Handling APIs in App 副作用が混在する
Application は、 副作用に特化したライブラリが便利です。 私が手に馴染んでいる React では、 今回の様な アプリ色が濃いものの場合、 Redux と redux-saga を使っています。 -
4-5. Side Effect Handling 4. Handling APIs in App Store
構成は「共有機能ドメイン」と「各ページドメイン」 に分けて、Reducer や Action をそれぞれ設けています。 これらのドメインをより集め、ひとつの Store とします。 各ページドメインは「共有機能ドメインの利用者」という位置付けです。 この構成により、各々の実装が単純になっています。 -
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)) } -
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)) } -
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 -
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 -
4-5. Side Effect Handling 今回のデモアプリで redux-saga が最も役にたったシーンは、 メディアの録画開始時に表示されるカウントダウン機能です。 これは演出以外にも、重要な役割を担っています。 カメラの起動直後(MediaStream
開始直後)暗い場所などでは 露出補正のため数秒間暗くなってしまう瞬間があります。 このタイミングで録画を始めてしまうと、良い画が撮れません。 4. Handling APIs in App -
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)) } -
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)) } -
まとめ Google Chrome の Experimental Features は、 面白いものが盛りだくさんです。 Native API
にどんどんリーチできる様になっています。 アイディア次第で「便利・おもしろい」 アウトプットができそうです。