Why does the service worker download a page instead of navigating to it? - progressive-web-apps

I've got a strange scenario with a service worker where instead of navigating to a page it will download a page instead of navigating to it.
On the link that it's supposed to navigate to there's nothing special, it's an anchor tag with an href attribute pointing to a sign out URL.
The relevant part of the service worker follows:
self.addEventListener("fetch", (event) => {
if (event.request.mode === "navigate") {
// this part I added to circumvent the issue
if (event.request.url.match(/SignOut/)) {
return false;
}
event.respondWith(
(async () => {
try {
const networkResponse = await fetch(event.request);
return networkResponse;
} catch (error) {
// This section is irrelevant, I've confirmed it doesn't run when the error happens
}
})()
);
}
});
I couldn't find any information on why the browser would download the page instead of navigating to it, but it only happens when the service worker processes the /SignOut/ URL, and only on that URL that I have noticed. Any ideas?

Related

PWA - Cache won't update for offline use

I have a PWA which works fine both online and offline (but only with the initial files). However, the offline cache (let’s say a javascript file) is not being refreshed so whenever I am offline the old javascript file is used, but when online the new version is used.
On an iPad I can use Safari to go to the website and add the PWA to the home page.
If I then go offline, it works fine – all pages work etc.
But if I make a change to say a javascript file (something like adding an alert) and also change the version in my service worker, when I am online the change is reflected but when offline it remains at the older version
To clarify let’s say from the start, on going into a page it alerts “A1”
I then change the javascript to alert “A2” and change the version in the service worker.
If I run the app when online, sure enough the app says New Update Available and All Good (some alerts from the main.js file)
Then when I go into the actual page o the alert says “A2” – so all good.
Then go offline.
The alert still says “A1”
It seems that when online it uses the server latest files but when it tries to use cache the files are old and at the moment seem to be the original files.
I have read many sites on this with no success – some suggest it will sort itself in 24 hours. Some suggest setting the maxage of the service worker to 0 (but how do you do this?). Some say the files need renaming each time they change which seems very clunky.
The service worker is definitely working
main.js
$(document).ready(function () {
'use strict';
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register("/sw.js")
.then(res => {
console.log("service worker registered");
res.onupdatefound = () => {
const installingWorker = res.installing;
installingWorker.onstatechange = () => {
switch (installingWorker.state) {
case 'installed':
if (navigator.serviceWorker.controller){
alert("new update available");
forceReload();
}
else {
alert("all good");
}
break;
}
}
}
})
.catch(err => console.log("service worker not registered", err))
}
});
const forceReload = () =>{
console.log("ForceReload");
navigator.serviceWorker
.getRegistrations()
.then((registrations) =>{
console.log(registrations);
//alert("reg");
Promise.all(registrations.map((r) => r.unregister()))
caches.keys().then(function(names) {
for (let name of names)
caches.delete(name);
});
},
)
.then(() => {setTimeout(() => {
location.reload();
}, 500);
})
}
sw.js
let version =5; // update this to send update.
var cacheName = 'cacheV5'
var filesToCache = [
'/',
'/manifest.json',
'/index.html',
'/sales10.html',
'/getdata.html',
....
....
'/js/siteJS/sales10.js',
'/js/siteJS/getdata.js',
'/js/jquery/3.4.1/jquery.min.js',
'/js/bootstrap/bootstrap.min.js',
'/js/bootstrap/popper.min.js'
];
/* Start the service worker and cache all of the app's content */
self.addEventListener('install', function(e) {
self.skipWaiting();
e.waitUntil(
caches.open(cacheName).then(function(cache) {
return cache.addAll(filesToCache);
})
);
});
/* Serve cached content when offline */
self.addEventListener('fetch', function(e) {
e.respondWith(
caches.match(stripQueryStringAndHashFromPath(e.request.url.replace(/^.*\/\/[^\/]+/, ''))).then(function(response) {
return response || fetch(e.request);
})
);
});
function stripQueryStringAndHashFromPath(url) { //added this so when url paramerters passed grabbing the cashed js works
return url.split("?")[0].split("#")[0];
}
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.filter(function(cacheName) {
return true;
}).map(function(cacheName) {
return caches.delete(cacheName);
})
);
})
);
});

Improving PWA Page Load

