We build a flutter web app and deployed it via firebase hosting. Unfortunately, we didn't configure any caching settings in our initial deploy.
Now we deployed a newer version of our website but people still get the old website shown form the first deploy. What we tried so far:
Adding version no. to our index.html:
<"script src="main.dart.js?version=1" type="application/javascript"></script>
Adding meta Data in our header in index.html:
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
In our firebase.json we added the following headers:
"headers": [
{
"source": "**",
"headers": [
{
"key": "Cache-Control",
"value": "max-age=10"
}
]
}
]
All of these attempts were without any luck. We think that the problem is that the newer version doesn't have those entries in the files. How can we force this to update to our newest version? We even consider opening a new firebase project if that might help.
Try this:
In your flutter app, in the index.html within the web folder, add a version number after the src="main.dart.js
So your new line will look like this:
<script src="main.dart.js?version=1" type="application/javascript"></script>
Then increment the version number to 2, etc before you do each build.
update june 2021: add this line in index.html as the last script in the body
I see Flutter now include this script by default in index.html if project is created recently which has serviceWorkerVersion element which updates the version when compiling:
<script>
var serviceWorkerVersion = null;
var scriptLoaded = false;
function loadMainDartJs() {
if (scriptLoaded) {
return;
}
scriptLoaded = true;
var scriptTag = document.createElement('script');
scriptTag.src = 'main.dart.js';
scriptTag.type = 'application/javascript';
document.body.append(scriptTag);
}
if ('serviceWorker' in navigator) {
// Service workers are supported. Use them.
window.addEventListener('load', function () {
// Wait for registration to finish before dropping the <script> tag.
// Otherwise, the browser will load the script multiple times,
// potentially different versions.
var serviceWorkerUrl = 'flutter_service_worker.js?v=' + serviceWorkerVersion;
navigator.serviceWorker.register(serviceWorkerUrl)
.then((reg) => {
function waitForActivation(serviceWorker) {
serviceWorker.addEventListener('statechange', () => {
if (serviceWorker.state == 'activated') {
console.log('Installed new service worker.');
loadMainDartJs();
}
});
}
if (!reg.active && (reg.installing || reg.waiting)) {
// No active web worker and we have installed or are installing
// one for the first time. Simply wait for it to activate.
waitForActivation(reg.installing ?? reg.waiting);
} else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
// When the app updates the serviceWorkerVersion changes, so we
// need to ask the service worker to update.
console.log('New service worker available.');
reg.update();
waitForActivation(reg.installing);
} else {
// Existing service worker is still good.
console.log('Loading app from service worker.');
loadMainDartJs();
}
});
// If service worker doesn't succeed in a reasonable amount of time,
// fallback to plaint <script> tag.
setTimeout(() => {
if (!scriptLoaded) {
console.warn(
'Failed to load app from service worker. Falling back to plain <script> tag.',
);
loadMainDartJs();
}
}, 4000);
});
} else {
// Service workers not supported. Just drop the <script> tag.
loadMainDartJs();
}
If you do not want to use the server worker caching, you can provide --pwa-strategy=none. Otherwise, the app will update after the new version has been downloaded and the page revisited.
Example:
flutter build web --release --pwa-strategy=none
It will Generate a service worker with no body. This is useful for local testing or in cases where the service worker caching functionality is not desirable
By default, it is set to offline-first: Attempt to cache the application shell eagerly and then lazily cache all subsequent assets as they are loaded. When making a network request for an asset, the offline cache wil be preferred.
Reference:
https://github.com/flutter/flutter/issues/71333#issuecomment-736732314
flutter help build web
When you build flutter web then in directory build/web generated a version.json file.
just increase build_number and version in this file.
Related
I have a blog and I want to create a PWA version of it. I already created the manifest but I am a bit confused about the service worker settings. I have already created a service worker file but I am not sure if it will work fine. I used the guides on the Workbox website. I want my PWA to be capable of pre-cache, first network, cache the images/js/css/fonts etc., offline analytics, background, and periodic sync. Is my code ok for these features?
Here is my service worker code:
// Yapi Tasarim Akademisi Service Worker
importScripts('https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js');
workbox.setConfig({
debug: true,
});
import {
pageCache,
imageCache,
staticResourceCache,
googleFontsCache,
offlineFallback,
} from 'workbox-recipes';
pageCache();
googleFontsCache();
staticResourceCache();
imageCache();
offlineFallback();
// Background sync
import {BackgroundSyncPlugin} from 'workbox-background-sync';
import {registerRoute} from 'workbox-routing';
import {NetworkOnly} from 'workbox-strategies';
const bgSyncPlugin = new BackgroundSyncPlugin('myQueueName', {
maxRetentionTime: 24 * 60 // Retry for max of 24 Hours (specified in minutes)
});
registerRoute(
/\/api\/.*\/*.json/,
new NetworkOnly({
plugins: [bgSyncPlugin]
}),
'POST'
);
const statusPlugin = {
fetchDidSucceed: ({response}) => {
if (response.status >= 500) {
// Throwing anything here will trigger fetchDidFail.
throw new Error('Server error.');
}
// If it's not 5xx, use the response as-is.
return response;
},
};
// Add statusPlugin to the plugins array in your strategy.
// Offline Analytics
import * as googleAnalytics from 'workbox-google-analytics';
googleAnalytics.initialize();
// Pre-cache
import {precacheAndRoute} from 'workbox-precaching';
precacheAndRoute([
{url: '/index.html', revision: '383676' },
{url: '/styles/app.0c9a31.css', revision: null},
{url: '/scripts/app.0d5770.js', revision: null},
// ... other entries ...
]);
I am attempting to setup a WordPress Theme as a Progressive Web App. When I run Chromes Audit tool (lighthouse?) I get an uninformative error that I don't know what exactly the problem is. The error is:
Failures: Service worker does not successfully serve the manifest start_url. Unable to fetch start url via service worker
I have hardcoded my start url which is a valid url. Any suggestions on what the issue could be?
https://mywebsite.com/wp-content/themes/mytheme/web.manifest:
...
"scope": "/",
"start_url": "https://mywebsite.com",
"serviceworker": {
"src": "dist/assets/sw/service-worker.js",
"scope": "/aw/",
"update_via_cache": "none"
},
...
}
https://mywebsite.com/wp-content/themes/mytheme/dist/assets/sw/service-worker.js:
...
// 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;
});
});
});
})
);
}
});
I register my SW with the following code and it outputs that it has successfully registered the SW:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register(Vue.prototype.$ASSETS_PATH + 'sw/service-worker.js')
.then(function(registration) {
console.log('Registration successful, scope is:', registration.scope);
})
.catch(function(error) {
console.log('Service worker registration failed, error:', error);
});
}
Please change your start_url to
"start_url": "/"
It has to be a relative url. Please see the documentaion
From here https://developers.google.com/web/fundamentals/getting-started/primers/service-workers
If the service worker is at the root of the domain, this means
that the service worker's scope will be the entire origin.
But If we register the service worker file at /example/sw.js, then
the service worker would only see fetch events for pages whose
URL starts with /example/ (i.e. /example/page1/, /example/page2/).
Second point mentions only fetch won't work at / (root or other than example) if I place the service worker at /example/.
But subscription (generation of sub object) itself not getting generated if the service worker is at /example/ and if the web page is at / (root or other than example), which the doc clearly doesn't explain.
Please let me know, if even the generation of subscription (pushManager.getSubscription) in the service worker itself won't happen.
PS: I have tried it on Chrome 54.0.2840.100 Ubuntu 16.04 LTS
Generation of subscription object does not depend on service worker scope. You can do it any where
Eg.
permission.js
export function allowNotifications(scope){
if (navigator.serviceWorker && Notification){
if( Notification.permission !== "granted") {
navigator.serviceWorker.ready.then(function(reg) {
subscribe(reg);
});
}
}
}
function subscribe(reg) {
reg.pushManager.subscribe({userVisibleOnly: true}).then(function(pushSubscription) {
bindUserToDevice(JSON.stringify(pushSubscription));
}, function (err) {
console.log('error');
});
}
export function bindUserToDevice(subscriptionObj) {
// received subsciption object should be send to backend to bind the device for pushes
var data = {
type: 'POST',
url : '/bind',
body: subscriptionObj,
};
fetch('/bind', data);
}
allowNotifications function can be called from anywhere. Only the service worker file should be present on root which should have push event
global.addEventListener('push', function(event) {
var pushObj = event.data.json();
var pushData = pushObj.data;
var title = pushData && pushData.title;
var body = pushData && pushData.body;
var icon = '/img/logo.png';
event.waitUntil(global.registration.showNotification(title, {
body: body,
icon: icon,
data:pushData
}));
});
I am developing an Web Application that uses GCM for push notifications. I am following this documentation. I am having problem with initializeState function in docs.
This is how I added service worker script: I just created a service-worker.js empty file and pointed to this file in code to register service. Totally empty file.
This is my JavaScript code
<script type="text/javascript">
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register("{{ url('js/service-worker.js') }}").then(initialiseState).catch(function(error){
alert('Unable to register service worker for notification')
});
} else {
alert('Your browser does not support service worker that is used to receive push notification');
}
function subscribe()
{
alert('Subscribed')
}
function unsubscribe()
{
alert('Unsubscribed')
}
// the problem is inside this function. This function is called after service worker registration
function initialiseState() {
if (!('showNotification' in ServiceWorkerRegistration.prototype)) {
alert("Notification is not supported in this browser")
return;
}
if (Notification.permission === 'denied') {
alert('Notifications are blocked')
return;
}
if (!('PushManager' in window)) {
alert('Push messaging is not supported in thiss browser');
return;
}
// Every code working until here. I already checked using else if to statement.
// We need the service worker registration to check for a subscription
navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {
//The problem is here. It is not alerting anything
alert('Ready to check')
});
}
</script>
I commented in the code to highlight where the problem is. What I know is it should alert anything if it is working. So it is not alerting anything. So I cannot do other steps. I checked the console and it is not showing any error. Is it not working because my service-worker.js is empty? If not, how can I fix my code to work?
Hello I'm creating a application with ionic framework and cordova and want to use a sqlite database to store the data.
So how can I use a existing sqlite database for this purpose?
Where do I have to save the database so that cordova can find it and uses it if I compile for a platform?
Edit:
I get the following error if I try to open the db in the recommend way:
[Error] TypeError: undefined is not an object (evaluating 'n.sqlitePlugin.openDatabase')
openDB (ng-cordova.min.js, line 9)
(anonyme Funktion) (app.js, line 19)
(anonyme Funktion) (ionic.bundle.js, line 44243)
onPlatformReady (ionic.bundle.js, line 2396)
onWindowLoad (ionic.bundle.js, line 2375)
Here is the head section of my index.html:
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
<title></title>
<link href="lib/ionic/css/ionic.css" rel="stylesheet">
<link href="css/style.css" rel="stylesheet">
<!-- ionic/angularjs js -->
<script src="lib/ionic/js/ionic.bundle.js"></script>
<!-- cordova sqlite extenseion -->
<script src="js/ng-cordova.min.js"></script>
<!-- cordova script (this will be a 404 during development) -->
<script src="cordova.js"></script>
<!-- your app's js -->
<script src="js/app.js"></script>
<script src="js/controllers.js"></script>
</head>
My app.js looks like this:
//Database variable
var db = null;
angular.module('starter', ['ionic', 'starter.controllers', 'ngCordova'])
.run(function($ionicPlatform, $cordovaSQLite) {
$ionicPlatform.ready(function() {
// Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
// for form inputs)
if (window.cordova && window.cordova.plugins.Keyboard) {
cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
}
if (window.StatusBar) {
// org.apache.cordova.statusbar required
StatusBar.styleDefault();
}
db = $cordovaSQLite.openDB({ name: "db.spareParts" });
var query = "SELECT cars.name FROM cars";
$cordovaSQLite.execute(db, query).then(function(res) {
if(res.rows.length > 0) {
console.log("SELECTED -> " + res.rows.item(0).name);
} else {
console.log("No results found");
}
}, function (err) {
console.error(err);
});
});
})
The database db.spareParts is stored in the root www directory and is a sqlite3 database.
i fixed this problem after a very long suffering :)
the prepopulated database MUST be in the "dataDirectory" of your application
at first you need to check if the db file exists in that directory, if not, you must copy it from application directory (i.e. /www folder)
check this piece of code
$ionicPlatform.ready(function() {
src = cordova.file.dataDirectory + 'data.sqlite';
window.resolveLocalFileSystemURL(src, function () {
db = sqlitePlugin.openDatabase('data.sqlite');
}, function () {
$scope.copyDb('data.sqlite');
});
$scope.copyDb = function (dbName) {
var sourceFileName = cordova.file.applicationDirectory + 'www/' + dbName;
var targetDirName = cordova.file.dataDirectory;
return Promise.all([
new Promise(function (resolve, reject) {
resolveLocalFileSystemURL(sourceFileName, resolve, reject);
}),
new Promise(function (resolve, reject) {
resolveLocalFileSystemURL(targetDirName, resolve, reject);
})
]).then(function (files) {
var sourceFile = files[0];
var targetDir = files[1];
return new Promise(function (resolve, reject) {
targetDir.getFile(dbName, {}, resolve, reject);
}).then(function () {
console.log("file already copied");
}).catch(function () {
console.log("file doesn't exist, copying it");
return new Promise(function (resolve, reject) {
sourceFile.copyTo(targetDir, dbName, resolve, reject);
}).then(function () {
console.log("database file copied");
db = sqlitePlugin.openDatabase('data.sqlite');
});
});
});
};
check this link
prepopulated db demo
So how can I use a existing sqlite database for this purpose?
You can use the existing sqlite database by using the built-in WebSQL. You do not need to install any plugin. Please see the link here for more details.
Note that there is limitation to the amount of storage using the standard approach.(I think it is about 5MB).
If you want to store more than the allowed standard storage than you need to use the sqlite plugin. Link here for details.
Where do I have to save the database so that cordova can find it and uses it if I compile for a platform?
You do not need to specify the location path for storing the database. The database open function call will automatically creates and stores it in a convenient location.
Edit 1:
What I understand from your edited question is that you require a pre-populated sqlite3 db to be used for your application.
Copying your db to the www directory will not work. You need to copy the pre-populated db to the default location where the app will use it for your application. The location is different for different platforms (iOS, Android, Windows). Also within a platform the location path for the db storage is different. E.g for Android Jelly bean the location path is different to the one in Android KitKat. Hence as far as I know you will not a find a complete solution on the Web for copying the db to the default location of the app.
This is a completely a different proposition.
You need to search stackoverflow for pre populated sqlite with cordova to get useful answers.
What I have answered is to create a new db in your application from scratch.
Also relating to your error.
You need to check that the deviceready event is fired before calling any cordova/phonegap related code.
if(window.cordova) {
// App syntax
db = $cordovaSQLite.openDB("myapp.db");
} else {
// Ionic serve syntax
db = window.openDatabase("myapp.db", "1.0", "My app", -1);
}