├── 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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |

4 |
Welcome to pwa project with vue
5 |
user
6 |
7 |
8 |
9 |
14 |
15 |
16 |
29 |
--------------------------------------------------------------------------------
/src/components/User.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
Chess
5 |
home
6 |
This is my introduce
7 |
Hello World
8 |
9 |
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 | 
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 | 
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 | 
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 | 
169 |
170 | ### 消息推送
171 | 
172 |
173 | 消息推送需要满足以下条件:
174 | 1. sw处于activated状态
175 | 2. 用户允许通知
176 | 3. 询问用户是否允许的对话框只会显示一次,可以在chrome地址栏点击i图标,将“通知”项改为“使用全局默认设置(询问)”即可发起询问对话框
177 | 4. 用户允许后会得到订阅对象,其中endpoint指向谷歌的推送服务器,因此接收时需要全局翻墙
178 |
179 | 
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 |
--------------------------------------------------------------------------------