From 4e74756905d700a9ed81c0cbb561927e812eb395 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= Date: Sun, 10 Jun 2018 13:30:22 +0200 Subject: [PATCH] pwa: add generic service worker (#24405) --- .../pwa/templates/combo/service-worker.js | 126 ++++++++++++++++++ combo/apps/pwa/urls.py | 7 +- combo/apps/pwa/views.py | 7 + tests/test_pwa.py | 3 + 4 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 combo/apps/pwa/templates/combo/service-worker.js diff --git a/combo/apps/pwa/templates/combo/service-worker.js b/combo/apps/pwa/templates/combo/service-worker.js new file mode 100644 index 0000000..00155ff --- /dev/null +++ b/combo/apps/pwa/templates/combo/service-worker.js @@ -0,0 +1,126 @@ +{% load gadjo %} +/* global self, caches, fetch, URL, Response */ +'use strict'; + +var config = { + version: 'v{% start_timestamp %}', + staticCacheItems: [ + '/offline/' + ], + cachePathPattern: /^\/static\/.*/, + handleFetchPathPattern: /.*/, + offlinePage: '/offline/' +}; + +function cacheName (key, opts) { + return `${opts.version}-${key}`; +} + +function addToCache (cacheKey, request, response) { + if (response.ok && cacheKey !== null) { + var copy = response.clone(); + caches.open(cacheKey).then( cache => { + cache.put(request, copy); + }); + } + return response; +} + +function fetchFromCache (event) { + return caches.match(event.request).then(response => { + if (!response) { + throw Error(`${event.request.url} not found in cache`); + } + return response; + }); +} + +function offlineResponse (resourceType, opts) { + if (resourceType === 'content') { + return caches.match(opts.offlinePage); + } + return undefined; +} + +self.addEventListener('install', event => { + function onInstall (event, opts) { + var cacheKey = cacheName('static', opts); + return caches.open(cacheKey) + .then(cache => cache.addAll(opts.staticCacheItems)); + } + + event.waitUntil( + onInstall(event, config).then( () => self.skipWaiting() ) + ); +}); + +self.addEventListener('activate', event => { + function onActivate (event, opts) { + return caches.keys() + .then(cacheKeys => { + var oldCacheKeys = cacheKeys.filter(key => key.indexOf(opts.version) !== 0); + var deletePromises = oldCacheKeys.map(oldKey => caches.delete(oldKey)); + return Promise.all(deletePromises); + }); + } + + event.waitUntil( + onActivate(event, config) + .then( () => self.clients.claim() ) + ); +}); + +self.addEventListener('fetch', event => { + + function shouldHandleFetch (event, opts) { + var request = event.request; + var url = new URL(request.url); + var criteria = { + matchesPathPattern: opts.handleFetchPathPattern.test(url.pathname), + isGETRequest : request.method === 'GET', + isFromMyOrigin : url.origin === self.location.origin + }; + var failingCriteria = Object.keys(criteria) + .filter(criteriaKey => !criteria[criteriaKey]); + return !failingCriteria.length; + } + + function onFetch (event, opts) { + var request = event.request; + var url = new URL(request.url); + var acceptHeader = request.headers.get('Accept'); + var resourceType = 'static'; + var cacheKey; + + if (acceptHeader.indexOf('text/html') !== -1) { + resourceType = 'content'; + } else if (acceptHeader.indexOf('image') !== -1) { + resourceType = 'image'; + } + + cacheKey = null; + if (opts.cachePathPattern.test(url.pathname)) { + cacheKey = cacheName(resourceType, opts); + } + + /* always network first */ + if (true || resourceType === 'content') { + event.respondWith( + fetch(request) + .then(response => addToCache(cacheKey, request, response)) + .catch(() => fetchFromCache(event)) + .catch(() => offlineResponse(resourceType, opts)) + ); + } else { + event.respondWith( + fetchFromCache(event) + .catch(() => fetch(request)) + .then(response => addToCache(cacheKey, request, response)) + .catch(() => offlineResponse(resourceType, opts)) + ); + } + } + if (shouldHandleFetch(event, config)) { + onFetch(event, config); + } +}); diff --git a/combo/apps/pwa/urls.py b/combo/apps/pwa/urls.py index 5842d45..fb7ce24 100644 --- a/combo/apps/pwa/urls.py +++ b/combo/apps/pwa/urls.py @@ -16,6 +16,9 @@ from django.conf.urls import url -from .views import manifest_json +from .views import manifest_json, service_worker_js -urlpatterns = [url('^manifest.json', manifest_json)] +urlpatterns = [ + url('^manifest.json', manifest_json), + url('^service-worker.js', service_worker_js), +] diff --git a/combo/apps/pwa/views.py b/combo/apps/pwa/views.py index fe96996..3971bf6 100644 --- a/combo/apps/pwa/views.py +++ b/combo/apps/pwa/views.py @@ -17,9 +17,16 @@ from django.http import HttpResponse, Http404 from django.template.loader import get_template, TemplateDoesNotExist + def manifest_json(request, *args, **kwargs): try: template = get_template('combo/manifest.json') except TemplateDoesNotExist: raise Http404() return HttpResponse(template.render({}, request), content_type='application/json') + + +def service_worker_js(request, *args, **kwargs): + template = get_template('combo/service-worker.js') + return HttpResponse(template.render({}, request), + content_type='application/javascript; charset=utf-8') diff --git a/tests/test_pwa.py b/tests/test_pwa.py index 19b17d5..07ebaf7 100644 --- a/tests/test_pwa.py +++ b/tests/test_pwa.py @@ -13,3 +13,6 @@ def test_manifest_json(app): templates_settings[0]['DIRS'] = ['%s/templates-1' % os.path.abspath(os.path.dirname(__file__))] with override_settings(TEMPLATES=templates_settings): assert app.get('/manifest.json', status=200).json['name'] == 'test' + +def test_service_worker(app): + app.get('/service-worker.js', status=200) -- 2.17.1