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

Give Apps Online Superpowers by Optimizing them for Offline

Give Apps Online Superpowers by Optimizing them for Offline

Presentation from JSConf Iceland 2018 about building Progressive Web Apps enhanced with Orbit.js.

Dan Gebhardt

March 01, 2018
Tweet

More Decks by Dan Gebhardt

Other Decks in Programming

Transcript

  1. @ d g e b G I V E A

    P P S O N L I N E S U P E R P O W E R S B Y O P T I M I Z I N G T H E M F O R O F F L I N E D a n G e b h a rd t J S C o n f I c e l a n d 2 0 1 8
  2. O F F L I N E W E B

    A P P S A G E N T L E G U I D E T O
  3. P R O G R E S S I V

    E W E B A P P C H E C K L I S T • Site is served over HTTPS • Pages are responsive • App URLs load while offline • Add to Home screen • Fast first load • And more ...
  4. B A S I C E L E M E

    N T S O F A P WA
  5. A P P S H E L L T H

    E M I N I M A L H T M L , C S S , A N D J AVA S C R I P T T O B O O T S T R A P Y O U R A P P.
  6. S E R V I C E W O R

    K E R A B A C K G R O U N D S C R I P T T H AT A C T S A S A P R O X Y B E T W E E N Y O U R A P P L I C AT I O N A N D I T S E N V I R O N M E N T.
  7. // Register service worker if ('serviceWorker' in navigator) { window.addEventListener('load',

    function() { navigator.serviceWorker.register('/sw.js') .then(function(registration) { // Registration succeeded }, function(err) { // Registration failed }); }); } S E R V I C E W O R K E R / a p p . j s
  8. var CACHE_NAME = 'todo-dino-v1'; Var CACHE_URLS = ['/', '/app.js', '/app.css'];

    self.addEventListener('install', function(event) { // Install app shell event.waitUntil( caches.open(CACHE_NAME) .then(function(cache) { return cache.addAll(CACHE_URLS); }) ); }); S E R V I C E W O R K E R / s w. j s
  9. self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request) // Check cache .then(function(response) {

    if (response) { return response; // Return from cache } return fetch(event.request); // Fetch if needed }) ); }); S E R V I C E W O R K E R / s w. j s
  10. D E S C R I B E S Y

    O U R W E B A P P T O T H E B R O W S E R . W E B A P P M A N I F E S T
  11. { "name": "ToDoDino", "short_name": "ToDoDino", "start_url": ".", "display": "standalone", "background_color":

    "#fff", "description": "ToDo Tracker for Dinos.", "icons": [{ "src": "images/touch/homescreen48.png", "sizes": "48x48", "type": "image/png" }] } W E B A P P M A N I F E S T / m a n i f e s t . w e b m a n i f e s t
  12. W E B S TA N D A R D

    S T O T H E R E S C U E ?
  13. O F F L I N E W E B

    A P P S A G E N T L E G U I D E T O
  14. O F F L I N E W E B

    A P P S N E X T L E V E L A G E N T L E G U I D E T O
  15. A P P L I C AT I O N

    A R C H I T E C T U R E
  16. O F F L I N E S TAT E

    M A N A G E M E N T C H A L L E N G E S • Track and queue changes • Generate and assign identities to records • Serialize and persist all changesets and queues • Sync changes when back online • Reconcile any conflicts or other problems • Allow reversion to an earlier state
  17. UPDATING AN "UPDATABLE" SOURCE store.update( transform ); // optionally pass

    details such as a label store.update( transform, { label: "Save Contact" } );
  18. TRANSFORMS Transforms consist of an array of operations: [{"op": "addRecord",

    "record": { type: 'planet', id: 'p1', attributes: { name: 'Jupiter' }}} {"op": "addRecord", "record": { type: 'moon', id: 'm1', attributes: { name: 'Io' }}}, {"op": "addToRelatedRecords", "record": { type: 'planet', id: 'p1' }, "relationship": "moons", "record": { type: 'moon', id: 'm1' }}]
  19. BUILDING TRANSFORMS Transform builders improve ergonomics: store.update(t => [ t.addRecord(jupiter),

    t.addRecord(io), t.addToRelatedRecords(jupiter, 'moons', io) ]);
  20. QUERY EXPRESSIONS An example query expression for finding a sorted

    collection: { op: 'findRecords', type: 'planet', sort: [{ kind: 'attribute', attribute: 'name', order: 'ascending' }] }
  21. @ o r b i t / c o o

    rd i n a t o r
  22. SYNC'ING CHANGES // Sync all changes to the store with

    backup coordinator.addStrategy(new SyncStrategy({ source: 'store', target: 'backup', blocking: true }));
  23. OPTIMISTIC UPDATES // Push update requests to the server. coordinator.addStrategy(new

    RequestStrategy({ source: 'store', on: 'beforeUpdate', target: 'remote', action: 'push' }));
  24. PESSIMISTIC UPDATES // Push update requests to the server. coordinator.addStrategy(new

    RequestStrategy({ source: 'store', on: 'beforeUpdate', target: 'remote', action: 'push', blocking: true }));
  25. HANDLING FAILURES (1/2) coordinator.addStrategy(new RequestStrategy({ source: 'remote', on: 'pushFail', action(transform,

    e) { if (e instanceof NetworkError) { // When network errors are encountered, try again in 5s console.log('NetworkError - will try again soon'); setTimeout(() => { remote.requestQueue.retry(); }, 5000); } // Else see Part 2 }, blocking: true }));
  26. HANDLING FAILURES (2/2) // When non-network errors occur, notify the

    user and // reset state. let label = transform.options && transform.options.label; if (label) { alert(`Unable to complete "${label}"`); } else { alert(`Unable to complete operation`); } // Roll back store to position before transform if (store.transformLog.contains(transform.id)) { console.log('Rolling back - transform:', transform.id); store.rollback(transform.id, -1); } return remote.requestQueue.skip();
  27. BROWSER STORAGE // Warm the store's cache from backup BEFORE

    activating // the coordinator backup.pull(qb.records()) .then(transforms => store.sync(transforms)) .then(() => coordinator.activate());
  28. O R B I T P WA - S E

    R V I C E W O R K E R • Service worker handles caching + fetching app shell resources • Service worker does NOT typically intercept API fetches
  29. O R B I T P WA - S O

    U R C E S • Store (@orbit/store) • Backup (@orbit/indexeddb or @orbit/localstorage) • Remote (@orbit/jsonapi or other) • More? (websockets, SSEs, etc.)
  30. O R B I T P WA - B O

    O T 1. App shell fetched from service worker when offline 2. Store populated with data from Backup 3. Coordinator is activated
  31. O R B I T P WA - C O

    O R D I N AT I O N S T R AT E G I E S • Store --sync--> Backup • Remote --sync--> Store • Store.query --request--> Remote.pull • Store.update --request--> Remote.push
  32. @ d g e b T h a n k

    s ! J S C o n f I c e l a n d 2 0 1 8