Add a service worker and cache assets

Assets are served from the local cache preferably. They are fetched from
the network if not available. This new addition also enables the "Add to
homescreen" in Chrome/Chromium.

Fix #22.
This commit is contained in:
Lucas Verney 2018-10-25 16:50:38 +02:00
parent 80b650c2a9
commit 903ad14bbc
13 changed files with 195 additions and 61 deletions

View File

@ -43,6 +43,7 @@ module.exports = {
}
],
'no-bitwise': 'off',
'no-unused-expressions': ["error", { "allowShortCircuit": true }],
'function-paren-newline': 'off',
}
}

View File

@ -8,6 +8,7 @@ const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const ServiceWorkerWebpackPlugin = require("serviceworker-webpack-plugin");
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
@ -189,6 +190,9 @@ module.exports = {
child_process: 'empty',
},
plugins: [
new ServiceWorkerWebpackPlugin({
entry: path.join(__dirname, '../src/sw.js'),
}),
new VueLoaderPlugin(),
new MiniCssExtractPlugin({
filename: utils.assetsPath('css/[name].[contenthash:4].css'),

View File

@ -21,6 +21,7 @@
"nosleep.js": "^0.7.0",
"ol": "^5.1.3",
"roboto-fontface": "^0.9.0",
"serviceworker-webpack-plugin": "^1.0.1",
"vue": "^2.5.2",
"vue-i18n": "^8.0.0",
"vue-router": "^3.0.1",

View File

@ -63,6 +63,8 @@
</template>
<script>
import runtime from 'serviceworker-webpack-plugin/lib/runtime';
import { DELAY_BETWEEN_API_BATCH_REQUESTS } from '@/constants';
import ReportErrorModal from '@/components/ReportErrorModal.vue';
@ -136,6 +138,13 @@ export default {
});
},
},
mounted() {
if ('serviceWorker' in navigator) {
runtime.register().catch((error) => {
console.log(`Registration failed with ${error}.`);
});
}
},
};
</script>

View File

@ -46,6 +46,7 @@ import compassIcon from '@/assets/compass.svg';
import compassNorthIcon from '@/assets/compassNorth.svg';
import unknownMarkerIcon from '@/assets/unknownMarker.svg';
import * as constants from '@/constants';
import REPORT_TYPES from '@/report-types';
import { distance } from '@/tools';
const MAIN_VECTOR_LAYER_NAME = 'MAIN';
@ -177,7 +178,7 @@ export default {
? constants.LARGE_ICON_SCALE
: constants.NORMAL_ICON_SCALE
),
src: constants.REPORT_TYPES[marker.type].marker,
src: REPORT_TYPES[marker.type].marker,
}),
}));
// Add the marker to the map and keep a reference to it

View File

@ -49,7 +49,8 @@
</template>
<script>
import { REPORT_TYPES, REPORT_ALARM_VIBRATION_SEQUENCE } from '@/constants';
import { REPORT_ALARM_VIBRATION_SEQUENCE } from '@/constants';
import REPORT_TYPES from '@/report-types';
import { distanceInWordsToNow } from '@/tools/date';
import beepSound from '@/assets/beep.mp3';

View File

@ -22,7 +22,8 @@
</template>
<script>
import { REPORT_TYPES, REPORT_TYPES_ORDER } from '@/constants';
import { REPORT_TYPES_ORDER } from '@/constants';
import REPORT_TYPES from '@/report-types';
import ReportErrorModal from '@/components/ReportErrorModal.vue';
import ReportTile from './ReportTile.vue';

View File