I have a PWA, which is essentially a book reader. As a result, it needs lots of data (viz. the book text) to operate. When analyzed by Lighthouse, it scores poorly on the Page Load Check.
My question is: What methods could I employ to improve the page load, while still ensuring offline functionality?
I could have a minimal start page (e.g., just display a 'Please wait, downloading text' message) and then dynamically download (via injected script tag or AJAX) the JSON data file. However, I'm not sure how I would subsequently ensure that the data is fetched from the cache.
Just wondering how others have handled this issue...
Since this question has gone tumbleweed, I decided to post the results of my attempts.
Based on Jake's article, I used the following script and Chrome DevTools to study service worker events:
'use strict';
let container = null;
let updateFound = false;
let newInstall = false;
window.onload = () => {
container = document.querySelector('.container');
let loading = document.createElement('div');
loading.classList.add('loading');
loading.innerHTML = 'Downloading application.<br>Please wait...';
container.appendChild(loading);
console.log(`window.onload: ${Date.now()}`);
swEvents();
};
let swEvents = () => {
if (navigator.serviceWorker) {
navigator.serviceWorker.ready.then(() => {
console.log(`sw.ready: ${Date.now()}`);
if (!updateFound) {
loadApp();
return;
}
newInstall = true;
console.log(`new install: ${Date.now()}`);
}).catch((error) => {
console.log(`sw.ready error: ${error.message}`);
});
}
navigator.serviceWorker.register('/sw.js').then((reg) => {
reg.onupdatefound = () => {
updateFound = true;
console.log(`reg.updatefound: ${Date.now()}`);
const newWorker = reg.installing;
newWorker.onstatechange = (event) => {
if (event.target.state === 'activated') {
console.log(`nw.activated: ${Date.now()}`);
if (newInstall) {
loadApp();
return;
}
refresh();
}
};
};
}).catch((error) => {
console.log(`reg.error: ${error.message}`);
});
};
let refresh = () => {
console.log(`refresh(): ${Date.now()}`);
// window.location.reload(true);
};
let loadApp = () => {
console.log(`loadApp(): ${Date.now()}`);
let child;
while (child = container.firstChild) {
container.removeChild(child);
}
let message = document.createComment('p');
message.textContent = 'Application loading';
container.appendChild(message);
let tag = document.createElement('script');
tag.src = './app.js';
document.body.appendChild(tag);
};
Along the way, I learned that once a service worker is registered, it immediately begins downloading all cached resources. I had assumed that resources were cached only after the page loaded them. I also found some definitive event patterns to indicate which lifecycle phase was occurring.
For a new install, the following events are logged in the above script:
window.onload -> reg.updatefound -> sw.ready -> nw.activated
For this case, when sw.ready fires, all resources have been cached. At this point, I can switch the app from the 'please wait' phase and dynamically load the cached resources and start the app.
For a simple page refresh, the following events are logged:
window.onload -> sw.ready
This will be the event sequence if the app has already been downloaded and no updates are available. At this point, I can again switch phase and start the app.
For a page refresh when the service worker script has been updated, the following events are logged:
window.onload -> sw.ready -> reg.updatefound -> nw.activated
In this case, when nw.activated fires, all cached resources have been updated. Another page refresh is required to actually load the changes. At this point, the user could be prompted to update. Or the app would update on its own the next time it was started.
By tracking these event patterns, it is easy to tell which lifecycle phase the service worker is in and take the appropriate action.

ionic reload a page from another page/modal

EDIT
I noticed that the subscribe event must come first before and publish get called. But it will be silly to ask user to open TabOut page every time when app start.
I do not need to always reloading the TabOut page, so I need this event sort of method to do the job. Or else could've just call the reload on ionViewDidEnter().
I have 2 Tabs and 1 modal. /TabIn, /TabOut, and /ModalIn.
The Tabs page serve as data listing which display the data from database on ionViewDidLoad().
The ModalIn page serve as data entry for the user to key in and submit data. This page resides in the TabIn page and will get called when user clicked on each of the list of data.
After successfully submit the form in the ModalIn page I want to call refresh again on the TabOut page (no matter it has been loaded before or not). I tried using events publish it is not working. Below are my code.
ModalIn .ts
let headers: any = new HttpHeaders({ 'Content-Type': 'application/x-www-form-urlencoded' }),
options: any = { "username": val, formValue },
url: any = "some_url_here";
this.http.post(url, options, headers)
.subscribe((data: any) => {
if (data.status == 'success') {
this.events.publish('shouldReloadData');
} else {
}
},
(error: any) => {
console.log(error);
});
TabOut .ts
constructor(public events: Events) {
events.subscribe('shouldReloadData', () => {
// Reload the page here
console.log("should reloadddd"); // <- This is not working
});
}
Call subscribe with this keyword inside TabOut.ts.
constructor(public events: Events) {
this.events.subscribe('shouldReloadData', () => {
});
}

