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.
Related
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);
})
});
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
I want to create a PWA in which i am getting data from firebase. Once the page is loaded ADS( in form of objects ) are retrieved from firebase and then displayed. What i want is that once page is loaded if user has lost the connection or is offline, the cache stores all objects that are once loaded at time user was having connection and made them available for offline use i.e without connection to internet.
Link to Download JSON FILE of firebase data
and here is the code i am using in service worker
var cacheName = 'test me';
const filesToCache = [
'./',
'home.html',
'details.html',
'/css/main.css',
'/css/home.css',
'/images/nokia.png',
'/js/app.js',
'/js/main.js',
'/js/home.js'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v1')
.then(res => {
console.log('wait.........!')
return res.addAll(filesToCache);
})
);
console.log('installed');
});
self.addEventListener('activate', (event) => {
console.log('activated');
});
You can use the CacheAPI for that:
const url = '/myAPI/endpoint'
fetch(url).then((res) => {
return caches.open('v1').then((cache) => {
return cache.put(url, res)
})
})
To retrive it:
caches.open('v1').then((cache) => {
cache.match('/myAPI/endpoint').then((res) => {
//res is the Response Object
})
})
You can find a lot more here: https://developers.google.com/web/ilt/pwa/caching-files-with-service-worker