如今网络上已经有相关的文章介绍,如何将网站变身成为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 } });