Slide 1

Slide 1 text

進化したWeb 技術でPWA を ネイティブアプリに近づける Yuhei FUJITA 2023-11-18@ZORKS 沖縄

Slide 2

Slide 2 text

2 / 31 自己紹介 名前:Yuhei FUJITA X :@Yuhei_FUJITA コミュニティ運営 Vue Fes PWA Night VS Code Meetup 趣味 キャンプ フィルムカメラ

Slide 3

Slide 3 text

3 / 31 一昨日から沖縄満喫してた人です Yuhei FUJITA @Yuhei_FUJITA·Follow 鍾乳洞すごかった、湿度が #front_okinawa 8:27 PM · Nov 16, 2023 2 Reply Copy link Read more on X Yuhei FUJITA @Yuhei_FUJITA·Follow 美ら海水族館行く #front_okinawa 12:29 PM · Nov 17, 2023 1 Reply Copy link Read more on X

Slide 4

Slide 4 text

4 / 31 今日の話

Slide 5

Slide 5 text

5 / 31 Progressive Web Apps (PWA ) Reach Capabilities 改めてPWA とは Using the latest web features to bring enhanced capabilities and reliability, Progressive Web Apps allow what you build to be installed by anyone, anywhere, on any device with a single codebase. 翻訳 プログレッシブウェブアプリは、最新のWeb 機能を 使用して機能と信頼性を強化し、構築したものを誰 でも、どこでも、どのデバイスでも、単一のコードベ ースでインストールできるようにします。 What are Progressive Web Apps? | Articles | web.dev

Slide 6

Slide 6 text

6 / 31 3 つの柱 Capable / 機能性 Web API Web Push geoLocation WebRTC Reliable / 信頼性 オフライン動作 Service Worker 高速な読み込み Pre Cache 安全な通信 HTTPS Installable / インストール可能 アプリ化 Standalone ホーム画面に追加 アプリアイコン What are Progressive Web Apps? | Articles | web.dev

Slide 7

Slide 7 text

7 / 31 なぜネイティブアプリにするのか? よりパワフルなアプリを作れる 処理速度・OS の提供するAPI モバイルSafari のサポート 使えないAPI ・わかりにくいインストール手順 ユーザーの認知度が高い アプリストア・インストールの導線

Slide 8

Slide 8 text

8 / 31 Google Trends で見るPWA 上が日本、下が世界

Slide 9

Slide 9 text

9 / 31 より良いPWA にするには?

Slide 10

Slide 10 text

10 / 31 今回はPWA 化の話は割愛 去年の「PWA をインストールしやすく するための実装 by まぁし(知念)さ ん」がわかりやすいのでそちらを参照 してください。 PWA をインストールしやすくするための実装 by まぁし(知念)さん フロントエンドカンファレンス沖縄2022 - YouTube

Slide 11

Slide 11 text

11 / 31 maskable icon さまざまな形のアイコンに対応できる。 https://web.dev/articles/maskable-icon?hl=ja 1 // manifest.json 2 { 3 "icons": [ 4 { 5 "src": "maskable_icon.png", 6 "sizes": "196x196", 7 "type": "image/png", 8 "purpose": "maskable" 9 }, 10 ], 11 }

Slide 12

Slide 12 text

11 / 31 maskable icon さまざまな形のアイコンに対応できる。 https://web.dev/articles/maskable-icon?hl=ja 8 "purpose": "maskable" 1 // manifest.json 2 { 3 "icons": [ 4 { 5 "src": "maskable_icon.png", 6 "sizes": "196x196", 7 "type": "image/png", 9 }, 10 ], 11 }

Slide 13

Slide 13 text

11 / 31 maskable icon さまざまな形のアイコンに対応できる。 https://web.dev/articles/maskable-icon?hl=ja 1 // manifest.json 2 { 3 "icons": [ 4 { 5 "src": "maskable_icon.png", 6 "sizes": "196x196", 7 "type": "image/png", 8 "purpose": "maskable" 9 }, 10 ], 11 }

Slide 14

Slide 14 text

12 / 31 より良いユーザー体験を 提供するために

Slide 15

Slide 15 text

13 / 31 PWA のチェックリスト Core Progressive Web App checklist Starts fast, stays fast (すばやく起動、常に高速で快適) Works in any browser (どのブラウザでも動作) Responsive to any screen size (あらゆる画面サイズに応答) Provides a custom offline page (カスタムのオフライン ページを用意) Is installable (インストール可能) より良いWeb 体験で重要なこと ネイティブアプリでも重要なこと What makes a good Progressive Web App? | Articles | web.dev

