How to cache React application in service worker - reactjs

I'm trying to create a React PWA from scratch. So far my project outputs the minified files to a dist/js folder.
In my service worker file I'm using Workbox to precache the app. This is my setting so far:
importScripts("./node_modules/workbox-sw/build/workbox-sw.js");
const staticAssets = [
"./",
"./images/favicon.png",
]
workbox.precaching.precacheAndRoute(staticAssets);
Currently if I enable offline from dev tools > Service Workers, it throws these errors and the app fails to load:
3localhost/:18 GET http://localhost:8080/js/app.min.js net::ERR_INTERNET_DISCONNECTED
localhost/:1 GET http://localhost:8080/manifest.json net::ERR_INTERNET_DISCONNECTED
3:8080/manifest.json:1 GET http://localhost:8080/manifest.json net::ERR_INTERNET_DISCONNECTED
logger.mjs:44 workbox Precaching 0 files. 2 files are already cached.
5:8080/manifest.json:1 GET http://localhost:8080/manifest.json net::ERR_INTERNET_DISCONNECTED
How can I fix this?

this means your resources are not getting cached properly,
you need to add them to cache before accessing,
workbox by default do it for you.
it shows 2 files cached, as they present in your array, expected result
same do it for all remaining too.
const staticAssets = [
"./",
"./images/favicon.png",
"./js/app.min.js",
"./manifest.json",
{ url: '/index.html', revision: '383676' }
]
you can try to add eventlistener,
self.addEventListener('install', event => {
console.log('Attempting to install service worker and cache static assets');
event.waitUntil(
caches.open("staticCacheName")
.then(cache => {
return cache.addAll(staticAssets);
})
);
});
self.addEventListener('fetch', function(event) {
event.respondWith(caches.match(event.request)
.then(function(response) {
if (response) {
return response;
}
return fetch(event.request);
})
);
});

Related

Not able to view cached static files in PWA

I have a React based PWA deployed on AWS Amplify and I'm trying to cache a few PDF documents for offline use. Using USB debugging I found that the documents are effectively added to the cache. However, when I try to open a document in offline mode, I'm presented with an blank page which seems to correspond to the bare index.html of my app. My documents are located in public/documents, and service-worker.js has this added at the end:
var CACHE_NAME = "app-documents";
var urlsToCache = [
"/documents/document_1.pdf",
"/documents/document_2.pdf",
"/documents/document_3.pdf"
];
self.addEventListener("install", event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function (cache) {
return cache.addAll(urlsToCache);
})
);
});
I'm linking from my app to the documents with a simple hyperlink:
<p>Document</p>

How to cache a React app with service workers

Im just wondering how to handle this. I want to have my whole app cached.
Im have tried something like this which doesnt seem to work
self.addEventListener('install',(e)=>{
console.log('installed');
})
self.addEventListener('activate',(e)=>{
console.log('activated');
self.skipWaiting();
})
self.addEventListener('fetch',(e)=>{
e.respondWith(
fetch(e.request)
.then(res=>{
const resClone = res.clone();
caches.open(cacheName).then(cache=>{
cache.put(e.request, resClone);
})
return res;
}).catch(err => {
console.log('no connection');
caches.match(e.request).then(res => { return res })
})
)
})
Does anyone know how to approach this?
Like one childish way would be to view the page source and check what js, css files are being used by react and cache them manually.
This will not work in production, you will have to manually check the files in the build directory and update the service-worker
Or a better and sensible way of doing it would be to use workbox (a npm package from google) which is going to handle all this clutter
This works for me,
self.addEventListener("fetch", function(event) {
event.respondWith(
caches.match(event.request).then(function(response) {
if (response) {
return response;
} else {
return fetch(event.request)
.then(function(res) {
return caches.open(CACHE_NAME).then(function(cache) {
cache.put(event.request.url, res.clone());
return res;
});
})
}
})
);
});
This tells the service worker to read from cache first and then network if there is no response from cache.
One thing to keep in mind, this will not check for any updates made to the files. If you change your react code, the service worker will load the previous files it has in cache.
To solve this you can use workbox's staleWhileRevalidate which updates the cache whenever there is network connection.
A less convenient solution would be to delete the cache on service worker activation:
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys()
.then(function(keyList) {
return Promise.all(keyList.map(function(key) {
return caches.delete(key);
}));
})
);
return self.clients.claim();
});
Whenever a new service worker is installed the cache is removed and a new one is created.

CSP headers are missing in JS files (while CSS not) when served by service worker

