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.

E01ec1de2f7783812d2235a6a9aaaeea?s=128

Dan Gebhardt

March 01, 2018
Tweet

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. None
  3. None
  4. None
  5. None
  6. None
  7. None
  8. 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
  9. P WA

  10. None
  11. None
  12. 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 ...
  13. None
  14. None
  15. None
  16. None
  17. None
  18. None
  19. B A S I C E L E M E

    N T S O F A P WA
  20. 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.
  21. 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.
  22. // 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
  23. 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
  24. 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
  25. 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
  26. <link rel="manifest" href="/manifest.webmanifest"> W E B A P P M

    A N I F E S T / i n d e x . h t m l
  27. { "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
  28. None
  29. None
  30. None
  31. None
  32. None
  33. W E B S TA N D A R D

    S T O T H E R E S C U E ?
  34. None
  35. None
  36. 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
  37. 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
  38. P WA

  39. P WA + +

  40. A P P L I C AT I O N

    A R C H I T E C T U R E
  41. B A C K E N D

  42. SELECT * FROM EVERYTHING;

  43. None
  44. None
  45. 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
  46. g i t ?

  47. None
  48. None
  49. None
  50. U S E C A S E S

  51. OFFLINE (BROWSER CACHE)

  52. CLIENT-FIRST DEVELOPMENT

  53. PLUGGABLE SOURCES

  54. DATA SYNCHRONIZATION

  55. EDITING CONTEXTS

  56. UNDO / REDO

  57. OPTIMISTIC UI

  58. B A S I C C O N C E

    P T S
  59. DISPARATE SOURCES

  60. DISPARATE DATA

  61. COMMON INTERFACES

  62. NORMALIZED DATA

  63. EVENTED CONNECTIONS

  64. FLOW CONTROL

  65. CHANGE TRACKING

  66. IMMUTABLE DATA

  67. @ o r b i t / d a t

    a
  68. SCHEMA { MODELS RELATIONSHIPS KEYS

  69. { SOURCES SCHEMA TRANSFORM LOG QUEUES (REQUEST + SYNC) OPTIONAL

    INTERFACES
  70. { PULLABLE PUSHABLE QUERYABLE RESETTABLE SYNCABLE UPDATABLE OPTIONAL SOURCE INTERFACES

  71. UPDATING AN "UPDATABLE" SOURCE store.update( transform ); // optionally pass

    details such as a label store.update( transform, { label: "Save Contact" } );
  72. 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' }}]
  73. BUILDING TRANSFORMS Transform builders improve ergonomics: store.update(t => [ t.addRecord(jupiter),

    t.addRecord(io), t.addToRelatedRecords(jupiter, 'moons', io) ]);
  74. QUERYING A "QUERYABLE" SOURCE store.query( queryExpression );

  75. QUERY EXPRESSIONS An example query expression for finding a sorted

    collection: { op: 'findRecords', type: 'planet', sort: [{ kind: 'attribute', attribute: 'name', order: 'ascending' }] }
  76. QUERY BUILDERS Query builders improve ergonomics of building expressions: store.query(q

    => q.records('planet') .sort('name'));
  77. @ o r b i t / c o o

    rd i n a t o r
  78. COORDINATOR { SOURCES STRATEGIES

  79. SYNC'ING CHANGES // Sync all changes to the store with

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

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

    RequestStrategy({ source: 'store', on: 'beforeUpdate', target: 'remote', action: 'push', blocking: true }));
  82. 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 }));
  83. 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();
  84. 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());
  85. + P WA

  86. 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
  87. 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.)
  88. 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
  89. 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
  90. None
  91. None
  92. None
  93. None
  94. orbitjs.com @orbitjs

  95. @ 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