I'm introducing service worker on my site.And i'm using app-shell approach for responding to requests.Below is my code structure.
serviceWorker.js
self.addEventListener("fetch", function(event) {
if (requestUri.indexOf('-spid-') !== -1) {
reponsePdPage(event,requestUri);
}else{
event.respondWith(fetch(requestUri,{mode: 'no-cors'}).catch(function (error){
console.log("error in fetching => "+error);
return new Response("not found");
})
);
}
});
function reponsePdPage(event,requestUri){
var appShellResponse=appShellPro();
event.respondWith(appShellResponse); //responds with app-shell
event.waitUntil(
apiResponse(requestUri) //responds with dynamic content
);
}
function appShellPro(){
return fetch(app-shell.html);
}
function apiResponse(requestUri){
var message=['price':'12.45cr'];
self.clients.matchAll().then(function(clients){
clients.forEach(function (client) {
if(client.url == requestUri)
client.postMessage(JSON.stringify(message));
});
});
}
App-shell.html
<html>
<head>
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.onmessage = function (evt) {
var message = JSON.parse(evt.data);
document.getElementById('price').innerHTML=message['price'];
}
}
</script>
</head>
<body>
<div id="price"></div>
</body>
</html>
serviceWorker.js is my only service worker file. whenever i'm getting request of -spid- in url i calls reponsePdPage function.In reponsePdPage function i'm first responding with app-shell.html. after that i'm calling apiResponse function which calls postmessage and send the dynamic data.The listener of post message is written in app-shell.html.
The issue i'm facing is, sometimes post message gets called before the listener registration.It means the apiResponse calls post message but their is not register listener to that event. So i cant capture the data.?Is their something wrong in my implementation.
I'm going to focus on just the last bit, about the communication between the service worker and the controlled page. That question is separate from many of the other details you provide, such as using PHP and adopting the App Shell model.
As you've observed, there's a race condition there, due to the fact that the code in the service worker and the parsing and execution of the HTML are performed in separate processes. I'm not surprised that the onmessage handler isn't established in the page yet at the time the service worker calls client.postMessage().
You've got a few options if you want to pass information from the service worker to controlled pages, while avoiding race conditions.
The first, and probably simplest, option is to change the direction of communication, and have the controlled page use postMessage() to send a request to the service worker, which then responds with the same information. If you take that approach, you'll be sure that the controlled page is ready for the service worker's response. There's a full example here, but here's a simplified version of the relevant bit, which uses a Promise-based wrapper to handle the asynchronous response received from the service worker:
Inside the controlled page:
function sendMessage(message) {
// Return a promise that will eventually resolve with the response.
return new Promise(function(resolve) {
var messageChannel = new MessageChannel();
messageChannel.port1.onmessage = function(event) {
resolve(event.data);
};
navigator.serviceWorker.controller.postMessage(message,
[messageChannel.port2]);
});
}
Inside the service worker:
self.addEventListener('message', function(event) {
// Check event.data to see what the message was.
// Put your response in responseMessage, then send it back:
event.ports[0].postMessage(responseMessage);
});
Other approaches include setting a value in IndexedDB inside the service worker, which is then read from the controlled page once it loads.
And finally, you could actually take the HTML you retrieve from the Cache Storage API, convert it into a string, modify that string to include the relevant information inline, and then respond with a new Response that includes the modified HTML. That's probably the most heavyweight and fragile approach, though.
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 have a logging API in which is executed before a link. The link will be redirecting the user to other place and I'm executing fetch before the user is redirected. So the script is like this now:
loggingAPI({
timestamp: moment()
})
window.location = "http://.......com"
The logging api is just a normal fetch wrapper.
However, the server doesn't receive the API request right now. I think it's because of it doesn't even get the chance to send the request to the api.
So can I wait for the request to be sent but not waiting for the response?
Using sendBeacon it's very simple
without seeing the code for you function loggingAPI the following is a best guess
Note: sendBeacon uses a POST request, so the server side may need to be modified to accept such a request - though, seeing as your loggingAPI is sending data, I imagine it is already using POST - so this may be a non-issue
somewhere in your code, set up an unload event for windows
window.addEventListener("unload", () => {
sendBeacon("same url as loggingAPI", JSON.stringify({timestamp: moment()}));
}, false);
Then, when you
window.location = "http://.......com"
the loggingAPI function gets called for you
edit: sorry, I didn't flesh out the code fully, I missed a few steps!!
You can send the request in a service worker.
https://developers.google.com/web/fundamentals/primers/service-workers/
Here's some fetch specific information:
https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent
You would register the service worker, and then send a message to it before redirecting.
The upside to the initial complexity is that once you start using service workers, they open up a whole new world of programming; You will end up using them for much more then queuing up messages to send.
Step 1 Register a service worker
index.html
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('service-worker.js').then(function(registration) {
// Registration was successful
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}, function(err) {
// registration failed :(
console.log('ServiceWorker registration failed: ', err);
});
});
}
Step 2 Create the service worker script
service-worker.js
self.addEventListener('install', function(e) {
return Promise.resolve(null)
});
Step 3 Create a listener in server worker script
service-worker.js
self.addEventListener('message', function (event) {
console.log('message', event.data)
// call fetch here, catching and responding to what you stashed in the message
});
Step 4 Send the message before you redirect
index.html
Just a demo to simulate your client.
setTimeout(() => {
navigator.serviceWorker.controller.postMessage({message: 'A LOG MESSAGE'});
}, 2000)
After you put all pieces in place, MAKE SURE YOU CLOSE ALL TABS AND REOPEN, or have chrome dev tools set up to deal with refreshing the worker.
An old question, but if you're using fetch you can use the keepalive flag.
The keepalive option can be used to allow the request to outlive the page. Fetch with the keepalive flag is a replacement for the Navigator.sendBeacon() API.
https://developer.mozilla.org/en-US/docs/Web/API/fetch#keepalive
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 recently been working on a project with both a client (in the browser) and node.js. I am using web sockets to communicate between the two. I have implemented an API on my server that communicates over the WebSockets and the server works just fine. In the browser, I am implementing the following class (in javascript) to interface with the API.
class ApiHandlerV1 {
constructor(apiUrl) {
//create the object
this.ws = null;
}
makeRequest(request,callback) {
//make a request
}
connect() {
//connect the websocket
this.ws = new WebSocket(this.url);
this.ws.onmessage = function () {
//call callback?
}
}
}
The issue that I am caught up on is that I want to be able to call makeRequest, provide a callback, and once the socket has gotten data back trigger the callback. I have thought about just re-defining .onmessage every time that I make a request but that just seems dirty to me and there is most likely a nice and easy solution to this.
Clarifications: Because of how I implemented my server I will only get a single message back from the server.
As Dominik pointed out in the comments I should also say that I am going to call .connect() before I make a request. I will be calling makeRequest multiple times after in other parts of my code.
I am new to webworker but I managed to send xmlhttprequest to my rest api and I got json back. But I want send this request again and again (in a loop), until the page is active.
I actually want to show values in real time. I want to make a simple web application in which when data is inserted in database my webworker should show that data without refreshing the page.
Is there any better way to do so. Kindly help me in it.
sorry for bad English.
You can use EventSource to get stream from server until .close() is called at Worker, or message is passed to Worker signalling Worker to call .close().
const es = new EventSource("/path/to/server");
es.addEventListener("open", function(event) {
console.log("event source open")
});
es.addEventListener("message", function(event) {
// do stuff with `event.data`
console.log(event.data);
});
es.addEventListener("error", function(event) {
console.log("event source error", event)
});
button.addEventListener("click", function() {
es.close();
});