Workbox webpack plugin display offline screen - javascript

I'm using workbox-webpack-plugin to register service worker.
My frontend app is react-redux app configured with webpack. If you visit app url, you can always see login view.
My plugin inside webpack.config.js:
new InjectManifest({
swSrc: path.join('src', 'service-worker.js')
})
Service worker:
workbox.skipWaiting();
workbox.clientsClaim();
workbox.precaching.precacheAndRoute(self.__precacheManifest);
My service worker caches all my splitted routes. But that doesn't matter - even if they all are cached, when user without connection visits my app, he cannot login. That's why I need a way to check if user is in offline mode, and instead of returning login, return 'offline.html' page.
I found out that my env.config.js file (which contains API URLS and is requested on login page) is not cached, so I think it would be easy to catch error while not getting this file. So I added following in my service worker:
workbox.routing.registerRoute(
new RegExp('/env.config.js'),
({event}) => {
return networkFirstHandler.handle({event})
.catch(() => caches.match('/offline.html'));
}
);
But it doesn't return offline.html in browser. It seems like 'offline.html' file is returned instead of 'env.config.js' file.
How to accomplish this? I'm new to workbox plugin and it would be great to see some suggestions.

importScripts("/precache-manifest.81b400bbc7dc89de30f4854961b64d1d.js", "https://storage.googleapis.com/workbox-cdn/releases/3.4.1/workbox-sw.js");
workbox.skipWaiting();
workbox.clientsClaim();
const STATIC_FILES = [
'/env.config.js',
];
self.__precacheManifest = STATIC_FILES.concat(self.__precacheManifest || []);
workbox.precaching.precacheAndRoute(self.__precacheManifest);
Update - since I decided to cache env.config.js file I'm only getting API error while using app offline. Maybe this API call (which returns error because of no connection) is a good trigger to display offline page? I think it is, but I still don't know.
When I try something like this:
workbox.routing.registerRoute(
new RegExp(API_REGEX_GOES_HERE),
({event}) => {
return networkFirstHandler.handle({event})
.catch(() => caches.match('/offline.html'));
}
);
The "offline.html" page will be returned instead of API request. So it will not be displayed like a page...

Related

404 Page not found Vercel Deployment - using dynamic routing in Next JS

