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

進化したWeb技術でPWAをネイティブアプリに近づける / frontend-conf-2023

Yuhei FUJITA
November 18, 2023

進化したWeb技術でPWAをネイティブアプリに近づける / frontend-conf-2023

フロントエンドカンファレンス沖縄2023の登壇資料です。

https://frontend-conf.okinawa.jp/

PDF出力の関係で一部レイアウトが崩れてるので、アニメーションありは以下のリンクから表示できます。

https://yuheifujita.github.io/frontend-conf-2023/

Yuhei FUJITA

November 18, 2023
Tweet

More Decks by Yuhei FUJITA

Other Decks in Programming

Transcript

  1. 進化したWeb
    技術でPWA

    ネイティブアプリに近づける
    Yuhei FUJITA
    2023-11-18@ZORKS
    沖縄

    View full-size slide

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

    View full-size slide

  3. 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

    View full-size slide

  4. 4 / 31
    今日の話

    View full-size slide

  5. 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

    View full-size slide

  6. 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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  11. 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 }

    View full-size slide

  12. 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 }

    View full-size slide

  13. 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 }

    View full-size slide

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

    View full-size slide

  15. 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

    View full-size slide

  16. 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

    View full-size slide

  17. 15 / 31
    PWA

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  21. 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) {

    View full-size slide

  22. 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) {

    View full-size slide

  23. 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) {

    View full-size slide

  24. 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")

    View full-size slide

  25. 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) {

    View full-size slide

  26. 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) {

    View full-size slide

  27. 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) {

    View full-size slide

  28. 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) {

    View full-size slide

  29. 20 / 31
    For Android Developers
    Intent | Android Developers
    これがないとAndroid
    アプリはPWA
    からの共有を受け取れない。
    1
    2
    3
    4
    5 8 />
    9 10 android:name="android.intent.category.BROWSABLE"
    11 />
    12
    13

    View full-size slide

  30. 20 / 31
    For Android Developers
    Intent | Android Developers
    これがないとAndroid
    アプリはPWA
    からの共有を受け取れない。
    9 10 android:name="android.intent.category.BROWSABLE"
    11 />
    1
    2
    3
    4
    5 8 />
    12
    13

    View full-size slide

  31. 20 / 31
    For Android Developers
    Intent | Android Developers
    これがないとAndroid
    アプリはPWA
    からの共有を受け取れない。
    1
    2
    3
    4
    5 8 />
    9 10 android:name="android.intent.category.BROWSABLE"
    11 />
    12
    13

    View full-size slide

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

    View full-size slide

  33. 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
    ` ` ` `
    ` ` ` `
    ` ` ` `
    ` `

    View full-size slide

  34. 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

    View full-size slide

  35. 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

    View full-size slide

  36. 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

    View full-size slide

  37. 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

    View full-size slide

  38. 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

    View full-size slide

  39. 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

    View full-size slide

  40. 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"'

    View full-size slide

  41. 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"'

    View full-size slide

  42. 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"'

    View full-size slide

  43. 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"'

    View full-size slide

  44. 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"'

    View full-size slide

  45. 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"'

    View full-size slide

  46. 25 / 31
    Shortcuts API

    View full-size slide

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

    View full-size slide

  48. 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 ]

    View full-size slide

  49. 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 }

    View full-size slide

  50. 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 ]

    View full-size slide

  51. 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 ]

    View full-size slide

  52. 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 ]

    View full-size slide

  53. 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 ]

    View full-size slide

  54. 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 ]

    View full-size slide

  55. 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 ]

    View full-size slide

  56. 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 ]

    View full-size slide

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

    View full-size slide

  58. 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

    View full-size slide

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

    View full-size slide

  60. 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
    に共有機能を実装する

    View full-size slide