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

Building the Google I/O Web App: Launching a Progressive Web App on Google.com

Building the Google I/O Web App: Launching a Progressive Web App on Google.com

Eric Bidelman

May 19, 2016
Tweet

More Decks by Eric Bidelman

Other Decks in Technology

Transcript

  1. +Eric Bidelman @ebidel May 18, 2016 Building the Google I/O

    Web App launching a progressive web app on google.com
  2. App manifest, Custom Elements, CSS will-change,, Service Worker, offline caching

    & analytics, Push Notifications, ES6, a11y, <meta name=“theme-color”> , HTML Imports, app install banner, Polymer, <template>, Google Sheets API , Google Sign-in, Gulp, Firebase, Google Cloud Messaging , App Engine, YouTube, material design, Sass , Promises, Go , Web Animations API, Vulcanize , Google Web Components, Add to Homescreen, <canvas> I/O web app specs
  3. +Eric Bidelman @ebidel lead / frontend / arch +Jeff Posnick

    @jeffposnick service worker / offline +Alex Vaghin @crhym3 backend, API design +Nicolas Garnier @nivco firebase / notifications +Peng Ying @pengying frontend / surveys +Rob Dodson @rob_dodson a11y +Mat Scales @wibblymat notifications
  4. PROGRESSIVE WEB APPS notifications, bg sync ADD TO HOMESCREEN HTTPS

    SERVICE WORKER SPLASH SCREEN developers.google.com/web/progressive-web-apps
  5. PROGRESSIVE WEB APPS notifications, bg sync ADD TO HOMESCREEN HTTPS

    SERVICE WORKER SPLASH SCREEN developers.google.com/web/progressive-web-apps
  6. PROGRESSIVE WEB APPS notifications, bg sync OFFLINE ADD TO HOMESCREEN

    HTTPS SERVICE WORKER SPLASH SCREEN developers.google.com/web/progressive-web-apps
  7. Service Worker unlocks use cases thestocks.im Send reminders rate a

    session sessions starting / updated Fully offline experience offline analytics
  8. Service Worker unlocks use cases thestocks.im Performance tool Send reminders

    rate a session sessions starting / updated Fully offline experience offline analytics
  9. Perf Track in Google Analytics const load = window.chrome.loadTimes(); let

    fp = (load.firstPaintTime - load.startLoadTime) * 1000; if (fp) { ga('send', 'timing', 'load', 'firstpaint', fp); }
  10. Service worker / IDB / Firebase OFFLINE / NOTIFICATIONS effects

    / colors / flashy stuff MATERIAL DESIGN web components / Polymer COMPONENTS
  11. Router page transitions triggered off URL changes Router.prototype.init = function()

    { window.addEventListener('popstate', function(e) { this.runPageTransition(); }.bind(this)); };
  12. class Router { } Animation flow promises, promises, promises runPageTransition()

    { let endPage = this.state.end.page; this.fire(‘page-transition-start'); // 1. Let current page know it’s starting. IOWA.PageAnimation.runExitAnimation() // 2. Play exist animation sequence. .then(() => { IOWA.Elements.LazyPages.selected = endPage; // 3. Activate new page in <lazy-pages>. this.state.current = this.parseUrl(this.state.end.href); }) .then(IOWA.PageAnimation.runEnterAnimation()) // 4. Play entry animation sequence. .then(() => this.fire(‘page-transition-done')) // 5. Tell new page transitions are done. .catch(e => IOWA.Util.reportError(e)); }
  13. Web Animations API function runExitAnimation(sec) { let main = sec.querySelector('.slide-up');

    let masthead = sec.querySelector(‘.masthead’); let start = {transform: ‘translate(0,0)', opacity: 1}; let end = {transform: ‘translate(0,-100px)', opacity: 0}; let opts = {duration: 400, easing: ‘cubic-bezier(.4, 0, .2, 1)’}; let opts_delay = {duration: 400, delay: 200}; return new GroupEffect([ new KeyframeEffect(masthead, [start, end], opts), new KeyframeEffect(main, [{opacity: 1}, {opacity: 0}], opts_delay) ]); } Expandable. Just add another animation to the group if one is needed later. polyfill: github.com/web-animations/web-animations-js
  14. Page lifecycle Hook into custom element lifecycle callbacks createdCallback() {

    // properties are ready, Shady DOM is stamped. }, attachedCallback() { // element is attached to the DOM. }, detachedCallback() { // element is removed from the DOM. }, onPageTransitionDone() { // page transition animations are complete. }, onSubpageTransitionDone() { // sub nav/tab page transitions are complete. }
  15. Pages are inert @ page load <template is="dom-if" name="schedule"> <io-schedule-page

    date=“2016-05-18T17:00:00Z”></io-schedule-page> </template>
  16. <template is="dom-if" name="home"> <io-home-page date=“2016-05-18T17:00:00Z"></io-home-page> </template> <template is="dom-if" name="schedule"> <io-schedule-page

    date=“2016-05-18T17:00:00Z”></io-schedule-page> </template> <template is="dom-if" name="attend"> <io-attend-page></io-attend-page> </template> Pages are activated lazily
  17. <template is="dom-if" name="home"> <io-home-page date=“2016-05-18T17:00:00Z"></io-home-page> </template> <template is="dom-if" name="schedule"> <io-schedule-page

    date=“2016-05-18T17:00:00Z”></io-schedule-page> </template> <template is="dom-if" name="attend"> <io-attend-page></io-attend-page> </template> Pages are activated lazily <lazy-pages> </lazy-pages> <lazy-pages> stamps each template when route changes
  18. <template is="dom-if" name="home"> <io-home-page date=“2016-05-18T17:00:00Z”></io-home-page> </template> <template is="dom-if" name="schedule"> <io-schedule-page

    date=“2016-05-18T17:00:00Z”></io-schedule-page> </template> <template is="dom-if" name="attend"> <io-attend-page></io-attend-page> </template> <lazy-pages> </lazy-pages>
  19. <template is="dom-if" name="home"> <io-home-page date=“2016-05-18T17:00:00Z” app=“[[app]]”></io-home-page> </template> <template is="dom-if" name="schedule">

    <io-schedule-page date=“2016-05-18T17:00:00Z” app=“{{app}}”></io-schedule-page> </template> <template is="dom-if" name="attend"> <io-attend-page></io-attend-page> </template> <lazy-pages> </lazy-pages>
  20. <template is="dom-if" name="home"> <io-home-page date=“2016-05-18T17:00:00Z” app=“[[app]]”></io-home-page> </template> <template is="dom-if" name="schedule">

    <io-schedule-page date=“2016-05-18T17:00:00Z” app=“{{app}}”></io-schedule-page> </template> <template is="dom-if" name="attend"> <io-attend-page></io-attend-page> </template> <lazy-pages> </lazy-pages> <google-signin client-id=“…” scopes="profile email” user="{{app.currentUser}}"></google-signin> <iron-media-query query="(min-width:320px) and (max-width:768px)” query-matches="{{app.isPhoneSize}}"></iron-media-query> <iron-media-query query=“(min-width:960px)” query-matches="{{app.isDesktopSize}}"></iron-media-query>
  21. Loading state app.pageTransitionDone, app.splashRemoved UI state app.headerReveals, app.fullscreenVideoActive Breakpoints app.isPhoneSize,

    app.isTabletSize, app.isDesktopSize Schedule app.scheduleData, app.savedSessions, app.watchedVideos User app.currentUser Device app.isIOS, app.isAndroid Shared state across pages
  22. class Firebase { ... _setFirebaseData(path, value) { let ref =

    this.firebaseRef.child(path); // data/<UID>/my_sessions/<SESSION_ID> return this._enqueueIDB(path, value) // 1. Stash update in IDB. .then(() => ref.set(value)) // 2. Update live Firebase DB. .then(() => this._dequeueIDB(path), error => { // 3. Undo #1. // If Firebase returns an error, remove the queued operation from IDB. return this._dequeueOperation(path).then(() => Promise.reject(error)); }); } } Making Firebase work offline put IndexedDB between you and the fire
  23. Bookmarking sessions while offline! class Firebase { toggleSession(sessionUUID, inSchedule, currentUser=null)

    { let value = {timestamp: Date.now() + this.clockOffset, in_schedule: inSchedule}; if (this.isAuthed()) { let userId = this.firebaseRef.getAuth().uid; // Online. Use the fresh uid. return this._setFirebaseData(`data/${userId}/my_sessions/${sessionUUID}`, value); } else if (currentUser && currentUser.id) { // Offline. Queue the user’s update, to be replayed when they start up online. return this._queueOperation( `data/google:${currentUser.id}/my_sessions/${sessionUUID}`, value); } return Promise.reject('Not currently authorized with Firebase.'); } }
  24. App start up function initApp() { ... // Can't do

    anything until main schedule is fetched. IOWA.Schedule.getSchedule().then(() => { let replayFromCache = true; IOWA.Schedule.loadUserSchedule(replayFromCache); }); }
  25. read stashed sessions class Schedule { getSchedule() { ... }

    loadUserSchedule(replayFromCache=false) { const updateSchedulePageUI = (sessionId, data) => { ... }; if (replayFromCache) { // Populate the schedule page with cached data from IndexedDB. IOWA.SimpleDB.instance('firebase-reads').then(db => { // For each IDB entry, call updateSchedulePageUI(). }).catch(error => ...); } else { // Setup live Firebase listeners. IOWA.Firebase.clearCachedReads().then(() => { IOWA.Firebase.listenForSessionUpdates(updateSchedulePageUI); }); } } } App start up
  26. Starting up when online class Firebase { get NUM_FIREBASE_SHARDS() {

    return 10; } auth(userId, accessToken) { // Shard user to correct bug. #scale let shardIndex = parseInt(crc32(userId), 16) % this.NUM_FIREBASE_SHARDS; this.firebaseRef = new Firebase(`https://io2016-${shardIndex}.firebaseio.com/`); return this._setClockOffset() .then(() => this.firebaseRef.authWithOAuthToken('google', accessToken)) .then(() => this._bumpLastActivityTimestamp()) .then(() => this._replayQueuedOperations()) // Replay any offline changes. .then(() => IOWA.Schedule.loadUserSchedule()); }).catch(error => ...); } }
  27. 1. Bookmark a session 2. Get sign-in toast 3. Sign

    in 4. Enable browser permission 5. Get confirmation toast Enabling notifications flow Notifications enabled
  28. Notifications No notifications <paper-button class="notify-feature" onclick="setReminder()">Set a reminder </paper-button> <script>

    const SUPPORTED = 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window && 'showNotification' in ServiceWorkerRegistration.prototype; if (!SUPPORTED) { document.body.classList.add('nosupport-notifications'); } </script> <style> .nosupport-notifications .notify-feature { display: none; } <style> Handling UI tied to notifications
  29. Notifications No notifications <paper-button class="notify-feature" onclick="setReminder()">Set a reminder </paper-button> <script>

    const SUPPORTED = 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window && 'showNotification' in ServiceWorkerRegistration.prototype; if (!SUPPORTED) { document.body.classList.add('nosupport-notifications'); } </script> <style> .nosupport-notifications .notify-feature { display: none; } <style> Handling UI tied to notifications
  30. Notifications No notifications <paper-button class="notify-feature" onclick="setReminder()">Set a reminder </paper-button> <script>

    const SUPPORTED = 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window && 'showNotification' in ServiceWorkerRegistration.prototype; if (!SUPPORTED) { document.body.classList.add('nosupport-notifications'); } </script> <style> .nosupport-notifications .notify-feature { display: none; } <style> Handling UI tied to notifications
  31. Notifications No notifications <paper-button class="notify-feature" onclick="setReminder()">Set a reminder </paper-button> <script>

    const SUPPORTED = 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window && 'showNotification' in ServiceWorkerRegistration.prototype; if (!SUPPORTED) { document.body.classList.add('nosupport-notifications'); } </script> <style> .nosupport-notifications .notify-feature { display: none; } <style> Handling UI tied to notifications
  32. Notifications No notifications <paper-button class="notify-feature" onclick="setReminder()">Set a reminder </paper-button> <script>

    const SUPPORTED = 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window && 'showNotification' in ServiceWorkerRegistration.prototype; if (!SUPPORTED) { document.body.classList.add('nosupport-notifications'); } </script> <style> .nosupport-notifications .notify-feature { display: none; } <style> Handling UI tied to notifications
  33. self.addEventListener('notificationclick', function(e) { var url = new URL('schedule#myschedule', location.href); url.search

    += (url.search ? '&' : '') + 'utm_source=notification'; self.clients.openWindow(url.toString()); }); Re-engagement from a notification service-worker.js
  34. PRO TIP ‣ In dev, register SW without caching. Gulp

    task generate different sw.js for dev / stage /prod. Debugging Service Worker
  35. PRO TIP ‣ In dev, register SW without caching. Gulp

    task generate different sw.js for dev / stage /prod. ‣ ⌘+⇧+R always reloads page without registering a SW. Debugging Service Worker
  36. PRO TIP ‣ In dev, register SW without caching. Gulp

    task generate different sw.js for dev / stage /prod. ‣ ⌘+⇧+R always reloads page without registering a SW. ‣ about:serviceworker-internals Debugging Service Worker
  37. PRO TIP ‣ In dev, register SW without caching. Gulp

    task generate different sw.js for dev / stage /prod. ‣ ⌘+⇧+R always reloads page without registering a SW. ‣ about:serviceworker-internals ‣ DevTools Resources > Service Workers panel. Debugging Service Worker
  38. Save Battery! Close Firebase connection when they’re unneeded document.addEventListener('visibilitychange', e

    => { document.hidden ? Firebase.goOffline() : Firebase.goOnline(); }); PRO TIP
  39. Reporting errors to Google Analytics // Tip: setup error tracking

    before any JS runs. window.onerror = (msg, file, line, column, error=null) => { // Wrap in try/catch so further errors don't trigger handler! try { if (error) { // error param not supported everywhere. msg = error.stack; } ga('send', 'event', 'error', `${file}:${line}`, msg); } catch (e) { // no-op } };
  40. Report unhandled promise rejections let timeoutId; let unhandled = [];

    window.addEventListener('unhandledrejection', e => { // Keep track of rejected promises. unhandled.push({promise: e, reason: e.reason}); // Wait to log a rejected promise. There's a chance it'll be caught later! if (!timeoutId) { timeoutId = setTimeout(logRejectedPromises, 10000); } }); window.addEventListener('rejectionhandled', e => { // A previously rejected promise was handled. Remove it from the list. unhandled = unhandled.filter(rej => rej.promise !== e.promise); });
  41. Report unhandled promise rejections function logRejectedPromises() { unhandled.forEach(({reason}) => ga(‘send’,

    ‘event’, ‘error’, ‘UnhandledPromise’, reason)); unhandledRejections = []; timeoutId = null; }
  42. Service worker / IDB / Firebase OFFLINE / NOTIFICATIONS effects

    / colors / flashy stuff MATERIAL DESIGN web components / Polymer COMPONENTS