From 504e8ffd50e2658d9012e321f4ca144af46e8154 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Mon, 27 Apr 2015 20:28:43 +0200 Subject: [PATCH] Add simple prefetch service-worker --- Gruntfile.js | 83 ++++++++++++++++++++++++++++++---- package.json | 9 ++-- server.js | 7 ++- src/js/app.js | 19 +------- src/js/offline-cache.js | 98 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 185 insertions(+), 31 deletions(-) create mode 100644 src/js/offline-cache.js diff --git a/Gruntfile.js b/Gruntfile.js index 58218f2..a58d35f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -535,7 +535,7 @@ module.exports = function(grunt) { watch: { css: { files: ['src/sass/**/*.scss'], - tasks: ['dist-css', 'manifest', 'dist-styleguide'] + tasks: ['dist-css', 'offline-cache', 'dist-styleguide'] }, styleguide: { files: ['src/styleguide/**/*.hbs', 'src/styleguide/**/*.js'], @@ -555,15 +555,15 @@ module.exports = function(grunt) { }, icons: { files: ['src/index.html', 'src/img/icons/*.svg', '!src/img/icons/all.svg'], - tasks: ['svgmin', 'svgstore', 'string-replace', 'dist-styleguide', 'manifest'] + tasks: ['svgmin', 'svgstore', 'string-replace', 'dist-styleguide', 'offline-cache'] }, lib: { files: ['src/lib/**/*.js'], - tasks: ['copy:lib', 'manifest'] + tasks: ['copy:lib', 'offline-cache'] }, app: { files: ['src/*.js', 'src/*.html', 'src/tpl/**/*.html', 'src/**/*.json', 'src/manifest.*', 'src/img/**/*', 'src/font/**/*'], - tasks: ['copy:app', 'copy:tpl', 'copy:img', 'copy:font', 'manifest-dev', 'manifest'] + tasks: ['copy:app', 'copy:tpl', 'copy:img', 'copy:font', 'manifest-dev', 'offline-cache'] } }, @@ -582,6 +582,15 @@ module.exports = function(grunt) { } }, + // Offline caching + + swPrecache: { + prod: { + handleFetch: true, + rootDir: 'dist' + } + }, + manifest: { generate: { options: { @@ -594,6 +603,9 @@ module.exports = function(grunt) { 'manifest.webapp', 'manifest.mobile.json', 'background.js', + 'service-worker.js', + 'styleguide/css/styleguide.min.css', + 'styleguide/index.html', 'js/app.templates.js', 'js/app.js.map', 'js/app.min.js.map', @@ -654,6 +666,59 @@ module.exports = function(grunt) { }); + // generate service-worker stasks + grunt.registerMultiTask('swPrecache', function() { + var fs = require('fs'); + var path = require('path'); + var swPrecache = require('sw-precache'); + var packageJson = require('./package.json'); + + var done = this.async(); + var rootDir = this.data.rootDir; + var handleFetch = this.data.handleFetch; + + generateServiceWorkerFileContents(rootDir, handleFetch, function(error, serviceWorkerFileContents) { + if (error) { + grunt.fail.warn(error); + } + fs.writeFile(path.join(rootDir, 'service-worker.js'), serviceWorkerFileContents, function(error) { + if (error) { + grunt.fail.warn(error); + } + done(); + }); + }); + + function generateServiceWorkerFileContents(rootDir, handleFetch, callback) { + var config = { + cacheId: packageJson.name, + // If handleFetch is false (i.e. because this is called from swPrecache:dev), then + // the service worker will precache resources but won't actually serve them. + // This allows you to test precaching behavior without worry about the cache preventing your + // local changes from being picked up during the development cycle. + handleFetch: handleFetch, + logger: grunt.log.writeln, + dynamicUrlToDependencies: { + 'socket.io/socket.io.js': ['node_modules/socket.io/node_modules/socket.io-client/socket.io.js'], + }, + staticFileGlobs: [ + rootDir + '/*.html', + rootDir + '/tpl/*.html', + rootDir + '/js/**/*.min.js', + rootDir + '/css/**/*.css', + rootDir + '/img/**/*.svg', + rootDir + '/img/*-universal.png', + rootDir + '/font/**.*', + rootDir + '/*.json' + ], + maximumFileSizeToCacheInBytes: 100 * 1024 * 1024, + stripPrefix: path.join(rootDir, path.sep) + }; + + swPrecache(config, callback); + } + }); + // Load the plugin(s) grunt.loadNpmTasks('grunt-browserify'); grunt.loadNpmTasks('grunt-contrib-concat'); @@ -688,7 +753,7 @@ module.exports = function(grunt) { 'concat:app', 'concat:readSandbox', 'concat:pbkdf2Worker', - 'manifest' + 'offline-cache' ]); grunt.registerTask('dist-js-unitTest', [ 'browserify:unitTest', @@ -706,6 +771,8 @@ module.exports = function(grunt) { // generate styleguide after manifest to forward version number to styleguide grunt.registerTask('dist', ['clean:dist', 'shell', 'dist-css', 'dist-js', 'dist-assets', 'dist-copy', 'manifest', 'dist-styleguide']); + grunt.registerTask('offline-cache', ['manifest', 'swPrecache:prod']); + // Test/Dev tasks grunt.registerTask('dev', ['connect:dev']); grunt.registerTask('test', ['jshint', 'connect:test', 'mocha_phantomjs']); @@ -770,9 +837,9 @@ module.exports = function(grunt) { fs.writeFileSync(path, JSON.stringify(manifest, null, 2)); } - grunt.registerTask('release-dev', ['dist', 'manifest-dev', 'compress']); - grunt.registerTask('release-test', ['dist', 'manifest-test', 'clean:release', 'compress']); - grunt.registerTask('release-prod', ['dist', 'manifest-prod', 'clean:release', 'compress']); + grunt.registerTask('release-dev', ['dist', 'manifest-dev', 'swPrecache:prod', 'compress']); + grunt.registerTask('release-test', ['dist', 'manifest-test', 'clean:release', 'swPrecache:prod', 'compress']); + grunt.registerTask('release-prod', ['dist', 'manifest-prod', 'clean:release', 'swPrecache:prod', 'compress']); grunt.registerTask('default', ['release-dev']); }; \ No newline at end of file diff --git a/package.json b/package.json index 1ea397c..c22c0eb 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "socket.io": "^1.0.6" }, "devDependencies": { + "assemble": "~0.4.42", "axe-logger": "~0.0.2", "browsercrow": "https://github.com/whiteout-io/browsercrow/tarball/master", "browsersmtp": "https://github.com/whiteout-io/browsersmtp/tarball/master", @@ -61,6 +62,7 @@ "grunt-string-replace": "~1.0.0", "grunt-svgmin": "~1.0.0", "grunt-svgstore": "~0.3.4", + "handlebars-helper-compose": "~0.2.12", "iframe-resizer": "^2.8.3", "imap-client": "~0.14.1", "jquery": "~2.1.1", @@ -72,10 +74,9 @@ "pgpbuilder": "~0.6.0", "pgpmailer": "~0.9.1", "sinon": "~1.7.3", + "sw-precache": "^1.3.0", "tcp-socket": "~0.5.0", "time-grunt": "^1.0.0", - "wo-smtpclient": "~0.6.0", - "assemble": "~0.4.42", - "handlebars-helper-compose": "~0.2.12" + "wo-smtpclient": "~0.6.0" } -} \ No newline at end of file +} diff --git a/server.js b/server.js index 8600dae..982927f 100644 --- a/server.js +++ b/server.js @@ -88,10 +88,13 @@ app.use(function(req, res, next) { res.set('Cache-control', 'public, max-age=0'); next(); }); -app.use('/appcache.manifest', function(req, res, next) { +app.use('/service-worker.js', noCache); +app.use('/appcache.manifest', noCache); + +function noCache(req, res, next) { res.set('Cache-control', 'no-cache'); next(); -}); +} app.use('/tpl/read-sandbox.html', function(req, res, next) { res.set('X-Frame-Options', 'SAMEORIGIN'); next(); diff --git a/src/js/app.js b/src/js/app.js index 3663b73..60e19eb 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -1,22 +1,7 @@ 'use strict'; -// -// AppCache -// - -if (typeof window.applicationCache !== 'undefined') { - window.onload = function() { - // Check if a new AppCache is available on page load. - window.applicationCache.onupdateready = function() { - if (window.applicationCache.status === window.applicationCache.UPDATEREADY) { - // Browser downloaded a new app cache - if (window.confirm('A new version of Whiteout Mail is available. Restart the app to update?')) { - window.location.reload(); - } - } - }; - }; -} +// use service-worker or app-cache for offline caching +require('./offline-cache'); // // Angular app config diff --git a/src/js/offline-cache.js b/src/js/offline-cache.js new file mode 100644 index 0000000..6cb5089 --- /dev/null +++ b/src/js/offline-cache.js @@ -0,0 +1,98 @@ +/** + * Copyright 2015 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var UPDATE_MSG = 'A new version of Whiteout Mail is available. Restart the app to update?'; + +if ('serviceWorker' in navigator && + // See http://www.chromium.org/Home/chromium-security/prefer-secure-origins-for-powerful-new-features + (window.location.protocol === 'https:' || + window.location.hostname === 'localhost' || + window.location.hostname.indexOf('127.') === 0)) { + // prefer new service worker cache + useServiceWorker(); + +} else if ('applicationCache' in window) { + // Fall back to app cache + useAppCache(); +} + +function useServiceWorker() { + // Your service-worker.js *must* be located at the top-level directory relative to your site. + // It won't be able to control pages unless it's located at the same level or higher than them. + // *Don't* register service worker file in, e.g., a scripts/ sub-directory! + // See https://github.com/slightlyoff/ServiceWorker/issues/468 + navigator.serviceWorker.register('service-worker.js', { + scope: './' + }).then(function(registration) { + // Check to see if there's an updated version of service-worker.js with new files to cache: + // https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#service-worker-registration-update-method + if (typeof registration.update === 'function') { + registration.update(); + } + + // updatefound is fired if service-worker.js changes. + registration.onupdatefound = function() { + // The updatefound event implies that registration.installing is set; see + // https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#service-worker-container-updatefound-event + var installingWorker = registration.installing; + + installingWorker.onstatechange = function() { + switch (installingWorker.state) { + case 'installed': + if (navigator.serviceWorker.controller) { + // At this point, the old content will have been purged and the fresh content will + // have been added to the cache. + // It's the perfect time to display a "New content is available; please refresh." + // message in the page's interface. + console.log('New or updated content is available.'); + if (window.confirm(UPDATE_MSG)) { + window.location.reload(); + } + } else { + // At this point, everything has been precached, but the service worker is not + // controlling the page. The service worker will not take control until the next + // reload or navigation to a page under the registered scope. + // It's the perfect time to display a "Content is cached for offline use." message. + console.log('Content is cached, and will be available for offline use the ' + + 'next time the page is loaded.'); + } + break; + + case 'redundant': + throw 'The installing service worker became redundant.'; + } + }; + }; + }).catch(function(e) { + console.error('Error during service worker registration:', e); + }); +} + +function useAppCache() { + window.onload = function() { + // Check if a new AppCache is available on page load. + window.applicationCache.onupdateready = function() { + if (window.applicationCache.status === window.applicationCache.UPDATEREADY) { + // Browser downloaded a new app cache + if (window.confirm(UPDATE_MSG)) { + window.location.reload(); + } + } + }; + }; +} \ No newline at end of file