|
2. Understanding the App Manifest
Useful Links
Add to home screen (A2HS) - Browser Support |
|
MDN Article on the Web App Manifest (includes List of all Properties) |
|
A detailed Web App Manifest Explanation by Google |
|
More about the "Web App Install Banner" (including Requirements) |
2.17. Understanding App Manifest Properties
{
"name": "Sweaty - Activity Tracker",
"short_name": "Sweaty",
"start_url": "/index.html",
"scope": ".",
"display": "standalone",
"background_color": "#fff",
"theme_color": "#3f51b5",
"description": "Keep running until you're super sweaty!",
"dir": "ltr",
"language": "en-US",
"orientation": "portrait-primary",
"icons": [
{
"src": "/src/images/icons/app-icon-48x48.png",
"type": "image/png",
"size": "48x48"
},
{
"src": "/src/images/icons/app-icon-96x96.png",
"type": "image/png",
"size": "96x96"
}
],
"related_applications": [
{
"platform": "play",
"url": "https://play.google.com/store/apps/details?id=com.example.app1",
"id": "com.example.app1"
}
]
}
3. The Service Workers
The App Install Banner: |
|
DevTools |
Make sure you enabled "Developer Mode" on your Device! You do that by tapping your Android Build Number (in the Settings) 7 times. |
4. Promise and Fetch
Useful Resources
Promises
Introduction to Promises (MDN): |
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise |
Introduction to Promises (Google): |
https://developers.google.com/web/fundamentals/getting-started/primers/promises |
Fetch API
An Overview on MDN: |
|
Detailed Usage Guide (MDN): |
https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch |
Detailed Usage Guide (and comparison with XMLHttpRequest): |
|
Introduction to Fetch (Google): |
https://developers.google.com/web/updates/2015/03/introduction-to-fetch |
Assignment
Task 1
// Create a new Promise here and use setTimeout inside the function you pass to the constructor
let promise = new Promise(
function (resolve) {
setTimeout(function () { // <- Store this INSIDE the Promise you created!
// Resolve the following URL: https://swapi.co/api/people/1
resolve('https://swapi.dev/api/people/1');
}, 3000);
}
);
promise.then(function (url) {
// Handle the Promise "response" (=> the value you resolved) and returß a fetch()
// call to the value (= URL) you resolved (use a GET request)
return fetch(url, { mode: "no-cors" });
}).then(function (response) {
// Handle the response of the fetch() call and extract the JSON data, return that
// and handle it in yet another then() block
return response.json();
}).then(function (data) {
// Finally, output the "name" property of the data you got back (e.g. data.name) inside
// the "output" element (see variables at top of the file)
console.log('data:', data);
output.innerHTML = data.name;
});
sw.js:12 Cross-Origin Read Blocking (CORB) blocked cross-origin response https://swapi.dev/api/people/1 with MIME type application/json. See https://www.chromestatus.com/feature/5629709824032768 for more details. |
Task 2
// Repeat the exercise with a PUT request you send to https://httpbin.org/put
let promise = new Promise(
function (resolve) {
setTimeout(function () { // <- Store this INSIDE the Promise you created!
resolve('https://httpbin.org/put');
}, 1000);
}
);
// Make sure to set the appropriate headers (as shown in the lecture)
// Send any data of your choice, make sure to access it correctly when outputting it
// Example: If you send {person: {name: 'Max', age: 28}}, you access data.json.person.name
// to output the name (assuming your parsed JSON is stored in "data")
promise.then(function (url) {
return fetch(url, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify({
name: "John"
})
});
}).then(function (response) {
return response.json();
}).then(function (data) {
console.log('data:', data);
output.innerHTML = JSON.parse(data.data).name;
});
Task 3
// Handle the error in catch()
let promise = new Promise(
function (resolve) {
setTimeout(function () {
resolve('https://swapi.dev/api/people/1');
}, 500);
}
);
promise.then(function (url) {
return fetch(url, { mode: "no-cors" });
}).then(function (response) {
return response.json();
}).then(function (data) {
console.log('-- data:', data);
output.innerHTML = data.name;
}).catch(function (err) {
console.log('-- err:', err);
});
Task 4
// Handle the error as a second argument to then()
let promise = new Promise(
function (resolve) {
setTimeout(function () {
resolve('https://swapi.dev/api/people/1');
}, 500);
}
);
promise.then(function (url) {
return fetch(url);
},
function (err) {
console.log('-- err:', err);
}).then(function (response) {
return response.json();
}).then(function (data) {
console.log('-- data:', data);
output.innerHTML = data.name;
});
5. Service Workers - Caching
Useful Resources
About Cache Persistence and Storage Limits: |
https://jakearchibald.com/2014/offline-cookbook/#cache-persistence |
Learn more about Service Workers: |
https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API |
Static and Dynamic Caching
var CACHE_STATIC_NAME = "static-v2";
var CACHE_DYNAMIC_NAME = "dynamic-v2";
self.addEventListener('install', function(event) {
console.log('[Service Worker] Installing Service Worker ...', event);
event.waitUntil(
caches.open(CACHE_STATIC_NAME)
.then(function(cache) {
console.log('[Service Worker] Precaching App Shell');
cache.addAll([
'/',
'/index.html',
'/src/js/app.js',
'/src/js/feed.js',
'/src/js/promise.js',
'/src/js/fetch.js',
'/src/js/material.min.js',
'/src/css/app.css',
'/src/css/feed.css',
'/src/images/main-image.jpg',
'https://fonts.googleapis.com/css?family=Roboto:400,700',
'https://fonts.googleapis.com/icon?family=Material+Icons',
'https://cdnjs.cloudflare.com/ajax/libs/material-design-lite/1.3.0/material.indigo-pink.min.css'
]);
})
);
});
self.addEventListener('activate', function(event) {
console.log('[Service Worker] Activating Service Worker ....', event);
event.waitUntil(
caches.keys()
.then(function (keyList) {
return Promise.all(keyList.map(function (key) {
if (key !== CACHE_STATIC_NAME && key !== CACHE_DYNAMIC_NAME) {
console.log('[Service Worker] Removing old cache:',key);
return caches.delete(key);
}
}));
})
);
return self.clients.claim();
});
self.addEventListener('fetch', function(event) {
event.respondWith(caches.match(event.request)
.then(function (response) {
if (response) {
return response;
} else {
return fetch(event.request)
.then(function (res) {
return caches.open(CACHE_DYNAMIC_NAME)
.then(function (cache) {
cache.put(event.request.url, res.clone());
return res;
});
})
.catch(function (err) {
});
}
})
);
});
6. Service Workers - Advanced Caching
Useful Resources
Offline Cookbook: |
|
Advanced Caching Guide: |
https://www.afasterweb.com/2017/01/31/upgrading-your-service-worker-cache/ |
Mozilla Strategy Cookbook: |
https://serviceworke.rs/strategy-cache-and-update_service-worker_doc.html |
80. Offering "Cache on Demand"
function onSaveButtonClicked(event) {
console.log('clicked');
if ('caches' in window) {
caches.open('user-requested')
.then(function (cache) {
cache.add('https://httpbin.org/get');
cache.add('/src/images/sf-boat.jpg');
});
}
}
81. Providing an Offline Fallback Page
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
return response;
} else {
return fetch(event.request)
.then(function(res) {
return caches.open(CACHE_DYNAMIC_NAME)
.then(function(cache) {
cache.put(event.request.url, res.clone());
return res;
})
})
.catch(function(err) {
return caches.open(CACHE_STATIC_NAME)
.then(function (cache) {
return cache.match('/offline.html'); (1)
});
});
}
})
);
});
1 | Return offline page stored in a static cache |
82. Strategy: Cache with Network Fallback
This is what we implemented above.
85. Strategy: Network with Cache Fallback
self.addEventListener('fetch', function(event) {
event.respondWith(
fetch(event.request)
.then(function (res) {
return caches.open(CACHE_DYNAMIC_NAME)
.then(function(cache) {
cache.put(event.request.url, res.clone());
return res;
})
})
.catch(function (err) {
return caches.match(event.request);
})
);
});
86. Strategy: Cache then Network
var url = 'https://httpbin.org/get';
var networkDataReceived = false;
fetch(url)
.then(function(res) {
return res.json();
})
.then(function(data) {
networkDataReceived = true;
console.log('From web', data);
clearCards();
createCard();
});
if ('caches' in window) {
caches.match(url)
.then(function (response) {
if (response) {
return response.json();
}
})
.then(function (data) {
console.log('From cache', data);
if (!networkDataReceived) {
clearCards();
createCard();
}
})
}
87. Cache then Network & Dynamic Caching
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.open(CACHE_DYNAMIC_NAME)
.then(function (cache) {
return fetch(event.request)
.then(function (res) {
cache.put(event.request, res.clone());
return res;
})
})
);
});
88. Cache then Network with Offline Support
self.addEventListener('fetch', function(event) {
var url = 'https://httpbin.org/get';
if (event.request.url.indexOf(url) > -1) {
event.respondWith(
caches.open(CACHE_DYNAMIC_NAME)
.then(function (cache) {
return fetch(event.request)
.then(function (res) {
cache.put(event.request, res.clone());
return res;
})
})
);
} else {
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
return response;
} else {
return fetch(event.request)
.then(function(res) {
return caches.open(CACHE_DYNAMIC_NAME)
.then(function(cache) {
cache.put(event.request.url, res.clone());
return res;
})
})
.catch(function(err) {
return caches.open(CACHE_STATIC_NAME)
.then(function (cache) {
return cache.match('/offline.html');
});
});
}
})
);
}
});
7. IndexedDB and Dynamic Data
var dbPromise = idb.open('posts-store', 1, function (db) {
if (!db.objectStoreNames.contains('posts')) {
db.createObjectStore('posts', { keyPath: 'id' });
}
});
function writeData(st, data) {
return dbPromise
.then(function (db) {
var tx = db.transaction(st, 'readwrite');
var store = tx.objectStore(st);
store.put(data);
return tx.complete;
});
}
function readAllData(st) {
return dbPromise
.then(function (db) {
var tx = db.transaction(st, 'readonly');
var store = tx.objectStore(st);
return store.getAll();
});
}
function clearAllData(st) {
return dbPromise
.then(function (db) {
var tx = db.transaction(st, 'readwrite');
var store = tx.objectStore(st);
store.clear();
return tx.complete;
});
}
function deleteItemFromData(st, id) {
dbPromise
.then(function (db) {
var tx = db.transaction(st, 'readwrite');
var store = tx.objectStore(st);
store.delete(id);
return tx.complete;
})
.then(function () {
console.log('Item deleted');
});
}
Useful Resources
IDB on Github: |
|
IndexedDB explained on MDN: |
https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API |
Alternative to IDB: |
8. Creating a Responsive User Interface
Useful Resources
Responsive Design Basics by Google: |
|
Responsive Design Patterns (Google): |
|
Responsive Images (Google): |
|
Using CSS Media Queries: |
https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries |
Responsive Images (MDN): |
https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images |
Responsive Images in CSS: |
|
Using CSS Animations: |
http://learn.shayhowe.com/advanced-html-css/transitions-animations/ |
9. Background Sync
Useful Resources
Firebase Pricing: |
|
Firebase Console: |
|
Firebase Hosting: |
|
Introducing Background Sync: |
|
A Basic Guide to Background Sync: |
|
Cloud Functions for Firebase: |
|
Create and Deploy Your First Cloud Functions: |
https://firebase.google.com/docs/functions/write-firebase-functions |
Saving Data in Realtime Database: |
|
|
feed.js
var MY_FIREBASE_ID = '...'; (7)
var MY_TEST_IMAGE_URL = '...'; (8)
function sendData() { (2)
fetch(MY_FIREBASE_ID + '.firebasedatabase.app/posts.json', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
id: new Date().toISOString(),
title: titleInput.value,
location: locationInput.value,
image: MY_TEST_IMAGE_URL
})
})
.then(function(res) {
console.log('Sent data', res);
updateUI();
});
}
form.addEventListener('submit', function(event) { (6)
event.preventDefault();
if (titleInput.value.trim() === '' || locationInput.value.trim() === '') {
alert('Please enter valid data!');
return;
}
closeCreatePostModal();
if ('serviceWorker' in navigator && 'SyncManager' in window) {
navigator.serviceWorker.ready (3)
.then(function(sw) {
var post = {
id: new Date().toISOString(),
title: titleInput.value,
location: locationInput.value
};
writeData('sync-posts', post)
.then(function () {
return sw.sync.register('sync-new-posts') (4)
})
.then(function () {
var snackbarContainer = document.querySelector("#confirmation-toast"); (5)
var data = {message: "Your Post was saved for syncing!"};
snackbarContainer.MaterialSnackbar.showSnackbar(data);
}).catch(function (err) {
console.log(err);
});
});
} else {
sendData(); (1)
}
});
1 | отправляем данные если браузер не поддерживает background sync |
2 | отправка данных |
3 | background sync |
4 | регистрируем background sync event tag |
5 | показать toast что в оффлайне мы сохранили сообщение в idb |
6 | background sync происходит по сабмиту формы |
7 | id of my firebase |
8 | url of my test image |
sw.js
var MY_FIREBASE_ID = '...'; (1)
var MY_TEST_IMAGE_URL = '...'; (3)
self.addEventListener('sync', function(event) {
console.log('[Service Worker] Background syncing', event);
if (event.tag === 'sync-new-posts') {
console.log('[Service Worker] Syncing new Post');
event.waitUntil(
readAllData('sync-posts')
.then(function(data) {
for (var dt of data) {
fetch(MY_FIREBASE_ID + '.firebasedatabase.app/posts.json', { (4)
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
id: dt.id,
title: dt.title,
location: dt.location,
image: MY_TEST_IMAGE_URL
})
})
.then(function(res) {
console.log('Sent data', res);
if (res.ok) {
deleteItemFromData('sync-posts', dt.id); (2)
}
})
.catch(function (err) {
console.log('Error while sending data', err);
});
}
})
);
}
});
1 | id of my firebase |
2 | bug here - async call cannot work with loop variable |
3 | url of my test image |
4 | когда мы заменили этот url на функцию firebase storePostData возникла проблема CORS |
functions
const functions = require("firebase-functions");
var admin = require('firebase-admin');
var cors = require('cors')({ origin: true });
var serviceAccount = require("./pwagram-fb-key.json");
var MY_DB_URL = '...'; (1)
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: MY_DB_URL
});
exports.storePostData = functions.https.onRequest((request, response) => {
cors(request, response, function () {
var reqBody = request.body;
functions.logger.info('-- reqBody:', reqBody, '.'); (2)
var newPost = {
id: reqBody.id,
title: reqBody.title,
location: reqBody.location,
image: reqBody.image
};
functions.logger.info('-- newPost:', newPost, '.');
admin.database().ref('posts').push(newPost)
.then(function () {
response.status(201).json({ message: 'Data stored', id: request.body.id });
})
.catch(function (err) {
response.status(500).json({ error: err });
});
});
// response.send("Hello from Firebase!");
});
1 | url of my realtime database |
2 | последний элемент лога пусть будет просто строкой, иначе он может пропасть из лога :-( |
10. Web Push Notifications
Useful Resources
More about Web Push by Google: |
|
More about VAPID: |
https://blog.mozilla.org/services/2016/04/04/using-vapid-with-webpush/ |
More about VAPID by Google: |
|
The web-push npm Package: |
|
More about showNotification (display Notifications from Service Workers): |
https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification |
The Notification API: |
|
The Push API: |
app.js : askForNotificationPermission
function displayConfirmNotification() { (5)
var options = {
body: "You successfully subscribed to our Notification service!"
};
new Notification('Successfully subscribed!', options);
}
function askForNotificationPermission() {
Notification.requestPermission(function(result) { (4)
console.log('User Choice', result);
if (result !== 'granted') {
console.log('No notification permission granted!');
} else {
//displayConfirmNotification(); (6)
configurePushSub() (7)
}
});
}
if ('Notification' in window) { (1)
for (var i = 0; i < enableNotificationsButtons.length; i++) {
enableNotificationsButtons[i].style.display = 'inline-block'; (2)
enableNotificationsButtons[i].addEventListener('click', askForNotificationPermission); (3)
}
}
1 | Can I use Web Push Notifications? |
2 | Сделать видимой кнопку Enable Notifications |
3 | Повесить обработчик на кнопку |
4 | Запросить разрешение на отправку пуш-нотификаций |
5 | Показать нотификацию, первая версия, без параметров |
6 | Изначально мы просто показывали нотификацию |
7 | А теперь подписываемся на нотификацию от сервера |
app.js : displayConfirmNotification
function displayConfirmNotification() { (4)
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then(function(swreg) {
var options = {
body: "You successfully subscribed to our Notification service!",
icon: "/src/images/icons/app-icon-96x96.png",
image: "/src/images/sf-boat.jpg",
dir: "ltr",
lang: "en-US", // BCP 47
vibrate: [100, 50, 200],
badge: "/src/images/icons/app-icon-96x96.png",
tag: "confirm-notification", (2)
renotify: true, (3)
actions: [
{ action: "confirm", title: "Okay", icon: "/src/images/icons/app-icon-96x96.png" },
{ action: "cancel", title: "Cancel", icon: "/src/images/icons/app-icon-96x96.png" }
]
};
swreg.showNotification('Successfully subscribed (from SW)!', options); (1)
});
}
}
1 | Display notification from service worker |
2 | Notifications can be stacked on some OSes |
3 | Additional notifications should also produce a sound |
4 | Показать нотификацию, вторая версия, с параметрами |
sw.js : notificationclick
self.addEventListener('notificationclick', function(event) { (1)
var notification = event.notification;
var action = event.action;
console.log(notification);
if (action === 'confirm') {
console.log('Confirm was chosen');
} else {
console.log(action);
}
notification.close(); (2)
});
1 | Обработчик кнопки на нотификации. В этом случае возникает событие notificationclick |
2 | Закрываем нотификацию |
sw.js : notificationclose
self.addEventListener('notificationclose', function(event) { (1)
console.log('Notification was closed', event);
});
1 | событие notificationclose возникает при закрытии нотификации |
app.js : configurePushSub
function configurePushSub() { (1)
if (!('serviceWorker' in navigator)) {
return;
}
var reg;
navigator.serviceWorker.ready
.then(function (swreg) {
reg = swreg;
return swreg.pushManager.getSubscription(); (2)
})
.then(function (sub) {
if (sub === null) {
// Create a new subscription
var vapidPublicKey = "..."; (3)
var convertedVapidPublicKey = urlBase64ToUint8Array(vapidPublicKey); (4)
return reg.pushManager.subscribe({ (5)
userVisibleOnly: true,
applicationServerKey: convertedVapidPublicKey
});
} else {
// We have a subscription
}
})
.then(function (newSub) { (6)
return fetch(MY_FIREBASE_ID + ".firebasedatabase.app/subscriptions.json", { (8)
method: 'POST',
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify(newSub)
});
})
.then(function (res) { (7)
if (res.ok) {
displayConfirmNotification();
}
})
.catch(function(err) {
console.log(err);
});
}
1 | Подписываемся на нотификацию от сервера |
2 | Получаем подписку из pushManager |
3 | В папке functions мы выполнили npm install web-push а затем npm run web-push generate-vapid-keys В итоге получили Public Key и Private Key |
4 | Конвертировать ключ в Uint8Array |
5 | Получить подписку у браузера |
6 | Сохранить подписку в базе |
7 | Показать нотификацию, если подписка успешно сохранена |
8 | id of my firebase |
utility.js : urlBase64ToUint8Array
function urlBase64ToUint8Array(base64String) { (1)
var padding = '='.repeat((4 - base64String.length % 4) % 4);
var base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
var rawData = window.atob(base64);
var outputArray = new Uint8Array(rawData.length);
for (var i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
1 | Конвертация строки Base64 в Uint8Array |
sw.js : push
self.addEventListener('push', function(event) { (1)
console.log('Push Notification received', event);
var data = {title: "New!", content:"Something new happened!"}; (2)
if (event.data) {
data = JSON.parse(event.data.text()); (3)
}
var options = {
body: data.content,
icon: '/src/images/icons/app-icon-96x96.png',
badge: '/src/images/icons/app-icon-96x96.png'
};
event.waitUntil(
self.registration.showNotification(data.title, options) (4)
);
});
1 | Получаем пуш-нотификацию |
2 | Дефолтное сообщение |
3 | Сообщение с сервера |
4 | Показать нотификацию на устройстве |
sw.js : notificationclick
self.addEventListener('notificationclick', function(event) {
var notification = event.notification;
var action = event.action;
console.log(notification);
if (action === 'confirm') {
console.log('Confirm was chosen');
notification.close();
} else {
console.log(action);
event.waitUntil(
clients.matchAll() (3)
.then(function (clis) {
var client = clis.find(function (c) {
return c.visibilityState === 'visible'
});
if (client !== undefined) {
client.navigate('http://localhost:8080'); (1)
client.focus();
} else {
clients.openWindow('http://localhost:8080'); (2)
}
notification.close();
})
);
}
});
1 | Клик по пуш-нотификации может перейти на указанную страницу… |
2 | …либо открыть новое окно браузера |
3 | Сервис-воркеру доступен список связанных с ним табов в браузере, и мы можем выбрать текущий активный |
sw.js : push
self.addEventListener('notificationclick', function(event) {
var notification = event.notification;
var action = event.action;
console.log(notification);
if (action === 'confirm') {
console.log('Confirm was chosen');
notification.close();
} else {
console.log(action);
event.waitUntil(
clients.matchAll()
.then(function (clis) {
var client = clis.find(function (c) {
return c.visibilityState === 'visible'
});
if (client !== undefined) {
client.navigate(notification.data.url); (2)
client.focus();
} else {
clients.openWindow(notification.data.url);
}
notification.close();
})
);
}
});
self.addEventListener('push', function(event) {
console.log('Push Notification received', event);
var data = {title: "New!", content:"Something new happened!", openUrl: "/"};
if (event.data) {
data = JSON.parse(event.data.text());
}
var options = {
body: data.content,
icon: '/src/images/icons/app-icon-96x96.png',
badge: '/src/images/icons/app-icon-96x96.png',
data: {
url: data.openUrl (1)
}
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
1 | Как вариант, мы можем передать имя страницы, на которую нужно перейти по пуш-нотификации… |
2 | …и перейти на указанную страницу по клику на нотификации |
11. Native Device Features
Useful Resources
More about the Media Stream API on MDN: |
https://developer.mozilla.org/en-US/docs/Web/API/Media_Capture_and_Streams_API/Constraints |
More about |
https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia |
How to use geolocation: |
https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API |
feed.js : initializeMedia
function initializeMedia() {
if (!('mediaDevices' in navigator)) {
navigator.mediaDevices = {};
}
if (!('getUserMedia' in navigator.mediaDevices)) {
navigator.mediaDevices.getUserMedia = function (constraints) {
var getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; (1)
if (!getUserMedia) {
return Promise.reject(new Error('getUserMedia is not implemented!'));
}
return new Promise(function (resolve, reject) {
getUserMedia.call(navigator, constraints, resolve, reject); (2)
});
};
}
navigator.mediaDevices.getUserMedia({video: true})
.then(function(stream) {
videoPlayer.srcObject = stream; (3)
videoPlayer.style.display = 'block';
})
.catch(function (err) {
imagePickerArea.style.display = 'block';
});
}
1 | Смотрим кастомную имплементацию из Safari или Firefox |
2 | Возвращаем Promise , который резолвит нашу функцию в контексте navigator |
3 | If we can use video, then send the stream to videoPlayer |
feed.js : captureButton
captureButton.addEventListener('click', function(event) { (1)
canvasElement.style.display = 'block'; (2)
videoPlayer.style.display = 'none';
captureButton.style.display = 'none';
var context = canvasElement.getContext('2d');
context.drawImage(videoPlayer, 0, 0, canvas.width, videoPlayer.videoHeight / (videoPlayer.videoWidth / canvas.width)); (3)
videoPlayer.srcObject.getVideoTracks().forEach(function(track) {
track.stop(); (4)
});
picture = dataURItoBlob(canvasElement.toDataURL()); (5)
});
1 | Нажатие на кнопку Capture |
2 | Canvas становится видимым, а videoPlayer и captureButton прячутся |
3 | Выводим на canvas изображение видеоплейера. |
4 | Останавливаем все треки видеоплейера. |
5 | Копируем изображение из видеоплейера в blob. |
utility.js : dataURItoBlob
function dataURItoBlob(dataURI) {
var byteString = atob(dataURI.split(',')[1]);
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]
var ab = new ArrayBuffer(byteString.length);
var ia = new Uint8Array(ab);
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
var blob = new Blob([ab], {type: mimeString}); (1)
return blob;
}
1 | Преобразование строки данных sв blob. |
sw.js : sync
self.addEventListener('sync', function(event) {
console.log('[Service Worker] Background syncing', event);
if (event.tag === 'sync-new-posts') {
console.log('[Service Worker] Syncing new Posts');
event.waitUntil(
readAllData('sync-posts')
.then(function(data) {
for (var dt of data) {
var postData = new FormData(); (1)
postData.append("id", dt.id);
postData.append("title", dt.title);
postData.append("location", dt.location);
postData.append("file", dt.picture, dt.id + ".png");
fetch(MY_CLOUD_FUNC + '.cloudfunctions.net/storePostData', { (2)
method: 'POST',
body: postData
})
.then(function(res) {
console.log('Sent data', res);
if (res.ok) {
res.json()
.then(function(resData) {
deleteItemFromData('sync-posts', resData.id);
});
}
})
.catch(function(err) {
console.log('Error while sending data', err);
});
}
})
);
}
});
1 | JSON мы заменяем на FormData чтобы иметь возможность передавать файл изображения |
2 | MY_CLOUD_FUNC : server id for my Firebase cloud functions |
functions
var functions = require("firebase-functions");
var admin = require("firebase-admin");
var cors = require("cors")({ origin: true });
var webpush = require("web-push");
var fs = require("fs");
var UUID = require("uuid-v4"); (3)
var os = require("os");
var Busboy = require("busboy"); (2)
var path = require('path');
// // Create and Deploy Your First Cloud Functions
// // https://firebase.google.com/docs/functions/write-firebase-functions
//
var serviceAccount = require("./pwagram-fb-key.json");
var gcconfig = {
projectId: "YOUR_PROJECT_ID", (5)
keyFilename: "pwagram-fb-key.json"
};
var gcs = require("@google-cloud/storage")(gcconfig); (1)
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://YOUR_PROJECT_ID.firebaseio.com/" (8)
});
exports.storePostData = functions.https.onRequest(function(request, response) {
cors(request, response, function() {
var uuid = UUID();
const busboy = Busboy({ headers: request.headers });
// These objects will store the values (file + fields) extracted from busboy
let upload;
const fields = {};
// This callback will be invoked for each file uploaded
busboy.on("file", (fieldname, file, info) => {
const { filename, encoding, mimetype } = info;
console.log(
`File [${fieldname}] filename: ${filename}, encoding: ${encoding}, mimetype: ${mimetype}`
);
const filepath = path.join(os.tmpdir(), filename);
upload = { file: filepath, type: mimetype };
file.pipe(fs.createWriteStream(filepath));
});
// This will invoked on every field detected
busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) {
fields[fieldname] = val;
});
// This callback will be invoked after all uploaded files are saved.
busboy.on("close", () => {
var bucket = gcs.bucket("YOUR_PROJECT_ID.appspot.com"); (5)
bucket.upload( (7)
upload.file,
{
uploadType: "media",
metadata: {
metadata: {
contentType: upload.type,
firebaseStorageDownloadTokens: uuid
}
}
},
function(err, uploadedFile) {
if (!err) {
admin
.database()
.ref("posts")
.push({
id: fields.id,
title: fields.title,
location: fields.location,
image: (6)
"https://firebasestorage.googleapis.com/v0/b/" +
bucket.name +
"/o/" +
encodeURIComponent(uploadedFile.name) +
"?alt=media&token=" +
uuid
})
.then(function() {
webpush.setVapidDetails(
"mailto:" + MY_EMAIL, (4)
MY_VAPID_PUBLIC_KEY,
MY_VAPID_PRIVATE_KEY
);
return admin
.database()
.ref("subscriptions")
.once("value");
})
.then(function(subscriptions) {
subscriptions.forEach(function(sub) { (9)
var pushConfig = {
endpoint: sub.val().endpoint,
keys: {
auth: sub.val().keys.auth,
p256dh: sub.val().keys.p256dh
}
};
webpush
.sendNotification(
pushConfig,
JSON.stringify({
title: "New Post",
content: "New Post added!",
openUrl: "/help"
})
)
.catch(function(err) {
console.log(err);
});
});
response
.status(201)
.json({ message: "Data stored", id: fields.id });
})
.catch(function(err) {
response.status(500).json({ error: err });
});
} else {
console.log(err);
}
}
);
});
// The raw bytes of the upload will be in request.rawBody. Send it to busboy, and get
// a callback when it's finished.
busboy.end(request.rawBody);
});
});
1 | npm install --save @google-cloud/storage@1.7 https://www.npmjs.com/package/@google-cloud/storage |
2 | npm install --save busboy https://www.npmjs.com/package/busboy |
3 | npm install --save uuid-v4 |
4 | Provide MY_EMAIL , MY_VAPID_PUBLIC_KEY , MY_VAPID_PRIVATE_KEY |
5 | My Firebase project id here |
6 | Firebase Storage URL |
7 | Upload file to Firebase Storage |
8 | For me databaseURL is MY_PROJECT_ID + ".firebasedatabase.app" |
9 | Отправить нотификацию в каждую из найденных подписок |
feed.js : imagePicker
imagePicker.addEventListener('change', function(event) {
picture = event.target.files[0]; (1)
});
1 | Загруженный файл |
feed.js : locationBtn
locationBtn.addEventListener('click', function(event) {
if (!('geolocation' in navigator)) {
return;
}
locationBtn.style.display = 'none';
locationLoader.style.display = 'block';
navigator.geolocation.getCurrentPosition(function (position) { (1)
locationBtn.style.display = 'inline';
locationLoader.style.display = 'none';
fetchedLocation = { lat: position.coords.latitude, lng: 0 };
locationInput.value = 'In Munich';
document.querySelector("#manual-location").classList.add('is-focused');
}, function (err) {
console.log(err);
locationBtn.style.display = 'inline';
locationLoader.style.display = 'none';
alert("Couldn't get location, please enter manually!");
fetchedLocation = { lat: null, lng: null };
}, { timeout: 7000 })
});
1 | Получаем координаты |
12. Service Worker Management with Workbox
Useful Resources
Official webpage/ docs: |
|
Workbox Github page: |
workbox-cli-config.js
module.exports = { (1)
"globDirectory": "public/",
"globPatterns": [
"**/*.{html,ico,json,css,js}",
"src/images/*.{png,jpg}" (2)
],
"swSrc": "public/sw-base.js", (3)
"swDest": "public/service-worker.js",
"globIgnores": [
"../workbox-cli-config.js",
"help/**"
]
};
1 | Сгенерировано командой workbox generate:sw в секции scripts для workbox ver.2npm install --save-dev workbox-cli@^2 |
2 | Кэшировать только изображения в папке images |
3 | Использовать шаблон сервис-воркера с командой workbox inject:manifest в секции scripts |
sw-base.js
importScripts('workbox-sw.prod.v2.1.3.js');
importScripts('/src/js/idb.js');
importScripts('/src/js/utility.js'); (4)
const workboxSW = new self.WorkboxSW();
workboxSW.router.registerRoute(/.*(?:googleapis|gstatic)\.com.*$/, (1)
workboxSW.strategies.staleWhileRevalidate({ (2)
cacheName: "google-fonts",
cacheExpiration: {
maxEntries: 3,
maxAgeSeconds: 60 * 60 * 24 * 30 (5)
}
}));
workboxSW.router.registerRoute("https://cdnjs.cloudflare.com/ajax/libs/material-design-lite/1.3.0/material.indigo-pink.min.css", (6)
workboxSW.strategies.staleWhileRevalidate({
cacheName: "material-css"
}));
workboxSW.router.registerRoute(/.*(?:firebasestorage\.googleapis)\.com.*$/, (8)
workboxSW.strategies.staleWhileRevalidate({
cacheName: "post-images"
}));
workboxSW.router.registerRoute(MY_FIREBASE_ID + '.firebasedatabase.app/posts.json', (7)
function(args) {
return fetch(args.event.request)
.then(function (res) {
var clonedRes = res.clone();
clearAllData('posts')
.then(function () {
return clonedRes.json();
})
.then(function (data) {
for (var key in data) {
writeData('posts', data[key])
}
});
return res;
})
});
workboxSW.precache([]); (3)
1 | Для того, чтобы закэшировать шрифты и иконки от Google, нужно выбрать урлы, содержащие googleapi или gstatic |
2 | Применить к ним стратегию сначала из кэша, потом из сети |
3 | Шаблон для подстановки списка ресурсов |
4 | Import our scripts as well |
5 | Cache expiration |
6 | Помещаем material css в отдельный кэш |
7 | Обработка background sync |
8 | Сюда идут наши фотки |
workboxSW.router.registerRoute(function (routeData) {
return (routeData.event.request.headers.get('accept').includes('text/html')); (1)
}, function (args) {
return caches.match(args.event.request)
.then(function (response) {
if (response) {
return response; (2)
} else {
return fetch(args.event.request) (3)
.then(function (res) {
return caches.open('dynamic')
.then(function (cache) {
cache.put(args.event.request.url, res.clone());
return res;
})
})
.catch(function (err) {
return caches.match('/offline.html') (4)
.then(function (res) {
return res;
});
});
}
})
});
1 | В качестве критерия используем функцию. Проверяем, что кэшируются все запросы, кторые должны возвращать HTML |
2 | Если что-то найдено в кэше, возвращаем |
3 | Иначе пробуем получить страницу по сети |
4 | А если нет доступа к сети и страницы нет в кэше, выбрасываем сообщение об ошибке |