Press → to see first slide Press ESC to see all slides

Jakub Sowiński

jakubsowinski.com | mail[at]jakubsowinski.com

Autostopem przez PWA

Autostopem
przez PWA

🚗🚗🚗

🗺️

  • 👉 PWA - co to jest i dlaczego to jest

  • 👉 Instalacja

  • 👉 Działanie offline

  • 👉 Push notifications

  • 👉 Co dalej?

53% of mobile site visits are abandoned if pages take longer than 3 seconds to load

source: https://www.doubleclickbygoogle.com/articles/mobile-speed-matters/

70% of mobile sites take longer than 10 seconds to load on 3G networks

source: https://www.thinkwithgoogle.com/marketing-resources/data-measurement/mobile-page-speed-new-industry-benchmarks/

Aplikacja vs strona internetowa

  • 👉 dostęp

  • 👉 instalacja

  • 👉 integracja z urządzeniem

  • 👉 responsywność

  • 👉 działanie offline

  • 👉 działanie w tle

  • 👉 notyfikacje

Progressive Web Apps are user experiences that have the reach of the web, and are:

  • 👉 Reliable - Load instantly

  • 👉 Fast - Respond quickly to user interactions

  • 👉 Engaging - Feel like a natural app on the device

source: https://developers.google.com/web/progressive-web-apps/

Jak to się ma do rzeczywistości?

🤔

📝

PWA to strona internetowa która posiada cechy natywnej aplikacji

  • 👉 szybkie ładowanie i interakcje

  • 👉 działanie offline

  • 👉 integracja z urządzeniem

🗺️

  • 👉 PWA - co to jest i dlaczego to jest ✔️

  • 👉 Instalacja

  • 👉 Działanie offline

  • 👉 Push notifications

  • 👉 Co dalej?

Just ISS Tracker (not PWA)

  • 👉 index.html

  • 👉 styles.css

  • 👉 app.js

🤖

Instalacja

/static/manifest.json
{
  "name": "PWA ISS Tracker",
  "short_name": "PWA ISS Tracker",
  "icons": [
    {
      "src": "/images/iss-icon-128x128",
      "sizes": "128x128",
      "type": "image/png"
    },
    //...
  ],
  "start_url": "/index.html",
  "display": "standalone",
  "background_color": "#111",
  "theme_color": "#111"
}
   
/src/index.html
<link rel="manifest" href="manifest.json">
   
/src/service-worker.js
let deferredInstallPrompt = null;

window.addEventListener('beforeinstallprompt', (evt) => {
  deferredInstallPrompt = evt;
  installButton.classList.remove('hidden');
});

installButton.addEventListener('click', () => {
  deferredInstallPrompt.prompt();
  installButton.classList.add('hidden');

  deferredInstallPrompt.userChoice.then((choice) => {
    console.log(`User ${choice.outcome} the A2HS prompt`, choice);
    deferredInstallPrompt = null;
  });
});
   

Instalacja - testowanie

🤔

/src/tests/snapshot/manifest.test.js
const mainfest = require('../../../static/manifest.json');

describe('manifest', () => {
  it('did not change', () => {
    expect(mainfest).toMatchSnapshot();
  });
});
   
/src/tests/snapshot/__snapshots__/manifest.test.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`manifest did not change 1`] = `
  Object {
    "background_color": "#111",
    "display": "standalone",
    // ...
  }
`;

Google Lighthouse

console
npm install --save-dev @lhci/cli
   
/package.json
"scripts": {
  "test:lhci": "npm run build && lhci autorun",
}
   
/lighthouserc.js
module.exports = {
  ci: {
    assert: {
      preset: 'lighthouse:recommended',
      assertions: {
        'redirects-http': 'off',
        'uses-http2': 'off',
      },
    },
  },
};
   

📝

  • 👉 instalacja PWA wymaga Web App Manifestu, a na niektórych urządzeniach dodatkowego oprogramowania

  • 👉 instalację PWA można przetestować przy pomocy Lighthouse

