Cache all requests from an app without explicitly specifying urlsToCache. So I will cache stuff under fetch event.
To respond to requests from the cache.
Update the cache when fetch is success.
Initially,
this.addEventListener('fetch', function(event) {
var fetchReq = event.request.clone(),
cacheReq = event.request.clone();
event.respondWith(fetch(fetchReq).then(function(response) {
var resp = response.clone();
caches.open(CACHE_NAME).then(function(cache) {
req = event.request.clone();
cache.put(req, resp);
});
return response;
}).catch(function() {
return caches.match(cacheReq);
}));
});
The offline situations were handled perfectly well. But the problem here was with the slow connections. The user has to wait till fetch times out or throws an error to get the response from cache.
self.addEventListener('fetch', function(event) {
var cacheRequest = event.request.clone();
event.respondWith(caches.match(cacheRequest).then(function(response) {
if(response) return response;
var fetchRequest = event.request.clone();
return fetch(fetchRequest).then(function(response) {
var responseToCache = response.clone();
caches.open(cache_name).then(function(cache) {
var cacheSaveRequest = event.request.clone();
cache.put(cacheSaveRequest, responseToCache);
});
return response;
});
}));
});
With the cache taking precedence, the responses served were fine. But the problem here is that when the code updates. When /public/main.css served via sw is updated, on page reload only the cache is served, the updated content is not served.
I also tried modifying the cache_name to cache-v2 from cache-v1 (so that sw binary diff exists and sw is updated and that old cache can be cleared), and cleared cache-v1 on activate event. But it gave rise to new problems where two service workers were running at the same time under the same Registration ID. More on this is in this other SO question: How to stop older service workers?
Two service workers running at the same time are not technically a problem—it's working as designed. (See my answer to How to stop older service workers?) Make sure that you close other tabs that might have an older version of your service worker active.
You're running into the inevitable tradeoffs between the different cache vs. network scenarios here. If you haven't yet read through the offline cookbook, it's a great starting point when trying to decide which caching strategy works best for your specific resources.
Related
I have this service worker:
//IMPORT POLYFILL
importScripts('cache-polyfill.js');
//INSTALL
self.addEventListener('install', function(e) {
e.waitUntil(
caches.open('stock_item_balance_v1').then(function(cache) {
return cache.addAll([
'/app/offline_content/purchase/stock_items/stock_items_balance.php',
'/app/offline_content/purchase/stock_items/js/stock_items_balance.js'
]);
})
);
});
//FETCH (FETCH IS WHEN YOU CHECK FOR INTERNET)
self.addEventListener('fetch', function(event) {
//console.log(event.request.url);
event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
);
});
In "stock_items_balance.php" i fetch data from my DB. So in every 30 minutes i would like to update my cached pages and reload the window.
So first i have a script that checks for internet connection.
If true, i want to clean/update the cache and reload the page.
How can i do that?
//INTERVAL
setInterval(function(){
//CLEAN/UPDATE CACHED FILES
serviceworker.update(); // ???
//RELOAD PAGE
window.location.reload();
}, 180000);
(I think you have a larger question as to whether the approach you describe is actually going to give a good, predictable, offline-capable experience for your users, but I'm just going to focus on the actual technical question you asked.)
Messaging the service worker
First off, you should keep in mind that it's possible to have multiple tabs open for the same URL, and if you, you're going to end up with your update code potentially running multiple times. The code in this answer handles the "reload" step for you from inside of the service worker, after the asynchronous cache update has completed, by getting a list of all the active clients of the service worker and telling each to navigate to the current URL (which is effectively a reload).
// Additions to your service worker code:
self.addEventListener('message', (event) => {
// Optional: if you need to potentially send different
// messages, use a different identifier for each.
if (event.data === 'update') {
event.waitUntil((async () => {
// TODO: Move these URLs and cache names into constants.
const cache = await caches.open('stock_item_balance_v1');
await cache.addAll([
'/app/offline_content/purchase/stock_items/stock_items_balance.php',
'/app/offline_content/purchase/stock_items/js/stock_items_balance.js'
]);
const windowClients = await clients.matchAll();
for (const windowClient of windowClients) {
// Optional: check windowClient.url first and
// only call navigate() if it's the URL for one
// specific page.
windowClient.navigate(windowClient.url);
}
})());
}
});
// Additions to your window/page code:
setInterval(() => {
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage('update');
}
}, 180000);
What won't work
The Cache Storage API is available from both inside a service worker and inside of your page's window scope. Normally what I'd recommend that folks do is to open up the same cache from the window context, and call cache.add() to update the cached entry with the latest from the network. However, calling cache.add() from the window context will cause the network request to be intercepted by your fetch handler, and at that point, your response won't actually come from the network. By calling cache.add() from inside your service worker, you can guarantee that the resulting network request won't trigger your fetch handler.
I've implemented a service worker which caches all requests for offline usage, this works fine. But everytime I load a page there are two requests hitting my webserver (one from the service worker and one from the browser)!
How can I cache the request and only load the page once?
service-worker.js
self.addEventListener('install', function(event) {
//load error page which will show if user has no internet
var errorPage = new Request('/?p=error&offline');
event.waitUntil(pushToCache(errorPage));
});
//If any fetch fails, it will look for the request in the cache and serve it from there first
self.addEventListener('fetch', function(event) {
event.waitUntil(pushToCache(event.request));
event.respondWith(
fetch(event.request) //try loading from internet
.catch(function (error) {
return fetchFromCache(event.request);
}) //no internet connection try getting it from cache
);
});
function pushToCache(request){
if(request.method == "GET"){
return caches.open('stm-app').then(function (cache) {
return fetch(request).then(function (response) {
return cache.put(request, response);
});
});
}
};
function fetchFromCache(request) {
return caches.open('stm-app').then(function (cache) {
return cache.match(request).then(function (matching) {
if(!matching || matching.status == 404){
return fetchFromCache(new Request('/?p=error&offline')); //show page that user is offline
}else{
return matching;
}
});
});
}
sw-register.js
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js')
.then(function(registration) {
console.log('Registered:', registration);
})
.catch(function(error) {
console.log('Registration failed: ', error);
});
}
So here's what happens whenever you make a request:
The webpage sends a fetch request to the server,
the Service Worker intercepts the request on the 'fetch' event,
pushToCache() fires a fetch request to the server in order to cache the response,
then you respond to the event with a fetch request to the server which will return a Promise for a Response from the web server.
Yup, that makes sense, that thing just sent two requests two the server for every request the page originally made.
One thing you might want to consider is responding from the cache first and then going on the network to get the latest data. This way you will avoid delays in loading in the case of connection issues and it will speed up the loading time of the page even when the user is online.
Let's consider the following scenario: Either the user or the server are offline. Once you fire the request, it will have to time out before it goes to the catch part of the promise and get the cached response.
What you could do once you intercept the event is check the caches for a match and if you find anything, respond to the event with that. Then start a fetch request in order to update the cache.
Now if you don't find anything, fire a fetch request, clone the response (because the response body can only be used once), respond with the original response and then update the cache with the cloned response.
What did we achieve with that?
The user gets an instant response, no matter if he's online, offline or on Lie-Fi!
The server gets at most one request and the caches will always get updated with the latest data from the server!
serviceworke.rs is a great resource that can help you understand how to do many interesting things with Service Workers.
This page in particular explains in a bit more detail how what I said above works.
I have built a web app using Backbone.js and it has lots of calls to a RESTful service and it works like a charm.
I tried adding a ServiceWorker to cache all the previous calls so they'll be available offline.
What I actually get is that the calls I do for the first time, dies with this error:
Failed to load resource: net::ERR_FAILED
However on page reload, I get it's cached data
My service worker fetch:
self.addEventListener('fetch', function(e) {
// e.respondWidth Responds to the fetch event
e.respondWith(
// Check in cache for the request being made
caches.match(e.request)
.then(function(response) {
// If the request is in the cache
if ( response ) {
console.log("[ServiceWorker] Found in Cache", e.request.url, response);
// Return the cached version
return response;
}
// If the request is NOT in the cache, fetch and cache
var requestClone = e.request.clone();
fetch(requestClone)
.then(function(response) {
if ( !response ) {
console.log("[ServiceWorker] No response from fetch ")
return response;
}
var responseClone = response.clone();
// Open the cache
caches.open(cacheName).then(function(cache) {
// Put the fetched response in the cache
cache.put(e.request, responseClone);
console.log('[ServiceWorker] New Data Cached', e.request.url);
// Return the response
return response;
}); // end caches.open
console.log("Response is.. ?", response)
return response;
})
.catch(function(err) {
console.log('[ServiceWorker] Error Fetching & Caching New Data', err);
});
}) // end caches.match(e.request)
); // end e.respondWith
});
edit:
I don't think there is a need for any Backbone.js web app code.
I use the fetch method from Backbone.js models and collections.
calls like
https://jsonplaceholder.typicode.com/posts/1
and
https://jsonplaceholder.typicode.com/posts/2
will replay show this error on first time. after refreshing the page, i do have this info without requesting. all from cache.
and all other request that i still didn't do, will stay error
i solved it after searching more.
Backbone.js my views in the Web app used to do:
this.listenTo(this.collection,"reset",this.render);
this.listenTo(this.collection,"add",this.addCollectionItem);
this.listenTo(this.collection,"error", this.errorRender);
while my Service worker is returning Promises.
I had to change my some code my Web app views to something like this:
this.collection.fetch({},{reset:true})
.then(_.bind(this.render, this))
.fail(_.bind(this.errorRender,this))
more or less...
The only problem I see is that when the request is not in the cache, then you do a fetch, but you do not return the result of that fetch to the enclosing then handler. You need to add a return so that you have:
return fetch(requestClone)
.then(function(response) {
None of the data provided by the return statements inside your then handler for the fetch will get transferred up the chain otherwise.
I also see that you do not return the promise provided by caches.open(cacheName).then.... This may be fine if you want to decouple saving a response in the cache from returning a result up the chain, but at the very least I'd put a comment saying that that's what I'm doing here rather than leave it to the reader to figure out whether a return statement is missing by accident, or it was done on purpose.
I am using Vue.js and Choices.js javascript plugin and I have to dynamically populate values of two select fields via ajax.
What I am trying achieve is initate a get request at page load and populate the universities select, and after a value in universities select is chosen start a new getrequest to populate the faculties select.
What is happening is that when I pick the university for the first time, everything will work normally. For example if I pick a university option with value="1" an ajax get request will be sent to /faculties?university_id=1.The console log will print onChange startedso we are sure the method is running correctly; the appropriate v-model="selectedUniversity"is updating too.
If I now change the value of the select field again, the ajax function won't be called anymore and no additional requests will be done to the server. The console.logwill still run, and the v-modelis still being updated. Does anyone understand what is going on here?
var Choices = require('choices.js');
module.exports = {
data: function() {
return {
selectedUniversity: '',
selectedFaculty: '',
universities: {},
faculties: {}
}
},
mounted: function () {
var self = this;
var universitySelect = new Choices(document.getElementById('university'));
universitySelect.ajax(function(callback) {
fetch('/universities')
.then(function(response) {
response.json().then(function(data) {
callback(data, 'id', 'name');
self.universities = data;
});
})
.catch(function(error) {
console.log(error);
});
});
},
methods: {
onChange: function () {
console.log("onChange started");
var self = this;
var url = '/faculties?university_id=' + self.selectedUniversity;
var facultySelect = new Choices(document.getElementById('faculty'));
//This part below only runs the first time when the university select is selected
facultySelect.ajax(function(callback) {
fetch(url)
.then(function(response) {
response.json().then(function(data) {
callback(data, 'id', 'name');
self.faculties = data;
});
})
.catch(function(error) {
console.log(error);
});
});
}
}
}
The Headers are set like this:
I think your request URL /faculties?university_id=1 is cached and that's why it worked on first time and second time, the response is coming from the cached response.
In your fetch API, set cache mode to ignore the cached response,
fetch(url, {cache: "no-store"}).then(....)
For complete list of cache modes for fetch() API,
https://hacks.mozilla.org/2016/03/referrer-and-cache-control-apis-for-fetch/
In case if above link is unavailable,
Fetch cache control APIs
The idea behind this API is specifying a caching policy for fetch to explicitly indicate how and when the browser HTTP cache should be consulted. It’s important to have a good understanding of the HTTP caching semantics in order to use these most effectively. There are many good articles on the web such as this one that describe these semantics in detail. There are currently five different policies that you can choose from.
“default” means use the default behavior of browsers when downloading resources. The browser first looks inside the HTTP cache to see if there is a matching request. If there is, and it is fresh, it will be returned from fetch(). If it exists but is stale, a conditional request is made to the remote server and if the server indicates that the response has not changed, it will be read from the HTTP cache. Otherwise it will be downloaded from the network, and the HTTP cache will be updated with the new response.
“no-store” means bypass the HTTP cache completely. This will make the browser not look into the HTTP cache on the way to the network, and never store the resulting response in the HTTP cache. Using this cache mode, fetch() will behave as if no HTTP cache exists.
“reload” means bypass the HTTP cache on the way to the network, but update it with the newly downloaded response. This will cause the browser to never look inside the HTTP cache on the way to the network, but update the HTTP cache with the downloaded response. Future requests can use that updated response if appropriate.
“no-cache” means always validate a response that is in the HTTP cache even if the browser thinks that it’s fresh. This will cause the browser to look for a matching request in the HTTP cache on the way to the network. If such a request is found, the browser always creates a conditional request to validate it even if it thinks that the response should be fresh. If a matching cached entry is not found, a normal request will be made. After a response has been downloaded, the HTTP cache will always be updated with that response.
“force-cache” means that the browser will always use a cached response if a matching entry is found in the cache, ignoring the validity of the response. Thus even if a really old version of the response is found in the cache, it will always be used without validation. If a matching entry is not found in the cache, the browser will make a normal request, and will update the HTTP cache with the downloaded response.
Let’s look at a few examples of how you can use these cache modes.
// Download a resource with cache busting, to bypass the cache
// completely.
fetch("some.json", {cache: "no-store"})
.then(function(response) { /* consume the response */ });
// Download a resource with cache busting, but update the HTTP
// cache with the downloaded resource.
fetch("some.json", {cache: "reload"})
.then(function(response) { /* consume the response */ });
// Download a resource with cache busting when dealing with a
// properly configured server that will send the correct ETag
// and Date headers and properly handle If-Modified-Since and
// If-None-Match request headers, therefore we can rely on the
// validation to guarantee a fresh response.
fetch("some.json", {cache: "no-cache"})
.then(function(response) { /* consume the response */ });
// Download a resource with economics in mind! Prefer a cached
// albeit stale response to conserve as much bandwidth as possible.
fetch("some.json", {cache: "force-cache"})
.then(function(response) { /* consume the response */ });
The service worker code I'm currently experimenting with looks like this in part
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('v1').then(function(cache) {
return cache.addAll([
'/react-redux/node_modules/react/dist/react-with-addons.js',
'/react-redux/node_modules/react-dom/dist/react-dom.js',
'/react-redux/a.js'
]);
})
);
});
With of course the standard fetch event listener that returns from cache, or runs a network request if the item is not there.
But what if, from the example above, a.js, and only a.js is updated—how do I get the service worker to update that file, but only that file; and how do I ensure that the next time the user browses to my page, they won't pull the now-stale version of the file from the service worker?
The best I can imagine would be to add a cache buster to those file urls, for example
'/react-redux/node_modules/react/dist/react-with-addons.js?hash=1MWRF3...'
then update whatever module loader I'm using to request these files with that same, current hash/cache buster, and then in the SW install event iterate over the current cache keys and delete anything that's stale, and add anything that's missing.
That would seem to solve both problems: when a file is updated, the network request that's sent won't match anything in the now-stale Service Worker, and so the same network fallback will happen; and the selective cache insertion in the Service Worker's install event wouldn't try to add things to the cache that are already there and current.
And of course the Service Worker code would change as these hash values change (automatically from a build process) and so getting the SW to re-install when files change would happen, as well.
But I can't help but think there's a simpler way. Is there?
Your understanding of what should ideally happen, and the difficulties in making sure that cached assets are updated efficiently and reliably, is spot-on.
While you could roll your own approach, there are existing tools that will automate the process of fingerprinting each file and then generating a service worker file that manages your cached assets. I've developed one of them, sw-precache. offline-plugin is another alternative that covers similar ground.
I ended up writing the code for exactly what you said, here is the code for anyone having difficulties writing it themselves:
Firstly, we need to write the code to add a timestamp/hash to URL of the bundle file everytime the bundle changes.
Most of us use webpack for bundling the application together, and every time the webpack config file is executed the bundle supposedly changes so we will do the hash/timestamp insertion in URL here. I have a file named index.template.html where I store the file served to the user so to modify the URL I did this:
// webpack.config.js
const webpack = require('webpack');
const fs = require('fs');
fs.readFile('./public/index.template.html', function (err, data) {
if (err) return console.log('Unable to read index.template file', err);
fs.writeFile('./public/index.template.html',
// finding and inserting current timestamp in front of the URL for cache busting
data.toString('utf8').replace(/bundle\.js.*"/g, "bundle\.js\?v=" + Math.floor(Date.now() / 1000) + "\""),
(err) => {
if (err) console.log("Unable to write to index.template.html", err);
});
});
module.exports = {
// configuration for webpack
};
Now here is the code for service worker which detects the change in URL and re-fetches and replaces the resource in the cache in case of a change, I've tried to explain the working in comments:
self.addEventListener("fetch", function (event) {
event.respondWith(
// intercepting response for bundle.js since bundle.js may change and we need to replace it in our cahce
event.request.url.indexOf('public/bundle.js') != -1 ?
checkBundle(event.request) : //if it is the bundle URL then use our custom function for handling the request
caches.match(event.request) //if its not then do the use service-worker code:
.then(function(response) {
// other requests code
})
);
});
// our custom function which does the magic:
function checkBundle(request) {
return new Promise(function(resolve, reject){ // respondWith method expects a Promise
caches.open(cacheName).then(function(cache) {
//first lets check whether its in cache already or not
// ignoreSearch parameter will ignore the query parameter while searching in cache, i.e., our cache busting timestmap
cache.keys(request, { ignoreSearch: true }).then(function(keys) {
if(keys.length == 0) {
// its not in cache so fetch it
return resolve(fetch(request).then(
function (response) {
if (!response || (response.status !== 200 && response.status !== 0)) {
return response;
}
cache.put(request, response.clone());
return response;
}
));
}
//it is in cache, so now we extract timestamp from current and cached URL and compare them
const lastVersion = /bundle.js\?v=(.*)$/.exec(keys[0].url)[1],
curVersion = /bundle.js\?v=(.*)$/.exec(request.url)[1];
if(lastVersion == curVersion) // if timestamp is change that means no change in the resource
return resolve(cache.match(request)); //return the cached resource
//bundle file has changed, lets delete it from cache first
cache.delete(keys[0]);
//now we fetch new bundle and serve it and store in cache
var fetchRequest = request.clone();
resolve(fetch(fetchRequest).then(
function (response) {
if (!response || (response.status !== 200 && response.status !== 0)) {
return response;
}
cache.put(request, response.clone());
return response;
}
));
});
});
});
}
As mentioned by Jeff Posnick in the comment of other answers generally these types of method require N+1 visits to see the updated resource but this one doesn't as the resource is re-fetched then served to the client and replaced in the cache at the same time.