Maxim Salnikov

Webアプリをオフラインに

(いい意味で)

マキシム サルニコフ

@webmaxru

Taking your web app offline (in a good sense)

What is an offline-ready web application

And how to build it today

Maxim Salnikov

  • PWAConf (London, April 19) igniter

  • PWA Oslo / PWA London meetups organizer

  • ngVikings / Mobile Era conferences organizer

  • Google Dev Expert in Web Technologies

Developer Audience Lead at Microsoft

Web as an app platform is amazing

Browsers

  • Almost on every device with UI

  • Evergreen

  • APIs to access device hardware

Web as an app platform is amazing

JavaScript

Browsers

  • Versatile language

  • Powerful tooling

  • Evolving in a smart way

Web as an app platform is amazing

JavaScript

JS Engines

Browsers

  • Focus on the performance

  • Embedding possibilities

Web as an app platform is amazing

JavaScript

JS Engines

UI Layer

Browsers

  • Convenient tools to build responsive UIs

  • Focus on accessibility

  • Variety of high-quality components and libraries

Web as an app platform is amazing

Community

JavaScript

JS Engines

UI Layer

Browsers

69.7%

Web as an app platform is amazing

Community

JavaScript

JS Engines

UI Layer

Browsers

Issues?

Historically depends on the "connection status"

Solutions

Caching

Installing

  • HTTP Cache?

  • AppCache

  • Save page as... (complete)

  • Chrome Apps

  • Electron

  • NativeScript, React Native

What is PWA at all?

Progressive web apps use modern web APIs along with traditional progressive enhancement strategy to create cross-platform web applications.

These apps work everywhere and provide several features that give them the same user experience advantages as native apps.

works everywhere*

* but not everything**

natively

** use progressive enhancement strategy

Proper offline-ready web app

  • App itself

  • Online runtime data

  • Offline runtime data

  • Connection failures

  • Updates

  • Platform features

  • Always available

  • Thoughtfully collected

  • Safely preserved

  • Do not break the flow

  • Both explicit and implicit

  • For the win!

While keeping its web nature!

Application UI

Let's build an App shell

My App

  • Define assets

  • Put in the cache

  • Serve from the cache

  • Manage versions

}

Service worker

Logically

Physically

-file(s)

App

Service worker

Browser/OS

Event-driven worker

Cache

fetch
push
sync

Own service worker

self.addEventListener('install', event => {
    // Use Cache API to cache html/js/css
})

self.addEventListener('activate', event => {
    // Clean the cache from the obsolete versions
})

self.addEventListener('fetch', event => {
    // Serve assets from cache or network
})

handmade-service-worker.js

It's only partially a joke

Because...

Redirects?

Fallbacks?

Opaque response?

Versioning?

Cache invalidation?

Spec updates?

Cache storage space?

Variable asset names?

Feature detection?

Minimal required cache update?

Caching strategies?

Routing?

Fine-grained settings?

Kill switch?

I see the old version!!!

  • Define assets

  • Put in the cache

  • Serve from the cache

  • Manage versions

}

Is there a helper?

While having our own service worker

  • Application shell

  • Runtime caching

  • Replaying failed network requests

  • Offline Google Analytics

  • Broadcasting updates

# Installing the Workbox Node module
$ npm install workbox-build --save-dev

Configuration

// Sample configuration with the basic options
var workboxConfig = {
  globDirectory: 'dist/',
  globPatterns: [
    '**/*.{txt,png,ico,html,js,json,css}'
  ],
  swSrc: 'src/workbox-service-worker.js',
  swDest: 'dist/sw.js'
}

workbox-build.js

Source service worker

// Importing Workbox itself from Google CDN
importScripts('https://googleapis.com/.../workbox-sw.js');

// Precaching and setting up the routing
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST)

src/workbox-service-worker.js

Caching, serving, managing versions

Build service worker

// We will use injectManifest mode
const {injectManifest} = require('workbox-build')

// Sample configuration with the basic options
var workboxConfig = {...}

// Calling the method and output the result
injectManifest(workboxConfig).then(({count, size}) => {
    console.log(`Generated ${workboxConfig.swDest},
    which will precache ${count} files, ${size} bytes.`)
})

workbox-build.js

