I'm developing an audio-based PWA and, since I'm not familiar with this technology, I have a couple of doubts regading the cache management and invalidation in the service worker.
The application need to work offline, that I covered using a SW precache.
My only doubt is the amount of data: in the experience there are 5 use case scenarios. Each scenario has ~30MB of audio content, that means around 150MB + all images, js and css in total to precache.
I know that this exceeds the limit of some browsers (se this question and this article)
and in general you must be careful with the storage size, that also depends on the user's device available space on disk.
So that's what I thought: since between one scenario and another, the users will stop by a desk with WiFi connection, my idea is to empty the cache runtime after an user's action (like pressing a button), and replace it with thw new content.
This way I would store only one scenario at a time, that means ~35MB, a reasonable size.
Do you think that's a good approach?
What's the best way to implement this?
Here's my current code:
service-worker.js
const PRECACHE = 'precache-test-v1';
// A list of local resources we always want to be cached.
const PRECACHE_URLS = [
'/',
'/audio/scenario1.mp3',
'/audio/scenario2.mp3',
'/audio/scenario3.mp3',
'/audio/scenario4.mp3',
'/audio/scenario5.mp3',
'/css/style.css',
'/js/bundle.js',
'/img/favicon.png',
'/img/logo.png',
'/img/image1.png',
'/img/image2.png',
'/img/image3.png',
'/img/image4.png',
'/img/image5.png',
];
// never cache these resources
const TO_SKIP = [/* empty for now */];
// The install handler takes care of precaching the resources we always need.
self.addEventListener('install', event => {
const now = new Date();
console.log(`PWA Service Worker installing - :: ${now} ::`);
event.waitUntil(caches.open(PRECACHE).then(cache => {
return cache.addAll(PRECACHE_URLS).then(() => {
self.skipWaiting();
});
}));
});
// The activate handler takes care of cleaning up old caches.
self.addEventListener('activate', event => {
const now = new Date();
console.log(`PWA Service Worker activating - :: ${now} ::`);
const currentCaches = [PRECACHE];
event.waitUntil(
caches.keys().then(cacheNames => {
return cacheNames.filter(cacheName => !currentCaches.includes(cacheName));
}).then(cachesToDelete => {
return Promise.all(cachesToDelete.map(cacheToDelete => {
return caches.delete(cacheToDelete);
}));
}).then(() => self.clients.claim())
);
});
// The fetch handler serves responses for same-origin resources from a cache.
self.addEventListener('fetch', event => {
// Skip cross-origin requests, like those for Google Analytics and the other provided urls.
if (event.request.url.startsWith(self.location.origin) && TO_SKIP.every(url => !event.request.url.includes(url))) {
event.respondWith(
caches.match(event.request).then(resp => {
return resp || fetch(event.request).then(response => {
return caches.open(PRECACHE).then(cache => {
cache.put(event.request, response.clone());
return response;
});
});
})
);
}
});
index.js
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(registration => {
console.log('Registration successful, scope is:', registration.scope);
}).catch(error => {
console.log('Service worker registration failed, error:', error);
});
}
Thank you for your time,
Francesco
Hmm.. instead of precaching 5 videos, you could provide an button Save for offline so that the user can save only that videos that he wants to see later offline:
let videoUrl = url to that video:
button.addEventListener('click', function(event) {
event.preventDefault();
caches.open("myVideoCache").then(function(cache) {
fetch(videoUrl)
.then(function(video) {
cache.add(video);
});
});
});
Do delete 1 entry you need to open your cache and delete it. Pass the path that you stored.
caches.open('myVideoCache').then(function(cache) {
cache.delete('/path/to/audio.mp4').then(function(response) {
console.log("entry deleted");
});
})
More details you can find here: https://developers.google.com/web/ilt/pwa/caching-files-with-service-worker
Related
I have a simple forum that I want to make work offline. I have a dynamic and static cahce. The static cache fills on the install event and the dynamic as you go allong and look at posts.
The problem is that the pages it caches include the header where you have a link to the profile if you are logged in and link to registration page if you are not logged in.
After logging in it still shows the registration link instead of the profile link. The way to fix it would be to refresh the cache?
Is there a way to do this or is there some other fix for this type of issue(besides network first approach)?
I am relativly new to PWAs and I can't find any useful tips.
My service worker looks like this:
const staticCacheName = "ScroocCacheV1";
const dynamicCacheName = "ScroocDynamicCacheV1";
const assets = [
'/',
'/css/main_styles.css',
'/js/ui.js',
'/about',
'/policies',
'/register',
'/createTopic',
'/stats',
'/proposals',
];
const limitCacheSize = (name, size) => {
caches.open(name).then(cache => {
cache.keys().then(keys => {
if(keys.length > size) {
cache.delete(keys[0]).then(limitCacheSize(name, size));
}
});
});
}
const dynamicCacheLimit = 18;
// Install service worker
self.addEventListener('install', evt => {
evt.waitUntil(
caches.open(staticCacheName).then(cache => {
cache.addAll(assets);
})
);
});
// Activate event
self.addEventListener('activate', evt => {
evt.waitUntil(
caches.keys().then(keys => {
keys.map((key => {
if (key !== staticCacheName && key !== dynamicCacheName) {
return caches.delete(key); //Deleting the old cache (cache v1)
}
}))
})
)
});
// Intercept fetch
self.addEventListener('fetch', evt => {
evt.respondWith(
fetch(evt.request).then(fetchRes => {
return caches.open(dynamicCacheName).then(cache => {
return caches.match(evt.request).then(function(result) {
if (result) {
return result;
} else {
cache.put(evt.request.url, fetchRes.clone());
limitCacheSize(dynamicCacheName, dynamicCacheLimit);
return fetchRes;
}
});
});
}).catch(function() {
return caches.match(evt.request).catch((error) => {
console.log(error)
return caches.match('/img/fallbackImage.png');
});
})
);
});
This worked for me!
Before you could attempt to empty the cache, the service worker must first be successfully installed. So for the record, your sw.js file should begin with the usual
self.addEventListener("install", ...etc
Now this is where we get to cleaning up. Create a variable to store the name of the cache you wish to purge/update (makes targeting different caches easier)
var TargetCache= 'NameOfCacheToClean';
Next, add an EventListener that triggers each time the service worker is activated (the activate-event occurs on page reload/refresh)
self.addEventListener('activate', event =>
{
const currentCaches = [TargetCache];
event.waitUntil
(
caches.keys()
.then(cacheNames => {return cacheNames.filter(cacheName => !currentCaches.includes(cacheName));})
.then(cachesToDelete => {return Promise.all(cachesToDelete.map(cacheToDelete => {return caches.delete(cacheToDelete);})); })
.then(() => self.clients.claim())
);
});
Just in-case, i normally add the event listener that intercepts the outgoing fetch-requests after the code that clears the old cache.
self.addEventListener('fetch', function(event) {...etc
The way to fix it would be to refresh the cache?
That is correct, assuming you cached the path /login, the service worker will always display what was cached under that path, based on your code.
Is there a way to do this or is there some other fix for this type of issue(besides network first approach)?
It's not really something to "fix", what you described is somewhat expected behaviour.
There are several ways around this tho, network first is just one:
use a message to update cache on login
use different urls or url parts like query to skip cache when user is logged in
hide the UI that you don't need on the client depending on user state
Probably many more.
I created a new React Project with Service Worker already written. I am making 3 different API requests in App. But only the results from the Pokémon API gets displayed when I disconnect from the internet and reload. The other 2 aren't getting cached. Here is the code in App. Really simple.
function App() {
const [pokemon, setPokemon] = useState([])
const [word, setWord] = useState("")
const [color, setColor] = useState("")
useEffect(() => {
getAPI()
}, [])
const getAPI = async () => {
const response = await fetch("https://pokeapi.co/api/v2/pokemon?limit=100")
const data = await response.json()
setPokemon(data.results)
const response1 = await fetch("https://random-words-api.vercel.app/word")
const data1 = await response1.json()
setWord(data1[0].word)
const response2 = await fetch("https://random-data-api.com/api/color/random_color")
const data2 = await response2.json()
setColor(data2.color_name)
}
return (
<div className="App">
<p>Random Word: {word}</p>
<p>Random Color: {color}</p>
{pokemon.map((p,i) => <p onClick={(e)=> console.log(e.target)} key={i}>{ `${i} - ${p.name} - ${p.url}`}</p>)}
</div>
);
}
export default App;
This is the prebuilt service worker file.
/* eslint-disable no-restricted-globals */
// This service worker can be customized!
// See https://developers.google.com/web/tools/workbox/modules
// for the list of available Workbox modules, or add any other
// code you'd like.
// You can also remove this file if you'd prefer not to use a
// service worker, and the Workbox build step will be skipped.
import { clientsClaim } from 'workbox-core';
import { ExpirationPlugin } from 'workbox-expiration';
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate } from 'workbox-strategies';
clientsClaim();
// Precache all of the assets generated by your build process.
// Their URLs are injected into the manifest variable below.
// This variable must be present somewhere in your service worker file,
// even if you decide not to use precaching. See https://cra.link/PWA
precacheAndRoute(self.__WB_MANIFEST);
// Set up App Shell-style routing, so that all navigation requests
// are fulfilled with your index.html shell. Learn more at
// https://developers.google.com/web/fundamentals/architecture/app-shell
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
registerRoute(
// Return false to exempt requests from being fulfilled by index.html.
({ request, url }) => {
// If this isn't a navigation, skip.
if (request.mode !== 'navigate') {
return false;
} // If this is a URL that starts with /_, skip.
if (url.pathname.startsWith('/_')) {
return false;
} // If this looks like a URL for a resource, because it contains // a file extension, skip.
if (url.pathname.match(fileExtensionRegexp)) {
return false;
} // Return true to signal that we want to use the handler.
return true;
},
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
);
// An example runtime caching route for requests that aren't handled by the
// precache, in this case same-origin .png requests like those from in public/
registerRoute(
// Add in any other file extensions or routing criteria as needed.
({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'), // Customize this strategy as needed, e.g., by changing to CacheFirst.
new StaleWhileRevalidate({
cacheName: 'images',
plugins: [
// Ensure that once this runtime cache reaches a maximum size the
// least-recently used images are removed.
new ExpirationPlugin({ maxEntries: 50 }),
],
})
);
// This allows the web app to trigger skipWaiting via
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
self.addEventListener('message', (event) => {
console.error("Message")
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
self.addEventListener('fetch', function (event) {
event.respondWith(
fetch(event.request).catch(function () {
return caches.match(event.request);
}),
);
});
// Any other custom service worker logic can go here.
Whats the cause of this behaviour?
EDIT: It is so smart it only caches duplicate responses. But i would like it to cache the last value anyway. How?
Ok, i only see you have a cache match, but i don't see there's code to validate the cache, ex. when there's no match.
self.addEventListener('fetch', function(event) {
console.log('Handling fetch event for', event.request.url);
event.respondWith(
caches.open(CURRENT_CACHES.font).then(function(cache) {
return cache.match(event.request).then(function(response) {
if (response) {
// If there is an entry in the cache for event.request, then response will be defined
// and we can just return it. Note that in this example, only font resources are cached.
console.log(' Found response in cache:', response);
return response;
}
// Otherwise, if there is no entry in the cache for event.request, response will be
// undefined, and we need to fetch() the resource.
console.log(' No response for %s found in cache. About to fetch ' +
'from network...', event.request.url);
// We call .clone() on the request since we might use it in a call to cache.put() later on.
// Both fetch() and cache.put() "consume" the request, so we need to make a copy.
// (see https://developer.mozilla.org/en-US/docs/Web/API/Request/clone)
return fetch(event.request.clone()).then(function(response) {
console.log(' Response for %s from network is: %O',
event.request.url, response);
if (response.status < 400 &&
response.headers.has('content-type') &&
response.headers.get('content-type').match(/^font\//i)) {
// This avoids caching responses that we know are errors (i.e. HTTP status code of 4xx or 5xx).
// We also only want to cache responses that correspond to fonts,
// i.e. have a Content-Type response header that starts with "font/".
// Note that for opaque filtered responses (https://fetch.spec.whatwg.org/#concept-filtered-response-opaque)
// we can't access to the response headers, so this check will always fail and the font won't be cached.
// All of the Google Web Fonts are served off of a domain that supports CORS, so that isn't an issue here.
// It is something to keep in mind if you're attempting to cache other resources from a cross-origin
// domain that doesn't support CORS, though!
// We call .clone() on the response to save a copy of it to the cache. By doing so, we get to keep
// the original response object which we will return back to the controlled page.
// (see https://developer.mozilla.org/en-US/docs/Web/API/Request/clone)
console.log(' Caching the response to', event.request.url);
cache.put(event.request, response.clone());
} else {
console.log(' Not caching the response to', event.request.url);
}
// Return the original response object, which will be used to fulfill the resource request.
return response;
});
}).catch(function(error) {
// This catch() will handle exceptions that arise from the match() or fetch() operations.
// Note that a HTTP error response (e.g. 404) will NOT trigger an exception.
// It will return a normal response object that has the appropriate error code set.
console.error(' Error in fetch handler:', error);
throw error;
});
})
);
This is from the documentation, https://developer.mozilla.org/en-US/docs/Web/API/Cache
Every time that I edit my PWA App source, Like html or js files, I have to manually delete browser cached files and history in order to load new data that i uploaded.
my serviceWorker routine try to cache my main url html only but it seems all files being cached automatically.
here is my serviceWorker file:
var cacheName = 'app-pwa';
var filesToCache = [
'/',
'/index.html'
];
/* Start the service worker and cache all of the app's content */
self.addEventListener('install', function(e) {
e.waitUntil(
caches.open(cacheName).then(function(cache) {
return cache.addAll(filesToCache);
})
);
});
/* Serve cached content when offline */
self.addEventListener('fetch', function(e) {
e.respondWith(
caches.match(e.request).then(function(response) {
return response || fetch(e.request);
})
);
});
So anyone can help me how to refresh cached files in browser?
thank you
well I find the way myself and will share here for anyone who may need it.
the way is to define versions in the end of cache file name:
var cacheName = 'app-pwa-v1';
and increase version for any update. and define another service worker to delete previous cached files.
self.addEventListener('activate', (e) => {
e.waitUntil(caches.keys().then((keyList) => {
Promise.all(keyList.map((key) => {
if (key === cacheName) { return; }
caches.delete(key);
}))
})));
});
this will delete old cached files after second app startup.
I can't get my service worker to work offline. No matter what tutorial I use.
I registered the service worker in my index.html file like:
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/serviceworker.js')
.then((reg) => console.log('success: ', reg.scope))
.catch((err) => console.log('Failure: ', err))
})
}
</script>
The serviceworker.js looks like:
const CACHE_NAME = "version-1"
const urlsToCache = [ 'index.html' ]
const self = this
// Install Service Worker
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('Opened cache')
return cache.addAll(urlsToCache)
})
)
})
// Activate Service Worker
self.addEventListener('activate', (event) => {
const cacheWhitelist = []
cacheWhitelist.push(CACHE_NAME)
event.waitUntil(
caches.keys().then((cacheNames) => Promise.all(
cacheNames.map((cacheName) => {
if(!cacheWhitelist.includes(cacheName)) {
return caches.delete(cacheName)
}
})
))
)
})
I'm not sure what I've forgotten or what mistake I have. The serviceworker.js is right beside the index.html, manifest.json etc.
I sometimes get an error with "An unknown error occured when fetching the script".
Kind regards
Your service worker only contains code to create a cache and store the HTML file in that local cache. This cache is managed by you, the browser does not care about it when fetching web pages normally.
When your browser fetches that index.html web page, it does not know about that cache. So what you need to do is to intercept that fetch. For this, you need to register an event listener for the fetch event and respond with your cache. The browser will then use that file instead of sending a request to the server.
self.addEventListener('fetch', event => {
if (event.request.method != 'GET') return;
event.respondWith(async function() {
const cache = await caches.open(CACHE_NAME);
const cached = await cache.match(event.request);
// If no cached version, fall back to server fetch
return cached ? cached : fetch(event.request);
})
});
Hello there guys im new to this technology and i would like to ask for help for my code. What im trying to do is to cache assets files and have returned from service worker.
This is the code im using to register the service worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/serviceworker.js')
.then(function(reg) {
// registration worked
console.log('Registration succeeded. Scope is ' + reg.scope);
}).catch(function(error) {
// registration failed
console.log('Registration failed with ' + error);
});
}
And this is the code inside service worker
importScripts('/cache-poli.js');
var CACHE_VERSION = 'app-v2';
var CACHE_FILES = [
'/',
'/js/plugins/bootstrap/js/bootstrap.min.js',
'/js/plugins/bootstrap-select/bootstrap-select.min.js',
'/js/plugins/prettyphoto/js/jquery.prettyPhoto.js',
'/js/plugins/jquery.sticky.min.js',
'/js/plugins/jquery.easing.min.js',
'/js/plugins/animate/js/animate.js',
'/js/jquery.fancybox.js',
'/js/plugins/jquery/jquery-ui-1.11.1.min.js',
'/js/jquery.scrollbar.min.js',
'/js/plugins/owlcarousel2/owl.carousel.min.js',
'/js/plugins/elevateZoom/jquery.elevateZoom-3.0.8.min.js',
'/js/theme.js',
'/js/cmsfuncs.js',
'/js/theme-config.js',
'/js/jquery.mCustomScrollbar.concat.min.js',
'/js/plugins/jquery/jquery-2.1.4.min.js',
'/js/jquery.cookie.js',
'/js/plugins/bootstrap/css/bootstrap.min.css',
'/fonts/fontawesome/css/font-awesome.min.css',
'/fonts/webfont/css/simple-line-icons.css',
'/fonts/elegantfont/css/elegantfont.css',
'/js/plugins/bootstrap-select/bootstrap-select.min.css',
'/js/plugins/owlcarousel2/assets/owl.carousel.min.css',
'/js/plugins/prettyphoto/css/prettyPhoto.css',
'/js/plugins/animate/css/animate.css',
'/s/plugins/accordion/css/magicaccordion.css',
'/css/jquery.scrollbar.css',
'/css/megamenu.css',
'/css/theme.css',
'/css/slider/slide.css',
'/css/jquery.mCustomScrollbar.css',
'/css/responsive.css',
'/css/theme.css'
];
self.addEventListener('install', function (event) {
event.waitUntil(
caches.open(CACHE_VERSION)
.then(function (cache) {
console.log('Opened cache');
return cache.addAll(CACHE_FILES);
})
);
});
self.addEventListener('activate', function (event) {
event.waitUntil(
caches.keys().then(function(keys){
return Promise.all(keys.map(function(key, i){
if(key !== CACHE_VERSION){
return caches.delete(keys[i]);
}
}))
})
)
});
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.open(CACHE_VERSION).then(function(cache){
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
})
)
});
Im using google chrome dev tools to see the installation proccess everything is cached as it should and the service worker shows no errors but when im trying to access the website again it gives me an error.
This site can’t be reached
The webpage at domain.com might be temporarily down or it may have moved permanently to a new web address.
I too had the same error.
Actually, the problem is quite self-explanatory. What the browser is telling is that the path you are trying to reach is not reachable.
In your code, it looks like you have cached the root '/'. I assume you were facing this problem when you were trying to access some other path like '/somepath'.
Because you haven't cached those , you were getting this error.
So in your array if you also add :
var CACHE_FILES = ['/',
'/somepath', ...];
The error won't occur.
I used the exact same method and the error was gone.