Cache tiles locally within the ServiceWorker. Fix #40.
This commit is contained in:
parent
76f54932bf
commit
07ab80542d
11
src/App.vue
11
src/App.vue
@ -172,8 +172,15 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
if ('serviceWorker' in navigator) {
|
// Service worker is for caching only, so it needs both SW support and
|
||||||
runtime.register().catch((error) => {
|
// caching API support.
|
||||||
|
if ('serviceWorker' in navigator && 'caches' in window) {
|
||||||
|
runtime.register().then(
|
||||||
|
// Clean expired tiles from the cache at startup
|
||||||
|
() => navigator.serviceWorker.controller.postMessage(JSON.stringify({
|
||||||
|
action: 'PURGE_EXPIRED_TILES',
|
||||||
|
})),
|
||||||
|
).catch((error) => {
|
||||||
console.log(`Registration failed with ${error}.`);
|
console.log(`Registration failed with ${error}.`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
182
src/sw.js
182
src/sw.js
@ -1,4 +1,10 @@
|
|||||||
import { VERSION as CACHE_NAME } from '@/constants';
|
import localforage from 'localforage';
|
||||||
|
|
||||||
|
import {
|
||||||
|
TILE_SERVERS,
|
||||||
|
VERSION as CACHE_NAME,
|
||||||
|
} from '@/constants';
|
||||||
|
import { loadDataFromStorage } from '@/storage';
|
||||||
|
|
||||||
const DEBUG = (process.env.NODE_ENV !== 'production');
|
const DEBUG = (process.env.NODE_ENV !== 'production');
|
||||||
|
|
||||||
@ -17,11 +23,52 @@ assetsToCache = assetsToCache.filter(
|
|||||||
const ALLOW_CACHING_FROM = [
|
const ALLOW_CACHING_FROM = [
|
||||||
global.location.origin,
|
global.location.origin,
|
||||||
];
|
];
|
||||||
|
const TILE_SERVERS_ORIGINS = [];
|
||||||
|
// Allow alterations of tile servers requests
|
||||||
|
Object.keys(TILE_SERVERS).forEach((tileServer) => {
|
||||||
|
const tileServerURL = TILE_SERVERS[tileServer].url;
|
||||||
|
if (tileServerURL.indexOf('{a-c}') !== -1) {
|
||||||
|
TILE_SERVERS_ORIGINS.push(
|
||||||
|
(new URL(tileServerURL.replace('{a-c}', 'a'))).origin,
|
||||||
|
);
|
||||||
|
TILE_SERVERS_ORIGINS.push(
|
||||||
|
(new URL(tileServerURL.replace('{a-c}', 'b'))).origin,
|
||||||
|
);
|
||||||
|
TILE_SERVERS_ORIGINS.push(
|
||||||
|
(new URL(tileServerURL.replace('{a-c}', 'c'))).origin,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
TILE_SERVERS_ORIGINS.push((new URL(tileServerURL)).origin);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Get duration (in s) before (cache) expiration from headers of a fetch
|
||||||
|
// request.
|
||||||
|
function getExpiresFromHeaders(headers) {
|
||||||
|
// Try to use the Cache-Control header (and max-age)
|
||||||
|
if (headers.get('cache-control')) {
|
||||||
|
const maxAge = headers.get('cache-control').match(/max-age=(\d+)/);
|
||||||
|
return parseInt(maxAge ? maxAge[1] : 0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise try to get expiration duration from the Expires header
|
||||||
|
if (headers.get('expires')) {
|
||||||
|
return (
|
||||||
|
parseInt(
|
||||||
|
(new Date(headers.get('expires'))).getTime() / 1000,
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
- (new Date()).getTime()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
global.self.addEventListener('install', (event) => {
|
global.self.addEventListener('install', (event) => {
|
||||||
DEBUG && console.log('SW: installing…');
|
DEBUG && console.log('SW: installing…');
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
!DEBUG && global.caches.open(CACHE_NAME) // Don't cache during dev
|
global.caches.open(CACHE_NAME) // Don't cache during dev
|
||||||
.then((cache) => {
|
.then((cache) => {
|
||||||
DEBUG && console.log('SW: cache opened.');
|
DEBUG && console.log('SW: cache opened.');
|
||||||
cache.addAll(assetsToCache).then(
|
cache.addAll(assetsToCache).then(
|
||||||
@ -61,14 +108,17 @@ global.self.addEventListener('fetch', (event) => {
|
|||||||
|
|
||||||
// Do not touch requests which are not GET
|
// Do not touch requests which are not GET
|
||||||
if (request.method !== 'GET') {
|
if (request.method !== 'GET') {
|
||||||
DEBUG && console.log(`SW: ignore non-GET request: ${request.method}`);
|
DEBUG && console.log(`SW: ignore non-GET request: ${request.method}.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not touch requests from a different origin
|
// Only touch requests which we can cache or from tiles servers.
|
||||||
const requestURL = new URL(request.url);
|
const requestURL = new URL(request.url);
|
||||||
if (ALLOW_CACHING_FROM.indexOf(requestURL.origin) === -1) {
|
if (
|
||||||
DEBUG && console.log(`SW: ignore different origin ${requestURL.origin}`);
|
ALLOW_CACHING_FROM.indexOf(requestURL.origin) === -1
|
||||||
|
&& TILE_SERVERS_ORIGINS.indexOf(requestURL.origin) === -1
|
||||||
|
) {
|
||||||
|
DEBUG && console.log(`SW: ignore different origin ${requestURL.origin}.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,28 +126,140 @@ global.self.addEventListener('fetch', (event) => {
|
|||||||
if (requestURL.pathname.startsWith('/api')) {
|
if (requestURL.pathname.startsWith('/api')) {
|
||||||
// Note that if API is on a different location, it will be ignored by
|
// Note that if API is on a different location, it will be ignored by
|
||||||
// the previous rule.
|
// the previous rule.
|
||||||
DEBUG && console.log(`SW: ignore API call ${requestURL.pathname}`);
|
DEBUG && console.log(`SW: ignore API call ${requestURL.pathname}.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve tiles from cache or download and cache them
|
||||||
|
if (TILE_SERVERS_ORIGINS.indexOf(requestURL.origin) !== -1) {
|
||||||
|
event.respondWith(global.caches.open(`${CACHE_NAME}-tiles`).then(
|
||||||
|
cache => cache.match(request).then(
|
||||||
|
(response) => {
|
||||||
|
// If there is a match from the cache
|
||||||
|
if (response) {
|
||||||
|
DEBUG && console.log(`SW: serving ${request.url} from cache.`);
|
||||||
|
const expirationDate = Date.parse(response.headers.get('sw-cache-expires'));
|
||||||
|
const now = new Date();
|
||||||
|
// Check it is not already expired and return from the
|
||||||
|
// cache
|
||||||
|
if (expirationDate > now) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, let's fetch it from the network
|
||||||
|
DEBUG && console.log(`SW: no match in cache for ${request.url}, using network.`);
|
||||||
|
return fetch(request.url).then((liveResponse) => {
|
||||||
|
// Get tile caching duration from settings
|
||||||
|
let tileCachingDurationPromise = (
|
||||||
|
Promise.resolve(0) // no caching by default
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
localforage.driver() === localforage.INDEXEDDB
|
||||||
|
) {
|
||||||
|
tileCachingDurationPromise = loadDataFromStorage(
|
||||||
|
'settings', 'tileCachingDuration',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return tileCachingDurationPromise.then((tileCachingDuration) => {
|
||||||
|
// This is caching duration from settings
|
||||||
|
const cachingDuration = (
|
||||||
|
tileCachingDuration || 0
|
||||||
|
);
|
||||||
|
// This is caching duration specified in HTTP headers
|
||||||
|
const cachingDurationFromHeaders = getExpiresFromHeaders(
|
||||||
|
liveResponse.headers,
|
||||||
|
) || 0;
|
||||||
|
|
||||||
|
// If any form of caching is possible, do it
|
||||||
|
if (tileCachingDuration > 0 || cachingDurationFromHeaders > 0) {
|
||||||
|
// Compute expires date from caching duration
|
||||||
|
const expires = new Date();
|
||||||
|
expires.setSeconds(
|
||||||
|
expires.getSeconds()
|
||||||
|
// Caching duration cannot be less than
|
||||||
|
// specified by HTTP headers.
|
||||||
|
+ Math.max(cachingDuration, cachingDurationFromHeaders),
|
||||||
|
);
|
||||||
|
// Recreate a Response object from scratch to put
|
||||||
|
// it in the cache, with the extra header for
|
||||||
|
// managing cache expiration.
|
||||||
|
const cachedResponseFields = {
|
||||||
|
status: liveResponse.status,
|
||||||
|
statusText: liveResponse.statusText,
|
||||||
|
headers: { 'SW-Cache-Expires': expires.toUTCString() },
|
||||||
|
};
|
||||||
|
liveResponse.headers.forEach((v, k) => {
|
||||||
|
cachedResponseFields.headers[k] = v;
|
||||||
|
});
|
||||||
|
// We will consume body of the live response, so
|
||||||
|
// clone it before to be able to return it
|
||||||
|
// afterwards.
|
||||||
|
const returnedResponse = liveResponse.clone();
|
||||||
|
return liveResponse.blob().then((body) => {
|
||||||
|
DEBUG && console.log(
|
||||||
|
`SW: caching tiles ${request.url} until ${expires.toUTCString()}.`,
|
||||||
|
);
|
||||||
|
// Put the duplicated Response in the cache
|
||||||
|
cache.put(request, new Response(body, cachedResponseFields));
|
||||||
|
// Return the live response from the network
|
||||||
|
return returnedResponse;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Otherwise, just return the live result from the network
|
||||||
|
return liveResponse;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For the other requests, try to match it in the cache, otherwise do a
|
// For the other requests, try to match it in the cache, otherwise do a
|
||||||
// network call
|
// network call
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
// Never match from cache in production
|
// Never match from cache in development
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const resource = global.caches.open(CACHE_NAME).then(
|
const resource = global.caches.open(CACHE_NAME).then(
|
||||||
cache => cache.match(request).then(
|
cache => cache.match(request).then(
|
||||||
(response) => {
|
(response) => {
|
||||||
if (response) {
|
if (response) {
|
||||||
DEBUG && console.log(`SW: serving ${request.url} from cache`);
|
DEBUG && console.log(`SW: serving ${request.url} from cache.`);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
DEBUG && console.log(`SW: no match in cache for ${request.url}, using network`);
|
DEBUG && console.log(`SW: no match in cache for ${request.url}, using network.`);
|
||||||
return fetch(request);
|
return fetch(request);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
event.respondWith(resource);
|
event.respondWith(resource);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
global.self.addEventListener('message', (event) => {
|
||||||
|
console.log(`SW: received message ${event.data}.`);
|
||||||
|
|
||||||
|
const eventData = JSON.parse(event.data);
|
||||||
|
|
||||||
|
// Clean tiles cache
|
||||||
|
if (eventData.action === 'PURGE_EXPIRED_TILES') {
|
||||||
|
DEBUG && console.log('SW: purging expired tiles from cache.');
|
||||||
|
global.caches.open(`${CACHE_NAME}-tiles`).then(
|
||||||
|
cache => cache.keys().then(
|
||||||
|
keys => keys.forEach(
|
||||||
|
key => cache.match(key).then((cachedResponse) => {
|
||||||
|
const expirationDate = Date.parse(cachedResponse.headers.get('sw-cache-expires'));
|
||||||
|
const now = new Date();
|
||||||
|
// Check it is not already expired
|
||||||
|
if (expirationDate < now) {
|
||||||
|
DEBUG && console.log(`SW: purging (expired) tile ${key.url} from cache.`);
|
||||||
|
cache.delete(key);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user