I am making a full-stack web-application using Next JS where I allow the user to create and manage letters (applications) based on pre-defined templates. So when the user successfully creates an application, it is sent to the database (POSTGRES) which is hosted on Supabase. On the home page, the applications created by the user are fetched and displayed in the form of a list. Here on, when the user chooses to preview an application, dynamic routing is put in place where the application IDs work as the dynamic parameter. By using getStaticPaths() to get the route parameters from the database, and then fetching the data for the page from the database based on the application ID in the getStaticProps() method at build time, we render the page. It works seamlessly on localhost but not on Vercel. The interesting part however is,that dynamic routing works on Vercel for past applications for every deployment, that is if the user wants to preview their past applications they can do so without any problem, but when they create an application and then try to preview it, they are prompted with the 404 error. But if I trigger a redeployment either manually or by a commit to the main branch of my repository, the error is fixed for the particular application which was giving the error. `
export const getStaticPaths = async () => {
var APIendpoint;
if (process.env.NODE_ENV === 'development') {
APIendpoint = 'http://localhost:3000/api/fetchApplication'
}
else if (process.env.NODE_ENV === 'production') {
APIendpoint = 'https://templaterepo.vercel.app/api/fetchApplication';
}
const data = await getPaths(APIendpoint)
const paths = data.map((application) => {
return {
params: { id: application.appid.toString() }
}
})
return {
paths,
fallback: 'blocking'
}
}
export async function getStaticProps(context) {
const appID = context.params.id;
var APIendpoint;
if (process.env.NODE_ENV === 'development') {
APIendpoint = 'http://localhost:3000/api/fetchApplicationwithID'
}
else if (process.env.NODE_ENV === 'production') {
APIendpoint = 'https://templaterepo.vercel.app/api/fetchApplicationwithID';
}
let data = await getPageData(APIendpoint, appID);
return {
props: { data }
}
}
Here is the code for the dynamic [id].js page where in I first get the paths based on the application IDs and then in the getStaticProps() function I fetch the data for the page corresponding to the application ID at build time. It works as expected in localhost but in Vercel deployment, even before the functions are executed, I get a 404 error.
Note: Vercel Framework Preset is set to Next.js.
I tried a variety of solutions including adding to and as parameters in the Link component. Also I changed my vercel.json file to the below configuration
`
{
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}
`
But nothing seems to work.
When they create an application and then try to preview it, they are
prompted with the 404 error. But if I trigger a redeployment either
manually or by a commit to the main branch of my repository, the error
is fixed for the particular application which was giving the error.
This is expected, the data necessary for each dynamic page to be built is fetched ONLY at build time. Since you are using getStaticProps, you can implement ISR by adding a revalidate prop in getStaticProps, that way when a page (like a new application) has not been generated at build-time, Next.js will server-render it on first request and then cache it for subsequent requests.
On development mode, both getStaticProps and getStaticPaths run per-request (much like getServerSideProps), that's why you don't have this issue on the dev environment. Reference to this on the docs.
If you decide to implement ISR and want to display a loading UI while the page is being server-rendered, make sure to set fallback: true on getStaticPaths and at component level, you can access the router.isFallback flag to display the loading UI accordingly, otherwise, leave it as you already have with fallback: 'blocking'.
Also, make sure you write the server-side code directly in getStaticProps and getStaticPaths instead of calling your own API endpoints on these functions. This according to the docs.

Dynamic Cache Loading in Service Worker

Good day StackOverflow!
i got a concern regarding my sw. when opening the page. I dont know how to explain this using technical terms so i will just explain thin based on my observation.
When i open a new home page, it loads the page and saves the cache. when i login to the website, the page changes as it should, generating the image, username, etc. So far so good. When i logout, my expected page should be the default homepage but it still displaying the page like when a user log in to the site.
this happens on my local server, remote pc, and smart phones(both on web page and PWA app)
If i delete the generated cache, it returns to normal. What should I do in this situation?
here is my sw.js file:
const dynamicCacheName = 'ver-v1';
// activate event
self.addEventListener('activate', evt => {
evt.waitUntil(
caches.keys().then(keys => {
return Promise.all(keys
.filter(key => key !== dynamicCacheName)
.map(key => caches.delete(key))
);
})
);
});
// fetch event
self.addEventListener('fetch', evt => {
evt.respondWith(
caches.match(evt.request).then(cacheRes => {
return cacheRes || fetch(evt.request).then(fetchRes => {
return caches.open(dynamicCacheName).then(cache => {
cache.put(evt.request.url, fetchRes.clone());
return fetchRes;
})
});
})
);
});
You are caching the authenticated page. You need to capture when the user logs out and clear the cached page and fetch the unauthenticated version.
There are many, more complex ways to handle this scenario, but I have a feel a bit beyond where you are right now.
Just a hint. You can render and cache responses in your service worker. You can catch requests and serve different versions based on the request before they are sent to the network. For example, you can check to see if the Bearer token is in your request header. If so and it is current/valid then you serve the cached authenticated version, if not then the public version.
I do all sorts of things like this and more in my applications. Service worker is a fantastic tool, but it requires thinking differently than we did in the past and sometimes those concepts are difficult to grasp.
Good day. I used Workbox and now got it to work, but cache is too big to use.
This is what i did:
importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.4.1/workbox-sw.js');
if (workbox) {
const CACHE_PREFIX = 'v01';
workbox.setConfig({
clientsClaim: true,
debug: true,
skipWaiting: true
});
workbox.core.setLogLevel(workbox.core.LOG_LEVELS.warn);
// Javascript and CSS rule
workbox.routing.registerRoute(
new RegExp('^.*(?:js|css)'),
new workbox.strategies.NetworkFirst(
{ cacheName: `${CACHE_PREFIX}-asset` }
),
);
// Main rule
workbox.routing.registerRoute(
new RegExp('^.*(?:)'),
new workbox.strategies.NetworkFirst(
{ cacheName: `${CACHE_PREFIX}-main` }
),
);
} else {
console.log(`Boo! Workbox didn't load 😬`);
}

Can I catch 404s in a service worker?

Basically, I have an online app that uses a htaccess file to silently redirect all requests in a given /folder/ to the same html. Then, to decide what to show the user, the page calls
var page_name = location.href.split('/').pop();
This works well online, but could I use a ServiceWorker to support this folder/file model while the page is offline? Or will I always get the page cannot be found error unless I explicitly cache the URLs?
What you describe can be accomplished using the App Shell model.
Your service worker's exact code might look a little different, and tools like Workbox can automate some of this for you, but a very basic, "vanilla" example of a service worker that accomplishes this is:
self.addEvenListener('install', (event) => {
const cacheShell = async () => {
const cache = await caches.open('my-cache');
await cache.add('/shell.html');
};
event.waitUntil(cacheShell());
});
self.addEventListener('fetch', (event) => {
// If this is a navigation request...
if (event.request.mode === 'navigate') {
// ...respond with the cached shell HTML.
event.respondWith(caches.match('/shell.html'));
return;
}
// Any other caching/response logic can go here.
});
Regardless of what the location.href value is, when this service worker is in control, the App Shell HTML will be used to fulfill all navigation requests.

Service Worker: How to cache the first (dynamic) page