Build flow integration

{
  "scripts": {
    "build-pwa": "npm run build-app &&
                  node workbox-build.js"
  }
}

package.json

Runtime application data

Intercepting requests

self.addEventListener('fetch', event => {

  if (event.request.url.indexOf('/api/breakingnews') != -1) {
    event.respondWith(
      // Network-First Strategy
    )
  } else if (event.request.url.indexOf('/api/archive') != -1 {
    event.respondWith(
      // Cache-First Strategy
    )
  }
})

handmade-service-worker.js

Routes and strategies

workbox.routing.registerRoute(
  new RegExp('/api/breakingnews'),
  new workbox.strategies.NetworkFirst()
);

src/workbox-service-worker.js

workbox.routing.registerRoute(
  new RegExp('/api/archive'),
  new workbox.strategies.CacheFirst({
    plugins: [...]
  })
);

Strategies

  • CacheFirst

  • CacheOnly

  • NetworkFirst

  • NetworkOnly

  • StaleWhileRevalidate

Plugins

  • Expiration

  • CacheableResponse

  • BroadcastUpdate

  • BackgroundSync

  • ...your own plugin?

Save and sync offline actions

Background sync

  • One-off event for offline -> online

  • No active app tab required

navigator.serviceWorker.ready.then( swRegistration => {
  return swRegistration.sync.register('postTweet');
});

main.js

self.addEventListener('sync', event => {
  if (event.tag == 'postTweet') {
    event.waitUntil(
        // Do useful things...
    );
  }
});

handmade-service-worker.js

const postTweetPlugin =
    new workbox.backgroundSync.Plugin('tweetsQueue', {
        maxRetentionTime: 24 * 60 // Max retry period
    })

src/workbox-service-worker.js

workbox.routing.registerRoute(
  /(http[s]?:\/\/)?([^\/\s]+\/)post-tweet/,
  new workbox.strategies.NetworkOnly({
    plugins: [postTweetPlugin]
  }),
  'POST'
)

Schedule your updates

Periodic background sync

  • Like a "Cron" in the browser

  • Connection independent

  • Cadence set by the developer

  • Another API for that

  • Works only online

  • Browser decides

const registration = await navigator.serviceWorker.ready;
if ('periodicSync' in registration) {
  try {
    registration.periodicSync.register('refreshTweets', {
      // An interval of one day.
      minInterval: 24 * 60 * 60 * 1000,
    });
  } catch (error) {
    // PBS cannot be used.
  }
}

main.js

self.addEventListener('periodicsync', (event) => {
  if (event.tag === 'refreshTweets') { 
    event.waitUntil(
    	// [Maybe] Do the job
    );
  }
});

src/service-worker.js

  • Network connection type?

  • Data saver mode?

  • Available storage space?

Features flying soon

More than 100 new APIs

  • Full-fledged application platform

  • Offline-ready mechanisms are in production

  • Awesome tools are available

  • User experience & security is the key

And this is just the beginning!

Web platform today

  • 2000+ developers

  • Major browsers/frameworks/libs reps

Thank you!

Maxim Salnikov

@webmaxru

Maxim Salnikov

@webmaxru

Questions?

Taking your web app offline (in a good sense)

By Maxim Salnikov

Taking your web app offline (in a good sense)

アプリケーションプラットフォームとしてのWebは驚くべきものです。 UIを備えたほぼすべてのデバイスには、ブラウザーまたは何らかのWebビューが存在しています。今ではハードウェアにアクセスするためのネイティブAPIがあり、優れたツールを備えた汎用言語を使用されています。結局のところ、現在の開発者コミュニティ巨大で史上最大のサイズです!しかしWebはこれまで常にネットに接続されているのが前提となっていました。 私のセッションでは、最新のWeb APIといくつかのベストプラクティスにより、オフライン優先のWebアプリを構築できるケースを紹介します。常に利用可能、データ消費に配慮、保存と同期、接続に関係なく、ネイティブアプリに匹敵する(あるいはさらに優れている)UXを提供します。 さらにツールの現在の状態により、これらの機能を高速かつ信頼性の高い方法で追加できます。オフライン優先アプリを作成して、非常に実用的な方法でオフラインWebを実現しましょう!

  • 5,302