时间:2023-07-18 12:39:02 | 来源:网站运营
时间:2023-07-18 12:39:02 来源:网站运营
改造你的网站,变身 PWA:最近有很多关于 Progressive Web Apps(PWAs)的消息,很多人都在问这是不是(移动)web 的未来。我不想陷入native app 和 PWA 的纷争,但是有一件事是确定的 --- PWA极大的提升了移动端表现,改善了用户体验。好消息是开发一个 PWA 并不难。事实上,我们可以将现存的网站进行改进,使之成为PWA。这也是我这篇文章要讲的 -- 当你读完这篇文章,你可以将你的网站改进,让他看起来就像是一个 native web app。他可以离线工作并且拥有自己的主屏图标。
node ./server.js [port]
[port]是可配置的,默认为 8888。打开 Chrome 或者其他基于Blink内核的浏览器,比如 Opera 或者 Vivaldi,然后输入链接 http://localhost:8888/(或者你指定的某个端口)。你也可以打开开发者工具看一下各个console信息。{ "name" : "PWA Website", "short_name" : "PWA", "description" : "An example PWA website", "start_url" : "/", "display" : "standalone", "orientation" : "any", "background_color" : "#ACE", "theme_color" : "#ACE", "icons": [ { "src" : "/images/logo/logo072.png", "sizes" : "72x72", "type" : "image/png" }, { "src" : "/images/logo/logo152.png", "sizes" : "152x152", "type" : "image/png" }, { "src" : "/images/logo/logo192.png", "sizes" : "192x192", "type" : "image/png" }, { "src" : "/images/logo/logo256.png", "sizes" : "256x256", "type" : "image/png" }, { "src" : "/images/logo/logo512.png", "sizes" : "512x512", "type" : "image/png" } ]}
在页面的<head>中引入:<link rel="manifest" href="/manifest.json">
manifest 中主要属性有:if ('serviceWorker' in navigator) { // register service worker navigator.serviceWorker.register('/service-worker.js');}
如果你不需要离线功能,可以简单的创建一个空的 /service-worker.js文件 —— 用户会被提示安装你的 app。// configurationconst version = '1.0.0', CACHE = version + '::PWAsite', offlineURL = '/offline/', installFilesEssential = [ '/', '/manifest.json', '/css/styles.css', '/js/main.js', '/js/offlinepage.js', '/images/logo/logo152.png' ].concat(offlineURL), installFilesDesirable = [ '/favicon.ico', '/images/logo/logo016.png', '/images/hero/power-pv.jpg', '/images/hero/power-lo.jpg', '/images/hero/power-hi.jpg' ];
installStaticFiles()方法添加文件到缓存,这个方法用到了基于 promise的 Cache API。当必要的文件都被缓存后才会生成返回值。// install static assetsfunction installStaticFiles() { return caches.open(CACHE) .then(cache => { // cache desirable files cache.addAll(installFilesDesirable); // cache essential files return cache.addAll(installFilesEssential); });}
最后,我们添加install的事件监听函数。 waitUntil方法确保所有代码执行完毕后,service worker 才会执行 install。执行 installStaticFiles()方法,然后执行 self.skipWaiting()方法使service worker进入 active状态。// application installationself.addEventListener('install', event => { console.log('service worker: install'); // cache core files event.waitUntil( installStaticFiles() .then(() => self.skipWaiting()) );});
// clear old cachesfunction clearOldCaches() { return caches.keys() .then(keylist => { return Promise.all( keylist .filter(key => key !== CACHE) .map(key => caches.delete(key)) ); });}// application activatedself.addEventListener('activate', event => { console.log('service worker: activate'); // delete old caches event.waitUntil( clearOldCaches() .then(() => self.clients.claim()) );});
注意,最后的self.clients.claim()方法设置本身为active的service worker。// application fetch network dataself.addEventListener('fetch', event => { // abandon non-GET requests if (event.request.method !== 'GET') return; let url = event.request.url; event.respondWith( caches.open(CACHE) .then(cache => { return cache.match(event.request) .then(response => { if (response) { // return cached file console.log('cache fetch: ' + url); return response; } // make network request return fetch(event.request) .then(newreq => { console.log('network fetch: ' + url); if (newreq.ok) cache.put(event.request, newreq.clone()); return newreq; }) // app is offline .catch(() => offlineAsset(url)); }); }) );});
最后这个offlineAsset(url)方法通过几个辅助函数返回一个适当的值:// is image URL?let iExt = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'].map(f => '.' + f);function isImage(url) { return iExt.reduce((ret, ext) => ret || url.endsWith(ext), false);}// return offline assetfunction offlineAsset(url) { if (isImage(url)) { // return image return new Response( '<svg role="img" viewBox="0 0 400 300" xmlns="http://www.w3.org/2000/svg"><title>offline</title><path d="M0 0h400v300H0z" fill="#eee" /><text x="200" y="150" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif" font-size="50" fill="#ccc">offline</text></svg>', { headers: { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'no-store' }} ); } else { // return page return caches.match(offlineURL); }}
offlineAsset()方法检查是否是一个图片请求,如果是,那么返回一个带有 “offline” 字样的 SVG。如果不是,返回 offlineURL 页面。// load script to populate offline page listif (document.getElementById('cachedpagelist') && 'caches' in window) { var scr = document.createElement('script'); scr.src = '/js/offlinepage.js'; scr.async = 1; document.head.appendChild(scr);}
/js/offlinepage.js locates the most recent cache by version name, 取到所有 URL的key的列表,移除所有无用 URL,排序所有的列表并且把他们加到 ID 为cachedpagelist的 DOM 节点中:// cache nameconst CACHE = '::PWAsite', offlineURL = '/offline/', list = document.getElementById('cachedpagelist');// fetch all cacheswindow.caches.keys() .then(cacheList => { // find caches by and order by most recent cacheList = cacheList .filter(cName => cName.includes(CACHE)) .sort((a, b) => a - b); // open first cache caches.open(cacheList[0]) .then(cache => { // fetch cached pages cache.keys() .then(reqList => { let frag = document.createDocumentFragment(); reqList .map(req => req.url) .filter(req => (req.endsWith('/') || req.endsWith('.html')) && !req.endsWith(offlineURL)) .sort() .forEach(req => { let li = document.createElement('li'), a = li.appendChild(document.createElement('a')); a.setAttribute('href', req); a.textContent = a.pathname; frag.appendChild(li); }); if (list) list.appendChild(frag); }); }) });
Cache-Control: max-age=31536000
页面,CSS和 script 文件会经常变化,所以你应该改设置一个很短的缓存时间比如 24 小时,并在联网时与服务端文件进行验证:Cache-Control: must-revalidate, max-age=86400
译自 Retrofit Your Website as a Progressive Web App关键词:改造