PWA - Frontend doesn't update even after a hard refresh - javascript

Below is the configuration of our nuxt and nuxt-pwa configuration.
Nuxt pwa is recognising a new version available and we prompt user to do a hard refresh/reload.
And on reload - new UI also starts to work.
However, if we open the site in a new tab. A spinner gets shown & the latest frontend fails to load. And again, a hard refresh is required.
our frontend redirects to /dashboard on visiting localhost:8080 by default and this is getting loaded from serviceworker with cached data.
Please help us resolve this since this has been a critical issue for us.
Spinner seen on new tab opening :
export default {
ssr: false,
target: 'static',
head: {
titleTemplate: '',
title: 'NocoDB',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: process.env.npm_package_description || '' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: './favicon-32.png' }
]
},
plugins: [
// plugins
],
buildModules: [
'#nuxtjs/vuetify',
'#nuxtjs/pwa'
],
modules: [
// Doc: https://axios.nuxtjs.org/usage
'#nuxtjs/axios',
'vue-github-buttons/nuxt',
'#nuxtjs/toast'
],
axios: {
baseURL: process.env.NC_BACKEND_URL || (process.env.NODE_ENV === 'production' ? '..' : 'http://localhost:8080')
},
router: {
mode: 'hash',
base: process.env.NODE_ENV === 'production' ? './' : '',
middleware: ['auth']
},
vuetify: {
defaultAssets: {
icons: false
},
optionsPath: '#/config/vuetify.options.js',
treeShake: true,
customVariables: ['./config/variables.scss']
},
build: {
parallel: true,
plugins: [
new MonacoEditorWebpackPlugin({
languages: ['sql', 'json', 'javascript'],
features: ['!gotoSymbol']
})
],
extend(config, { isDev, isClient }) {
if (isDev) {
config.devtool = isClient ? 'source-map' : 'inline-source-map'
}
config.externals = config.externals || {}
config.externals['#microsoft/typescript-etw'] = 'FakeModule'
return config
}
},
pwa: {
workbox: {
assetsURLPattern: /\/_nuxt\//,
config: { debug: true }
},
icon: {
publicPath: './'
},
manifest: {
name: 'NocoDB',
start_url: '../?standalone=true',
theme_color: '#ffffff'
}
}
}
Lighthouse report :
Github issue reference : https://github.com/nuxt-community/pwa-module/issues/501

You need to set service worker which clears cache based on versions.
Setup service-worker.js as below. and update LATEST_VERSION on changes you deploy.
// service-worker.js
importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");
workbox.core.setCacheNameDetails({ prefix: 'pwa' })
//Change this value every time before you build to update cache
const LATEST_VERSION = 'v1.0.1'
self.addEventListener('activate', (event) => {
console.log(`%c ${LATEST_VERSION} `, 'background: #ddd; color: #0000ff')
if (caches) {
caches.keys().then((arr) => {
arr.forEach((key) => {
if (key.indexOf('pwa-precache') < -1) {
caches.delete(key).then(() => console.log(`%c Cleared ${key}`, 'background: #333; color: #ff0000'))
} else {
caches.open(key).then((cache) => {
cache.match('version').then((res) => {
if (!res) {
cache.put('version', new Response(LATEST_VERSION, { status: 200, statusText: LATEST_VERSION }))
} else if (res.statusText !== LATEST_VERSION) {
caches.delete(key).then(() => console.log(`%c Cleared Cache ${LATEST_VERSION}`, 'background: #333; color: #ff0000'))
} else console.log(`%c Great you have the latest version ${LATEST_VERSION}`, 'background: #333; color: #00ff00')
})
})
}
})
})
}
})
workbox.core.skipWaiting();
workbox.core.clientsClaim();
self.__precacheManifest = [].concat(self.__precacheManifest || [])
// workbox.precaching.suppressWarnings()
workbox.precaching.precacheAndRoute(self.__precacheManifest, {})

Don't know for nuxt, but for angular we need to bump the version element of ngsw-config.json on each deploy (1, 2, 3, ...) and also call SwUpdate.checkForUpdate() on startup. Only then the new version of the app will be retrieved from the server.

