Press → to see first slide Press ESC to see all slides
Jakub Sowiński
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
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
Press ← to see last slide Press ESC to see all slides