🗺️

  • 👉 PWA - co to jest i dlaczego to jest ✔️

  • 👉 Instalacja ✔️

  • 👉 Działanie offline

  • 👉 Push notifications

  • 👉 Co dalej?

🤖

Działanie offline

⚙️

Service worker

⚙️ Service worker

  • 👉 feature przeglądarki

  • 👉 działa w tle

  • 👉 działa po zamknięciu strony

  • 👉 zarządza requestami

⚙️ Service worker lifecycle

/src/app.js
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('/service-worker.js')
      .then((reg) => console.log('Service worker registered.', reg));
  });
}
   
/src/service-worker.js
const CACHE_NAME = 'cache-v1';

self.addEventListener('install', (evt) => {
  console.log('[ServiceWorker] Installing');
});

self.addEventListener('activate', (evt) => {
  console.log('[ServiceWorker] Activating');
});

self.addEventListener('fetch', (evt) => {
  console.log('[ServiceWorker] Fetching', evt.request.url);
});
   

Architektura aplikacji 🤔

  • 👉 statyczne pliki

  • 👉 dynamiczna odpowiedź z API

/src/service-worker.js
const STATIC_CACHE_NAME = 'static-cache-v1';
const STATIC_FILES_TO_CACHE = ['index.html'];

self.addEventListener('install', (evt) => {
  console.log('[ServiceWorker] Installing');
  evt.waitUntil(caches.open(STATIC_CACHE_NAME)
    .then((cache) => {
      console.log('[ServiceWorker] Pre-caching offline page');
      return cache.addAll(STATIC_FILES_TO_CACHE);
    })
    .then(() => {
      console.log('[ServiceWorker] Pre-caching completed');
      return self.skipWaiting();
    }));
});
   
/src/service-worker.js
const STATIC_CACHE_NAME = 'static-cache-v1';
const DATA_CACHE_NAME = 'data-cache-v1';

self.addEventListener('fetch', (evt) => {
  console.log('[ServiceWorker] Fetching', evt.request.url);

  if (resourceInStaticCache) {
    evt.respondWith(caches.open(STATIC_CACHE_NAME).then((cache) =>
    cache.match(evt.request).then((response) => response || fetch(evt.request))));
  } else {
    evt.respondWith(caches.open(DATA_CACHE_NAME).then((cache) =>
      fetch(evt.request).then((response) => {
        if (response.status === 200) {
          cache.put(evt.request.url, response.clone());
        }
        return response;
      }).catch((err) => cache.match(evt.request))
    ));
  }
});
   
/src/service-worker.js
const STATIC_CACHE_NAME = 'static-cache-v1';
const DATA_CACHE_NAME = 'data-cache-v1';

self.addEventListener('activate', (evt) => {
  console.log('[ServiceWorker] Activating');
  evt.waitUntil(caches.keys().then((keyList) =>
    Promise.all(keyList.map((key) => {
      if (key !== STATIC_CACHE_NAME && key !== DATA_CACHE_NAME) {
        console.log('[ServiceWorker] Removing old cache', key);
        return caches.delete(key);
      }
  }))));
});
   

🎉🎉🎉

Działanie offline - testowanie

🤔

Narzędzia do testowanie E2E ze wsparciem dla PWA

"Any application that can be written in JavaScript, will eventually be written in JavaScript."

source: Atwood's Law
/src/test/e2e/run.js
driver = await setupDriver();
await runTests(driver, tests);

driver = await switchDriverToOffline(driver);
await runTests(driver, tests);
   
/src/test/e2e/setup.js
const switchDriverToOffline = async (driver) => {
  const command = new Command('setNetworkConditions');
  command.setParameter('network_conditions', {
    offline: true,
    latency: 0,
    download_throughput: 0,
    upload_throughput: 0,
  });

  await driver.execute(command);
  return driver;
}
   
