├── static ├── .gitkeep ├── code.png ├── favicon.png ├── appicon_48.png ├── appicon_96.png ├── appicon_144.png └── unsubscribe.png ├── .eslintignore ├── test ├── unit │ ├── .eslintrc │ └── specs │ │ └── HelloWorld.spec.js └── e2e │ ├── specs │ └── test.js │ ├── custom-assertions │ └── elementCount.js │ ├── nightwatch.conf.js │ └── runner.js ├── src ├── assets │ ├── chess.png │ └── pwa-reliable.png ├── sw │ ├── register.simple.js │ ├── manifest.json │ ├── sw.js │ ├── register.dev.js │ ├── register.prd.js │ └── service-worker.tmpl ├── App.vue ├── router │ └── index.js ├── components │ ├── HelloWorld.vue │ └── User.vue └── main.js ├── config ├── prod.env.js ├── test.env.js ├── dev.env.js └── index.js ├── .editorconfig ├── .gitignore ├── .postcssrc.js ├── .babelrc ├── .eslintrc.js ├── index.html ├── package.json └── README.md /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | /test/unit/coverage/ 6 | -------------------------------------------------------------------------------- /static/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chesscai/simple-pwa/HEAD/static/code.png -------------------------------------------------------------------------------- /test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | }, 4 | "globals": { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chesscai/simple-pwa/HEAD/static/favicon.png -------------------------------------------------------------------------------- /src/assets/chess.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chesscai/simple-pwa/HEAD/src/assets/chess.png -------------------------------------------------------------------------------- /static/appicon_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chesscai/simple-pwa/HEAD/static/appicon_48.png -------------------------------------------------------------------------------- /static/appicon_96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chesscai/simple-pwa/HEAD/static/appicon_96.png -------------------------------------------------------------------------------- /static/appicon_144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chesscai/simple-pwa/HEAD/static/appicon_144.png -------------------------------------------------------------------------------- /static/unsubscribe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chesscai/simple-pwa/HEAD/static/unsubscribe.png -------------------------------------------------------------------------------- /src/assets/pwa-reliable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chesscai/simple-pwa/HEAD/src/assets/pwa-reliable.png -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"', 4 | NOTIFICATION_HOST: '"http://localhost:3000"' 5 | } 6 | -------------------------------------------------------------------------------- /config/test.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const devEnv = require('./dev.env') 4 | 5 | module.exports = merge(devEnv, { 6 | NODE_ENV: '"testing"' 7 | }) 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"', 7 | NOTIFICATION_HOST: '"http://localhost:3000"' 8 | }) 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | /test/unit/coverage/ 8 | /test/e2e/reports/ 9 | selenium-debug.log 10 | 11 | # Editor directories and files 12 | .idea 13 | .vscode 14 | *.suo 15 | *.ntvs* 16 | *.njsproj 17 | *.sln 18 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/sw/register.simple.js: -------------------------------------------------------------------------------- 1 | if (navigator.serviceWorker) { 2 | navigator.serviceWorker.register('./sw.js') 3 | .then(function (registration) { 4 | console.log('Registered events at scope: ', registration) 5 | }) 6 | .catch(function (err) { 7 | console.log('ServiceWorker registration failed: ', err) 8 | }) 9 | } -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"], 12 | "env": { 13 | "test": { 14 | "presets": ["env", "stage-2"] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/unit/specs/HelloWorld.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import HelloWorld from '@/components/HelloWorld' 3 | 4 | describe('HelloWorld.vue', () => { 5 | it('should render correct contents', () => { 6 | const Constructor = Vue.extend(HelloWorld) 7 | const vm = new Constructor().$mount() 8 | expect(vm.$el.querySelector('.hello h1').textContent) 9 | 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 23 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | export default new Router({ 7 | // mode: 'history', 8 | routes: [ 9 | { 10 | path: '/', 11 | name: 'HelloWorld', 12 | component: () => import('@/components/HelloWorld') 13 | }, 14 | { 15 | path: '/user', 16 | name: 'User', 17 | component: () => import('@/components/User') 18 | }, 19 | { 20 | path: '*', 21 | redirect: '/' 22 | } 23 | ] 24 | }) 25 | -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | 16 | 29 | -------------------------------------------------------------------------------- /src/components/User.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 | 33 | -------------------------------------------------------------------------------- /test/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // For authoring Nightwatch tests, see 2 | // http://nightwatchjs.org/guide#usage 3 | 4 | module.exports = { 5 | 'default e2e tests': function (browser) { 6 | // automatically uses dev Server port from /config.index.js 7 | // default: http://localhost:8080 8 | // see nightwatch.conf.js 9 | const devServer = browser.globals.devServerURL 10 | 11 | browser 12 | .url(devServer) 13 | .waitForElementVisible('#app', 5000) 14 | .assert.elementPresent('.hello') 15 | .assert.containsText('h1', 'Welcome to Your Vue.js App') 16 | .assert.elementCount('img', 1) 17 | .end() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import router from './router' 6 | import NProgress from 'nprogress' 7 | import 'nprogress/nprogress.css' 8 | 9 | Vue.config.productionTip = false 10 | 11 | router.beforeEach((to, from, next) => { 12 | NProgress.start() 13 | next() 14 | }) 15 | router.afterEach(() => { 16 | NProgress.done() 17 | }) 18 | 19 | /* eslint-disable no-new */ 20 | new Vue({ 21 | el: '#app', 22 | router, 23 | components: { App }, 24 | template: '' 25 | }) 26 | -------------------------------------------------------------------------------- /src/sw/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PWA Lite", 3 | "short_name": "PWA", 4 | "display": "standalone", 5 | "start_url": "./index.html", 6 | "theme_color": "#ffffff", 7 | "background_color": "#ffffff", 8 | "gcm_sender_id": "PWA_LITE_GCMAPIKey", 9 | "icons": [ 10 | { 11 | "src": "./static/appicon_144.png", 12 | "sizes": "144x144", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "./static/appicon_96.png", 17 | "sizes": "96x96", 18 | "type": "image/png" 19 | }, 20 | 21 | { 22 | "src": "./static/appicon_48.png", 23 | "sizes": "48x48", 24 | "type": "image/png" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /test/e2e/custom-assertions/elementCount.js: -------------------------------------------------------------------------------- 1 | // A custom Nightwatch assertion. 2 | // The assertion name is the filename. 3 | // Example usage: 4 | // 5 | // browser.assert.elementCount(selector, count) 6 | // 7 | // For more information on custom assertions see: 8 | // http://nightwatchjs.org/guide#writing-custom-assertions 9 | 10 | exports.assertion = function (selector, count) { 11 | this.message = 'Testing if element <' + selector + '> has count: ' + count 12 | this.expected = count 13 | this.pass = function (val) { 14 | return val === this.expected 15 | } 16 | this.value = function (res) { 17 | return res.value 18 | } 19 | this.command = function (cb) { 20 | var self = this 21 | return this.api.execute(function (selector) { 22 | return document.querySelectorAll(selector).length 23 | }, [selector], function (res) { 24 | cb.call(self, res) 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parserOptions: { 6 | parser: 'babel-eslint' 7 | }, 8 | env: { 9 | browser: true, 10 | }, 11 | extends: [ 12 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 13 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 14 | 'plugin:vue/essential', 15 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 16 | 'standard' 17 | ], 18 | // required to lint *.vue files 19 | plugins: [ 20 | 'vue' 21 | ], 22 | // add your custom rules here 23 | rules: { 24 | // allow async-await 25 | 'generator-star-spacing': 'off', 26 | // allow debugger during development 27 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/e2e/nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | var config = require('../../config') 3 | 4 | // http://nightwatchjs.org/gettingstarted#settings-file 5 | module.exports = { 6 | src_folders: ['test/e2e/specs'], 7 | output_folder: 'test/e2e/reports', 8 | custom_assertions_path: ['test/e2e/custom-assertions'], 9 | 10 | selenium: { 11 | start_process: true, 12 | server_path: require('selenium-server').path, 13 | host: '127.0.0.1', 14 | port: 4444, 15 | cli_args: { 16 | 'webdriver.chrome.driver': require('chromedriver').path 17 | } 18 | }, 19 | 20 | test_settings: { 21 | default: { 22 | selenium_port: 4444, 23 | selenium_host: 'localhost', 24 | silent: true, 25 | globals: { 26 | devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port) 27 | } 28 | }, 29 | 30 | chrome: { 31 | desiredCapabilities: { 32 | browserName: 'chrome', 33 | javascriptEnabled: true, 34 | acceptSslCerts: true 35 | } 36 | }, 37 | 38 | firefox: { 39 | desiredCapabilities: { 40 | browserName: 'firefox', 41 | javascriptEnabled: true, 42 | acceptSslCerts: true 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-pwa 9 | 25 | 26 | 27 |
28 | 29 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /test/e2e/runner.js: -------------------------------------------------------------------------------- 1 | // 1. start the dev server using production config 2 | process.env.NODE_ENV = 'testing' 3 | 4 | const webpack = require('webpack') 5 | const DevServer = require('webpack-dev-server') 6 | 7 | const webpackConfig = require('../../build/webpack.prod.conf') 8 | const devConfigPromise = require('../../build/webpack.dev.conf') 9 | 10 | let server 11 | 12 | devConfigPromise.then(devConfig => { 13 | const devServerOptions = devConfig.devServer 14 | const compiler = webpack(webpackConfig) 15 | server = new DevServer(compiler, devServerOptions) 16 | const port = devServerOptions.port 17 | const host = devServerOptions.host 18 | return server.listen(port, host) 19 | }) 20 | .then(() => { 21 | // 2. run the nightwatch test suite against it 22 | // to run in additional browsers: 23 | // 1. add an entry in test/e2e/nightwatch.conf.js under "test_settings" 24 | // 2. add it to the --env flag below 25 | // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox` 26 | // For more information on Nightwatch's config file, see 27 | // http://nightwatchjs.org/guide#settings-file 28 | let opts = process.argv.slice(2) 29 | if (opts.indexOf('--config') === -1) { 30 | opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js']) 31 | } 32 | if (opts.indexOf('--env') === -1) { 33 | opts = opts.concat(['--env', 'chrome']) 34 | } 35 | 36 | const spawn = require('cross-spawn') 37 | const runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' }) 38 | 39 | runner.on('exit', function (code) { 40 | server.close() 41 | process.exit(code) 42 | }) 43 | 44 | runner.on('error', function (err) { 45 | server.close() 46 | throw err 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /src/sw/sw.js: -------------------------------------------------------------------------------- 1 | var cacheStorageKey = 'vue-pwa@v1.0.0' 2 | var assetHostPattern = /\/\/(localhost:8080)/ 3 | 4 | var cacheList = [ 5 | './', 6 | './index.html', 7 | './static/favicon.png' 8 | ] 9 | 10 | self.addEventListener('install', e => { 11 | e.waitUntil( 12 | caches.open(cacheStorageKey) 13 | .then(cache => cache.addAll(cacheList)) 14 | .then(() => self.skipWaiting()) 15 | ) 16 | }) 17 | 18 | self.addEventListener('fetch', function(e) { 19 | if (assetHostPattern.test(e.request.url)) { 20 | e.respondWith( 21 | caches.match(e.request).then(function(response) { 22 | if (response != null) { 23 | return response 24 | } 25 | return fetch(e.request.url) 26 | }) 27 | ) 28 | } 29 | }) 30 | 31 | self.addEventListener('activate', e => { 32 | // delete old deprecated caches. 33 | caches.keys().then(cacheNames => Promise.all( 34 | cacheNames 35 | .map(cacheName => caches.delete(cacheName)) 36 | )) 37 | e.waitUntil(self.clients.claim()) 38 | }) 39 | 40 | self.addEventListener('push', function(e) { 41 | if (!(self.Notification && self.Notification.permission === 'granted')) { 42 | return 43 | } 44 | var data = {} 45 | if (e.data) { 46 | data = e.data.json() 47 | data.data = {} // url 只能通过 notificationclick 的 e.notification.data 对象传递 48 | data.data.url = data.url 49 | } 50 | console.log(data) 51 | e.waitUntil(self.registration.showNotification(data.title, data)) 52 | }) 53 | 54 | self.addEventListener('notificationclick', function (e) { 55 | console.log(e) 56 | let notification = e.notification 57 | let url = notification.data.url 58 | if (!url) { 59 | return notification.close() 60 | } 61 | 62 | e.waitUntil( 63 | clients.matchAll({ 64 | type: "window" 65 | }) 66 | .then(function (clientList) { 67 | for (var i = 0; i < clientList.length; i++) { 68 | var client = clientList[i]; 69 | if (client.url == '/' && 'focus' in client) 70 | return client.focus(); 71 | } 72 | if (clients.openWindow) { 73 | return clients.openWindow(url); 74 | } 75 | }) 76 | ); 77 | }) 78 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/', 13 | proxyTable: {}, 14 | 15 | // Various Dev Server settings 16 | host: 'localhost', // can be overwritten by process.env.HOST 17 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 18 | autoOpenBrowser: true, 19 | errorOverlay: true, 20 | notifyOnErrors: true, 21 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 22 | 23 | // Use Eslint Loader? 24 | // If true, your code will be linted during bundling and 25 | // linting errors and warnings will be shown in the console. 26 | useEslint: true, 27 | // If true, eslint errors and warnings will also be shown in the error overlay 28 | // in the browser. 29 | showEslintErrorsInOverlay: false, 30 | 31 | /** 32 | * Source Maps 33 | */ 34 | 35 | // https://webpack.js.org/configuration/devtool/#development 36 | devtool: 'cheap-module-eval-source-map', 37 | 38 | // If you have problems debugging vue-files in devtools, 39 | // set this to false - it *may* help 40 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 41 | cacheBusting: true, 42 | 43 | cssSourceMap: true 44 | }, 45 | 46 | build: { 47 | // Template for index.html 48 | index: path.resolve(__dirname, '../dist/index.html'), 49 | 50 | // Paths 51 | assetsRoot: path.resolve(__dirname, '../dist'), 52 | assetsSubDirectory: 'static', 53 | assetsPublicPath: './', 54 | 55 | /** 56 | * Source Maps 57 | */ 58 | 59 | productionSourceMap: true, 60 | // https://webpack.js.org/configuration/devtool/#production 61 | devtool: '#source-map', 62 | 63 | // Gzip off by default as many popular static hosts such as 64 | // Surge or Netlify already gzip all static assets for you. 65 | // Before setting to `true`, make sure to: 66 | // npm install --save-dev compression-webpack-plugin 67 | productionGzip: false, 68 | productionGzipExtensions: ['js', 'css'], 69 | 70 | // Run the build command with an extra argument to 71 | // View the bundle analyzer report after build finishes: 72 | // `npm run build --report` 73 | // Set to `true` or `false` to always turn it on or off 74 | bundleAnalyzerReport: process.env.npm_config_report 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-pwa", 3 | "version": "1.0.0", 4 | "description": "A pwa project with vue", 5 | "author": "caixiangqi ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 9 | "start": "npm run dev", 10 | "e2e": "node test/e2e/runner.js", 11 | "test": "npm run unit && npm run e2e", 12 | "lint": "eslint --ext .js,.vue src test/unit test/e2e/specs", 13 | "build": "node build/build.js" 14 | }, 15 | "dependencies": { 16 | "nprogress": "^0.2.0", 17 | "vue": "^2.5.2", 18 | "vue-router": "^3.0.1" 19 | }, 20 | "devDependencies": { 21 | "autoprefixer": "^7.1.2", 22 | "babel-core": "^6.22.1", 23 | "babel-eslint": "^8.2.1", 24 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 25 | "babel-loader": "^7.1.1", 26 | "babel-plugin-syntax-jsx": "^6.18.0", 27 | "babel-plugin-transform-runtime": "^6.22.0", 28 | "babel-plugin-transform-vue-jsx": "^3.5.0", 29 | "babel-preset-env": "^1.3.2", 30 | "babel-preset-stage-2": "^6.22.0", 31 | "babel-register": "^6.22.0", 32 | "chalk": "^2.0.1", 33 | "chromedriver": "^2.27.2", 34 | "copy-webpack-plugin": "^4.3.1", 35 | "cross-spawn": "^5.0.1", 36 | "css-loader": "^0.28.0", 37 | "eslint": "^4.15.0", 38 | "eslint-config-standard": "^10.2.1", 39 | "eslint-friendly-formatter": "^3.0.0", 40 | "eslint-loader": "^1.7.1", 41 | "eslint-plugin-import": "^2.7.0", 42 | "eslint-plugin-node": "^5.2.0", 43 | "eslint-plugin-promise": "^3.4.0", 44 | "eslint-plugin-standard": "^3.0.1", 45 | "eslint-plugin-vue": "^4.0.0", 46 | "extract-text-webpack-plugin": "^3.0.0", 47 | "file-loader": "^1.1.4", 48 | "friendly-errors-webpack-plugin": "^1.6.1", 49 | "html-webpack-plugin": "^2.30.1", 50 | "nightwatch": "^0.9.12", 51 | "node-notifier": "^5.1.2", 52 | "optimize-css-assets-webpack-plugin": "^3.2.0", 53 | "ora": "^1.2.0", 54 | "portfinder": "^1.0.13", 55 | "postcss-import": "^11.0.0", 56 | "postcss-loader": "^2.0.8", 57 | "postcss-url": "^7.2.1", 58 | "rimraf": "^2.6.0", 59 | "selenium-server": "^3.0.1", 60 | "semver": "^5.3.0", 61 | "shelljs": "^0.7.6", 62 | "sw-precache-webpack-plugin": "^0.11.4", 63 | "uglifyjs-webpack-plugin": "^1.1.1", 64 | "url-loader": "^0.5.8", 65 | "vue-loader": "^13.3.0", 66 | "vue-style-loader": "^3.0.1", 67 | "vue-template-compiler": "^2.5.2", 68 | "webpack": "^3.6.0", 69 | "webpack-bundle-analyzer": "^2.9.0", 70 | "webpack-dev-server": "^2.9.1", 71 | "webpack-merge": "^4.1.0" 72 | }, 73 | "engines": { 74 | "node": ">= 6.0.0", 75 | "npm": ">= 3.0.0" 76 | }, 77 | "browserslist": [ 78 | "> 1%", 79 | "last 2 versions", 80 | "not ie <= 8", 81 | "Android >= 4" 82 | ] 83 | } 84 | -------------------------------------------------------------------------------- /src/sw/register.dev.js: -------------------------------------------------------------------------------- 1 | var key 2 | var authSecret 3 | var endpoint 4 | var vapidPublicKey = 'your vapidPublicKey' 5 | var NOTIFICATION_HOST = 'https://yourservice.dev.host' 6 | 7 | function urlBase64ToUint8Array (base64String) { 8 | const padding = '='.repeat((4 - base64String.length % 4) % 4) 9 | const base64 = (base64String + padding) 10 | .replace(/\-/g, '+') 11 | .replace(/_/g, '/') 12 | const rawData = window.atob(base64) 13 | const outputArray = new Uint8Array(rawData.length) 14 | for (let i = 0; i < rawData.length; ++i) { 15 | outputArray[i] = rawData.charCodeAt(i) 16 | } 17 | return outputArray 18 | } 19 | 20 | function getStorageItem (key) { 21 | return window.localStorage.getItem(key) 22 | } 23 | 24 | function setStorageItem (key, value) { 25 | window.localStorage.removeItem(key) 26 | window.localStorage.setItem(key, value) 27 | } 28 | 29 | function removeStorageItem (key) { 30 | window.localStorage.removeItem(key) 31 | } 32 | 33 | function subscribe () { 34 | if (navigator.serviceWorker) { 35 | navigator.serviceWorker.register('./sw.js') 36 | .then(function (registration) { 37 | return registration.pushManager.getSubscription() 38 | .then(function(subscription) { 39 | // if subscription exit 40 | if (subscription) { 41 | return 42 | } 43 | // if user unsubscribe 44 | if (getStorageItem('vue_pwa_notification_disabled')) { 45 | return 46 | } 47 | 48 | // subscribe 49 | return registration.pushManager.subscribe({ 50 | userVisibleOnly: true, 51 | applicationServerKey: urlBase64ToUint8Array(vapidPublicKey) 52 | }) 53 | .then(function (subscription) { 54 | var rawKey = subscription.getKey ? subscription.getKey('p256dh') : '' 55 | var rawAuthSecret = subscription.getKey ? subscription.getKey('auth') : '' 56 | key = rawKey ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : '' 57 | authSecret = rawAuthSecret ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawAuthSecret))) : '' 58 | endpoint = subscription.endpoint 59 | 60 | // post user's subscription to server 61 | return fetch(NOTIFICATION_HOST + '/subscribe', { 62 | // todo: post {endpoint, key, authSecret} 63 | }) 64 | .then(function (res) { 65 | res.json() 66 | .then(json => { 67 | // save subscription user_id in localstorage for unsubscribe 68 | setStorageItem('vue_pwa_user_id', json.user_id) 69 | }) 70 | }) 71 | .catch(function (err) { 72 | console.log(err) 73 | }) 74 | }) 75 | }) 76 | }) 77 | .catch(function (err) { 78 | console.log('ServiceWorker registration failed: ', err) 79 | }) 80 | } 81 | } 82 | 83 | function unsubscribe() { 84 | if (navigator.serviceWorker) { 85 | navigator.serviceWorker.ready 86 | .then((serviceWorkerRegistration) => { 87 | serviceWorkerRegistration.pushManager.getSubscription() 88 | .then((subscription) => { 89 | // if subscription don't exit 90 | if (!subscription) { 91 | return 92 | } 93 | // save unsubscribe status in localstorage 94 | setStorageItem('vue_pwa_notification_disabled', '1') 95 | subscription.unsubscribe() 96 | .then(function () { 97 | var vuePwaUserId = getStorageItem('vue_pwa_user_id') 98 | if (vuePwaUserId) { 99 | // remove subscription user_id in localstorage 100 | removeStorageItem('vue_pwa_user_id') 101 | // delete user's subscription on server 102 | return fetch(NOTIFICATION_HOST + '/unsubscribe', { 103 | // todo: delete user 104 | }) 105 | } 106 | }) 107 | .catch((e) => { 108 | logger.error('Error thrown while unsubscribing from push messaging', e) 109 | }) 110 | }) 111 | }) 112 | } 113 | } 114 | 115 | // bind unsubscribe button event 116 | document.getElementById("unsubscribe").addEventListener("click", unsubscribe) 117 | // check subscription when serverWorker registed 118 | subscribe() -------------------------------------------------------------------------------- /src/sw/register.prd.js: -------------------------------------------------------------------------------- 1 | var key 2 | var authSecret 3 | var endpoint 4 | var vapidPublicKey = 'your vapidPublicKey' 5 | var NOTIFICATION_HOST = 'https://yourservice.prd.host' 6 | 7 | function urlBase64ToUint8Array (base64String) { 8 | const padding = '='.repeat((4 - base64String.length % 4) % 4) 9 | const base64 = (base64String + padding) 10 | .replace(/\-/g, '+') 11 | .replace(/_/g, '/') 12 | const rawData = window.atob(base64) 13 | const outputArray = new Uint8Array(rawData.length) 14 | for (let i = 0; i < rawData.length; ++i) { 15 | outputArray[i] = rawData.charCodeAt(i) 16 | } 17 | return outputArray 18 | } 19 | 20 | function getStorageItem (key) { 21 | return window.localStorage.getItem(key) 22 | } 23 | 24 | function setStorageItem (key, value) { 25 | window.localStorage.removeItem(key) 26 | window.localStorage.setItem(key, value) 27 | } 28 | 29 | function removeStorageItem (key) { 30 | window.localStorage.removeItem(key) 31 | } 32 | 33 | function subscribe () { 34 | if (navigator.serviceWorker) { 35 | navigator.serviceWorker.register('./sw.js') 36 | .then(function (registration) { 37 | return registration.pushManager.getSubscription() 38 | .then(function(subscription) { 39 | // if subscription exit 40 | if (subscription) { 41 | return 42 | } 43 | // if user unsubscribe 44 | if (getStorageItem('vue_pwa_notification_disabled')) { 45 | return 46 | } 47 | // subscribe 48 | return registration.pushManager.subscribe({ 49 | userVisibleOnly: true, 50 | applicationServerKey: urlBase64ToUint8Array(vapidPublicKey) 51 | }) 52 | .then(function (subscription) { 53 | var rawKey = subscription.getKey ? subscription.getKey('p256dh') : '' 54 | var rawAuthSecret = subscription.getKey ? subscription.getKey('auth') : '' 55 | key = rawKey ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : '' 56 | authSecret = rawAuthSecret ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawAuthSecret))) : '' 57 | endpoint = subscription.endpoint 58 | // post user's subscription to server 59 | return fetch(NOTIFICATION_HOST + '/subscribe', { 60 | // todo: post {endpoint, key, authSecret} 61 | }) 62 | .then(function (res) { 63 | res.json() 64 | .then(json => { 65 | // save subscription user_id in localstorage for unsubscribe 66 | setStorageItem('vue_pwa_user_id', json.user_id) 67 | }) 68 | }) 69 | .catch(function (err) { 70 | console.log(err) 71 | }) 72 | }) 73 | }) 74 | }) 75 | .catch(function (err) { 76 | console.log('ServiceWorker registration failed: ', err, err.code, err.message, err.name) 77 | }) 78 | } 79 | } 80 | 81 | function unsubscribe() { 82 | if (navigator.serviceWorker) { 83 | navigator.serviceWorker.ready 84 | .then((serviceWorkerRegistration) => { 85 | serviceWorkerRegistration.pushManager.getSubscription() 86 | .then((subscription) => { 87 | // if subscription don't exit 88 | if (!subscription) { 89 | return 90 | } 91 | // save unsubscribe status in localstorage 92 | setStorageItem('vue_pwa_notification_disabled', '1') 93 | subscription.unsubscribe() 94 | .then(function () { 95 | var vuePwaUserId = getStorageItem('vue_pwa_user_id') 96 | if (vuePwaUserId) { 97 | // remove subscription user_id in localstorage 98 | removeStorageItem('vue_pwa_user_id') 99 | // delete user's subscription on server 100 | return fetch(NOTIFICATION_HOST + '/unsubscribe', { 101 | // todo: delete user 102 | }) 103 | } 104 | }) 105 | .catch((e) => { 106 | logger.error('Error thrown while unsubscribing from push messaging', e) 107 | }) 108 | }) 109 | }) 110 | } 111 | } 112 | 113 | // bind unsubscribe button event 114 | document.getElementById("unsubscribe").addEventListener("click", unsubscribe) 115 | // check subscription when serverWorker registed 116 | subscribe() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SIMPLE-PWA 2 | 本项目是pwa搭载在vue的示例项目,pwa是一个渐进式web应用概念,不依赖任何框架,vue只是一个实现载体。 3 | 4 | ## 概念 5 | [Progressive Web Apps](https://developer.mozilla.org/zh-CN/Apps/Progressive "pwa") 是渐进式Web应用程序,运行在现代浏览器并展现出超级能力。支持可发现,可安装,离线运行,消息推送等APP特有的能力,本项目以最简单的方式封装了以上功能。 6 | 7 | ## demo 8 | ![pwa example](./static/code.png) 9 | 10 | 请使用android高版本(>5.0)最新版chrome浏览器访问。 11 | 12 | ## 目录 13 | - 任务清单,[查看所有可配置项](https://developer.mozilla.org/en-US/docs/Web/Manifest "配置项") 14 | - 离线浏览 15 | - 推送通知 16 | - 工具:[性能工具LightHouse](https://developers.google.com/web/tools/lighthouse "LightHouse"),[sw工具库sw-toolbox](https://github.com/GoogleChromeLabs/sw-toolbox "sw-toolbox") 17 | 18 | ## 开始 19 | ``` bash 20 | # install dependencies 21 | npm install 22 | 23 | # serve with hot reload at localhost:8080 24 | npm run dev 25 | 26 | # build for production with minification 27 | npm run build 28 | ``` 29 | 30 | ## 项目结构 31 | 由于本项目使用vue-cli生成结构,大部分目录同vue项目,重点看/src/sw、index.html。 32 | 33 | > **注意:** 如需开启推送功能,开发时将 sw/ 目录下各文件域名替换为自己的域名 34 | - index.html 35 | - /src/sw 36 | - manifest.json 37 | - register.dev.js 38 | - register.prd.js 39 | - register.simple.js 40 | - service-worker.tmpl 41 | - sw.js 42 | 43 | ## 运行过程 44 | pwa工作依赖于Service Worker(简称sw)。 45 | > 任务清单 -> sw注册 -> sw监听/代理 46 | 47 | 1. 首先在index.html引入manifest.json和用于注册sw的register.js; 48 | 2. 接着register.js会检测navigator.serviceWorker并将sw.js注册; 49 | 3. 最后sw.js持续监听和代理sw事件。 50 | 51 | > 注意:本项目使用copy-webpack-plugin和sw-precache-webpack-plugin将/src/sw下的文件编译并生成到项目根目录下,因此使用<%= htmlWebpackPlugin.files.publicPath %>变量获取 52 | 53 | **index.html:** 54 | ```html 55 | 56 | 57 | 58 | ... 59 | 60 | 61 | 62 | 63 | ... 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | ``` 72 | **register.js:** 73 | ```js 74 | if (navigator.serviceWorker) { 75 | navigator.serviceWorker.register('/sw.js') 76 | .then(function (registration) { 77 | console.log('Registered events at scope: ', registration) 78 | }) 79 | .catch(function (err) { 80 | console.log('ServiceWorker registration failed: ', err) 81 | }) 82 | } 83 | ``` 84 | **sw.js:** 85 | ```js 86 | self.addEventListener('some event', e => { 87 | // do something 88 | }) 89 | ``` 90 | 91 | ## 工作规则 92 | ### 添加到主屏幕 93 | 添加到主屏幕既可安装,需要满足以下条件: 94 | 1. 需要 manifest.json 文件 95 | 2. 清单文件需要启动 URL(start_url) 96 | 3. 清单文件至少需要 144x144 的 PNG 图标 97 | 4. 网站正在使用通过 HTTPS(或localhost) 运行的 Service Worker,既sw处于activated状态 98 | 5. 用户需要至少浏览网站两次,并且两次访问间隔在五分钟之上 99 | 100 | ![添加到主屏幕](https://user-gold-cdn.xitu.io/2018/3/12/16218297564a044a?w=586&h=253&f=png&s=76682) 101 | 102 | ```json 103 | { 104 | "name": "PWA Lite", 105 | "short_name": "PWA", 106 | "display": "standalone", 107 | "start_url": "/", 108 | "theme_color": "#ffffff", 109 | "background_color": "#ffffff", 110 | "icons": [ 111 | { 112 | "src": "./static/appicon_144.png", 113 | "sizes": "144x144", 114 | "type": "image/png" 115 | }, 116 | { 117 | "src": "./static/appicon_96.png", 118 | "sizes": "96x96", 119 | "type": "image/png" 120 | }, 121 | 122 | { 123 | "src": "./static/appicon_48.png", 124 | "sizes": "48x48", 125 | "type": "image/png" 126 | } 127 | ] 128 | } 129 | ``` 130 | 131 | 在chrome开发者工具中: 132 | 1. Application/Manifest 检查manifest.json有效性 133 | 2. Application/Service Workers 查看当前sw状态 134 | 3. Application/Service Workers 调试时可勾选"Update on reload" 135 | 136 | 添加到主屏幕只会显示一次,这在调试过程非常不便。不过,如果你使用chrome调试,可以访问 **chrome://flags/#bypass-app-banner-engagement-checks** 勾选忽略,这样每次访问都会显示。 137 | 138 | ### 离线浏览 139 | sw提供很多种 [缓存模式](https://jakearchibald.com/2014/offline-cookbook/ "缓存模式")。 140 | 141 | sw处于activated状态时,可以在sw.js监听fetch事件并拦截和处理任何请求: 142 | ```js 143 | // 请求命中缓存时直接返回缓存结果,否则发起请求 144 | self.addEventListener('fetch', e => { 145 | e.respondWith( 146 | caches.match(e.request).then(response => { 147 | if (response != null) { 148 | return response 149 | } 150 | return fetch(e.request.url) 151 | }) 152 | ) 153 | }) 154 | ``` 155 | 156 | ![离线浏览](https://user-gold-cdn.xitu.io/2018/3/12/162182ab8a3cddb2?w=592&h=251&f=png&s=56299) 157 | 158 | 在chrome开发者工具中: 159 | 1. Application/Cache/Cache Storage 查看缓存列表 160 | 2. 如果sw拦截成功,在Network中size列可以看到(from ServiceWorker)字样 161 | 3. 在Network中勾选offline然后刷新页面,可以看到已访问过的页面仍然可以访问,并不会出现“未连接到互联网”,这就是离线浏览的威力 162 | 163 | ### 关于浏览器缓存模式 164 | 1. **from memory cache** 内存,只存在浏览器运行时,如base64图片数据和静态资源,不可控 165 | 2. **from disk cache** 硬盘,长期缓存在硬盘中,如静态资源,不可控 166 | 3. **from ServiceWorker** sw代理,完全可控 167 | 168 | ![浏览器缓存模式](https://user-gold-cdn.xitu.io/2018/3/12/162182b497b534ca?w=752&h=144&f=png&s=46193) 169 | 170 | ### 消息推送 171 | ![notification](https://raw.githubusercontent.com/SangKa/PWA-Book-CN/master/assets/figure6.4.png "notification") 172 | 173 | 消息推送需要满足以下条件: 174 | 1. sw处于activated状态 175 | 2. 用户允许通知 176 | 3. 询问用户是否允许的对话框只会显示一次,可以在chrome地址栏点击i图标,将“通知”项改为“使用全局默认设置(询问)”即可发起询问对话框 177 | 4. 用户允许后会得到订阅对象,其中endpoint指向谷歌的推送服务器,因此接收时需要全局翻墙 178 | 179 | ![消息推送](https://user-gold-cdn.xitu.io/2018/3/12/162182bca562924b?w=513&h=263&f=png&s=51909) 180 | 181 | ```js 182 | // 服务器推送事件 183 | self.addEventListener('push', e => { 184 | // do something 185 | } 186 | 187 | // 推送消息对话框点击事件 188 | self.addEventListener('notificationclick', e => { 189 | // do something 190 | } 191 | ``` -------------------------------------------------------------------------------- /src/sw/service-worker.tmpl: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // DO NOT EDIT THIS GENERATED OUTPUT DIRECTLY! 18 | // This file should be overwritten as part of your build process. 19 | // If you need to extend the behavior of the generated service worker, the best approach is to write 20 | // additional code and include it using the importScripts option: 21 | // https://github.com/GoogleChrome/sw-precache#importscripts-arraystring 22 | // 23 | // Alternatively, it's possible to make changes to the underlying template file and then use that as the 24 | // new base for generating output, via the templateFilePath option: 25 | // https://github.com/GoogleChrome/sw-precache#templatefilepath-string 26 | // 27 | // If you go that route, make sure that whenever you update your sw-precache dependency, you reconcile any 28 | // changes made to this original template file with your modified copy. 29 | 30 | // This generated service worker JavaScript will precache your site's resources. 31 | // The code needs to be saved in a .js file at the top-level of your site, and registered 32 | // from your pages in order to be used. See 33 | // https://github.com/googlechrome/sw-precache/blob/master/demo/app/js/service-worker-registration.js 34 | // for an example of how you can register this script and handle various service worker events. 35 | 36 | /* eslint-env worker, serviceworker */ 37 | /* eslint-disable indent, no-unused-vars, no-multiple-empty-lines, max-nested-callbacks, space-before-function-paren, quotes, comma-spacing */ 38 | 'use strict'; 39 | 40 | var precacheConfig = <%= precacheConfig %>; 41 | var cacheName = 'vue-pwa-<%= version %>-<%= cacheId %>-' + (self.registration ? self.registration.scope : ''); 42 | 43 | var assetHostPattern = /\/\/(yourservice.prd.host)/ 44 | 45 | <% if (handleFetch) { %> 46 | var ignoreUrlParametersMatching = [<%= ignoreUrlParametersMatching %>]; 47 | <% } %> 48 | 49 | <% Object.keys(externalFunctions).sort().forEach(function(functionName) {%> 50 | var <%- functionName %> = <%= externalFunctions[functionName] %>; 51 | <% }); %> 52 | 53 | var hashParamName = '_sw-precache'; 54 | var urlsToCacheKeys = new Map( 55 | precacheConfig.map(function(item) { 56 | var relativeUrl = item[0]; 57 | var hash = item[1]; 58 | var absoluteUrl = new URL(relativeUrl, self.location); 59 | var cacheKey = createCacheKey(absoluteUrl, hashParamName, hash, <%= dontCacheBustUrlsMatching %>); 60 | return [absoluteUrl.toString(), cacheKey]; 61 | }) 62 | ); 63 | 64 | function setOfCachedUrls(cache) { 65 | return cache.keys().then(function(requests) { 66 | return requests.map(function(request) { 67 | return request.url; 68 | }); 69 | }).then(function(urls) { 70 | return new Set(urls); 71 | }); 72 | } 73 | 74 | self.addEventListener('install', function(event) { 75 | event.waitUntil( 76 | caches.open(cacheName).then(function(cache) { 77 | return setOfCachedUrls(cache).then(function(cachedUrls) { 78 | return Promise.all( 79 | Array.from(urlsToCacheKeys.values()).map(function(cacheKey) { 80 | // If we don't have a key matching url in the cache already, add it. 81 | if (!cachedUrls.has(cacheKey)) { 82 | var request = new Request(cacheKey, {credentials: 'same-origin'}); 83 | return fetch(request).then(function(response) { 84 | // Bail out of installation unless we get back a 200 OK for 85 | // every request. 86 | if (!response.ok) { 87 | throw new Error('Request for ' + cacheKey + ' returned a ' + 88 | 'response with status ' + response.status); 89 | } 90 | 91 | return cleanResponse(response).then(function(responseToCache) { 92 | return cache.put(cacheKey, responseToCache); 93 | }); 94 | }); 95 | } 96 | }) 97 | ); 98 | }); 99 | }).then(function() { 100 | <% if (skipWaiting) { %> 101 | // Force the SW to transition from installing -> active state 102 | return self.skipWaiting(); 103 | <% } %> 104 | }) 105 | ); 106 | }); 107 | 108 | self.addEventListener('activate', function(event) { 109 | var setOfExpectedUrls = new Set(urlsToCacheKeys.values()); 110 | 111 | event.waitUntil( 112 | caches.open(cacheName).then(function(cache) { 113 | return cache.keys().then(function(existingRequests) { 114 | return Promise.all( 115 | existingRequests.map(function(existingRequest) { 116 | if (!setOfExpectedUrls.has(existingRequest.url)) { 117 | return cache.delete(existingRequest); 118 | } 119 | }) 120 | ); 121 | }); 122 | }).then(function() { 123 | <% if (clientsClaim) { %> 124 | return self.clients.claim(); 125 | <% } %> 126 | }) 127 | ); 128 | }); 129 | 130 | self.addEventListener('push', function(e) { 131 | if (!(self.Notification && self.Notification.permission === 'granted')) { 132 | return 133 | } 134 | var data = {}; 135 | if (e.data) { 136 | data = e.data.json(); 137 | data.data = {}; 138 | data.data.url = data.url; 139 | } 140 | console.log(data); 141 | e.waitUntil(self.registration.showNotification(data.title, data)); 142 | }) 143 | 144 | self.addEventListener('notificationclick', function (e) { 145 | e.notification.close() 146 | 147 | var url = e.notification.data.url; 148 | if (!url) { 149 | return 150 | } 151 | e.waitUntil( 152 | clients.matchAll({ 153 | type: "window" 154 | }) 155 | .then(function (clientList) { 156 | for (var i = 0; i < clientList.length; i++) { 157 | var client = clientList[i]; 158 | if (client.url == '/' && 'focus' in client) 159 | return client.focus(); 160 | } 161 | if (clients.openWindow) { 162 | return clients.openWindow(url); 163 | } 164 | }) 165 | ); 166 | }) 167 | 168 | <% if (handleFetch) { %> 169 | self.addEventListener('fetch', function(event) { 170 | if (assetHostPattern.test(event.request.url)) { 171 | // Should we call event.respondWith() inside this fetch event handler? 172 | // This needs to be determined synchronously, which will give other fetch 173 | // handlers a chance to handle the request if need be. 174 | var shouldRespond; 175 | 176 | // First, remove all the ignored parameters and hash fragment, and see if we 177 | // have that URL in our cache. If so, great! shouldRespond will be true. 178 | var url = stripIgnoredUrlParameters(event.request.url, ignoreUrlParametersMatching); 179 | shouldRespond = urlsToCacheKeys.has(url); 180 | 181 | // If shouldRespond is false, check again, this time with 'index.html' 182 | // (or whatever the directoryIndex option is set to) at the end. 183 | var directoryIndex = '<%= directoryIndex %>'; 184 | if (!shouldRespond && directoryIndex) { 185 | url = addDirectoryIndex(url, directoryIndex); 186 | shouldRespond = urlsToCacheKeys.has(url); 187 | } 188 | 189 | // If shouldRespond is still false, check to see if this is a navigation 190 | // request, and if so, whether the URL matches navigateFallbackWhitelist. 191 | var navigateFallback = '<%= navigateFallback %>'; 192 | if (!shouldRespond && 193 | navigateFallback && 194 | (event.request.mode === 'navigate') && 195 | isPathWhitelisted(<%= navigateFallbackWhitelist %>, event.request.url)) { 196 | url = new URL(navigateFallback, self.location).toString(); 197 | shouldRespond = urlsToCacheKeys.has(url); 198 | } 199 | 200 | // If shouldRespond was set to true at any point, then call 201 | // event.respondWith(), using the appropriate cache key. 202 | if (shouldRespond) { 203 | event.respondWith( 204 | caches.open(cacheName).then(function(cache) { 205 | return cache.match(urlsToCacheKeys.get(url)).then(function(response) { 206 | if (response) { 207 | return response; 208 | } 209 | throw Error('The cached response that was expected is missing.'); 210 | }); 211 | }).catch(function(e) { 212 | // Fall back to just fetch()ing the request if some unexpected error 213 | // prevented the cached response from being valid. 214 | console.warn('Couldn\'t serve response for "%s" from cache: %O', event.request.url, e); 215 | return fetch(event.request); 216 | }) 217 | ); 218 | } 219 | } 220 | }); 221 | 222 | <% if (swToolboxCode) { %> 223 | // *** Start of auto-included sw-toolbox code. *** 224 | <%= swToolboxCode %> 225 | // *** End of auto-included sw-toolbox code. *** 226 | <% } %> 227 | 228 | <% if (runtimeCaching) { %> 229 | // Runtime cache configuration, using the sw-toolbox library. 230 | <%= runtimeCaching %> 231 | <% } %> 232 | <% } %> 233 | 234 | <% if (importScripts) { %> 235 | importScripts(<%= importScripts %>); 236 | <% } %> 237 | --------------------------------------------------------------------------------