I have this one-page app with a dynamic URL built with a token, like example.com/XV252GTH and various assets, like css, favicon and such.
Here is how I register the Service Worker:
navigator.serviceWorker.register('sw.js');
And in said sw.js, I pre-cache the assets while installing:
var cacheName = 'v1';
var cacheAssets = [
'index.html',
'app.js',
'style.css',
'favicon.ico'
];
function precache() {
return caches.open(cacheName).then(function (cache) {
return cache.addAll(cacheAssets);
});
}
self.addEventListener('install', function(event) {
event.waitUntil(precache());
});
Note that the index.html (that registers the Service Worker) page is just a template, that gets populated on the server before being sent to the client ; so in this pre-caching phase, I'm only caching the template, not the page.
Now, in the fetch event, any requested resource that is not in the cache gets copied to it:
addEventListener('fetch', event => {
event.respondWith(async function() {
const cachedResponse = await caches.match(event.request);
if (cachedResponse) return cachedResponse;
return fetch(event.request).then(updateCache(event.request));
}());
});
Using this update function
function updateCache(request) {
return caches.open(cacheName).then(cache => {
return fetch(request).then(response => {
const resClone = response.clone();
if (response.status < 400)
return cache.put(request, resClone);
return response;
});
});
}
At this stage, all the assets are in the cache, but not the dynamically generated page. Only after a reload, can I see another entry in the cache: /XV252GTH. Now, the app is offline-ready ; But this reloading of the page kind of defeats the whole Service Worker purpose.
Question: How can I send the request (/XV252GTH) from the client (the page that registers the worker) to the SW? I guess I can set up a listener in sw.js
self.addEventListener('message', function(event){
updateCache(event.request)
});
But how can I be sure that it will be honored in time, ie: sent by the client after the SW has finished installing? What is a good practice in this case?
OK, I got the answer from this page: To cache the very page that registers the worker at activation time, just list all the SW's clients, and get their URL (href attribute).
self.clients.matchAll({includeUncontrolled: true}).then(clients => {
for (const client of clients) {
updateCache(new URL(client.url).href);
}
});
Correct me if I understood you wrong!
You precache your files right here:
var cacheAssets = [
'index.html',
'app.js',
'style.css',
'favicon.ico'
];
function precache() {
return caches.open(cacheName).then(function (cache) {
return cache.addAll(cacheAssets);
});
}
It should be clear that you cache the template since you cache it before the site gets build and this approach is not wrong, at least not for all types of files.
Your favicon.ico for example is a file that you would probably consider as static. Also, it does not change very often or not at all and it isn't dynamic like your index.html.
Source
It should also be clear why you have the correct version after reloading the page since you have an update function.
The solution to this problem is the answer to your question:
How can I send the request (/XV252GTH) from the client (the page that registers the worker) to the SW?
Instead of caching it before the service-worker is installed you want to cache it if the back end built your web page. So here is how it works:
You have an empty cache or at least a cache without your index.html.
Normally a request would be sent to the server to get the index.html. Instead, we do a request to the cache and check if the index.html is in the cache, at least if you load the page for the first time.
Since there is no match in the cache, do a request to the server to fetch it. This is the same request the page would do if it would load the page normally. So the server builds your index.html and sends it back to the page.
After receiving the index.html load it to the page and store it in the cache.
An example method would be Stale-while-revalidate:
If there's a cached version available, use it, but fetch an update for next time.
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.open('mysite-dynamic').then(function(cache) {
return cache.match(event.request).then(function(response) {
var fetchPromise = fetch(event.request).then(function(networkResponse) {
cache.put(event.request, networkResponse.clone());
return networkResponse;
})
return response || fetchPromise;
})
})
);
});
Source
Those are the basics for your problem. Now you got a wide variety of options you can choose from that use the same method but have some additional features. Which one you choose is up to you and without knowing your project in detail no one can tell you which one to choose. You are also not limited to one option. In some cases you might combine two or more options together.
Google wrote a great guide about all the options you have and provided code examples for everything. They also explained your current version. Not every option will be interesting and relevant for you but I recommend you to read them all and read them thoroughly.
This is the way to go.

Create React App ServiceWorker.js Redirect When Offline

I have a React app created by using create-react-app. By default, this tool creates a serviceWorker.js file for us and I am using this to register a service-worker. Furthermore, the documents suggest using google's workbox wizard to create a service-worker.js used to manage my website for offline purposes. The goal is for me to store an offline.html page in the browsers cache and whenever there is no online connection, render the cached offline.html page.
I am successful in storing the offline.html in cache and as you can see below, it is stored in the precached URLS (check last two rows).
I can also manually navigate to the offline.html if i change the URL in my browser.
However, I am having trouble automatically grabbing this file and rendering it whenever there isn't a connection.
In the serviceWorker.js code that is generated for me from CRA theres a function called checkValidServiceWorker:
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.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.'
);
const OFFLINE_URL = '/.offline/offline.html';
return caches.match(OFFLINE_URL).then((response) => {
console.log(response)
});
});
}
So in the catch part of the function, I want to do my redirect because thats the logic that runs when we are offline. I read a lot of docs and my current solution doesn't work. Any ideas on how to redirect in my serviceWorker?

Categories

Resources