I am using a brand new app generated by create-react-app 3.4.1. It uses the default service worker file:
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
type Config = {
onSuccess?: (registration: ServiceWorkerRegistration) => void;
onUpdate?: (registration: ServiceWorkerRegistration) => void;
};
export function register(config?: Config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(
process.env.PUBLIC_URL,
window.location.href
);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker.'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl: string, config?: Config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl: string, config?: Config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' }
})
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then(registration => {
registration.unregister();
})
.catch(error => {
console.error(error.message);
});
}
}
I turned on service worker by changing the code in index.ts to
serviceWorker.register();
I hosted the static files generated by yarn build through https by an Express.js server with strict Content Security Policy (CSP) turned on by helmet.
helmet({
contentSecurityPolicy: {
directives: {
scriptSrc: [
/* Content Security Policy Level 3 */
"'strict-dynamic'",
`'nonce-${cspNonce}'`,
/* Content Security Policy Level 2 (backward compatible) */
"'self'",
// Workbox
'https://storage.googleapis.com',
// ...
],
styleSrc: [
"'self'",
],
// ...
},
},
})
When I first time opening the page, the browser fetch files from server. Both JS and CSS have CSP headers. The page shows well.
When I second time opening the page, the files are loaded from service worker. Many got blocked by CSP, as my console shows:
When I further check, CSS files served by service worker still have CSP headers (and nonce inside also changed to new value, create-react-app did it for us?), which load well.
However, the CSP headers on JS files are missing, which got blocked.
Any guide will be helpful. Thanks!
UPDATE
One thing I notice in Chrome, it shows
CAUTION: provisional headers are shown
and I found more info at
"CAUTION: provisional headers are shown" in Chrome debugger
Another thing I found, the page won't load on second call on Chrome and Safari after service worker (create-react-app uses Workbox internally) registered.
For Firefox, although CSP headers are not shown neither in JS and CSS files when read from cache, Firefox still can show the page.
It is likely that the first time you load the page the nonce in your CSP and your script tags are in sync. On the second load they are no longer present or in sync in your script tags. Check the difference in nonce values in the CSP header and inline script tags.
CSP applies to pages being rendered in the browser (content-type: "text/html"), it doesn't have any effect when set on the other resources loaded. Missing CSP header on js files doesn't have any effect. Your CSS files are included because you include "style-src 'self'", you should add this to script-src as well. If it is not sufficient you could add localhost:5000 in development.
As noticed Halvor Sakshaug above, you do not need to serve JS/CSS with CSP headers, CSP work only for page/code having document property.
As seen from you Chrome console warnings, there is at least 2 issues:
an inline scripts blocked (you do use < script>...< /script> or < tag unClick='...'> somewhere). So you have to add 'unsafe-inline' to script-src (or add nonce='server_generated_value' attribute to < script>...< /script>), BUT:
'strict-dynamic' cancels host-based allowlisting (incliding 'self') in CSP3-browsers, so your https://localhost (and other hosts) will be disabled. Also 'strict-dynamic' cancels 'unsafe-inline' ('nonce-value' and 'hash-value' cancel it too). Probably you do not sign inline scripts with nonce='server_generated_nonce' attribute. Or you do use scripts calls incompatible with 'strict-dynamic' (parser-inserted scripts, inline event handlers etc)
You have to revise Content Security Policy rules, they are inconsistent.

How can I cache data from API to Cache Storage in React PWA?

I built a Progressive Web App with ReactJS and have an issue. I am using mockApi to fetch the data.
When offline, my app doesn't work since the service worker only caches static assets.
How can I save HTTP GET calls from mockApi into the cache storage?
Along with your static assets, you can define which URLs do you want to cache:
var CACHE_NAME = 'my-cache_name';
var targetsToCache = [
'/styles/myStyles.scss',
'www.stackoverflow.com/'
];
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
return cache.addAll(targetsToCache);
})
);
});
Then you have to instruct your service worker to intercept the network requests and see if there is a match with the addresses in the targetsToCache:
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(response) {
// This returns the previously cached response
// or fetch a new once if not already in the cache
return response || fetch(event.request);
})
);
});
I wrote a series of articles about Progressive Web Apps if you are interested in learning more about it.

Add Service Worker to Create React App Project

I am installing a notification system called Subscribers with a project built with create react app. They ask the developer to download their service worker and include it in the root directory of the project. I have never had to install / use a service worker before, which is most likely the root of my misunderstanding.
How do you add a service worker into the root directory of a React project? The instructions say the service worker should appear in the root directory as https://yoursite.com/firebase-messaging-sw.js. In an attempt to register that URL, I included a service worker under src/index.js:
import Environment from './Environment'
export default function LocalServiceWorkerRegister() {
const swPath = `${Environment.getSelfDomain()}/firebase-messaging-sw.js`;
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register(swPath).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);
});
});
}
}
In production, I receive a 404 error. I have tried placing the firebase-messaging-sw.js file in the root directory, and under the src folder. Same error each time.
Here are the instructions from Subscribers:
https://subscribers.freshdesk.com/support/solutions/articles/35000013054-diy-installation-instructions
The public folder is the provided 'escape hatch' for adding assets from outside of the module system.
From the docs:
If you put a file into the public folder, it will not be processed by Webpack. Instead it will be copied into the build folder untouched. To reference assets in the public folder, you need to use a special variable called PUBLIC_URL
So, once copied to the public folder, you would reference the file like this:
import Environment from './Environment'
export default function LocalServiceWorkerRegister() {
const swPath = `${process.env.PUBLIC_URL}/firebase-messaging-sw.js`;
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register(swPath).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);
});
});
}
}

Resources