VueJS: Redirecting between two pages

I use Firebase with VueJS (and VueRouter).
I have a problem with redirecting. I want to redirect between two pages. The first page is used for authentication and the second one for content that should only be visible to logged-in users.
My state holds the firebase user key (which will be populated through a mutation, that calls firebase):
state: {
user: { key: null }
}
The authentication page these lines:
beforeCreate() {
if (this.$store.state.user.key !== null) {
this.$router.replace('/')
}
}
And the secret page these:
beforeCreate() {
if (this.$store.state.user.key === null) {
this.$router.replace('/new')
}
}
But: the redirect from the authentication page to the secret page doesn't take place.
My Vue-dev-tools show that the user-key is set.
What could be the solution to this problem?
EDIT:
This is the mutation that calls Firebase and sets the user-key:
updateSession(state) {
auth.onAuthStateChanged((user) => {
if (user) {
state.user.key = user.uid
}
})
}
Here is the action:
UPDATE_SESSION({ commit }) {
commit('updateSession')
}
I call the action in my root component (App.vue):
beforeCreate() {
this.$store.dispatch('UPDATE_SESSION')
}
EDIT 2:
Now my routes array:
routes: [
{ path: '/', component: Secret },
{ path: '/new', component: Authentication }
]
Take a look at the Per-Route Guards section of the docs: https://router.vuejs.org/en/advanced/navigation-guards.html
You might want to try something like the below. By putting the beforeEnter guard on the route, you are telling Vue to do that first. The next argument tells VueRouter what to do next, and can redirect if needed or continue on to the original route.
beforeEnter(to, from, next) {
if (this.$store.state.user.key === null) {
next('/new')
}
}
EDIT
You may also want to try using push instead of replace
As per the conversation we had in the comments looks like you require this:
store.dispatch can handle Promise returned by the triggered action handler and it also returns Promise. See docs.
So you can setup the login action to retirn a promise like this:
a_logInUser: ({state, commit}, userInput) => {
return new Promise((resolve, reject) => {
firebase.auth().signInWithEmailAndPassword(userInput.email, userInput.paswword);
resolve();
});
}
Then in your authentication page where you tale the login input details and click the login button , set this up as the click handler of your login button
loginUser(){
this.$store.dispatch('a_logInUser', {email: this.email, password: this.password})
.then((result) => {
this.$router.replace('/');
}, (err) => {
// stay on this pageS
//handle login error
});
}
}

How do I know that I'm still on the correct page when an async callback returns?

I'm building a Metro app using the single-page navigation model. On one of my pages I start an async ajax request that fetches some information. When the request returns I want to insert the received information into the displayed page.
For example:
WinJS.UI.Pages.define("/showstuff.html", {
processed: function (element, options) {
WinJS.xhr(...).done(function (result) {
element.querySelector('#target').innerText = result.responseText;
});
}
};
But how do I know that the user hasn't navigated away from the page in the meantime? It doesn't make sense to try to insert the text on a different page, so how can I make sure that the page that was loading when the request started is still active?
You can compare the pages URI with the current WinJS.Navigation.location to check if you are still on the page. You can use Windows.Foundation.Uri to pull the path from the pages URI to do this.
WinJS.UI.Pages.define("/showstuff.html", {
processed: function (element, options) {
var page = this;
WinJS.xhr(...).done(function (result) {
if (new Windows.Foundation.Uri(page.uri).path !== WinJS.Navigation.location)
return;
element.querySelector('#target').innerText = result.responseText;
});
}
};
I couldn't find an official way to do this, so I implemented a workaround.
WinJS.Navigation provides events that are fired on navigation. I used the navigating event to build a simple class that keeps track of page views:
var PageViewManager = WinJS.Class.define(
function () {
this.current = 0;
WinJS.Navigation.addEventListener('navigating',
this._handleNavigating.bind(this));
}, {
_handleNavigating: function (eventInfo) {
this.current++;
}
});
Application.pageViews = new PageViewManager();
The class increments a counter each time the user starts a new navigation.
With that counter, the Ajax request can check if any navigation occurred and react accordingly:
WinJS.UI.Pages.define("/showstuff.html", {
processed: function (element, options) {
var pageview = Application.pageViews.current;
WinJS.xhr(...).done(function (result) {
if (Application.pageViews.current != pageview)
return;
element.querySelector('#target').innerText = result.responseText;
});
}
};