Slide 16

Slide 16 text

14 / 31 PWA のチェックリスト Optimal Progressive Web App checklist Provides an offline experience (オフライン機能を利用できる) Is fully accessible (完全にアクセス可能) Can be discovered through search (検索で見つけられる) Works with any input type (すべての入力タイプに対応) Provides context for permission requests (権限リクエストのコンテキストを提供する) Follows best practices for healthy code (正常なコードのためのベスト プラクティスにしたがっている) What makes a good Progressive Web App? | Articles | web.dev

Slide 17

Slide 17 text

15 / 31 PWA を ネイティブアプリに 近づける (タイトル回収)

Slide 18

Slide 18 text

16 / 31 ネイティブアプリならある「あの機能」を PWA で提供する 共有する機能を提供するWeb Share API 共有される機能を提供するWeb Share Target API 特定機能を瞬時に呼び出すShortcuts API

Slide 19

Slide 19 text

17 / 31 Web Share API PWA から共有する

Slide 20

Slide 20 text

18 / 31 Web Share API 共有機能を提供するAPI OS 標準の共有メニューを呼び出せる 統一されたUI を提供可能 さまざまなファイルを共有可能 pdf audio image text video Web Share API - Web APIs | MDN

Slide 21

Slide 21 text

19 / 31 Web Share API 1 type ShareData = { 2 title?: string, 3 text?: string, 4 url?: string, 5 files? :File[], 6 } 7 8 const shareContent = async ( 9 data: ShareData 10 ): Promise => { 11 if(navigator.share) { 12 try { 13 await navigator.share(data); 14 console.log("success") 15 } catch (err) {

Slide 22

Slide 22 text

19 / 31 Web Share API 11 if(navigator.share) { 1 type ShareData = { 2 title?: string, 3 text?: string, 4 url?: string, 5 files? :File[], 6 } 7 8 const shareContent = async ( 9 data: ShareData 10 ): Promise => { 12 try { 13 await navigator.share(data); 14 console.log("success") 15 } catch (err) {

Slide 23

Slide 23 text

19 / 31 Web Share API 1 type ShareData = { 2 title?: string, 3 text?: string, 4 url?: string, 5 files? :File[], 6 } 7 8 const shareContent = async ( 9 data: ShareData 10 ): Promise => { 11 if(navigator.share) { 12 try { 13 await navigator.share(data); 14 console.log("success") 15 } catch (err) {

Slide 24

Slide 24 text

19 / 31 Web Share API 12 try { 15 } catch (err) { 1 type ShareData = { 2 title?: string, 3 text?: string, 4 url?: string, 5 files? :File[], 6 } 7 8 const shareContent = async ( 9 data: ShareData 10 ): Promise => { 11 if(navigator.share) { 13 await navigator.share(data); 14 console.log("success")

Slide 25

Slide 25 text

19 / 31 Web Share API 13 await navigator.share(data); 14 console.log("success") 1 type ShareData = { 2 title?: string, 3 text?: string, 4 url?: string, 5 files? :File[], 6 } 7 8 const shareContent = async ( 9 data: ShareData 10 ): Promise => { 11 if(navigator.share) { 12 try { 15 } catch (err) {

Slide 26

Slide 26 text

19 / 31 Web Share API 1 type ShareData = { 2 title?: string, 3 text?: string, 4 url?: string, 5 files? :File[], 6 } 9 data: ShareData 13 await navigator.share(data); 7 8 const shareContent = async ( 10 ): Promise => { 11 if(navigator.share) { 12 try { 14 console.log("success") 15 } catch (err) {

Slide 27

Slide 27 text

19 / 31 Web Share API 1 type ShareData = { 2 title?: string, 3 text?: string, 4 url?: string, 5 files? :File[], 6 } 7 8 const shareContent = async ( 9 data: ShareData 10 ): Promise => { 11 if(navigator.share) { 12 try { 13 await navigator.share(data); 14 console.log("success") 15 } catch (err) {

Slide 28

Slide 28 text

19 / 31 Web Share API 1 type ShareData = { 2 title?: string, 3 text?: string, 4 url?: string, 5 files? :File[], 6 } 7 8 const shareContent = async ( 9 data: ShareData 10 ): Promise => { 11 if(navigator.share) { 12 try { 13 await navigator.share(data); 14 console.log("success") 15 } catch (err) {

Slide 29

Slide 29 text

20 / 31 For Android Developers Intent | Android Developers これがないとAndroid アプリはPWA からの共有を受け取れない。 1 2 3 4 5 9 12 13

Slide 30

Slide 30 text

20 / 31 For Android Developers Intent | Android Developers これがないとAndroid アプリはPWA からの共有を受け取れない。 9 1 2 3 4 5 12 13

Slide 31

Slide 31 text

20 / 31 For Android Developers Intent | Android Developers これがないとAndroid アプリはPWA からの共有を受け取れない。 1 2 3 4 5 9 12 13

Slide 32

Slide 32 text

21 / 31 Web Share Target API PWA に共有する

Slide 33

Slide 33 text

22 / 31 Web Share Target API 他アプリからの共有を受け取るAPI Web Share API とは役割が逆 GET or POST リクエストで受け取る GET の場合は query で受け取る POST の場合は body で受け取る manifest.json で定義 受け取れる共有内容や受け取り方を記述 要インストール インストールしていない場合は利用不可 share_target - Web app manifests | MDN ` ` ` ` ` ` ` ` ` ` ` ` ` `

Slide 34

Slide 34 text

23 / 31 Web Share Target API GET の場合 ` ` 1 // manifest.json 2 { 3 "share_target": { 4 "action": "/receiver/", 5 "method": "GET", 6 "params": { 7 "title": "name", 8 "text": "description", 9 "url": "link" 10 } 11 } 12 } 1 curl 'https://example.com/receiver/'\ 2 ?name=foo\ 3 &description=bar\ 4 &link=piyo

Slide 35

Slide 35 text

23 / 31 Web Share Target API GET の場合 ` ` 2 { 10 } 1 // manifest.json 3 "share_target": { 4 "action": "/receiver/", 5 "method": "GET", 6 "params": { 7 "title": "name", 8 "text": "description", 9 "url": "link" 11 } 12 } 1 curl 'https://example.com/receiver/'\ 2 ?name=foo\ 3 &description=bar\ 4 &link=piyo

Slide 36

Slide 36 text

23 / 31 Web Share Target API GET の場合 ` ` 3 "share_target": { 1 // manifest.json 2 { 4 "action": "/receiver/", 5 "method": "GET", 6 "params": { 7 "title": "name", 8 "text": "description", 9 "url": "link" 10 } 11 } 12 } 1 curl 'https://example.com/receiver/'\ 2 ?name=foo\ 3 &description=bar\ 4 &link=piyo

Slide 37

Slide 37 text

23 / 31 Web Share Target API GET の場合 ` ` 4 "action": "/receiver/", 1 // manifest.json 2 { 3 "share_target": { 5 "method": "GET", 6 "params": { 7 "title": "name", 8 "text": "description", 9 "url": "link" 10 } 11 } 12 } 1 curl 'https://example.com/receiver/'\ 2 ?name=foo\ 3 &description=bar\ 4 &link=piyo

Slide 38

Slide 38 text

23 / 31 Web Share Target API GET の場合 ` ` 5 "method": "GET", 6 "params": { 7 "title": "name", 8 "text": "description", 9 "url": "link" 1 // manifest.json 2 { 3 "share_target": { 4 "action": "/receiver/", 10 } 11 } 12 } 1 curl 'https://example.com/receiver/'\ 2 ?name=foo\ 3 &description=bar\ 4 &link=piyo

Slide 39

Slide 39 text

23 / 31 Web Share Target API GET の場合 ` ` 1 // manifest.json 2 { 3 "share_target": { 4 "action": "/receiver/", 5 "method": "GET", 6 "params": { 7 "title": "name", 8 "text": "description", 9 "url": "link" 10 } 11 } 12 } 1 curl 'https://example.com/receiver/'\ 2 ?name=foo\ 3 &description=bar\ 4 &link=piyo

Slide 40

Slide 40 text

24 / 31 Web Share Target API POST の場合 ` ` 1 // manifest.json 2 { 3 "share_target": { 4 "action": "/receiver/", 5 "method": "POST", 6 "enctype": "multipart/form-data", 7 "params": { 8 "title": "name", 9 "text": "description", 1 curl 'https://example.com/receiver/' \ 2 --form 'name="foo"' \ 3 --form 'description="bar"' \ 4 --form 'link="https://example.com"' \ 5 --form 'files=@"/path/to/file.csv"'

Slide 41

Slide 41 text

24 / 31 Web Share Target API POST の場合 ` ` 4 "action": "/receiver/", 1 // manifest.json 2 { 3 "share_target": { 5 "method": "POST", 6 "enctype": "multipart/form-data", 7 "params": { 8 "title": "name", 9 "text": "description", 1 curl 'https://example.com/receiver/' \ 2 --form 'name="foo"' \ 3 --form 'description="bar"' \ 4 --form 'link="https://example.com"' \ 5 --form 'files=@"/path/to/file.csv"'

Slide 42

Slide 42 text

24 / 31 Web Share Target API POST の場合 ` ` 5 "method": "POST", 1 // manifest.json 2 { 3 "share_target": { 4 "action": "/receiver/", 6 "enctype": "multipart/form-data", 7 "params": { 8 "title": "name", 9 "text": "description", 1 curl 'https://example.com/receiver/' \ 2 --form 'name="foo"' \ 3 --form 'description="bar"' \ 4 --form 'link="https://example.com"' \ 5 --form 'files=@"/path/to/file.csv"'

Slide 43

Slide 43 text

24 / 31 Web Share Target API POST の場合 ` ` 6 "enctype": "multipart/form-data", 7 "params": { 8 "title": "name", 9 "text": "description", 1 // manifest.json 2 { 3 "share_target": { 4 "action": "/receiver/", 5 "method": "POST", 1 curl 'https://example.com/receiver/' \ 2 --form 'name="foo"' \ 3 --form 'description="bar"' \ 4 --form 'link="https://example.com"' \ 5 --form 'files=@"/path/to/file.csv"'

Slide 44

Slide 44 text

24 / 31 Web Share Target API POST の場合 ` ` 1 // manifest.json 2 { 3 "share_target": { 4 "action": "/receiver/", 5 "method": "POST", 6 "enctype": "multipart/form-data", 7 "params": { 8 "title": "name", 9 "text": "description", 1 curl 'https://example.com/receiver/' \ 2 --form 'name="foo"' \ 3 --form 'description="bar"' \ 4 --form 'link="https://example.com"' \ 5 --form 'files=@"/path/to/file.csv"'

Slide 45

Slide 45 text

24 / 31 Web Share Target API POST の場合 ` ` 1 // manifest.json 2 { 3 "share_target": { 4 "action": "/receiver/", 5 "method": "POST", 6 "enctype": "multipart/form-data", 7 "params": { 8 "title": "name", 9 "text": "description", 1 curl 'https://example.com/receiver/' \ 2 --form 'name="foo"' \ 3 --form 'description="bar"' \ 4 --form 'link="https://example.com"' \ 5 --form 'files=@"/path/to/file.csv"'

Slide 46

Slide 46 text

25 / 31 Shortcuts API

Slide 47

Slide 47 text

26 / 31 Shortcuts API ショートカットメニューを追加するAPI アプリアイコン長押し時に最大4 つ表示可能 任意の機能の呼び出し アプリを開くことなく直接機能を呼び出せる manifest.json で定義 ショートカットの内容を記述 shortcuts - Web app manifests | MDN ` `

Slide 48

Slide 48 text

27 / 31 Shortcuts API 1 // manifest.json 2 { 3 "shortcuts": [ 4 { 5 "name": "Open Play Later", 6 "short_name": "Play Later", 7 "description": "View the list of podcasts you saved 8 "url": "/play-later?utm_source=homescreen", 9 "icons": [ 10 { 11 "src": "/icons/play-later.png", 12 "sizes": "192x192" 13 } 14 ] 15 } 16 ]

Slide 49

Slide 49 text

27 / 31 Shortcuts API 3 "shortcuts": [ 16 ] 1 // manifest.json 2 { 4 { 5 "name": "Open Play Later", 6 "short_name": "Play Later", 7 "description": "View the list of podcasts you saved 8 "url": "/play-later?utm_source=homescreen", 9 "icons": [ 10 { 11 "src": "/icons/play-later.png", 12 "sizes": "192x192" 13 } 14 ] 15 }

Slide 50

Slide 50 text

27 / 31 Shortcuts API 4 { 5 "name": "Open Play Later", 6 "short_name": "Play Later", 7 "description": "View the list of podcasts you saved 8 "url": "/play-later?utm_source=homescreen", 9 "icons": [ 10 { 11 "src": "/icons/play-later.png", 12 "sizes": "192x192" 13 } 14 ] 15 } 1 // manifest.json 2 { 3 "shortcuts": [ 16 ]

Slide 51

Slide 51 text

27 / 31 Shortcuts API 5 "name": "Open Play Later", 1 // manifest.json 2 { 3 "shortcuts": [ 4 { 6 "short_name": "Play Later", 7 "description": "View the list of podcasts you saved 8 "url": "/play-later?utm_source=homescreen", 9 "icons": [ 10 { 11 "src": "/icons/play-later.png", 12 "sizes": "192x192" 13 } 14 ] 15 } 16 ]

Slide 52

Slide 52 text

27 / 31 Shortcuts API 6 "short_name": "Play Later", 1 // manifest.json 2 { 3 "shortcuts": [ 4 { 5 "name": "Open Play Later", 7 "description": "View the list of podcasts you saved 8 "url": "/play-later?utm_source=homescreen", 9 "icons": [ 10 { 11 "src": "/icons/play-later.png", 12 "sizes": "192x192" 13 } 14 ] 15 } 16 ]

Slide 53

Slide 53 text

27 / 31 Shortcuts API 7 "description": "View the list of podcasts you saved 1 // manifest.json 2 { 3 "shortcuts": [ 4 { 5 "name": "Open Play Later", 6 "short_name": "Play Later", 8 "url": "/play-later?utm_source=homescreen", 9 "icons": [ 10 { 11 "src": "/icons/play-later.png", 12 "sizes": "192x192" 13 } 14 ] 15 } 16 ]

Slide 54

Slide 54 text

27 / 31 Shortcuts API 8 "url": "/play-later?utm_source=homescreen", 1 // manifest.json 2 { 3 "shortcuts": [ 4 { 5 "name": "Open Play Later", 6 "short_name": "Play Later", 7 "description": "View the list of podcasts you saved 9 "icons": [ 10 { 11 "src": "/icons/play-later.png", 12 "sizes": "192x192" 13 } 14 ] 15 } 16 ]

Slide 55

Slide 55 text

27 / 31 Shortcuts API 9 "icons": [ 10 { 11 "src": "/icons/play-later.png", 12 "sizes": "192x192" 13 } 14 ] 1 // manifest.json 2 { 3 "shortcuts": [ 4 { 5 "name": "Open Play Later", 6 "short_name": "Play Later", 7 "description": "View the list of podcasts you saved 8 "url": "/play-later?utm_source=homescreen", 15 } 16 ]

Slide 56

Slide 56 text

27 / 31 Shortcuts API 1 // manifest.json 2 { 3 "shortcuts": [ 4 { 5 "name": "Open Play Later", 6 "short_name": "Play Later", 7 "description": "View the list of podcasts you saved 8 "url": "/play-later?utm_source=homescreen", 9 "icons": [ 10 { 11 "src": "/icons/play-later.png", 12 "sizes": "192x192" 13 } 14 ] 15 } 16 ]

Slide 57

Slide 57 text

28 / 31 ネイティブアプリっぽくなったPWA Web Share API Web Share Target API Shortcuts API

Slide 58

Slide 58 text

29 / 31 各ブラウザ対応状況 API Desktop Chrome Desktop Safari Mobile Chrome Mobile Safari Web Share API Yes Chrome OS Windows Yes Yes Yes Web Share Target API Yes No Yes No Shortcuts API Yes No Yes No

Slide 59

Slide 59 text

30 / 31 まとめ PWA の復習 PWA はWeb の最新技術を利用したWeb アプリ 3 つの柱を中心に構築する PWA チェックリスト PWA をより良いものにするためのチェックリスト PWA に限らずWeb のチェックリスト Web API による機能拡張 Web API を駆使すればネイティブアプリに近づけられる とはいえすべてのブラウザで対応しているわけではない

Slide 60

Slide 60 text

31 / 31 参考 What are Progressive Web Apps? | Articles | web.dev Adaptive icon support in PWAs with maskable icons | Articles | web.dev What makes a good Progressive Web App? | Articles | web.dev Web Share API - Web APIs | MDN Integrate with the OS sharing UI with the Web Share API | Articles | web.dev Intent | Android Developers share_target - Web app manifests | MDN Receiving shared data with the Web Share Target API - Chrome for Developers shortcuts - Web app manifests | MDN Web Share API でPWA に共有機能を実装する