Pro Yearly is on sale from $80 to $50! »

PWAでここまでできる

0ef7215f3270a621b53f790d749a619e?s=47 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

0ef7215f3270a621b53f790d749a619e?s=128

SAMUKEI

February 07, 2019
Tweet

Transcript

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

    #droidkaigi #room1 でつぶやいています!
  2. PWAでここまでできる @SAMUKEI

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

    / MAEMO LLC • やってること ◦ youbrideのサーバ・クライアントやってます • 宣伝 ◦ 会社でPodcast配信してます。聴いてください! https://podcast.diverse-inc.com/
  4. • 20年の運営実績を誇る日本最大級の婚活支援サービス ◦ 累計会員数: 170万人以上! ◦ 2018年成婚実績: 2,442人!! ◦ 1日6人が成婚!!!

    • Android / iOS / Web でサービス展開中 Web
  5. youbrideでは一緒にサービスを作るエンジニアを全力で採用中!! • Android (Flutter) エンジニア • サーバーサイドエンジニア 採用ページ: https://diverse-inc.co.jp/recruit/positions 公式FacebookとTwitterでも最新情報を公開中

    ! 上記以外にも エンジニアを積極採用中です! まずはぜひDiverseのブースまでお気軽にお越しください!!
  6. みなさんPWAってご存知ですか?

  7. 知ってる人

  8. 使ったことある人

  9. PWAとは?

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

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

    PWAとは?
  12. PWAの特徴

  13. PWAの特徴 • 段階的 ◦ プログレッシブ・エンハンスメントを基本理念としたアプリであるため、 ブラウザに関係なく、すべて のユーザーに利用してもらえます。 • レスポンシブ ◦

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

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

  16. PWAを構成する技術要素 Service Worker オフライン表示 バックグラウンド 同期 プッシュ Webアプリマニフェスト ホーム画面登録 HTML5

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

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

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

  20. 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); }); }
  21. Service Worker ブラウザ オフラインキャッシュ Cache API リソース保存

  22. オフラインキャッシュ 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); }) ); });
  23. Service Worker ブラウザ オフラインキャッシュ Cache API リソース取得

  24. オフラインキャッシュ Cache APIを用いてオフラインのキャッシュから取得します <service_worker.js> // リソースフェッチ self.addEventListener('fetch', (e) => {

    e.respondWith( caches .match(e.request) .then(function (response) { // キャッシュがあればロードする return response ? response : fetch(e.request); }) ); });
  25. オフラインキャッシュ 古いキャッシュを削除して、再度キャッシュします。 <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); } })); }) ); });
  26. Service Worker ブラウザ バックグラウンド同期 syncイベント発火 syncイベント発火設定

  27. バックグラウンド同期 バックグラウンド同期で呼び出す対象として登録する。 <app.js> navigator.serviceWorker.ready .then((registration) => { // ServiceWorkerRegistration の取得

    registration.sync.register('sync-xxx') // 同期対象を判断するためにタグを設定します .then(() => { console.log('sync registered'); ) })
  28. バックグラウンド同期 オフラインのときには呼び出されず、サーバと同期可能(オンライン)と判断された時に syncが発火します。 その後バックグラウンド同期対象をサーバに送信します。 <service_worker.js> self.addEventListener('sync', (e) => { if

    (e.tag.startsWith('sync-xxx')) { // 同期対象の判定 <<<ここでデータを取得して、サーバに送信する >>> } });
  29. Service Worker ブラウザ Service Worker→ブラウザのやり取り postMessage メッセージ送信

  30. 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"))); });
  31. Webマニフェストファ イル ブラウザ ホーム画面登録 ユーザにはこう見えます

  32. 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" } ], }
  33. では、実際にPWAアプリを作ってみます

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

  35. None
  36. みなさんわかりましたね?

  37. None
  38. そう、TikTokです

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

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

  41. TikTokっぽいアプリに必要な機能 • 15秒の動画撮影 ◦ 音楽再生 ◦ カメラからの映像取り込み ◦ 音楽と映像を合成して動画を保存 ◦

    録画を途中で一時停止でき、映像をつなぎ合わせられる • オフライン対応 ◦ ローカルストレージに保存する • オンラインストレージへの保存 ◦ オンライン時にアップロードする ◦ アップロード完了時に通知 (ローカルプッシュ)する ※ 今回はJavaScript + jQueryでの実装しています。 ※ 実際のTikTokはエフェクトなどガンガンかけられます。
  42. 音楽再生 今回は端末内の音源を再生させます。 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); }; });
  43. カメラからの映像取り込み 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(); // 許諾取れたらプレビュー再生 })
  44. 音楽と映像を合成して動画を保存 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(); // 録画と同時に再生
  45. オフライン対応 前述の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); }) ); });
  46. オフライン対応 <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); } })); }) ); });
  47. オフライン対応 <service_worker.js> // リソースフェッチ時のキャッシュロード処理 self.addEventListener('fetch', (e) => { e.respondWith( caches

    .match(e.request) .then(response => { return response ? response : fetch(e.request); }) ); });
  48. ローカルストレージに保存する 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(動画)を保存 };
  49. オンラインストレージへの保存 Firebase StorageのJavaScript API経由でアップロードします。 <app.js> // firebaseのinitialize const config =

    { apiKey: "API Key", authDomain: "ドメイン", databaseURL: "データベースURL", projectId: "プロジェクト名", storageBucket: "バケット名", messagingSenderId: "SENDER ID" }; firebase.initializeApp(config);
  50. オンラインストレージへの保存 <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); }; };
  51. アップロード完了したら通知(ローカルプッシュ)する 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('アップロード完了 '); }); });
  52. オフラインからオンラインになったときアップロードする 前述のService Workerのバックグラウンド同期で実現します。 ローカルストレージの保存完了後にsyncの登録をします。 <app.js> navigator.serviceWorker.ready .then((registration) => { //

    ServiceWorkerRegistration の取得 <<<ローカルストレージの保存処理 >>> registration.sync.register(''); // syncに登録 });
  53. オフラインからオンラインになったときアップロードする 今回は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"))); });
  54. できました https://github.com/SAMUKEI/droidkaigi2019-sample-pwa

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

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

  57. できます!

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

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

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

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

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

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

  64. できましたね

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

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

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

  68. Google Play Store now open for Progressive Web Apps

  69. Google Play Store now open for Progressive Web Apps

  70. Google Play Storeにあげるもう一つの方法 Trusted Web Activityを使ってPWAをAPKにすることができ、 Google Play Storeに公開できるようです。 ※

    iOSでは引き続きCordovaを使うことになります。 情報のソースは下記リンクを参照ください https://www.hypertextcandy.com/pwa-on-google-play-store
  71. iOSでも動くの?

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

  73. 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>
  74. iOSだと通知が未対応です! Apple(神)の対応を待ちましょう・・・

  75. まとめ

  76. まとめ • PWA = JavaScript + αの技術。怖くない。 • ストアにも上げられるので、怖くない。 •

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