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

Progressive Web Apps

Progressive Web Apps

A case-study talk on how we made DayTrip, an app that helps you leave your house on the weekends, into a minimum viable Progressive Web App with an offline service worker.

This talk was originally given at Austin JavaScript on a rainy, haunted night.

Dave Rupert

August 16, 2016
Tweet

More Decks by Dave Rupert

Other Decks in Programming

Transcript

  1. Dave Rupert / @davatron5000

    View Slide

  2. View Slide

  3. View Slide

  4. View Slide

  5. http://shoptalkshow.com

    View Slide

  6. http://godaytrip.com

    View Slide

  7. View Slide

  8. How many apps does the average mobile
    user download per month?
    0
    Percent drop-off for each stage of your
    mobile app onboarding?
    20%
    Number of companies in the Top 10 app
    downloads according to COMSCORE?
    4

    View Slide

  9. View Slide

  10. View Slide

  11. View Slide

  12. Cats vs. Dogs, Right vs. Left, Pants vs. No pants

    View Slide

  13. View Slide

  14. We received a lot of feedback like
    “Where can I download it?” and “If
    it’s not on my homescreen I forget
    about it.”

    View Slide

  15. View Slide

  16. Requirement for an app that takes you
    deep into the woods, it must work deep
    in the woods.

    View Slide

  17. Alex Russell, Google
    TL;DR – “The future of the Web hangs on
    something I invented.”

    View Slide

  18. A background web worker with a few superpowers.

    View Slide

  19. LEAVE THE MAIN THREAD ALONE

    View Slide

  20. View Slide

  21. View Slide

  22. Offline

    View Slide

  23. “I don’t have bars, but if I
    did, they’d be full bars.
    Everyone knows I have
    great bars. Full bars. If I
    ever had less than full
    bars, I’d remember. But I
    don’t recall ever having
    less than full bars.”

    View Slide

  24. Minimum Viable Product: An Offline Service Worker

    View Slide

  25. View Slide

  26. View Slide

  27. View Slide

  28. manifest.json
    offline.html
    service-worker.js
    (HTTPS too)

    View Slide

  29. manifest.json

    View Slide



  30. View Slide

  31. offline.html

    View Slide

  32. service-worker.js

    View Slide

  33. View Slide

  34. View Slide

  35. View Slide

  36. 10x Faster Than React, Angular, Ember, and jQuery Combined!

    View Slide

  37. View Slide

  38. View Slide

  39. View Slide

  40. Install
    Activate
    Fetch
    Sync
    Push

    View Slide

  41. self.addEventListener(‘install’, function( event ) {
    event.waitUntil(
    //
    // Prime the caches
    //
    )
    });

    View Slide

  42. “Hey event, wait until the stuff in these parenthesis are done
    before you tell the other events you fired.”

    View Slide

  43. self.addEventListener(‘activate’, function( event ) {
    event.waitUntil(
    //
    // Clear out old caches
    //
    )
    });

    View Slide

  44. self.addEventListener(‘fetch’, function( event ) {
    event.respondWith(
    //
    // Instead of fetching stuff from the network,
    // try this stuff instead.
    //
    )
    });

    View Slide

  45. “Hey (fetch) event, respond with this instead of what you were
    gonna do.”

    View Slide

  46. Quite literally one of the hardest problems in Computer Science.
    But at least we get to, you know, use JavaScript to do it.

    View Slide

  47. caches.open( cacheName )
    caches.keys()
    caches.match( request )
    caches.delete( key )

    View Slide

  48. ~100 Lines of Fun, Stolen from adactio.com/serviceworker.js

    View Slide

  49. var staticCacheName = 'static';
    var version = 'v1::';

    View Slide

  50. function updateStaticCache() {
    return caches.open(version + staticCacheName)
    .then(function (cache) {
    return cache.addAll([
    '/assets/application.css'
    '/assets/application.js',
    '/offline'
    ]);
    });
    };

    View Slide

  51. self.addEventListener('install', function(event) {
    event.waitUntil( updateStaticCache() );
    });

    View Slide

  52. self.addEventListener('activate', function (event) {
    event.waitUntil( caches.keys().then(function (keys) {
    // Remove caches whose name is no longer valid
    return Promise.all(
    keys.filter(function (key) {
    return key.indexOf(version) !== 0;
    })
    .map(function (key) {
    return caches.delete(key);
    })
    );
    }));
    });

    View Slide

  53. self.addEventListener('fetch', function (event) {
    // Step 1: Always fetch non-GET requests from the network
    // Step 2: For TEXT/HTML do this:
    // a) Try the network first
    // b) If that fails, fallback to the cache
    // c) If that doesn’t exist, show the offline page
    // Step 3: For non-TEXT/HTML (e.g. Images) do this:
    // a) Try the cache first
    // b) If that fails, try the network
    // c) If that fails, hijack the request
    });

    View Slide

  54. // Step 1: Always fetch non-GET requests from the network
    var request = event.request;
    if (request.method !== 'GET') {
    event.respondWith(
    fetch(request)
    .catch(function () {
    return caches.match('/offline');
    })
    );
    return;
    }

    View Slide

  55. // Step 2 For TEXT/HTML do this…
    if (request.headers.get('Accept').indexOf('text/html') !== -1) {
    event.respondWith(
    fetch(request)
    // Then Stuff
    // Catch Stuff
    );
    return;
    }

    View Slide

  56. // Step 2: Then Stuff…
    .then(function (response) {
    // Stash a copy of this page in the cache
    var copy = response.clone();
    caches.open(version + staticCacheName)
    .then(function (cache) {
    cache.put(request, copy);
    });
    return response;
    })

    View Slide

  57. // Step 2: Catch Stuff…
    .catch(function () {
    return caches.match(request).then(function (response) {
    return response || caches.match('/offline');
    })
    })

    View Slide

  58. // Step 3: For non-TEXT/HTML (e.g. Images) do this…
    event.respondWith(
    caches.match(request).then(function (response) {
    return response || fetch(request)
    .catch(function () {
    // If the request is for an image, show an offline placeholder
    if (request.headers.get('Accept').indexOf('image') !== -1) {
    return new Response('…', {
    headers: { 'Content-Type': 'image/svg+xml' }
    });
    }
    });
    })
    );

    View Slide

  59. The Last Step?

    View Slide

  60. if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register( '/service-worker.js', {
    scope: '/'
    }).then(function(reg) {
    console.log(‘Works! Scope is ' + reg.scope);
    }).catch(function(error) {
    console.log(‘Failed with ' + error);
    });
    }

    View Slide

  61. View Slide

  62. View Slide

  63. Browser Cache With Service Worker

    View Slide

  64. - Multiple visits
    - > 2 minute session time
    - ~5 minutes apart
    Depends on browser’s own heuristics.

    View Slide

  65. Asset pipelines, digest fingerprinting, and scope issues.

    View Slide

  66. function updateStaticCache() {
    return caches.open(version + staticCacheName)
    .then(function (cache) {
    return cache.addAll([
    '<%= url_to_stylesheet "application" %>'
    '<%= url_to_javascript "application" %>',
    '/offline'
    ]);
    });
    };

    View Slide

  67. Nope
    • assets/
    • serviceworker.js
    • logo.png
    • index.html
    • offline.html
    • manifest.json
    Yep
    • assets/
    • logo.png
    • index.html
    • offline.html
    • manifest.json
    • serviceworker.js

    View Slide

  68. • Fixes Scope Issues
    • Sets up route to Dynamic Service Worker in the root
    • Sets appropriate Expires headers

    View Slide

  69. The… *ahem*… intricacies… of building
    with Service Workers

    View Slide

  70. localhost:4000 localhost:4000

    View Slide

  71. View Slide

  72. View Slide

  73. Enterprise Ruby on Rails, Finally!

    View Slide

  74. View Slide

  75. @davatron5000

    View Slide