/src/tests/e2e/scenarios/seeAllElements.js
const testSeeAllElements = async (driver) => {
  const iss = await driver.wait(until.elementLocated(By.className('iss')), 5000);
  assert.equal(!!iss, true);
}
   
/package.json
"scripts": {
  "test:e2e": "npm run build && concurrently
      \"node server.js\"
      \"node src/tests/e2e/run.js\"
      --kill-others",
}
   

📝

  • 👉 działanie offline zapewnia service worker

  • 👉 service worker implementuje strategię cache'owanie

  • 👉 częściowo działanie offline można przetestować w Lighthouse

  • 👉 w pełni działanie offline można przetestować testem E2E

🗺️

  • 👉 PWA - co to jest i dlaczego to jest ✔️

  • 👉 Instalacja ✔️

  • 👉 Działanie offline ✔️

  • 👉 Push notifications

  • 👉 Co dalej?

🤖

Push notifications

/src/app.js
notifyButton.addEventListener('click', () =>
  askForNotificationPermission()
    .then(() => showNotification())
    .catch((error) => console.warn('Error when attempting to show notification:', error)),
);
   
/src/app.js
const askForNotificationPermission = () => {
  if (Notification.permission === 'granted') {
    return Promise.resolve();
  }
  return Notification.requestPermission()
    .then((permission) => permission === 'granted'
      ? Promise.resolve()
      : Promise.reject(),
    );
},
   
/src/app.js
const showNotification = () => {
  navigator.serviceWorker.ready.then((serviceWorker) => {
    const text = 'The ISS is flying...';
    const options = {
      body: text,
      icon: '/images/satellite-emoji-32x32.png',
    };

    serviceWorker.showNotification('ISS location notification', options);
  });
},
   
/src/service-worker.js
self.addEventListener('push', function(event) {
  console.log('[Service Worker] Push Received.');
  console.log(`[Service Worker] Push had this data: "${event.data.text()}"`);

  const options = {/*...*/};
  event.waitUntil(
    self.registration.showNotification(
      'ISS location notification',
      options
    ),
  );
});
   

Push notifications - testowanie

🤔

/src/test/e2e/setup.js
const setupDriver = async () =>
  await new Builder()
    .forBrowser('chrome')
    .setChromeOptions(
      new chrome.Options().headless(),
      new chrome.Options().addArguments(
        '--enable-notifications',
        '--enable-native-notifications',
      ),
      new chrome.Options().setUserPreferences({
        'profile.default_content_setting_values.notifications': 1,
      })
    )
    .build();
   
/src/tests/e2e/scenarios/seePushNotification.js
const testSeePushNotification = async (driver) => {
  const notifyButton = await driver.wait(until.elementLocated(By.className('controls--notify')), 5000);
  await notifyButton.click();

  const notifications = await driver.executeScript(`
    (async () => {
      const sw = await navigator.serviceWorker.ready;
      const notifications = await sw.getNotifications();
      return notifications;
    })();
  `);

  if (!!notifications && Array.isArray(notifications)) {
    assert.equal(notifications.length, 1);
  } else {
    assert.fail('Notification not registered');
  }
};
   

📝

  • 👉 push notifications od klienta nie wymagają service workera

  • 👉 push notifications od serwera wymagają service workera

  • 👉 najlepiej jest je przetestować testem E2E

🗺️

  • 👉 PWA - co to jest i dlaczego to jest ✔️

  • 👉 Instalacja ✔️

  • 👉 Działanie offline ✔️

  • 👉 Push notifications ✔️

  • 👉 Co dalej?

🤔

Co dalej?

💡 Dalej:

  • 👉 server-side push notifications

  • 👉 background sync

  • 👉 Google Play Store / App Store

Thank you

jakubsowinski.com | mail[at]jakubsowinski.com

Press ← to see last slide Press ESC to see all slides