I've implemented ServiceWorker on some of my webapps to leverage offline capabilities and some other goodies related to the ServiceWorkers. Now, everything is working fine until I have added some external embedded scripts. There are about 3 external scripts that I've added on my webapp. Some of which fetch ads and display it on my app and some of which are used to gather analytics.
But, to my surprise, every external scripts are failing when the ServiceWorkers are enabled and throws below error in the console
I'm not sure why is this happening? Do I have to cache these scripts some way in ServiceWorker? Any help would be appreciated.
Here is my ServiceWorker code that I've added on my app.
importScripts('js/cache-polyfill.js');
var CACHE_VERSION = 'app-v18';
var CACHE_FILES = [
'/',
'index.html',
'js/app.js',
'js/jquery.min.js',
'js/bootstrap.min.js',
'css/bootstrap.min.css',
'css/style.css',
'favicon.ico',
'manifest.json',
'img/icon-48.png',
'img/icon-96.png',
'img/icon-144.png',
'img/icon-196.png'
];
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('fetch', function (event) {
event.respondWith(
caches.match(event.request).then(function(res){
if(res){
return res;
}
requestBackend(event);
})
)
});
function requestBackend(event){
var url = event.request.clone();
return fetch(url).then(function(res){
//if not a valid response send the error
if(!res || res.status !== 200 || res.type !== 'basic'){
return res;
}
var response = res.clone();
caches.open(CACHE_VERSION).then(function(cache){
cache.put(event.request, response);
});
return res;
})
}
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]);
}
}))
})
)
});
Here's what I did to resolve the issue.
I tried checking if the app is online or not using Navigator.onLine API in the fetch event listener and if it's online, then serve the response from the server and from ServiceWorker otherwise. This way the requests won't get blocked while the app is online and thus resolves this particular issue.
Here's how I've implemented it.
self.addEventListener('fetch', function (event) {
let online = navigator.onLine;
if(!online){
event.respondWith(
caches.match(event.request).then(function(res){
if(res){
return res;
}
requestBackend(event);
})
)
}
});
You could also check the entire ServiceWorker script here: https://github.com/amitmerchant1990/notepad/blob/master/sw.js
Related
I need some help here. I'm currently using Chrome 86, trying out a tutorial with JS Service Workers. (This tutorial) to be specific. However, it doesn't seem to work. Upon looking at chrome://serviceworker-internals, I found that my service worker doesn't appear to have a fetch handler. However, this shouldn't be the case since I have added a fetch event listener on my service worker script.
Below is the script.
// Service worker code
'use strict';
var cacheVersion = 1;
var currentCache = {
offline: 'offline-cache' + cacheVersion
};
const offlineURL = '/offline.html';
self.addEventListener('install', event => {
event.waitUntil(
caches.open(currentCache.offline).then(c => {
return c.addAll([
/* Additional resources for offline page, */
/* Don't touch this */
offlineURL
]);
})
);
});
self.addEventListener('fetch', event => { // This seems to not be detected
// request.mode = navigate isn't supported in all browsers
// so include a check for Accept: text/html header.
if (event.request.mode === 'navigate' || (event.request.method === 'GET' && event.request.headers.get('accept').includes('text/html'))) {
event.respondWith(
fetch(event.request.url).catch(error => {
// Return the offline page
return caches.match(offlineUrl);
})
);
}
else{
// Respond with everything else if we can
event.respondWith(caches.match(event.request)
.then(function (response) {
return response || fetch(event.request);
})
);
}
});
I've commented the portion that seems to be buggy.
If anyone could help, that would be appreciated!
Thanks.
I implemented the service worker from pwabuilder.com and it works just fine
The problem is that the service worker runs even if the browser is online, so every js functions runs twice, one from the service worker and one from my other js files
Should I look if it's an active service worker before I run my js functions or should I somehow make sure that the service worker is not running when the browser is online?
This is the code I run in my main index file
if (navigator.serviceWorker.controller) {
//console.log('[PWA Builder] active service worker found, no need to register')
} else {
//Register the ServiceWorker
navigator.serviceWorker.register('pwabuilder-sw.js', {
scope: './'
}).then(function (reg) {
//console.log('Service worker has been registered for scope:' + reg.scope);
});
}
The pwabuilder-sw.js looks like this:
self.addEventListener('install', function (event) {
var indexPage = new Request('');
event.waitUntil(
fetch(indexPage).then(function (response) {
return caches.open('pwabuilder-offline').then(function (cache) {
//console.log('[PWA Builder] Cached index page during Install' + response.url);
return cache.put(indexPage, response);
});
}));
});
//If any fetch fails, it will look for the request in the cache and serve it from there first
self.addEventListener('fetch', function (event) {
var updateCache = function (request) {
return caches.open('pwabuilder-offline').then(function (cache) {
return fetch(request).then(function (response) {
//console.log('[PWA Builder] add page to offline' + response.url);
return cache.put(request, response);
});
});
};
event.waitUntil(updateCache(event.request));
event.respondWith(
fetch(event.request).catch(function (error) {
//Check to see if you have it in the cache
//Return response
//If not in the cache, then return error page
return caches.open('pwabuilder-offline').then(function (cache) {
return cache.match(event.request).then(function (matching) {
var report = !matching || matching.status === 404 ? Promise.reject('no-match') : matching;
return report;
});
});
})
);
});
Service Workers are meant to work all the time once registered, installed and activated
Service workers are event driven and their primary use is to act as a caching agent, to handle network requests and to store content for offline use. Secondly to handle push messaging.
I trust you understand that in order to act as a caching agent the service worker will run regardless if the application is online or offline. You have various caching scenarios to consider.
It is hard to provide exact solution for the mentioned: 'every js functions runs twice'.I doubt that all JS functions would always run twice. It seems this is implementation dependant.
Service workers cannot have a scope above their own path, by default it will control all resources below the scope of the service worker, this scope can also be restricted.
navigation.serviceWorker.register(
'/pwabuilder-sw.js', { //SW located at the root level here
scope: '/app/' //to control all resource accessed form within path /app/
}
);
I believe that the script from pwabuilder.com does attempt to cache all resources even resources that should not be cached such as POST requests. You may need to modify the caching policy depending on what type of resources your are using.
There is no simple solution here and no easy answer can be provided.
In general you can use the service worker to cache resources in one of the following ways:
Cache falling back to network
self.addEventListener('fetch', (event) => {
event.responseWith(
caches.match(event.request)
.then((response) => {
return response || fetch(event.request);
})
);
});
Network falling back to cache
//Good for resources that update frequently
//Bad for Intermittend and slow connections
self.addEventListener('fetch', (event) => {
event.responseWith(
fetch(event.request).catch(() => {
return caches.match(event.request);
})
);
});
Cache then network
//Cache then network(1) (send request to cache and network simultaneousely)
//show cached version first, then update the page when the network data arrives
var networkDataReceived = false;
var networkUpdate = fetch('/data.json')
.then((response) => {
return response.json();
}).then((data) => {
networkDataReceived = true;
updatePage(data);
});
//Cache then network(2)
caches.match('/data.json')
.then ((response) => {
return response.json();
}).then((data) => {
if (!networkDataReceived) {
updatePage(data);
}
}).catch(() => {
return networkUpdate;
});
Generic fallback
self.addEventListener('fetch', (event) => {
event.responseWith(
caches.match(event.request)
.then((response) => {
return response || fetch(event.request);
}).catch(() => {
return caches.match('offline.html');
})
)
});
I hope the above helps at least a little find the exact issue you are facing. Cheers and happy codding!
React added this React.strictMode HOC that runs twice certain parts of the application, like class component constructor, render, and shouldComponentUpdate methods.
Check the docs:
https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects.
See if you have <React.StrictMode> at the top of your index.js file and if so, you might want to run a test without it.
This is my first PWA app with laravel. This code is working,it gets registered well, but if I do a change in the code, for example in the HTML, it is not getting update, and the console is not throwing errors, and I dont know why.
I'm using this code to call the service-worker.js
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);
});
});
}
And this is the code of the sw.js
var cache_name = 'SW_CACHE';
var urlsToCache = [
'/',
'/register'
];
self.addEventListener('install', function(event) {
event.waitUntil(precache());
});
addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// Cache hit - return response
if (response) {
return response;
}
var fetchRequest = event.request.clone();
return fetch(fetchRequest).then(
function(response) {
// Check if we received a valid response
if(!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
var responseToCache = response.clone();
caches.open(cache_name)
.then(function(cache) {
cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
});
var fromCache = function (request) {
return caches.open(cache_name).then(function (cache) {
cache.match(request).then(function (matching) {
return matching || Promise.resolve('no-match');
});
});
}
var update = function (request) {
return caches.open(cache_name).then(function (cache) {
return fetch(request).then(function (response) {
return cache.put(request, response);
});
});
}
var precache = function() {
return caches.open(cache_name).then(function (cache) {
return cache.addAll(urlsToCache);
});
}
Y also used skipWaiting(); method inner Install method, but it crash my app and have to unload the sw from chrome://serviceworker-internals/
This is what service worker lifecycle suppose to work: a new service worker won't take place, unless:
The window or tabs controlled by the older service worker are closed and reopened
'Update on reload' option is checked in Chrome devtools
Here is an official tutorial explained it well: The Service Worker Lifecycle
Service worker will always use the existing worker. Two thinks you can do is in chrome there is an option to set update on load
Goto InspectorWindow (f12) -> application -> and check update on reload.
if you want immediate update you can choose the network first cache approach. which will take the latest from server always and use the cache only in offline mode. see the link for more information
How API is getting cached effectively, Using Service worker in Angular 5 using Angular CLI
I have a nginx hosted Jekyll site.
I have made several changes to my site, update package version and changed my javascript to update the service worker. However my changes are still not reflecting in chrome;
main.js
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js', {scope: 'sw-test'}).then(function(registration) {
// registration worked
console.log('Registration succeeded.');
registration.update();
}).catch(function(error) {
// registration failed
console.log('Registration failed with ' + error)
});
}
sw.js
var PRECACHE = 'precache-{{site.version}}';
var RUNTIME = 'runtime';
// A list of local resources we always want to be cached.
var PRECACHE_URLS = [
'./',
'/index.html',
'/assets/css/main.css',
'/assets/js/vendor/modernizr-custom.js',
'/assets/js/bundle.js'
];
// The install handler takes care of precaching the resources we always need.
self.addEventListener('install', event => {
event.waitUntil(
caches.open(PRECACHE)
.then(cache => cache.addAll(PRECACHE_URLS))
.then(self.skipWaiting())
);
});
// The activate handler takes care of cleaning up old caches.
self.addEventListener('activate', event => {
var currentCaches = [PRECACHE, RUNTIME];
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.
// If no response is found, it populates the runtime cache with the response
// from the network before returning it to the page.
self.addEventListener('fetch', event => {
// Skip cross-origin requests, like those for Google Analytics.
if (event.request.url.startsWith(self.location.origin)) {
event.respondWith(
caches.match(event.request).then(cachedResponse => {
if (cachedResponse) {
return cachedResponse;
}
return caches.open(RUNTIME).then(cache => {
return fetch(event.request).then(response => {
// Put a copy of the response in the runtime cache.
return cache.put(event.request, response.clone()).then(() => {
return response;
});
});
});
})
);
}
});
Chrome Screenshot
Screenshot
Any help greatly appreciated!!
Your nginx config is most likely setting some caching headers on the SW script. When that's the case, the browser returns the old version from its local cache skipping network completely.
I started learning PWA (Progressive Web App) and I have problem, console "throws" error Uncaught (in promise) TypeError: Failed to fetch.
Anyone know what could be the cause?
let CACHE = 'cache';
self.addEventListener('install', function(evt) {
console.log('The service worker is being installed.');
evt.waitUntil(precache());
});
self.addEventListener('fetch', function(evt) {
console.log('The service worker is serving the asset.');
evt.respondWith(fromCache(evt.request));
});
function precache() {
return caches.open(CACHE).then(function (cache) {
return cache.addAll([
'/media/wysiwyg/homepage/desktop.jpg',
'/media/wysiwyg/homepage/bottom2_desktop.jpg'
]);
});
}
function fromCache(request) {
return caches.open(CACHE).then(function (cache) {
return cache.match(request).then(function (matching) {
return matching || Promise.reject('no-match');
});
});
}
I think this is due to the fact that you don't have a fallback strategy. event.respondWith comes with a promise which you have to catch if there's some error.
So, I'd suggest that you change your code from this:
self.addEventListener('fetch', function(evt) {
console.log('The service worker is serving the asset.');
evt.respondWith(fromCache(evt.request));
});
To something like this:
addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
return response; // if valid response is found in cache return it
} else {
return fetch(event.request) //fetch from internet
.then(function(res) {
return caches.open(CACHE_DYNAMIC_NAME)
.then(function(cache) {
cache.put(event.request.url, res.clone()); //save the response for future
return res; // return the fetched data
})
})
.catch(function(err) { // fallback mechanism
return caches.open(CACHE_CONTAINING_ERROR_MESSAGES)
.then(function(cache) {
return cache.match('/offline.html');
});
});
}
})
);
});
NOTE: There are many strategies for caching, what I've shown here is offline first approach. For more info this & this is a must read.
I found a solution to the same error, in my case the error showed when the service worker could not find a file*, fix it by following the network in dev tool of chrome session, and identified the nonexistent file that the service worker did not find and removed array of files to register.
'/processos/themes/base/js/processos/step/Validation.min.js',
'/processos/themes/base/js/processos/Acoes.min.js',
'/processos/themes/base/js/processos/Processos.min.js',
'/processos/themes/base/js/processos/jBPM.min.js',
'/processos/themes/base/js/highcharts/highcharts-options-white.js',
'/processos/themes/base/js/publico/ProcessoJsController.js',
// '/processos/gzip_457955466/bundles/plugins.jawrjs',
// '/processos/gzip_N1378055855/bundles/publico.jawrjs',
// '/processos/gzip_457955466/bundles/plugins.jawrjs',
'/mobile/js/about.js',
'/mobile/js/access.js',
*I bolded the solution for me... I start with just a file for cache and then add another... till I get the bad path to one, also define the scope {scope: '/'} or {scope: './'} - edit by lawrghita
I had the same error and in my case Adblock was blocking the fetch to an url which started by 'ad' (e.g. /adsomething.php)
In my case, the files to be cached were not found (check the network console), something to do with relative paths, since I am using localhost and the site is inside a sub-directory because I develop multiple projects on a XAMPP server.
So I changed
let cache_name = 'Custom_name_cache';
let cached_assets = [
'/',
'index.php',
'css/main.css',
'js/main.js'
];
self.addEventListener('install', function (e) {
e.waitUntil(
caches.open(cache_name).then(function (cache) {
return cache.addAll(cached_assets);
})
);
});
To below: note the "./" on the cached_assets
let cache_name = 'Custom_name_cache';
let cached_assets = [
'./',
'./index.php',
'./css/main.css',
'./js/main.js'
];
self.addEventListener('install', function (e) {
e.waitUntil(
caches.open(cache_name).then(function (cache) {
return cache.addAll(cached_assets);
})
);
});
Try to use / before adding or fetching any path like /offline.html or /main.js
Cached files reference should be correct otherwise the fetch will fail. Even if one reference is incorrect the whole fetch will fail.
let cache_name = 'Custom_name_cache';
let cached_files = [
'/',
'index.html',
'css/main.css',
'js/main.js'
];
// The reference here should be correct.
self.addEventListener('install', function (e) {
e.waitUntil(
caches.open(cache_name).then(function (cache) {
return cache.addAll(cached_files);
})
);
});