Progressive Web App (PWA)

Progressive Web Apps 是結合了 web 和 原生應用中最好功能的一種體驗。可以像原生應用那樣添加至主屏,能夠有全屏瀏覽的體驗。

PWA參考

下面是本站PWA的作法

index.html加入

// 註冊 service worker
    if (navigator.serviceWorker) {
        navigator.serviceWorker.register('service-worker.js')
		console.log('註冊 service worker');
    }

service-worker.js

importScripts('./cache-polyfill.js');

var cacheName = 'mikeBlog-v1';

//要cache的清單
var files = [
    './',
    './index.html?utm=homescreen',
    './js/jekyll-search.js',
    './js/jquery.mmenu.min.all.js',
    './css/style.css',
    './css/highlightjs.piperita.css',
    './css/jquery.mmenu.all.css',
    './images/wechat-sql-n.png',
    './favicon/android-chrome-192x192.png',
    './manifest.json',
    './search.json',
    './404.html',
    './experience.html',
    './posts.html',
    './post-by-categories.html',
    './search.html',
];

//install
self.addEventListener('install', (event) => {
    console.info('Event: Install');

    event.waitUntil(
        caches.open(cacheName)
            .then((cache) => {
                return cache.addAll(files)
                    .then(() => {
                        console.info('All files are cached');
                        return self.skipWaiting();
                    })
                    .catch((error) => {
                        console.error('Failed to cache', error);
                    })
            })
    );
});


//fetch
self.addEventListener('fetch', (event) => {
    console.info('Event: Fetch');

    var request = event.request;
    
    event.respondWith(
        caches.match(request).then((response) => {
            if (response) {
                return response;
            }
            
            return fetch(request).then((response) => {
                var responseToCache = response.clone();
                caches.open(cacheName).then((cache) => {
                    cache.put(request, responseToCache).catch((err) => {
                        console.warn(request.url + ': ' + err.message);
                    });
                });

                return response;
            });
        })
    );
});


//activate
self.addEventListener('activate', (event) => {
    console.info('Event: Activate');
    
    event.waitUntil(
        caches.keys().then((cacheNames) => {
            return Promise.all(
                cacheNames.map((cache) => {
                    if (cache !== cacheName) {
                        return caches.delete(cache);
                    }
                })
            );
        })
            .then(function () {
                console.info("Old caches are cleared!");                
                return self.clients.claim();
            })
    );
});


//push
self.addEventListener('push', (event) => {
    console.info('Event: Push');

    var title = 'Push notification demo';
    var body = {
        'body': 'click to return to application',
        'tag': 'demo',
        'icon': './favicons/apple-touch-icon.png',
        'badge': './favicons/apple-touch-icon.png',
        //Custom actions buttons
        'actions': [
            { 'action': 'yes', 'title': 'I ♥ this app!' },
            { 'action': 'no', 'title': 'I don\'t like this app' }
        ]
    };

    event.waitUntil(self.registration.showNotification(title, body));
});

//sync
self.addEventListener('sync', (event) => {
    console.info('Event: Sync');

    if (event.tag === 'github' || event.tag === 'test-tag-from-devtools') {
        event.waitUntil(
            self.clients.matchAll().then((all) => {
                return all.map((client) => {
                    return client.postMessage('online'); 
                })
            })
                .catch((error) => {
                    console.error(error);
                })
        );
    }
});


//notification
self.addEventListener('notificationclick', (event) => {
    var url = 'https://demopwa.in/';

    if (event.action === 'yes') {
        console.log('I ♥ this app!');
    }
    else if (event.action === 'no') {
        console.warn('I don\'t like this app');
    }

    event.notification.close(); 

    event.waitUntil(
        clients.matchAll({
            type: 'window'
        })
            .then((clients) => {
                for (var i = 0; i < clients.length; i++) {
                    var client = clients[i];
                    if (client.url === url && 'focus' in client) {
                        return client.focus();
                    }
                }

                if (clients.openWindow) {
                    return clients.openWindow('/');
                }
            })
            .catch((error) => {
                console.error(error);
            })
    );
});

cache-polyfill.js

/*
  Source: https://github.com/coonsta/cache-polyfill
  Author: https://github.com/coonsta
*/

if (!Cache.prototype.add) {
  Cache.prototype.add = function add(request) {
    return this.addAll([request]);
  };
}

if (!Cache.prototype.addAll) {
  Cache.prototype.addAll = function addAll(requests) {
    var cache = this;

    // Since DOMExceptions are not constructable:
    function NetworkError(message) {
      this.name = 'NetworkError';
      this.code = 19;
      this.message = message;
    }
    NetworkError.prototype = Object.create(Error.prototype);

    return Promise.resolve().then(function() {
      if (arguments.length < 1) throw new TypeError();

      // Simulate sequence<(Request or USVString)> binding:
      var sequence = [];

      requests = requests.map(function(request) {
        if (request instanceof Request) {
          return request;
        }
        else {
          return String(request); // may throw TypeError
        }
      });

      return Promise.all(
        requests.map(function(request) {
          if (typeof request === 'string') {
            request = new Request(request);
          }

          var scheme = new URL(request.url).protocol;

          if (scheme !== 'http:' && scheme !== 'https:') {
            throw new NetworkError("Invalid scheme");
          }

          return fetch(request.clone());
        })
      );
    }).then(function(responses) {
      // TODO: check that requests don't overwrite one another
      // (don't think this is possible to polyfill due to opaque responses)
      return Promise.all(
        responses.map(function(response, i) {
          return cache.put(requests[i], response);
        })
      );
    }).then(function() {
      return undefined;
    });
  };
}

if (!CacheStorage.prototype.match) {
  // This is probably vulnerable to race conditions (removing caches etc)
  CacheStorage.prototype.match = function match(request, opts) {
    var caches = this;

    return this.keys().then(function(cacheNames) {
      var match;

      return cacheNames.reduce(function(chain, cacheName) {
        return chain.then(function() {
          return match || caches.open(cacheName).then(function(cache) {
            return cache.match(request, opts);
          }).then(function(response) {
            match = response;
            return match;
          });
        });
      }, Promise.resolve());
    });
  };
}


manifest.json

{
  "name": "麥克的筆記本",
  "short_name": "麥克的筆記本",
  "icons": [
    {
      "src": "favicons/android-chrome-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    }
  ],
  "start_url": "./index.html?utm_source=homescreen",
  "background_color": "#000",
  "theme_color": "#536878",
  "display": "standalone",
  "orientation": "portrait",
  "author": {
    "name": "Mike Chen",
    "website": "https://mike2014mike.github.io",
    "github": "https://github.com/mike2014mike",
    "source-repo": ""
  }
}

注意:如有更新文章, service-worker.js 中的 cacheName 也要變更,不然使用者都只會看到舊文章。