Slide 1

Slide 1 text

こういう人が聞くと嬉しい発表です(多分) ● PWAがどんなものなのかわからない ● PWAに興味があるけど、触ったことない ● PWAに興味があって、少し触ったことがある ● Webエンジニアで、ネイティブっぽいアプリを作ってみたい また、コードが多いので、スライドをお手元で合わせて見てください! #droidkaigi #room1 でつぶやいています!

Slide 2

Slide 2 text

PWAでここまでできる @SAMUKEI

Slide 3

Slide 3 text

自己紹介 ● 名前 ○ さむけい(@SAMUKEI) ● 所属 ○ Diverse Inc. / MAEMO LLC ● やってること ○ youbrideのサーバ・クライアントやってます ● 宣伝 ○ 会社でPodcast配信してます。聴いてください! https://podcast.diverse-inc.com/

Slide 4

Slide 4 text

● 20年の運営実績を誇る日本最大級の婚活支援サービス ○ 累計会員数: 170万人以上! ○ 2018年成婚実績: 2,442人!! ○ 1日6人が成婚!!! ● Android / iOS / Web でサービス展開中 Web

Slide 5

Slide 5 text

youbrideでは一緒にサービスを作るエンジニアを全力で採用中!! ● Android (Flutter) エンジニア ● サーバーサイドエンジニア 採用ページ: https://diverse-inc.co.jp/recruit/positions 公式FacebookとTwitterでも最新情報を公開中 ! 上記以外にも エンジニアを積極採用中です! まずはぜひDiverseのブースまでお気軽にお越しください!!

Slide 6

Slide 6 text

みなさんPWAってご存知ですか?

Slide 7

Slide 7 text

知ってる人

Slide 8

Slide 8 text

使ったことある人

Slide 9

Slide 9 text

PWAとは?

Slide 10

Slide 10 text

PWAとは? Progressive Web Appsの略称で、 ● インストール不要でホーム画面に追加する ● ブラウザを感じさせない全画面での表示 などWebでは提供できなかったネイティブアプリのUXを提供するものです。

Slide 11

Slide 11 text

PWAの導入事例として、 ● Twitter ● 日経電子版 ● Retty ● SUUMO など様々なサービスで導入されています。 PWAとは?

Slide 12

Slide 12 text

PWAの特徴

Slide 13

Slide 13 text

PWAの特徴 ● 段階的 ○ プログレッシブ・エンハンスメントを基本理念としたアプリであるため、 ブラウザに関係なく、すべて のユーザーに利用してもらえます。 ● レスポンシブ ○ パソコンでもモバイルでもタブレットでも、次世代の端末でも、 あらゆるフォームファクタに適合しま す。 ● ネットワーク接続に依存しない ○ Service Worker の活用により、オフラインでも、 ネットワーク環境が良くない場所でも動作します。 ● アプリ感覚 ○ App Shell モデルに基づいて作られているため、アプリ感覚で操作できます。 ● 常に最新 ○ Service Worker の更新プロセスにより、常に最新の状態に保たれます。

Slide 14

Slide 14 text

PWAの特徴 ● 安全 ○ 覗き見やコンテンツの改ざんを防ぐため、 HTTPS 経由で配信されます。 ● 発見しやすい ○ W3C のマニフェストとService Worker の登録スコープにより、「アプリケーション」として認識されつ つ、検索エンジンからも発見することができます。 ● 再エンゲージメント可能 ○ プッシュ通知のような機能を通じで容易に再エンゲージメントを促すことができます。 ● インストール可能 ○ ユーザーが気に入ればアプリのリンクをホーム画面に残しておくことができ、アプリストアで探し回 る必要はありません。 ● リンク可能 ○ URL を使って簡単に共有でき、複雑なインストールの必要はありません。           ※ はじめてのプログレッシブ ウェブアプリより引用

Slide 15

Slide 15 text

PWAを構成する技術要素

Slide 16

Slide 16 text

