Slide 1

Slide 1 text

@ 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

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

P WA

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

No content

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

No content

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

B A S I C E L E M E N T S O F A P WA

Slide 20

Slide 20 text

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.

Slide 21

Slide 21 text

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.

Slide 22

Slide 22 text

// 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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

W E B A P P M A N I F E S T / i n d e x . h t m l

Slide 27

Slide 27 text

{ "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

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

No content

Slide 30

Slide 30 text

No content

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

No content

Slide 33

Slide 33 text

W E B S TA N D A R D S T O T H E R E S C U E ?

Slide 34

Slide 34 text

No content

Slide 35

Slide 35 text

No content

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

P WA

Slide 39

Slide 39 text

P WA + +

Slide 40

Slide 40 text

A P P L I C AT I O N A R C H I T E C T U R E

Slide 41

Slide 41 text

B A C K E N D

Slide 42

Slide 42 text

SELECT * FROM EVERYTHING;

Slide 43

Slide 43 text

No content

Slide 44

Slide 44 text

No content

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

g i t ?

Slide 47

Slide 47 text

No content

Slide 48

Slide 48 text

No content

Slide 49

Slide 49 text

No content

Slide 50

Slide 50 text

U S E C A S E S

Slide 51

Slide 51 text

OFFLINE (BROWSER CACHE)

Slide 52

Slide 52 text

CLIENT-FIRST DEVELOPMENT

Slide 53

Slide 53 text

PLUGGABLE SOURCES

Slide 54

Slide 54 text

DATA SYNCHRONIZATION

Slide 55

Slide 55 text

EDITING CONTEXTS

Slide 56

Slide 56 text

UNDO / REDO

Slide 57

Slide 57 text

OPTIMISTIC UI

Slide 58

Slide 58 text

B A S I C C O N C E P T S

Slide 59

Slide 59 text

DISPARATE SOURCES

Slide 60

Slide 60 text

DISPARATE DATA

Slide 61

Slide 61 text

COMMON INTERFACES

Slide 62

Slide 62 text

NORMALIZED DATA

Slide 63

Slide 63 text

EVENTED CONNECTIONS

Slide 64

Slide 64 text

FLOW CONTROL

Slide 65

Slide 65 text

CHANGE TRACKING

Slide 66

Slide 66 text

IMMUTABLE DATA

Slide 67

Slide 67 text

@ o r b i t / d a t a

Slide 68

Slide 68 text

SCHEMA { MODELS RELATIONSHIPS KEYS

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

{ PULLABLE PUSHABLE QUERYABLE RESETTABLE SYNCABLE UPDATABLE OPTIONAL SOURCE INTERFACES

Slide 71

Slide 71 text

UPDATING AN "UPDATABLE" SOURCE store.update( transform ); // optionally pass details such as a label store.update( transform, { label: "Save Contact" } );

Slide 72

Slide 72 text

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' }}]

Slide 73

Slide 73 text

BUILDING TRANSFORMS Transform builders improve ergonomics: store.update(t => [ t.addRecord(jupiter), t.addRecord(io), t.addToRelatedRecords(jupiter, 'moons', io) ]);

Slide 74

Slide 74 text

QUERYING A "QUERYABLE" SOURCE store.query( queryExpression );

Slide 75

Slide 75 text

QUERY EXPRESSIONS An example query expression for finding a sorted collection: { op: 'findRecords', type: 'planet', sort: [{ kind: 'attribute', attribute: 'name', order: 'ascending' }] }

Slide 76

Slide 76 text

QUERY BUILDERS Query builders improve ergonomics of building expressions: store.query(q => q.records('planet') .sort('name'));

Slide 77

Slide 77 text

@ o r b i t / c o o rd i n a t o r

Slide 78

Slide 78 text

COORDINATOR { SOURCES STRATEGIES

Slide 79

Slide 79 text

SYNC'ING CHANGES // Sync all changes to the store with backup coordinator.addStrategy(new SyncStrategy({ source: 'store', target: 'backup', blocking: true }));

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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 }));

Slide 83

Slide 83 text

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();

Slide 84

Slide 84 text

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());

Slide 85

Slide 85 text

+ P WA

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

No content

Slide 91

Slide 91 text

No content

Slide 92

Slide 92 text

No content

Slide 93

Slide 93 text

No content

Slide 94

Slide 94 text

orbitjs.com @orbitjs

Slide 95

Slide 95 text

@ 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