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

PWAでここまでできる

SAMUKEI
February 07, 2019

 PWAでここまでできる

スライド内の関連リンク

* DiverseのPodcast
https://podcast.diverse-inc.com/
* 採用ページ
https://diverse-inc.co.jp/recruit/positions

* 今回のサンプルGithub
https://github.com/SAMUKEI/droidkaigi2019-sample-pwa
* 今回のサンプル動画
https://youtu.be/ehW6397JceI

* はじめてのプログレッシブ ウェブアプリ
https://developers.google.com/web/fundamentals/codelabs/your-first-pwapp/?hl=ja

* PWA Builder
https://www.pwabuilder.com/

* How to Package Android
https://docs.pwabuilder.com/jekyll/update/2018/02/03/how-to-package-android.html

* Google Play StoreでPWAを配信できるらしい
https://www.hypertextcandy.com/pwa-on-google-play-store

SAMUKEI

February 07, 2019
Tweet

More Decks by SAMUKEI

Other Decks in Technology

Transcript

  1. 自己紹介 • 名前 ◦ さむけい(@SAMUKEI) • 所属 ◦ Diverse Inc.

    / MAEMO LLC • やってること ◦ youbrideのサーバ・クライアントやってます • 宣伝 ◦ 会社でPodcast配信してます。聴いてください! https://podcast.diverse-inc.com/
  2. PWAの特徴 • 段階的 ◦ プログレッシブ・エンハンスメントを基本理念としたアプリであるため、 ブラウザに関係なく、すべて のユーザーに利用してもらえます。 • レスポンシブ ◦

    パソコンでもモバイルでもタブレットでも、次世代の端末でも、 あらゆるフォームファクタに適合しま す。 • ネットワーク接続に依存しない ◦ Service Worker の活用により、オフラインでも、 ネットワーク環境が良くない場所でも動作します。 • アプリ感覚 ◦ App Shell モデルに基づいて作られているため、アプリ感覚で操作できます。 • 常に最新 ◦ Service Worker の更新プロセスにより、常に最新の状態に保たれます。
  3. PWAの特徴 • 安全 ◦ 覗き見やコンテンツの改ざんを防ぐため、 HTTPS 経由で配信されます。 • 発見しやすい ◦

    W3C のマニフェストとService Worker の登録スコープにより、「アプリケーション」として認識されつ つ、検索エンジンからも発見することができます。 • 再エンゲージメント可能 ◦ プッシュ通知のような機能を通じで容易に再エンゲージメントを促すことができます。 • インストール可能 ◦ ユーザーが気に入ればアプリのリンクをホーム画面に残しておくことができ、アプリストアで探し回 る必要はありません。 • リンク可能 ◦ URL を使って簡単に共有でき、複雑なインストールの必要はありません。           ※ はじめてのプログレッシブ ウェブアプリより引用
  4. Service Workerの登録 Service Workerを登録します PWAには必須の設定です。 パス周りでハマる可能性があるので、ルート配置がオススメ。 <app.js> // Service Workerの登録

    if ('serviceWorker' in navigator) { navigator.serviceWorker .register('/service_worker.js').then(registration => { console.log('ServiceWorker registration successful with scope: ', registration.scope); }); }
  5. オフラインキャッシュ Cache APIを用いてオフラインにキャッシュします <service_worker.js> // 登録時のイベント self.addEventListener('install', (e) => {

    e.waitUntil( caches .open("version::1::sample") // バージョン .then(function (cache) { const urlsToCache = [ // キャッシュするパス '/', '/css/hogehoge.css', '/js/hogehoge.js', ]; // キャッシュに追加 return cache.addAll(urlsToCache); }) ); });
  6. オフラインキャッシュ Cache APIを用いてオフラインのキャッシュから取得します <service_worker.js> // リソースフェッチ self.addEventListener('fetch', (e) => {

    e.respondWith( caches .match(e.request) .then(function (response) { // キャッシュがあればロードする return response ? response : fetch(e.request); }) ); });
  7. オフラインキャッシュ 古いキャッシュを削除して、再度キャッシュします。 <service_worker.js> // Service Workerが有効になった時 self.addEventListener('activate', (e) => {

    event.waitUntil( caches.keys().then(keyList => { return Promise.all(keyList.map(key => { if ("version::1::sample".indexOf(key) === -1) { // 現在のバージョンのキャッシュではない場合      // 古いキャッシュの削除 return caches.delete(key); } })); }) ); });
  8. バックグラウンド同期 バックグラウンド同期で呼び出す対象として登録する。 <app.js> navigator.serviceWorker.ready .then((registration) => { // ServiceWorkerRegistration の取得

    registration.sync.register('sync-xxx') // 同期対象を判断するためにタグを設定します .then(() => { console.log('sync registered'); ) })
  9. Service Worker→ブラウザのやり取り Service WorkerからpostMessageを呼び出すことでブラウザにイベントを通知できま す。 <app.js> navigator.serviceWorker.addEventListener('message', e => {

    // メッセージを受け取る Promise.resolve() .then(() => { const data = e.data; // console: “message1” }) }); <service_worker.js> self.addEventListener('sync', (e) => { // clientを取得する self.clients.matchAll().then(clients => // postMessageでメッセージを送信する clients.forEach(client => client.postMessage("message1"))); });
  10. Webマニフェストファイルによりホーム画面への登録が可能です。 PWAには必須の設定です。 パス周りでハマる可能性があるので、ルート配置がオススメ。 ホーム画面登録 <index.html> <link rel="manifest" href="manifest.json"> <manifest.json> {

    "short_name": "ホーム画面でのアプリ名 ", "name": "アプリ名", "start_url": "最初に開くパス(相対パス)" "icons": [ { "src": "launcher-icon-4x.png", "type": "image/png", "sizes": "192x192" } ], }
  11. TikTokっぽいアプリに必要な機能 • 15秒の動画撮影 ◦ 音楽再生 ◦ カメラからの映像取り込み ◦ 音楽と映像を合成して動画を保存 ◦

    録画を途中で一時停止でき、映像をつなぎ合わせられる • オフライン対応 ◦ ローカルストレージに保存する • オンラインストレージへの保存 ◦ オンライン時にアップロードする ◦ アップロード完了時に通知 (ローカルプッシュ)する ※ 今回はJavaScript + jQueryでの実装しています。 ※ 実際のTikTokはエフェクトなどガンガンかけられます。
  12. 音楽再生 今回は端末内の音源を再生させます。 HTML5のaudioタグで実現可能です。 <index.html> <input type="file" id="music-file" accept="music/*"> <audio id="music"></audio>

    <app.js> // fileタグのチェンジを受け取り srcをaudioタグに設定 $("#music-file").change(e => { // 音楽ファイルのセット let music = $("#music").get(0); music.src = URL.createObjectURL(e.target.files[0]); music.onend = function (e) { URL.revokeObjectURL(e.target.src); }; });
  13. カメラからの映像取り込み MediaDevices APIでカメラの許諾を行い、 HTML5のvideoタグに映像を流します。 <index.html> <video id="camera"></video> <app.js> // カメラの許諾

    navigator.mediaDevices.getUserMedia({ audio: false, video: {facingMode: {exact: "environment"}} // リアカメラの指定 }) .then(stream => { const camera = $("#camera").get(0); camera.srcObject = stream; camera.play(); // 許諾取れたらプレビュー再生 })
  14. 音楽と映像を合成して動画を保存 MediaRecorder APIで映像と音楽のストリームを録画できます。 録画の一時停止・再開などもできるため、 TikTokのように映像をつなぎ合わせも可能です。 <app.js> const mixedStream = new

    MediaStream(); const cameraStream = $("#camera").get(0).captureStream(); const musicStream = $("#music").get(0).captureStream(); // 動画と音楽のトラックを取得 mixedStream.addTrack(cameraStream.getVideoTracks()[0]); mixedStream.addTrack(musicStream.getAudioTracks()[0]); const options = { mimeType: 'video/webm;codecs=vp9'}; // webm形式で保存(ChromeではWebmがデフォルト) recorder = new MediaRecorder(mixedStream, options); recorder.start(); music.play(); // 録画と同時に再生
  15. オフライン対応 前述のService WorkerのCache APIにて実現できます。 <app.js> // キャッシュ名とキャッシュファイルの指定 const version =

    `v1::static-resources`; const urlsToCache = [ '/', '/assets/css/main.min.css', '/assets/javascript/bundle.js', ]; // インストール時の処理 (キャッシュコントロール ) self.addEventListener('install', (e) => { e.waitUntil( caches .open(version) .then(cache => { return cache.addAll(urlsToCache); }) ); });
  16. オフライン対応 <service_worker.js> // 古いキャッシュの削除 self.addEventListener('activate', (e) => { event.waitUntil( caches.keys().then(keyList

    => { return Promise.all(keyList.map(key => { if (version.indexOf(key) === -1) { return caches.delete(key); } })); }) ); });
  17. ローカルストレージに保存する Indexed Databaseを利用します。 非同期のイベント型、大容量、オブジェクトストアのDatabaseです。 <app.js> const indexDatabase = indexedDB.open("droidkaigi2019", 1);

    // DBの新規作成、バージョンアップ時の処理 indexDatabase.onupgradeneeded = function (e) { const db = e.target.result; // オブジェクトストア (RDBのtable)を作る、keyはid db.createObjectStore("blob", {keyPath: "id"}); }; // DBのオープン成功時の処理 indexDatabase.onsuccess = function (e) { const db = e.target.result; const transaction = db.transaction(["blob"], "readwrite"); // read/write属性でトランザクション取得 const blobStore = transaction.objectStore("blob"); // オブジェクトストア (RDBのtable)を取得 blobStore.put({id: 1, blob: blob}); // IDとblob(動画)を保存 };
  18. オンラインストレージへの保存 Firebase StorageのJavaScript API経由でアップロードします。 <app.js> // firebaseのinitialize const config =

    { apiKey: "API Key", authDomain: "ドメイン", databaseURL: "データベースURL", projectId: "プロジェクト名", storageBucket: "バケット名", messagingSenderId: "SENDER ID" }; firebase.initializeApp(config);
  19. オンラインストレージへの保存 <app.js> const indexDatabase = indexedDB.open("droidkaigi2019", 1); indexDatabase.onsuccess = function

    (e) { const db = e.target.result; const transaction = db.transaction(["blob"], "readwrite"); // read write属性でトランザクション取得 const objectStore = transaction.objectStore("blob"); const keyRange = IDBKeyRange.only(1); // ID:1の要素を取得 objectStore.openCursor(keyRange).onsuccess = function (event) { const cursor = event.target.result; const blob = cursor.value.blob; // firebaseにアップロード const storageRef = firebase.storage().ref(); const uploadRef = storageRef.child("mov.webm"); // ファイル名を設定する uploadRef.put(blob); }; };
  20. アップロード完了したら通知(ローカルプッシュ)する Web Notifications APIを利用して通知の許諾を得ます。 その後アップロード完了時に通知を発火します。 <app.js> // 通知許諾 if (Notification.permission

    !== 'denied') { // 通知のパーミッションが拒否以外 Notification.requestPermission(); // 通知のパーミッションを要求 } // firebaseにアップロード const storageRef = firebase.storage().ref(); const uploadRef = storageRef.child("mov.webm"); // ファイル名を設定する uploadRef.put(blob).then(snapshot => { navigator.serviceWorker.ready .then((registration) => { // ServiceWorkerRegistration の取得 // アップロード成功で通知を送信 registration.showNotification('アップロード完了 '); }); });
  21. オフラインからオンラインになったときアップロードする 今回はService Workerで処理せず、前述のメッセージを送信してアップロードの処理を 移譲します。 <app.js> navigator.serviceWorker.addEventListener('message', e => { //

    メッセージを受け取る Promise.resolve() .then(() => { const data = e.data; if ( data == "upload" ) { <<<アップロード処理 >>> } }) }); <service_worker.js> self.addEventListener('sync', (e) => { // postMessageでメッセージを送信する self.clients.matchAll().then(clients => clients.forEach(client => client.postMessage("upload"))); });
  22. Google Play Storeにあげるもう一つの方法 Trusted Web Activityを使ってPWAをAPKにすることができ、 Google Play Storeに公開できるようです。 ※

    iOSでは引き続きCordovaを使うことになります。 情報のソースは下記リンクを参照ください https://www.hypertextcandy.com/pwa-on-google-play-store
  23. iOSではgetUserMediaでvideoタグを使う場合には、playsinline属性が必須です。 追加することで回避できます。 iOSだとカメラが表示されない! // カメラの許諾(リアカメラ) navigator.mediaDevices.getUserMedia({ audio: false, video: {facingMode:

    {exact: "environment"}} }) .then(stream => { const camera = $("#camera").get(0); camera.srcObject = stream; camera.play(); // 許諾取れたらプレビュー再生 }) <video id="camera" playsinline></video>
  24. まとめ • PWA = JavaScript + αの技術。怖くない。 • ストアにも上げられるので、怖くない。 •

    JavaScript + HTML5で色々できる。Webエンジニアもネイティブアプリっぽいの作 ることができる。怖くない。 • iOS(Safari)も今後対応される”はず”なので。怖くない。