chaihongjun.me

将网站改造成PWA

如今网络上已经有相关的文章介绍,如何将网站变身成为PWA,(Progressive Web App) 类似native app原生应用,这里不对PWA做解释,只是记录一下本站的改造过程。完成pwa改造,需要给网站添加两个概念"serviceworker"和"manifest"的技术,至于这两个是什么东西,可以搜索了解。

serviceworker

首先在网站公共部分添加JS代码:

if('serviceWorker'in navigator){navigator.serviceWorker.register('/serviceworker.js');}

这段代码是在页面注册serviceworker,放置在页面公共部分是使,网站所有页面都可以注册serviceworker并使用它。

然后,如上代码所示,再在网站的根目录创建文件serviceworker.js,这个文件的内容如下:

'use strict';    
const version = 'v20190212';    
const __DEVELOPMENT__ = false;    
const __DEBUG__ = false;    
const offlineResources = ['/', '/offline.html', '/offline.svg'];   
//  ignoreFetch 里面是忽略抓取的内容,主要是匹配URL和文件类型
//  注意正则匹配
const ignoreFetch = [    
/https?://xiongzhang.baidu.com//,    
/https?://ae.bdstatic.com//,    
/https?://msite.baidu.com//,    
/https?://s.bdstatic.com//,    
/https?://timg01.bdimg.com//,    
/https?://zz.bdstatic.com//,    
/https?://hm.baidu.com//,    
/https?://jspassport.ssl.qhimg.com//,    
/https?://s.ssl.qhres.com//,    
/https?://changyan.itc.cn//,    
/https?://changyan.sohu.com//,    
/.php$/,     
];    
function onInstall(event) { log('install event in progress.');    
event.waitUntil(updateStaticCache()); }    
function updateStaticCache() { return caches.open(cacheKey('offline')).then((cache) => { return cache.addAll(offlineResources); }).then(() => { log('installation complete!'); }); }    
function onFetch(event) { const request = event.request; if (shouldAlwaysFetch(request)) { event.respondWith(networkedOrOffline(request)); return; } if (shouldFetchAndCache(request)) { event.respondWith(networkedOrCached(request)); return; } event.respondWith(cachedOrNetworked(request)); }    
function networkedOrCached(request) { return networkedAndCache(request).catch(() => { return cachedOrOffline(request) }); }    
function networkedAndCache(request) { return fetch(request).then((response) => { var copy = response.clone();    
caches.open(cacheKey('resources')).then((cache) => { cache.put(request, copy); });    
log("(network: cache write)", request.method, request.url); return response; }); }    
function cachedOrNetworked(request) { return caches.match(request).then((response) => { log(response ? '(cached)' : '(network: cache miss)', request.method, request.url); return response || networkedAndCache(request).catch(() => { return offlineResponse(request) }); }); }    
function networkedOrOffline(request) { return fetch(request).then((response) => { log('(network)', request.method, request.url); return response; }).catch(() => { return offlineResponse(request); }); }    
function cachedOrOffline(request) { return caches.match(request).then((response) => { return response || offlineResponse(request); }); }    
function offlineResponse(request) { log('(offline)', request.method, request.url); if (request.url.match(/.(jpg|png|gif|svg|jpeg)(?.*)?$/)) { return caches.match('/offline.svg'); } else { return caches.match('/offline.html'); } }    
function onActivate(event) { log('activate event in progress.');    
event.waitUntil(removeOldCache()); }    
function removeOldCache() { return caches.keys().then((keys) => { return Promise.all(keys.filter((key) => { return !key.startsWith(version); }).map((key) => { return caches.delete(key); })); }).then(() => { log('removeOldCache completed.'); }); }    
function cacheKey() { return [version, ...arguments].join(':'); }    
function log() { if (developmentMode()) { console.log("SW:", ...arguments); } }    
function shouldAlwaysFetch(request) { return __DEVELOPMENT__ || request.method !== 'GET' || ignoreFetch.some(regex => request.url.match(regex)); }    
function shouldFetchAndCache(request) { return ~request.headers.get('Accept').indexOf('text/html'); }    
function developmentMode() { return __DEVELOPMENT__ || __DEBUG__; } log("Hello from ServiceWorker land!", version);    
self.addEventListener('install', onInstall);    
self.addEventListener('fetch', onFetch);    
self.addEventListener("activate", onActivate);