PWAを構成する技術要素 Service Worker オフライン表示 バックグラウンド 同期 プッシュ Webアプリマニフェスト ホーム画面登録 HTML5 / API データベース ローカルプッシュ 課金 etc... etc...

Slide 17

Slide 17 text

Service Workerとは Webページの表示とは別にバックグラウンドで処理を行う イベント駆動型のスクリプトを実行するローカルプロキシです。 Service Worker ブラウザ サーバ

Slide 18

Slide 18 text

では、実際にサンプルコードを交えて 一部機能の説明をします

Slide 19

Slide 19 text

Service Worker ブラウザ Service Workerの登録 Service Workerを登録

Slide 20

Slide 20 text

Service Workerの登録 Service Workerを登録します PWAには必須の設定です。 パス周りでハマる可能性があるので、ルート配置がオススメ。 // Service Workerの登録 if ('serviceWorker' in navigator) { navigator.serviceWorker .register('/service_worker.js').then(registration => { console.log('ServiceWorker registration successful with scope: ', registration.scope); }); }

Slide 21

Slide 21 text

Service Worker ブラウザ オフラインキャッシュ Cache API リソース保存

Slide 22

Slide 22 text

オフラインキャッシュ Cache APIを用いてオフラインにキャッシュします // 登録時のイベント 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); }) ); });

Slide 23

Slide 23 text

Service Worker ブラウザ オフラインキャッシュ Cache API リソース取得

Slide 24

Slide 24 text

オフラインキャッシュ Cache APIを用いてオフラインのキャッシュから取得します // リソースフェッチ self.addEventListener('fetch', (e) => { e.respondWith( caches .match(e.request) .then(function (response) { // キャッシュがあればロードする return response ? response : fetch(e.request); }) ); });

Slide 25

Slide 25 text

オフラインキャッシュ 古いキャッシュを削除して、再度キャッシュします。 // 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); } })); }) ); });

Slide 26

Slide 26 text

Service Worker ブラウザ バックグラウンド同期 syncイベント発火 syncイベント発火設定

Slide 27

Slide 27 text