Related

Displaying image component from inside a list. How do you do this?

I hope you could help me out.
Before going through the code, let me quickly explain what I want:
I have two components that I use for uploading and displaying images. I have FileResourceService that is used for uploading, and FileResourceImage which is used for storing and displaying the data. These work together with a v-model called profilePictureFileResourceId which basically just ties the images to specific users on the page, depending on who is logged on.
When displaying the image on a template, it is very straightforward. I just grab the FileResourceImage component and tie it with the v-model.
<file-resource-image v-model="form.user.profilePictureFileResourceId" can-upload style="width: 100px; height: 100px;" />
That is all very easy, but I have some pages where I use tables that contain information about my users, and I would like for the user’s profile images to actually be displayed in the table. Here is an example of a list used for the table.
fields() {
return [
{
key: "email",
label: this.$t('email'),
sortable: true,
template: {type: 'email'}
},
{
key: "name",
label: this.$t('name'),
sortable: true
},
{
key: 'type',
label: this.$t('type'),
formatter: type => this.$t(`model.user.types.${type}`),
sortable: true,
sortByFormatted: true,
filterByFormatted: true
},
{
key: 'status',
label: this.$t('status'),
formatter: type => this.$t(`model.user.status.${type}`),
sortable: true,
sortByFormatted: true,
filterByFormatted: true
},
{
key: "actions",
template: {
type: 'actions',
head: [
{
icon: 'fa-plus',
text: 'createUser',
placement: 'left',
to: `/users/add`,
if: () => this.$refs.areaAuthorizer.fullControl
}
],
cell: [
{
icon: 'fa-edit',
to: data => `/users/${data.item.id}/edit`
}
]
}
I know that I cannot just make an array that looks like this:
fields() {
return [
{
<file-resource-image v-model="form.user.profilePictureFileResourceId" can-upload />
}
]
}
So how would you make the component display from within in the list? I believe it can be done with props, but I am totally lost at what to do.
By the way, these are the two components I use for uploading and display. I thought I might as well show them, so you can get an idea of what they do.
For upload:
import axios from '#/config/axios';
import utils from '#/utils/utils';
export const fileResourceService = {
getFileResource(fileResourceId) {
return axios.get(`file/${fileResourceId}`);
},
getFileResourceFileContent(fileResourceId) {
return axios.get(`file/${fileResourceId}/download`, {responseType: 'arraybuffer', timeout: 0});
},
downloadFileResource(fileResourceId) {
return fileResourceService.getPublicDownloadToken(fileResourceId)
.then(result => fileResourceService.downloadPublicTokenFile(result.data));
},
downloadPublicTokenFile(fileResourcePublicDownloadTokenId) {
const tempLink = document.createElement('a');
tempLink.style.display = 'none';
tempLink.href =
`${axios.defaults.baseURL}/file/public/${fileResourcePublicDownloadTokenId}/download`;
tempLink.setAttribute('download', '');
document.body.appendChild(tempLink);
tempLink.click();
setTimeout(() => document.body.removeChild(tempLink), 0);
},
getPublicDownloadToken(fileResourceId) {
return axios.get(`file/${fileResourceId}/public-download-token`);
},
postFileResource(fileResource, file) {
return axios.post(`file`, utils.toFormData([
{name: 'fileResource', type: 'json', data: fileResource},
{name: 'file', data: file}
]), {timeout: 0});
}
};
Then we have the component that is used for DISPLAYING the images:
<template>
<div :style="style" #click="upload" style="cursor: pointer;">
<div v-if="url === null">
<i class="fas fa-camera"></i>
</div>
<div v-if="canUpload" class="overlay">
<i class="fas fa-images"></i>
</div>
</div>
</template>
<script>
import {fileResourceService} from '#/services/file-resource';
import utils from '#/utils/utils';
export default {
model: {
prop: 'fileResourceId',
event: 'update:fileResourceId'
},
props: {
fileResourceId: String,
canUpload: Boolean,
defaultIcon: {
type: String,
default: 'fas fa-camera'
}
},
data() {
return {
url: null
};
},
computed: {
style() {
return {
backgroundImage: this.url && `url(${this.url})`,
backgroundSize: 'contain',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat'
};
}
},
methods: {
upload() {
if(this.canUpload) {
utils.openFileDialog()
.then(([file]) => fileResourceService.postFileResource({}, file))
.then(result => this.$emit('update:fileResourceId', result.data.id))
.catch(() => this.$bvModalExt.msgBoxError())
}
}
},
watch: {
fileResourceId: {
immediate: true,
handler() {
this.url = null;
if (this.fileResourceId) {
fileResourceService.getFileResourceFileContent(this.fileResourceId).then(result => {
const reader = new FileReader();
reader.onload = event => this.url = event.target.result;
reader.readAsDataURL(new Blob([result.data]));
});
}
}
}
}
};
</script>

Add RTL condition in head with Nuxt Js i18n

I would like to add a condition as follows
if(app.i18n.locale == 'ar')
I use Arabic with English in this project. If the current language is Arabic, bootstrap-rtl.css is added in the head, and if the current language en is called bootstrap.css, I have tried more than one method and did not succeed.
Add a file in plugins folder for example direction-control.js
export default ({ app }, inject) => {
const dir = () =>
app.i18n.locales.find((x) => x.code === app.i18n.locale)?.dir;
inject('dir', dir);
};
While your nuxt.config.js will need similar code to this
plugins: [
'~/plugins/direction-control', // your plugins file name
// other plugins
],
modules: [
[
'nuxt-i18n',
{
locales: [
{
code: 'en',
file: 'en.json',
dir: 'ltr',
name: 'English',
},
{
code: 'ar',
file: 'ar.json',
dir: 'rtl', // that will be passed to your app
name: 'عربي',
},
],
langDir: 'translations/',
defaultLocale: 'ar',
},
],
],
Now it's time to get dir
export default {
mounted() {
console.log(this.$dir()); // logs your direction 'ltr' or 'rtl'
console.log(this.$i18n.locale);
if (this.$i18n.locale == "ar") {
// make some action
}
},
}
or inside template like this
<template>
<div :dir="$dir()">
<Nuxt />
</div>
</template>

Fetching data from an API works locally but not live

First time building a site with Nuxt.js and I'm requesting data from an API that works great locally but when I access the site through Netlify, none of the code seems to run. I'm assuming this has something to do with my misunderstanding of how static sites work.
What I'm trying to do is check if there is a browser cookie, if not create one and fetch the API to get the user's general location and check their city against an array of cities in NY. If there is a match then close the v-banner. If there is already a browser cookie then close the v-banner.
Abridged index.vue:
<template>
<v-app v-bind:class="{ alertOpen: alertOpen }">
<v-main>
<v-banner
class="state-alert"
transition="slide-y-transition"
v-bind:class="{ alertOpen: alertOpen }"
>
By our calculations, it looks like you might be visiting our website from outside of New York. Unfortunately at this time, we can't sell our Granola outside of New York. If you are buying a gift for someone with a New York address then please proceed.
<template>
<v-btn
icon
color="alert"
v-on:click="alertOpen = false"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</template>
</v-banner>
</v-main>
</v-app>
</template>
<script>
let nyList = ['Albany', 'Amsterdam', 'Auburn', 'Batavia', 'Beacon', 'Binghamton', 'Buffalo', 'Canandaigua', 'Cohoes', 'Corning', 'Cortland', 'Dunkirk', 'Elmira', 'Fulton', 'Geneva', 'Glen Cove', 'Glens Falls', 'Gloversville', 'Hornell', 'Hudson', 'Ithaca', 'Jamestown', 'Johnstown', 'Kingston', 'Lackawanna', 'Little Falls', 'Lockport', 'Long Beach', 'Mechanicville', 'Middletown', 'Mount Vernon', 'New Rochelle', 'New York', 'Newburgh', 'Niagara Falls', 'North Tonawanda', 'Norwich', 'Ogdensburg', 'Olean', 'Oneida', 'Oneonta', 'Oswego', 'Peekskill', 'Plattsburgh', 'Port Jervis', 'Poughkeepsie', 'Rensselaer', 'Rochester', 'Rome', 'Rye', 'Salamanca', 'Saratoga Springs', 'Schenectady', 'Sherrill', 'Syracuse', 'Tonawanda', 'Troy', 'Utica', 'Watertown', 'Watervliet', 'White Plains', 'Yonkers'];
export default ({
head() {
return {
script: [{
body: true
}]
}
},
data() {
return {
geoData: [],
alertOpen: true
}
},
async fetch() {
const locationCookie = this.$cookies.get('location-cookie');
if(!locationCookie) {
console.log('no cookie');
this.$cookies.set('location-cookie', 'true', {
path: '/',
maxAge: 31556952
});
this.geoData = await fetch(
'https://ipgeolocation.abstractapi.com/v1/?api_key=1eef312cdda9428cac26815c9d3bdd26'
).then(res => res.json());
var vm = this;
compareCity(this.geoData.city);
function compareCity(city) {
var i;
for(i = 0; i < nyList.length; i++) {
if(nyList[i].toLowerCase().replace(/\s/g, '') == city.toLowerCase().replace(/\s/g, '')) {
console.log(city);
vm.alertOpen = false;
}
}
}
} else {
console.log('yes cookie');
this.alertOpen = false;
}
}
});
</script>
Not sure if ya'll also need to see the nuxt.config.js:
export default {
mode: "universal",
// Global page headers (https://go.nuxtjs.dev/config-head)
head: {
title: 'homemade-crunch',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '' }
],
link: [
{ rel: "preconnect", href: "https://app.snipcart.com" },
{ rel: "preconnect", href: "https://cdn.snipcart.com" },
{ rel: 'stylesheet', href: 'https://cdn.snipcart.com/themes/v3.0.23/default/snipcart.css', defer: true },
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Anton&family=Open+Sans&display=swap' }
],
script: [
{ src: "https://cdn.snipcart.com/themes/v3.0/default/snipcart.js", defer: true }
]
},
// Global CSS (https://go.nuxtjs.dev/config-css)
css: [
],
// Plugins to run before rendering page (https://go.nuxtjs.dev/config-plugins)
plugins: [
'#plugins/vuetify'
],
// Auto import components (https://go.nuxtjs.dev/config-components)
components: true,
// Modules for dev and build (recommended) (https://go.nuxtjs.dev/config-modules)
buildModules: [],
// Modules (https://go.nuxtjs.dev/config-modules)
modules: [
'vue-scrollto/nuxt',
'cookie-universal-nuxt'
],
// Build Configuration (https://go.nuxtjs.dev/config-build)
build: {
},
server: {
port: 3333,
host: '0.0.0.0'
}
}
The git repo: https://github.com/mat148/Homemade-crunch
and live site: https://www.homemadecrunch.com/
Let me know if I've forgotten something or done something silly.
Any help would be greatly appreciated.
Thank you!
I decided to move my logic from the index.vue to a javascript file

What should my next.config.js file look like when exporting static HTML and using next-sass plugin?

I am trying to render my Next.js site to static HTML, but I don't know how to add both exportPathMap and the next-sass plugin to module.exports.
Here is my current next.config.js file:
module.exports = {
exportPathMap: async function (defaultPathMap) {
return {
'/': { page: '/' },
'/releases': { page: '/releases' },
'/release?=schlagen': { page: '/release', query: { slug: 'schlagen' }},
'/release?=nicholas-k-pt-1': { page: '/release', query: { slug: 'nicholas-k-pt-1' }},
'/release?=static-and-shades': { page: '/release', query: { slug: 'static-and-shades' }},
'/release?=digital-romance': { page: '/release', query: { slug: 'digital-romance' }},
'/release?=2011': { page: '/release', query: { slug: '2011' }},
'/artwork': { page: '/artwork' },
'/artwork/weekend-mixtapes': { page: '/weekendMixtapes' },
'/artwork/posters': { page: '/posters' }
}
}
}
const withSass = require('#zeit/next-sass')
module.exports = withSass()
How can I add both to my configuration?
According to the documentation at https://nextjs.org/docs/#customizing-babel-config
Multiple configurations can be combined together with function
composition. For example:
module.exports = (phase, {defaultConfig}) => {
return withSass({
exportPathMap: ...
})
}

Configure durandal for multiple SPA

I have a site, which technically consists of a number of separate SPAs.
App/Admin
App/Signup
App/MainApp
How should I organize the App folder and the gulp build to make this into 3-4 separate files / modules that are loaded when required.
Since I had to spend a whole day getting this to work, I will answer my own question.
First, prepare a loader site. This would be the main site that is actually loaded that is responsible from loading all the other sites.
App/main.js - Contains the requirejs setup
In this we would run app.setRoot('shell', 'entrance');
App/shell.js - contains the router config
return router.map([
{ route: 'Admin/*all', moduleId: "Admin/index", nav: true },
{ route: 'Signup/*all', moduleId: "Signup/index", nav: true },
{ route: '*all', moduleId: "MainApp/index", nav: true }
])
.buildNavigationModel()
.activate();
App/shell.html - contains the router usage and common header/footer/loading shield
<div style="height: 100%" data-bind="css: { loading: isLoading }">
<!-- ko compose: 'loading.html'--><!-- /ko -->
<div class="header">...</div>
<!-- ko router: { transition:'entrance' } --><!-- /ko -->
</div>
App/*.js - Files shared among all the SPAs
App/MainApp/index.js - Contains the sub router for that SPA
define(["require", "exports", "plugins/router"], function (require, exports, router) {
"use strict";
var childRouter = router.createChildRouter()
.makeRelative({
moduleId: 'MainApp',
fromParent: false //<!----- FALSE for MainApp, TRUE for other SPA
})
.map([
{ route: '', moduleId: "viewmodels/test", nav: true },
])
.mapUnknownRoutes("viewmodels/err", 'err')
.buildNavigationModel();
return (function () {
function class_1() {
this.router = childRouter;
}
return class_1;
}());
});
App/MainApp/index.html - Uses the router, and performs any rendering of submenues or whatever. Here the bare minimum:
<div data-bind="router: { }"></div>
App/Admin/index.js
Is identical to the MainApp.js, except for the fromParent set to true and moduleId set to the SPAs name.
This should work fine in debug mode now.
The gulp setup becomes:
gulpfile.js
var durandal = require('gulp-durandal');
gulp.task('build', [], function () {
return gulp.start(['build-admin', 'build-signup', 'build-mainapp', 'build-base']);
});
function buildModule(name) {
var path = name + '/';
return durandal({
baseDir: 'App',
output: name + '.js',
minify: true,
almond: false,
verbose: false,
moduleFilter: function (moduleName) {
return moduleName.indexOf(path) > -1;
},
rjsConfigAdapter: function (config) {
config.stubModules = config.exclude;
config.exclude = ["main"];
config.stubModules.push('text');
return config;
}
})
.pipe(gulp.dest('dist'));
}
gulp.task('build-admin', [], function () {
return buildModule('Admin');
});
gulp.task('build-signup', [], function () {
return buildModule('Signup');
});
gulp.task('build-mainapp', [], function () {
return buildModule('MainApp');
});
gulp.task('build-base', [], function () {
return durandal({
baseDir: 'App',
output: 'main.js',
minify: true,
verbose: false,
moduleFilter: function (moduleName) {
return moduleName.indexOf('Admin/') === -1 && moduleName.indexOf('MainApp/') === -1 && moduleName.indexOf('Signup/') === -1;
},
rjsConfigAdapter: function (config) {
config.stubModules = config.exclude;
config.exclude = [];
return config;
}
})
.pipe(gulp.dest('dist'));
});
This will now result in
dist/main.js
dist/Admin.js
dist/MainApp.js
dist/Signup.js
In your default.html (or whatever you call it), you need to tell it how to load these files:
<script>window.require = {
bundles: { 'MainApp': ['MainApp/index'],'Signup': ['Signup/index'], 'Admin': ['Admin/index'] }
};</script>
<script type="text/javascript" data-main="main" src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.2.0/require.min.js"></script>
The reason for this being in the html files rather than the main.js, is that the bundles are only used for the compiled version.

Categories

Resources