本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 [署名 4.0 国际 (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/deed.zh) 本文作者: 苏洋 创建时间: 2018年10月16日 统计字数: 3728字 阅读时间: 8分钟阅读 本文链接: https://soulteary.com/2018/10/16/use-traditional-front-end-technology-to-enhance-client-caching-capabilities.html ----- # 使用传统前端技术增强客户端缓存能力 前几天重构之后,Lighthouse 中有一个评分让我念念不忘:`Progressive Web App`。 `PWA` 不算一个新话题了,所以概念性的东西和 `API` 我就不多做介绍,下面简单介绍一个无干预更新的缓存方案,整体代码量在一百行以内,如果你也想在不“大动干戈”的情况下对站点或者 `Web App` 进行性能提升的话,可以了解一下。 ## 浏览器客户端代码 说到 `PWA` ,我们能直接想到的,无非是 `增强缓存` 和 `推送能力`。而这两个能力,都是 `ServiceWorker` API 实现的。(添加桌面图标这个需求,我不需要,就不介绍了,感兴趣可以自行搜索) 我在之前的[重构文章](https://soulteary.com/2018/10/14/site-optimization-record.html)中有简单聊过访客数据,其中有一部分访客使用的客户端并不支持 `Service Worker`,所以在使用它的时候,需要使用**能力探测**的方式引入,比如: ```js try { ('serviceWorker' in navigator) && navigator.serviceWorker.register('/sw.js'); } catch (error) { console.error(error); } ``` 当然,构建压缩之后,你得到的结果应该是 `drop console` 的最简代码。不过,如果你不确定你的运行环境是否有问题,可以使用下面带有调试日志的版本。 ```js try { if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.min.js').then(function(registration) { console.log('ServiceWorker registration successful with scope: ', registration.scope); }).catch(function(err) { console.error('ServiceWorker registration failed: ', err); }); } } catch (error) { console.error(error); } ``` ## ServiceWorker 客户端代码 上面介绍浏览器客户端代码的时候,有引入一个外部脚本依赖 `sw.js`。 在分享代码之前,有做过 `PWA` 相关项目或者了解的同学,可能或多或少会在 `增强缓存` 这个地方被坑到,比如:缓存无法更新、缓存内容过多无法写入。 缓存无法更新有一个简单有效的解决方案:定时切换缓存使用的 `Store`。如果再引入**当前时间**这个因素,可以保障缓存使用的 `Store` 不存在资源争抢的问题。 结合站点内容特点,配合定期清理缓存的脚本,可以将缓存数量控制在一定的范围之内。 这里提供一个小思路,对服务端资源进行二次缓存的时候,可以设定一个最大缓存时间的策略,而这里有两个方案: - 对每个资源设定缓存 `TTL` - 对所有资源设定统一 `TTL` 我个人选择第二个方案,牺牲一定的缓存复用,但是有效降低资源之间的版本管理的复杂度。 而需要缓存的资源一般分为两类: - 短时间缓存:页面或者页面片段 - 相对长时间缓存:图片等媒体资源,或者有一定跨页面通用能力的脚本和样式资源 这里以10分钟(调试模式单位替换为秒)为一个时间段,为短时间缓存的资源进行缓存。设定星期数为其他资源进行缓存周期。 这里我们不必过分处理 `install` 和 `active` 事件,只需要统一在 `fetch` 事件中进行缓存更新和清理即可,比如下面这样: ```js function weekId() { var now = (new Date()); var dt = new Date(now.getFullYear(), 0, 1); return Math.ceil((((now - dt) / 86400000) + dt.getDay() + 1) / 7); } var isDevMode = false; var CACHE_LONG_TTL = 'WEEK_' + weekId(); var CACHE_TINY_TTL = 'TINY_'; function cleanCache(whiteList) { return caches.keys().then(function(buckets) { return Promise.all(buckets.filter(function(bucket) { return whiteList.indexOf(bucket) === -1; }).map(function(bucket) { return caches.delete(bucket); })); }); } self.addEventListener('activate', function(event) { event.waitUntil(cleanCache([CACHE_LONG_TTL])); }); self.addEventListener('fetch', function(event) { var url = new URL(event.request.url); if (url.protocol.toLowerCase() !== 'https:') return; var now = new Date; var CACHE_INTERVAL = Math.floor((isDevMode ? now.getSeconds() : now.getMinutes()) / 10); var CACHE_KEY = CACHE_TINY_TTL + CACHE_INTERVAL; if (url.pathname.endsWith('/') || url.pathname.endsWith('.html') || url.pathname.endsWith('.md') || url.pathname.endsWith('/feed/') || url.pathname.endsWith('/feed') || url.pathname.endsWith('sw.js')) { cleanCache([CACHE_LONG_TTL, CACHE_KEY]); event.respondWith(caches.open(CACHE_KEY).then(function(cache) { return cache.match(url).then(function(response) { if (response) return response; return fetch(event.request).then(function(response) { cache.put(event.request, response.clone()); return response; }); }); })); } else { event.respondWith(caches.open(CACHE_LONG_TTL).then(function(cache) { return cache.match(event.request).then(function(response) { if (response) return response; return fetch(event.request).then(function(response) { cache.put(event.request, response.clone()); return response; }); }); })); } }); ``` 如果你的访客有不少是 `Chrome v50` 和 `FireFox v46` 以下的客户端,那么你可能还需要引入下面的 `Polyfill` : [https://github.com/dominiccooney/cache-polyfill/blob/master/index.js](https://github.com/dominiccooney/cache-polyfill/blob/master/index.js) 当然,在引入的时候,建议还是进行压缩,进一步提高页面性能(哪怕它是独立于客户端脚本另外的异步进程)。 当一切都完成之后,如果顺利的话,你的网站的将支持有限时间(本文是10min)的离线访问,以及重复访问时更好的响应能力。 另外,你也不需要担心 `sw.js` 被缓存后,这个站点变成“完全离线”。因为 `sw.js` 按照规范可以保证它的最长存在生命周期是 1天,也就是说,未来你的策略更新最多延迟1天。 ## 最后 ![](https://attachment.soulteary.com/2018/10/16/service-worker-support.png "支持本地缓存后的评分") 再次跑分,发现 `Lighthouse` 已经全面绿色评价了,不过前文中我提过,我不太需要把网站变为纯粹的 `PWA` 应用,没有去设定 `mainfest` ,所以,依旧有 4 点改进建议。 1. 用户不能够“安装” Web App。 2. 没有定义启动屏幕(仿客户端体验)。 3. 没有定义顶栏的主题色。 4. viewport 没有优化。 这些后面再说吧。 --EOF