I developped a service worker which is serving pages from network first (and caching it) or, when offline, serving it from cache.
Ideally, I would like to inform the user (with a banner, or something like this) that the page has been served from the cache because we detected that he was offline.
Do you have an idea on how to implement this ?
Some ideas I had (but didn't really succeeded to implement) :
Inject some code in the cached response body (like, injecting some JS code triggering a offline-detected event which may or may not have an event listener on the webpage, depending on if I want to do something or not on this webpage while offline).
=> I didn't found how to append stuff in response's body coming from the cache to do this.
Send a postMessage from service worker to the webpage telling that it has been rendered using a cached content.
=> It doesn't seem to be possible as I don't have any MessagePort available in ServiceWorker's fetch event, making it impossible to send any postMessage() to it.
If you have any idea on how to implement this, I would be very happy to discuss about it.
Thanks in advance :)
I looked at different solutions :
Use navigator.onLine flag on HTML page : this is not reliable on my case, as my service worker might served cached page (because of, let's say, a timeout) whilst the browser can consider to be online
Inject custom headers when serving HTML response from cache : I don't see how it might work since response headers are generally not accessible on clientside
Call service-worker's postMessage() few seconds after content is served : problem is, in that case, that fetch event on the "starting" HTML page, doesn't have any clientId yet (we have a chicken & egg problem here, since service worker is not yet attached to the client at the moment the root html page is served from cache)
The only remaining solution I found was to inject some code in the cached response.
Something like this on the service worker side (the main idea is to inject some code on cached response body, triggering a served-offline-page event once DOM content has been loaded) :
async function createClonedResponseWithOfflinePageInjectedEvent(response) {
const contentReader = response.body.getReader();
let content = '', readResult = {done: false, value: undefined };
while(!readResult.done) {
readResult = await contentReader.read();
content += readResult.value?new TextDecoder().decode(readResult.value):'';
}
// Important part here, as we're "cloning" response by injecting some JS code in it
content = content.replace("</body>", `
<script type='text/javascript'>
document.addEventListener('DOMContentLoaded', () => document.dispatchEvent(new Event('served-offline-page')));
</script>
</body>
`);
return new Response(content, {
headers: response.headers,
status: response.status,
statusText: response.statusText
});
}
async function serveResponseFromCache(cache, request) {
try {
const response = await cache.match(request);
if(response) {
console.info("Used cached asset for : "+request.url);
// isCacheableHTMLPage() will return true on html pages where we're ok to "inject" some js code to notify about
if(isCacheableHTMLPage(request.url)) {
return createClonedResponseWithOfflinePageInjectedEvent(response);
} else {
return response;
}
} else {
console.error("Asset neither available from network nor from cache : "+request.url);
// Redirecting on offline page
return new Response(null, {
headers: {
'Location': '/offline.html'
},
status: 307
})
}
}catch(error) {
console.error("Error while matching request "+request.url+" from cache : "+error);
}
}
On the HTML page, this is simple, we only have to write this in the page :
document.addEventListener('served-offline-page', () => {
console.log("It seems like this page has been served as some offline content !");
})
I am new to Service Workers, and have had a look through the various bits of documentation (Google, Mozilla, serviceworke.rs, Github, StackOverflow questions). The most helpful is the ServiceWorkers cookbook.
Most of the documentation seems to point to caching entire pages so that the app works completely offline, or redirecting the user to an offline page until the browser can redirect to the internet.
What I want to do, however, is store my form data locally so my web app can upload it to the server when the user's connection is restored. Which "recipe" should I use? I think it is Request Deferrer. Do I need anything else to ensure that Request Deferrer will work (apart from the service worker detector script in my web page)? Any hints and tips much appreciated.
Console errors
The Request Deferrer recipe and code doesn't seem to work on its own as it doesn't include file caching. I have added some caching for the service worker library files, but I am still getting this error when I submit the form while offline:
Console: {"lineNumber":0,"message":
"The FetchEvent for [the form URL] resulted in a network error response:
the promise was rejected.","message_level":2,"sourceIdentifier":1,"sourceURL":""}
My Service Worker
/* eslint-env es6 */
/* eslint no-unused-vars: 0 */
/* global importScripts, ServiceWorkerWare, localforage */
importScripts('/js/lib/ServiceWorkerWare.js');
importScripts('/js/lib/localforage.js');
//Determine the root for the routes. I.e, if the Service Worker URL is http://example.com/path/to/sw.js, then the root is http://example.com/path/to/
var root = (function() {
var tokens = (self.location + '').split('/');
tokens[tokens.length - 1] = '';
return tokens.join('/');
})();
//By using Mozilla’s ServiceWorkerWare we can quickly setup some routes for a virtual server. It is convenient you review the virtual server recipe before seeing this.
var worker = new ServiceWorkerWare();
//So here is the idea. We will check if we are online or not. In case we are not online, enqueue the request and provide a fake response.
//Else, flush the queue and let the new request to reach the network.
//This function factory does exactly that.
function tryOrFallback(fakeResponse) {
//Return a handler that…
return function(req, res) {
//If offline, enqueue and answer with the fake response.
if (!navigator.onLine) {
console.log('No network availability, enqueuing');
return enqueue(req).then(function() {
//As the fake response will be reused but Response objects are one use only, we need to clone it each time we use it.
return fakeResponse.clone();
});
}
//If online, flush the queue and answer from network.
console.log('Network available! Flushing queue.');
return flushQueue().then(function() {
return fetch(req);
});
};
}
//A fake response with a joke for when there is no connection. A real implementation could have cached the last collection of updates and keep a local model. For simplicity, not implemented here.
worker.get(root + 'api/updates?*', tryOrFallback(new Response(
JSON.stringify([{
text: 'You are offline.',
author: 'Oxford Brookes University',
id: 1,
isSticky: true
}]),
{ headers: { 'Content-Type': 'application/json' } }
)));
//For deletion, let’s simulate that all went OK. Notice we are omitting the body of the response. Trying to add a body with a 204, deleted, as status throws an error.
worker.delete(root + 'api/updates/:id?*', tryOrFallback(new Response({
status: 204
})));
//Creation is another story. We can not reach the server so we can not get the id for the new updates.
//No problem, just say we accept the creation and we will process it later, as soon as we recover connectivity.
worker.post(root + 'api/updates?*', tryOrFallback(new Response(null, {
status: 202
})));
//Start the service worker.
worker.init();
//By using Mozilla’s localforage db wrapper, we can count on a fast setup for a versatile key-value database. We use it to store queue of deferred requests.
//Enqueue consists of adding a request to the list. Due to the limitations of IndexedDB, Request and Response objects can not be saved so we need an alternative representations.
//This is why we call to serialize().`
function enqueue(request) {
return serialize(request).then(function(serialized) {
localforage.getItem('queue').then(function(queue) {
/* eslint no-param-reassign: 0 */
queue = queue || [];
queue.push(serialized);
return localforage.setItem('queue', queue).then(function() {
console.log(serialized.method, serialized.url, 'enqueued!');
});
});
});
}
//Flush is a little more complicated. It consists of getting the elements of the queue in order and sending each one, keeping track of not yet sent request.
//Before sending a request we need to recreate it from the alternative representation stored in IndexedDB.
function flushQueue() {
//Get the queue
return localforage.getItem('queue').then(function(queue) {
/* eslint no-param-reassign: 0 */
queue = queue || [];
//If empty, nothing to do!
if (!queue.length) {
return Promise.resolve();
}
//Else, send the requests in order…
console.log('Sending ', queue.length, ' requests...');
return sendInOrder(queue).then(function() {
//Requires error handling. Actually, this is assuming all the requests in queue are a success when reaching the Network.
// So it should empty the queue step by step, only popping from the queue if the request completes with success.
return localforage.setItem('queue', []);
});
});
}
//Send the requests inside the queue in order. Waiting for the current before sending the next one.
function sendInOrder(requests) {
//The reduce() chains one promise per serialized request, not allowing to progress to the next one until completing the current.
var sending = requests.reduce(function(prevPromise, serialized) {
console.log('Sending', serialized.method, serialized.url);
return prevPromise.then(function() {
return deserialize(serialized).then(function(request) {
return fetch(request);
});
});
}, Promise.resolve());
return sending;
}
//Serialize is a little bit convolved due to headers is not a simple object.
function serialize(request) {
var headers = {};
//for(... of ...) is ES6 notation but current browsers supporting SW, support this notation as well and this is the only way of retrieving all the headers.
for (var entry of request.headers.entries()) {
headers[entry[0]] = entry[1];
}
var serialized = {
url: request.url,
headers: headers,
method: request.method,
mode: request.mode,
credentials: request.credentials,
cache: request.cache,
redirect: request.redirect,
referrer: request.referrer
};
//Only if method is not GET or HEAD is the request allowed to have body.
if (request.method !== 'GET' && request.method !== 'HEAD') {
return request.clone().text().then(function(body) {
serialized.body = body;
return Promise.resolve(serialized);
});
}
return Promise.resolve(serialized);
}
//Compared, deserialize is pretty simple.
function deserialize(data) {
return Promise.resolve(new Request(data.url, data));
}
var CACHE = 'cache-only';
// On install, cache some resources.
self.addEventListener('install', function(evt) {
console.log('The service worker is being installed.');
// Ask the service worker to keep installing until the returning promise
// resolves.
evt.waitUntil(precache());
});
// On fetch, use cache only strategy.
self.addEventListener('fetch', function(evt) {
console.log('The service worker is serving the asset.');
evt.respondWith(fromCache(evt.request));
});
// Open a cache and use `addAll()` with an array of assets to add all of them
// to the cache. Return a promise resolving when all the assets are added.
function precache() {
return caches.open(CACHE).then(function (cache) {
return cache.addAll([
'/js/lib/ServiceWorkerWare.js',
'/js/lib/localforage.js',
'/js/settings.js'
]);
});
}
// Open the cache where the assets were stored and search for the requested
// resource. Notice that in case of no matching, the promise still resolves
// but it does with `undefined` as value.
function fromCache(request) {
return caches.open(CACHE).then(function (cache) {
return cache.match(request).then(function (matching) {
return matching || Promise.reject('no-match');
});
});
}
Here is the error message I am getting in Chrome when I go offline:
(A similar error occurred in Firefox - it falls over at line 409 of ServiceWorkerWare.js)
ServiceWorkerWare.prototype.executeMiddleware = function (middleware,
request) {
var response = this.runMiddleware(middleware, 0, request, null);
response.catch(function (error) { console.error(error); });
return response;
};
this is a little more advanced that a beginner level. But you will need to detect when you are offline or in a Li-Fi state. Instead of POSTing data to an API or end point you need to queue that data to be synched when you are back on line.
This is what the Background Sync API should help with. However, it is not supported across the board just yet. Plus Safari.........
So maybe a good strategy is to persist your data in IndexedDB and when you can connect (background sync fires an event for this) you would then POST the data. It gets a little more complex for browsers that don't support service workers (Safari) or don't yet have Background Sync (that will level out very soon).
As always design your code to be a progressive enhancement, which can be tricky, but worth it in the end.
Service Workers tend to cache the static HTML, CSS, JavaScript, and image files.
I need to use PouchDB and sync it with CouchDB
Why CouchDB?
CouchDB is a NoSQL database consisting of a number of Documents
created with JSON.
It has versioning (each document has a _rev
property with the last modified date)
It can be synchronised with
PouchDB, a local JavaScript application that stores data in local
storage via the browser using IndexedDB. This allows us to create
offline applications.
The two databases are both “master” copies of
the data.
PouchDB is a local JavaScript implementation of CouchDB.
I still need a better answer than my partial notes towards a solution!
Yes, this type of service worker is the correct one to use for saving form data offline.
I have now edited it and understood it better. It caches the form data, and loads it on the page for the user to see what they have entered.
It is worth noting that the paths to the library files will need editing to reflect your local directory structure, e.g. in my setup:
importScripts('/js/lib/ServiceWorkerWare.js');
importScripts('/js/lib/localforage.js');
The script is still failing when offline, however, as it isn't caching the library files. (Update to follow when I figure out caching)
Just discovered an extra debugging tool for service workers (apart from the console): chrome://serviceworker-internals/. In this, you can start or stop service workers, view console messages, and the resources used by the service worker.
How can i get previouse url without magic and in backend?
Now i get it through policies:
module.exports = function(req, res, next) {
if (!req.session.previouseUrls) {
req.session.previouseUrls = [];
}
req.session.previouseUrl = req.session.currentUrl || "/";
req.session.currentUrl = req.url;
req.session.previouseUrls.push(req.session.previouseUrl);
next();
};
but it's uncomfortable. Can i get previouse Url from backend simpler?
If you need to know the history (or just previous page) of the user's page requests purely on the client-side, could you query native HTML5 History?
..or if you need to support older browsers, maybe History.js?
I would be concerned with how you're doing it now for 2 reasons:
It's going to start filling up your session store. This may not be a big issue if you have short-lives sessions or not many users.
It could record not just traditional page views "clicks" but any request. Unless you're very carful about scoping this policy you may end up adding an Ajax call that goes through this policy check and you probably didn't intend for that.
I have an app written in HTML5, Javascript, css3 using PhoneGap to compile for iOS and Android. It collects survey information and uploads this via Ajax call to online host. It has been working really well until recently the upload code appeared to stop working. WELL NOT QUITE! On the iPad it says successful but in fact nothing ever makes it to the host. This is VERY strange. I've tried re-writing the Ajax call based on articles on here but no luck.
iOS - 6.1.3, PhoneGAP 2.7.0, PhoneGap/Adobe Build used.
This is the upload piece...
function sendToWeb(){
var errorflag = false;
db.transaction(function (tx) {
tx.executeSql("SELECT weburl FROM settings", [], function(tx, results){
var webURL = results.rows.item(0).weburl;
tx.executeSql("SELECT * FROM surveypretransfer WHERE uploaded = '0'",[], function(tx, results){
if (results.rows.length == 0) {
alert("You have no surveys waiting to upload");
} else {
alert("You have " + results.rows.length + " surveys waiting to upload");
for (var i=0; i < results.rows.length; i++) {
var responseURL = webURL + "/feeds/saveinfo.php";
var responseString = results.rows.item(i).responsestring;
var localid = results.rows.item(i).id;
//alert(localid);
$.ajax({
type: 'POST',
data: responseString,
url: responseURL,
timeout: 30000,
success: function(data) {
alert('Success!' + data.join('\n'));
},
error: function(data) {
alert(data.join('\n'));
console.log("Results: " + localid);
alert("Error during Upload. Error is: "+ data.statusText);
}
}); //ajax
}; //for loop
alert("You have successfully uploaded "+ results.rows.length + " survey results");
tx.executeSql("UPDATE surveypretransfer SET uploaded = '1' WHERE uploaded = '0'");
}; //if statement
}); //tx.execute
});
}, errorCB);
}
Neither of the two alerts fire when loaded on iPad. Works fine on Android and has previously worked on iPad so I can't find what has changed.
UPDATE: Appears that this only applies to WiFi only iPads. All the 3G ones I tested were fine. Figure that!
Config.XML contains app id = "com.mydomain.myapp" (as an example)
URL for upload is "http://customer.mydomain.com/feeds/saveinfo.php?..."
Also added line 'access origin="http://mydomain.com" subdomains="true" '
Still no results. Is anyone seeing/having similar issues? Anyone see my mistake?
For iOS you might want to try <access origin="http://*.mydomain.com" />, as iOS is not documented in the PhoneGap API to support the subdomain property.
If that doesn't solve your issues, you will probably want to look into CORS (Cross Origin Resource Sharing). I had issues trying to do a POST request from my app to a local port on iOS. The W3C has a great article on how to enable CORS that will probably help. I know in my case, the system would attempt to do an OPTIONS request first, and if it didn't work, the whole thing would fail.
Another tool that you will probably find useful (if not now, in the future) is Fiddler. You can set up an iPad to proxy through your desktop, and then you will be able to observe all of the requests going to and from the device. This is how I found the OPTIONS request noted above.
I'd like to pass the user information from my user registration form to a web-component. Is this possible. Something like below:
app.addRequestHandler(
(req) => req.method == 'POST' && req.path == '/newUser',
(req, res) {
...
input.onClosed = () {
...
var user = new User();
user.name = params["user[first_name]"];
user.email = params["user[email]"];
res.outputStream.writeString('<x-MyWebComponent data-value="User: user"></x-MyWebComponent>');
res.outputStream.close();
};
...
}
);
Thanks for the question!
Sending data to a web component is no different than sending data to any web page. That is, your web page or component can open an AJAX (aka XMLHttpRequest aka HttpRequest) request to the server and get JSON data back.
Because web components need to be compiled into vanilla JavaScript and HTML (until the features land in the browser... coming soon!) you can't send back raw HTML that contains your custom element (like you have in your example).
Basically, create a handler on your server that creates the User in the database and sends a JSON response containing the user details. Your web page (or component) will receive the JSON response and can then bind that, via components, to the page.
There's a lot of moving parts here so I think we need an end-to-end sample. In the meantime, you could do something like this:
var user = new User();
user.name = params["user[first_name]"];
user.email = params["user[email]"];
res.headers.add('Content-Type', 'application/json');
res.outputStream.writeString(user.toJson());
res.outputStream.close();
This assumes you added a String toJson(); method to User class.