@ -8,7 +8,8 @@
</template>
<script>
import { REPORT_TYPES, REPORT_TYPES_ORDER } from '@/constants';
import { REPORT_TYPES_ORDER } from '@/constants';
import REPORT_TYPES from '@/report-types';
export default {
data() {

View File

@ -1,65 +1,9 @@
import accidentMarker from '@/assets/accidentMarker.svg';
import gcumMarker from '@/assets/gcumMarker.svg';
import interruptMarker from '@/assets/interruptMarker.svg';
import miscMarker from '@/assets/miscMarker.svg';
import obstacleMarker from '@/assets/obstacleMarker.svg';
import potholeMarker from '@/assets/potholeMarker.svg';
import accidentIcon from '@/assets/accident.svg';
import gcumIcon from '@/assets/gcum.svg';
import interruptIcon from '@/assets/interrupt.svg';
import miscIcon from '@/assets/misc.svg';
import obstacleIcon from '@/assets/obstacle.svg';
import potholeIcon from '@/assets/pothole.svg';
export const VERSION = '0.3';
export const NORMAL_ICON_SCALE = 0.625;
export const LARGE_ICON_SCALE = 1.0;
export const ICON_ANCHOR = [0.5, 1.0];
export const REPORT_TYPES = {
accident: {
description: 'reportLabels.accidentDescription',
label: 'reportLabels.accident',
image: accidentIcon,
marker: accidentMarker,
markerLarge: accidentMarker,
},
gcum: {
description: 'reportLabels.gcumDescription',
label: 'reportLabels.gcum',
image: gcumIcon,
marker: gcumMarker,
markerLarge: gcumMarker,
},
interrupt: {
description: 'reportLabels.interruptDescription',
label: 'reportLabels.interrupt',
image: interruptIcon,
marker: interruptMarker,
markerLarge: interruptMarker,
},
misc: {
description: 'reportLabels.miscDescription',
label: 'reportLabels.misc',
image: miscIcon,
marker: miscMarker,
markerLarge: miscMarker,
},
obstacle: {
description: 'reportLabels.obstacleDescription',
label: 'reportLabels.obstacle',
image: obstacleIcon,
marker: obstacleMarker,
markerLarge: obstacleMarker,
},
pothole: {
description: 'reportLabels.potholeDescription',
label: 'reportLabels.pothole',
image: potholeIcon,
marker: potholeMarker,
markerLarge: potholeMarker,
},
};
// Display order of the report types
export const REPORT_TYPES_ORDER = ['gcum', 'interrupt', 'obstacle', 'pothole', 'accident', 'misc'];

57
src/report-types.js Normal file
View File

@ -0,0 +1,57 @@
import accidentMarker from '@/assets/accidentMarker.svg';
import gcumMarker from '@/assets/gcumMarker.svg';
import interruptMarker from '@/assets/interruptMarker.svg';
import miscMarker from '@/assets/miscMarker.svg';
import obstacleMarker from '@/assets/obstacleMarker.svg';
import potholeMarker from '@/assets/potholeMarker.svg';
import accidentIcon from '@/assets/accident.svg';
import gcumIcon from '@/assets/gcum.svg';
import interruptIcon from '@/assets/interrupt.svg';
import miscIcon from '@/assets/misc.svg';
import obstacleIcon from '@/assets/obstacle.svg';
import potholeIcon from '@/assets/pothole.svg';
export default {
accident: {
description: 'reportLabels.accidentDescription',
label: 'reportLabels.accident',
image: accidentIcon,
marker: accidentMarker,
markerLarge: accidentMarker,
},
gcum: {
description: 'reportLabels.gcumDescription',
label: 'reportLabels.gcum',
image: gcumIcon,
marker: gcumMarker,
markerLarge: gcumMarker,
},
interrupt: {
description: 'reportLabels.interruptDescription',
label: 'reportLabels.interrupt',
image: interruptIcon,
marker: interruptMarker,
markerLarge: interruptMarker,
},
misc: {
description: 'reportLabels.miscDescription',
label: 'reportLabels.misc',
image: miscIcon,
marker: miscMarker,
markerLarge: miscMarker,
},
obstacle: {
description: 'reportLabels.obstacleDescription',
label: 'reportLabels.obstacle',
image: obstacleIcon,
marker: obstacleMarker,
markerLarge: obstacleMarker,
},
pothole: {
description: 'reportLabels.potholeDescription',
label: 'reportLabels.pothole',
image: potholeIcon,
marker: potholeMarker,
markerLarge: potholeMarker,
},
};

99
src/sw.js Normal file
View File

@ -0,0 +1,99 @@
import { VERSION as CACHE_NAME } from '@/constants';
const DEBUG = (process.env.NODE_ENV !== 'production');
// Define the assets to cache
const { assets } = global.serviceWorkerOption;
let assetsToCache = [...assets, './'];
assetsToCache = assetsToCache.map(
path => new URL(path, global.location).toString(),
);
assetsToCache = assetsToCache.filter(
// Remove some assets from cache, such as Webpack hot-reload stuff
url => !url.endsWith('hot-update.json'),
);
// Define the locations from which we allow caching
const ALLOW_CACHING_FROM = [
global.location.origin,
];
global.self.addEventListener('install', (event) => {
DEBUG && console.log('SW: installing…');
event.waitUntil(
global.caches.open(CACHE_NAME)
.then((cache) => {
DEBUG && console.log('SW: cache opened.');
cache.addAll(assetsToCache).then(
() => {
if (DEBUG) {
console.log(`SW: cached assets ${assetsToCache}.`);
console.log('SW: successfully installed!');
}
return global.self.skipWaiting(); // Immediately update the SW
},
);
}),
);
});
global.self.addEventListener('activate', (event) => {
DEBUG && console.log('SW: activating…');
event.waitUntil(
// Delete all caches but the current one
global.caches.keys().then(
cacheNames => Promise.all(
cacheNames.map((cacheName) => {
if (cacheName.indexOf(CACHE_NAME) !== 0) {
DEBUG && console.log(`SW: Deleting unused cache ${cacheName}.`);
return global.caches.delete(cacheName);
}
return null;
}),
).then(() => DEBUG && console.log('SW: activated!')),
),
);
});
global.self.addEventListener('fetch', (event) => {
const { request } = event;
// Do not touch requests which are not GET
if (request.method !== 'GET') {
DEBUG && console.log(`SW: ignore non-GET request: ${request.method}`);
return;
}
// Do not touch requests from a different origin
const requestURL = new URL(request.url);
if (ALLOW_CACHING_FROM.indexOf(requestURL.origin) === -1) {
DEBUG && console.log(`SW: ignore different origin ${requestURL.origin}`);
return;
}
// Never touch requests going from / to the API
if (requestURL.pathname.startsWith('/api')) {
// Note that if API is on a different location, it will be ignored by
// the previous rule.
DEBUG && console.log(`SW: ignore API call ${requestURL.pathname}`);
return;
}
// For the other requests, try to match it in the cache, otherwise do a
// network call
const resource = global.caches.open(CACHE_NAME).then(
cache => cache.match(request).then(
(response) => {
if (response) {
DEBUG && console.log(`SW: serving ${request.url} from cache`);
return response;
}
DEBUG && console.log(`SW: no match in cache for ${request.url}, using network`);
return fetch(request);
},
),
);
event.respondWith(resource);
});

View File

@ -46,6 +46,15 @@ server {
expires max; # Max caching for font files
}
# No caching for the service worker file
location = /sw.js {
add_header Last-Modified $date_gmt;
add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
if_modified_since off;
expires off;
etag off;
}
# Proxy pass the API calls to the server part
location /api {
limit_req zone=cycloAPI burst=3 nodelay; # Add rate-limiting on top of the API

View File

@ -7413,6 +7413,12 @@ serve-static@1.13.2:
parseurl "~1.3.2"
send "0.16.2"
serviceworker-webpack-plugin@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/serviceworker-webpack-plugin/-/serviceworker-webpack-plugin-1.0.1.tgz#481863288487e92da01d49745336c72ef8a6136b"
dependencies:
minimatch "^3.0.4"
set-blocking@^2.0.0, set-blocking@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"