バックグラウンド同期 バックグラウンド同期で呼び出す対象として登録する。 navigator.serviceWorker.ready .then((registration) => { // ServiceWorkerRegistration の取得 registration.sync.register('sync-xxx') // 同期対象を判断するためにタグを設定します .then(() => { console.log('sync registered'); ) })

Slide 28

Slide 28 text

バックグラウンド同期 オフラインのときには呼び出されず、サーバと同期可能(オンライン)と判断された時に syncが発火します。 その後バックグラウンド同期対象をサーバに送信します。 self.addEventListener('sync', (e) => { if (e.tag.startsWith('sync-xxx')) { // 同期対象の判定 <<<ここでデータを取得して、サーバに送信する >>> } });

Slide 29

Slide 29 text

Service Worker ブラウザ Service Worker→ブラウザのやり取り postMessage メッセージ送信

Slide 30

Slide 30 text

Service Worker→ブラウザのやり取り Service WorkerからpostMessageを呼び出すことでブラウザにイベントを通知できま す。 navigator.serviceWorker.addEventListener('message', e => { // メッセージを受け取る Promise.resolve() .then(() => { const data = e.data; // console: “message1” }) }); self.addEventListener('sync', (e) => { // clientを取得する self.clients.matchAll().then(clients => // postMessageでメッセージを送信する clients.forEach(client => client.postMessage("message1"))); });

Slide 31

Slide 31 text

Webマニフェストファ イル ブラウザ ホーム画面登録 ユーザにはこう見えます

Slide 32

Slide 32 text

Webマニフェストファイルによりホーム画面への登録が可能です。 PWAには必須の設定です。 パス周りでハマる可能性があるので、ルート配置がオススメ。 ホーム画面登録 { "short_name": "ホーム画面でのアプリ名 ", "name": "アプリ名", "start_url": "最初に開くパス(相対パス)" "icons": [ { "src": "launcher-icon-4x.png", "type": "image/png", "sizes": "192x192" } ], }

Slide 33

Slide 33 text

では、実際にPWAアプリを作ってみます

Slide 34

Slide 34 text

さて、今流行ってるアプリってなんでしょう?

Slide 35

Slide 35 text

No content

Slide 36

Slide 36 text

みなさんわかりましたね?

Slide 37

Slide 37 text

No content

Slide 38

Slide 38 text

そう、TikTokです

Slide 39

Slide 39 text

TikTokとは? Twitterとかの広告でよく見ると思います TikTokは15秒〜60秒のショート動画配信アプリで、動画を用いたコミュニティサービス です 今回はTikTokっぽいアプリケーションをPWAで作成します

Slide 40

Slide 40 text

TikTokっぽいアプリに必要な機能

Slide 41

Slide 41 text

TikTokっぽいアプリに必要な機能 ● 15秒の動画撮影 ○ 音楽再生 ○ カメラからの映像取り込み ○ 音楽と映像を合成して動画を保存 ○ 録画を途中で一時停止でき、映像をつなぎ合わせられる ● オフライン対応 ○ ローカルストレージに保存する ● オンラインストレージへの保存 ○ オンライン時にアップロードする ○ アップロード完了時に通知 (ローカルプッシュ)する ※ 今回はJavaScript + jQueryでの実装しています。 ※ 実際のTikTokはエフェクトなどガンガンかけられます。

Slide 42

Slide 42 text

音楽再生 今回は端末内の音源を再生させます。 HTML5のaudioタグで実現可能です。 // 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); }; });

Slide 43

Slide 43 text

カメラからの映像取り込み MediaDevices APIでカメラの許諾を行い、 HTML5のvideoタグに映像を流します。 // カメラの許諾 navigator.mediaDevices.getUserMedia({ audio: false, video: {facingMode: {exact: "environment"}} // リアカメラの指定 }) .then(stream => { const camera = $("#camera").get(0); camera.srcObject = stream; camera.play(); // 許諾取れたらプレビュー再生 })

Slide 44

Slide 44 text

音楽と映像を合成して動画を保存 MediaRecorder APIで映像と音楽のストリームを録画できます。 録画の一時停止・再開などもできるため、 TikTokのように映像をつなぎ合わせも可能です。 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(); // 録画と同時に再生

Slide 45

Slide 45 text

オフライン対応 前述のService WorkerのCache APIにて実現できます。 // キャッシュ名とキャッシュファイルの指定 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); }) ); });

Slide 46

Slide 46 text

オフライン対応 // 古いキャッシュの削除 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); } })); }) ); });

Slide 47

Slide 47 text

オフライン対応 // リソースフェッチ時のキャッシュロード処理 self.addEventListener('fetch', (e) => { e.respondWith( caches .match(e.request) .then(response => { return response ? response : fetch(e.request); }) ); });

Slide 48

Slide 48 text

ローカルストレージに保存する Indexed Databaseを利用します。 非同期のイベント型、大容量、オブジェクトストアのDatabaseです。 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(動画)を保存 };

Slide 49

Slide 49 text

オンラインストレージへの保存 Firebase StorageのJavaScript API経由でアップロードします。 // firebaseのinitialize const config = { apiKey: "API Key", authDomain: "ドメイン", databaseURL: "データベースURL", projectId: "プロジェクト名", storageBucket: "バケット名", messagingSenderId: "SENDER ID" }; firebase.initializeApp(config);

Slide 50

Slide 50 text

オンラインストレージへの保存 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); }; };

Slide 51

Slide 51 text

アップロード完了したら通知(ローカルプッシュ)する Web Notifications APIを利用して通知の許諾を得ます。 その後アップロード完了時に通知を発火します。 // 通知許諾 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('アップロード完了 '); }); });

Slide 52

Slide 52 text

オフラインからオンラインになったときアップロードする 前述のService Workerのバックグラウンド同期で実現します。 ローカルストレージの保存完了後にsyncの登録をします。 navigator.serviceWorker.ready .then((registration) => { // ServiceWorkerRegistration の取得 <<<ローカルストレージの保存処理 >>> registration.sync.register(''); // syncに登録 });

Slide 53

Slide 53 text

オフラインからオンラインになったときアップロードする 今回はService Workerで処理せず、前述のメッセージを送信してアップロードの処理を 移譲します。 navigator.serviceWorker.addEventListener('message', e => { // メッセージを受け取る Promise.resolve() .then(() => { const data = e.data; if ( data == "upload" ) { <<<アップロード処理 >>> } }) }); self.addEventListener('sync', (e) => { // postMessageでメッセージを送信する self.clients.matchAll().then(clients => clients.forEach(client => client.postMessage("upload"))); });

Slide 54

Slide 54 text

できました https://github.com/SAMUKEI/droidkaigi2019-sample-pwa

Slide 55

Slide 55 text

今回作成したアプリで撮影した動画はこんな感じです!

Slide 56

Slide 56 text

Google Play Storeでは公開できない?

Slide 57

Slide 57 text

できます!

Slide 58

Slide 58 text

Google Play Storeに公開する理由 藤原さんの発言のように、ストアはネイティブアプリの大きなメリットです。 ストアに公開できれば、ユーザ行動を同じままにできるのです。 https://twitter.com/satorufujiwara/status/976060194936430593

Slide 59

Slide 59 text

では、APKを作っちゃいましょう

Slide 60

Slide 60 text

APKを作成する方法 PWA Builderにアクセスし画面の指示通りに設定し、 Androidを「Download」します。

Slide 61

Slide 61 text

APKを作成する方法 解凍すると・・・・ そうです あのApache Cordovaのプロジェクトです Cordovaは以前はPhone Gapとして知られていたもので、 HTML/JS/CSSクロスプラットフォームを提供するフレームワークです PWA BuilderではこのCordovaが利用されています

Slide 62

Slide 62 text

APKを作成する方法 “android”ディレクトリをAndroid StudioでImportします

Slide 63

Slide 63 text

APKを作成する方法 “Build Variants”をreleaseに変更してビルドするとAPKが作成されます ※ 詳細はHow to Package Android を参照してください

Slide 64

Slide 64 text

できましたね

Slide 65

Slide 65 text

さて、APK作成のスライドも書いたし、 あとは本番だけだ!

Slide 66

Slide 66 text

さて、APK作成のスライドも書いたし、 あとは本番だけだ! なんかPWA関係で新展開があったようです

Slide 67

Slide 67 text

さて、APK作成のスライドも書いたし、 あとは本番だけだ! なんかPWA関係で新展開があったようです へぇ〜、どれどれ?

Slide 68

Slide 68 text

Google Play Store now open for Progressive Web Apps

Slide 69

Slide 69 text

Google Play Store now open for Progressive Web Apps

Slide 70

Slide 70 text

Google Play Storeにあげるもう一つの方法 Trusted Web Activityを使ってPWAをAPKにすることができ、 Google Play Storeに公開できるようです。 ※ iOSでは引き続きCordovaを使うことになります。 情報のソースは下記リンクを参照ください https://www.hypertextcandy.com/pwa-on-google-play-store

Slide 71

Slide 71 text

iOSでも動くの?

Slide 72

Slide 72 text

基本は動きます! ・・・けど動かない部分もあります

Slide 73

Slide 73 text

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(); // 許諾取れたらプレビュー再生 })

Slide 74

Slide 74 text

iOSだと通知が未対応です! Apple(神)の対応を待ちましょう・・・

Slide 75

Slide 75 text

まとめ

Slide 76

Slide 76 text

まとめ ● PWA = JavaScript + αの技術。怖くない。 ● ストアにも上げられるので、怖くない。 ● JavaScript + HTML5で色々できる。Webエンジニアもネイティブアプリっぽいの作 ることができる。怖くない。 ● iOS(Safari)も今後対応される”はず”なので。怖くない。

Slide 77

Slide 77 text

おわり