以上内容来源自《service worker配置优化版》并作修改,这段代码将会缓存网站的文件。

manifest.json

在网站的本目录创建一个manifest.json的文件:

{
    "name": "这里写网站的标题名称",
    "short_name": "这里写网站标题的简称",
    "start_url": "这里写网站起始URL一般使用/",
    "display": "这里写网站在浏览器中的显示模式一般使用standalone",
    "background_color": "网站的背景颜色#fff",
    "description": "这里写对于网站的描述",
    "orientation": "网站显示的方向portrait-primary",
    "theme_color": "网站的主题颜色#fff",
    "icons": [ {   //网站放置在桌面使用的图标,一般只需要192和512两个尺寸即可
        "src": "/img/192.png",
        "sizes": "192x192",
        "type": "image/png"
    }, {
        "src": "/img/512.png",
        "sizes": "512x512",
        "type": "image/png"
    }]
}


接着在网站首页头部添加

<link rel=manifest href="/manifest.json">

最后再在网站首页的头部添加

<meta name=theme-color content="#fff">

content是网站主题颜色值。

至此,网站的PWA改造就完成了。
当然,为了更好的用户体验,应当补全离线情况下的页面显示(offline.html):
 

<!DOCTYPE html>    
<html lang=zh-cmn-Hans>    
<head>    
<meta charset=utf-8>    
<meta name=theme-color content="#242628">    
<meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">    
<meta http-equiv=X-UA-Compatible content="ie=edge">    
<link rel=stylesheet href="css/offline.css">    
<title>网络异常 | Powered by chaihongjun.me</title>    
<style>body { background: #242628; color: #d6d7d9; padding: 0 1rem; }</style>    
</head>    
<body> 
<h1>网络异常</h1>    
<p>你的网络似乎遭遇了中断,无法从 <code>chaihongjun.me</code> 获取最新的数据。</p>    
<p>由于 Service Worker 技术,你之前访问过的页面已被缓存,可以从当前设备上离线访问,但部分资源(图片、样式等)可能无法显示。</p>    
<button class=center onclick="window.history.back();">返回上一页</button>    
<p>若已确认你的网络环境正常,可以点击下方的按钮或按浏览器刷新按钮,来重载当前页面。</p>    
<button class=center onclick="location.reload()">立即重载</button>    
</body>    
</html>

当然,本身serviceworker.js文件的加载也需要一定时间,为了加速这个过程,可以在服务器端进行push操作,进一步提升速度。

然后还有一些注意事项,serviceworker.js这个文件应当不让浏览器缓存,需要让浏览器每次都获取的是最新的。于是在nginx那里配置如下:

location ~* (serviceworker.js)$ {
  add_header Last-Modified $date_gmt;
  add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
  if_modified_since off;
  expires off;
  etag off;
}

另外pagespeed也要避免缓存这个文件

pagespeed Disallow  "serviceworker.js";

然后,最好添加如下代码可以监测SW和PWA的使用情况:

// 控制台显示service worker缓存占用情况
if ('storage' in navigator && 'estimate' in navigator.storage) {
    navigator.storage.estimate().then(estimate => {
        console.log(`Using ${estimate.usage/1024/1024} out of ${estimate.quota/1024/1024} MB.And the proportion is ${estimate.usage/estimate.quota*100}%`);
    });
}

//PWA 用户端监测
self.addEventListener('error', function (event) {
  var msg = {
    message: event.message,
    filename: event.filename,
    lineno: event.lineno,
    stack: event.error && event.error.stack
  };
  // report error msg
});

self.addEventListener('unhandledrejection', function (event) {
  // event.reason
  if (/Quota exceeded/i.test(event.reason)) {
    // maybe clean some cache here
  }
});


知识共享许可协议本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。作者:柴宏俊»