缓存&PWA 实际
一、背景
从上一篇《前端动画实现与原理剖析》,咱们从 Performance 进行动画的性能剖析,并依据 Performance 剖析来优化动画。但,前端不仅仅是实现晦涩的动画。ToB 我的项目会常常与数据的保留、渲染打交道。例如开发中,为了进步用户体验,遇到了一些场景,其实就是在利用某些伎俩,来进行性能优化。
- Select 下拉:做前端分页展现 → 防止一次性渲染数据造成浏览器的假死状态;
- indexedDB:存储数据 → 用户下一次进入时,保留上一次编辑的状态 ……
那未免引发思考,咱们从缓存与数据存储来思考,该如何优化?
二、 HTTP 缓存
是什么?
Http 缓存其实就是浏览器保留通过 HTTP 获取的所有资源,
是浏览器将网络资源存储在本地的一种行为。
申请的资源在哪里?
- 6.8kB + 200 状态码: 没有命中缓存,理论的申请,从服务器上获取资源;
- memory cache: 资源缓存在内存中,不会申请服务器,个别曾经加载过该资源且缓存在内存中,当敞开页面时,此资源就被内存开释掉了;
- disk cache: 资源缓存在磁盘中,不会申请服务器,然而该资源也不会随着敞开页面就开释掉;
- 304 状态码:申请服务器,发现资源没有被更改,返回 304 后,资源从本地取出;
- service worker: 利用级别的存储伎俩;
HTTP 缓存分类
1. 强缓存
- 浏览器加载资源时,会先依据本地缓存资源的 header 中的信息判断是否命中强缓存。如果命中,则不会像服务器发送申请,而是间接从缓存中读取资源。
- 强缓存能够通过设置 HTTP Header 来实现:
http1.0 → Expires: 响应头蕴含日期/工夫, 即在此时候之后,响应过期。
http1.1 → Cache-Control:max-age=: 设置缓存存储的最大周期,超过这个工夫缓存被认为过期(单位秒)。与Expires
相同,工夫是绝对于申请的工夫
Cache-control
- cache-control: max-age=3600 :示意绝对工夫
- cache-control:no-cache → 能够存储在本地缓存的,只是在原始服务器进行新鲜度在验证之前,缓存不能将其提供给客户端应用
- cache-control: no-store → 禁止缓存对响应进行复制,也就是真正的不缓存数据在本地;
- catch-control:public → 能够被所有用户缓存(多用户共享),包含终端、CDN 等
cache-control: private → 公有缓存
2. 协商缓存
- 当浏览器对某个资源的申请没有命中强缓存,就会发一个申请到服务器,验证协商缓存是否命中,如果协商缓存命中,申请返回的 http 状态 304,并且会显示 Not Modified 的字符串;
- 协商缓存通过【last-Modified,if-Modified-Since】与【ETag, if-None-Match】来治理的。
- 「Last-Modified、If-Modified-Since」
last-Modified : 示意本地文件最初批改的日期,浏览器会在申请头中带上 if-Modified-since(也是上次返回的 Last-Modified 的值),服务器会将这个值与资源批改的工夫进行匹配,如果工夫不统一,服务器会返回新的资源,并且将 Last-modified 值更新,并作为响应头返回给浏览器。如果工夫统一,示意资源没有更新,服务器会返回 304 状态,浏览器拿到响应状态码后从本地缓存中读取资源。
- 「ETag、If-None-Match」
http 1.1 中, 服务器通过 Etag 来设置响应头缓存标示。Etag 是由服务器来生成的。
第一次申请时,服务器会将资源和 ETag 一并返回浏览器,浏览器将两者缓存到本地缓存中。
第二次申请时,浏览器会将 ETag 的值放到 If-None-Match 申请头去拜访服务器,服务器接管申请后,会将服务器中的文件标识与浏览器发来的标识进行比对,如果不同, 服务器会返回更新的资源和新的 Etag,如果雷同,服务器会返回 304 状态码,浏览器读取缓存。
思考为什么有了 Last-Modified 这一对儿,还须要 Etag 来标识是否过期进行命中协商缓存
- 文件的周期性更改,然而文件的内容没有扭转,仅仅扭转了批改工夫,这个时候,并不心愿客户端认为该文件被批改了,而从新获取。
- 如果文件文件频繁批改,比方 1s 改了 N 次,If-Modified-Since 无奈判断批改的,会导致文件曾经批改然而获取的资源还是旧的,会存在问题。
- 某些服务器不能准确失去文件的最初批改工夫,导致资源获取的问题。
⚠️ 如果 Etag 与 Last-Modified 同时存在,服务器会先查看 ETag,而后在查看 Last-Modified, 最终确定是返回 304 或 200。
HTTP 缓存实际
测试环境: 利用 Koa,搭建一个 node 服务,用来测试如何命中强缓存还是协商缓存
当 index.js 没有配置任何对于缓存的配置时, 无论怎么刷新 chrome,都没有缓存机制的;
- 留神⚠️:在开始试验之前要把 network 面板的 Disable cache 勾选去掉,这个选项示意禁用浏览器缓存,浏览器申请会带上 Cache-Control: no-cache 和 Pragma: no-cache 头部信息。
1. 命中强缓存
app.use(async (ctx) => { // ctx.body = 'hello Koa' const url = ctx.request.url; if(url === '/'){ // 拜访跟门路返回 index.html ctx.set('Content-type', 'text/html'); ctx.body = await parseStatic('./index.html') }else { ctx.set('Content-Type', parseMime(url)) ctx.set('Expires', new Date(Date.now() + 10000).toUTCString()) // 试验1 ctx.set('Cache-Control','max-age=20') // 试验2 ctx.body = await parseStatic(path.relative('/', url)) }})app.listen(3000, () => { console.log('starting at port 3000')})
2. 命中协商缓存
/** * 命中协商缓存 * 设置 Last-Modified, If-Modified-Since */ ctx.set('cache-control', 'no-cache'); // 敞开强缓存 const isModifiedSince = ctx.request.header['if-modified-since']; const fileStat = await getFileStat(filePath); if(isModifiedSince === fileStat.mtime.toGMTString()){ ctx.status = 304 }else { ctx.set('Last-Modified', fileStat.mtime.toGMTString()) } ctx.body = await parseStatic(path.relative('/', url)) /** * 命中协商缓存 * 设置 Etag, If-None-Match */ ctx.set('cache-control', 'no-cache'); const ifNoneMatch = ctx.request.headers['if-none-match']; const fileBuffer = await parseStatic(filePath); const hash = crypto.createHash('md5'); hash.update(fileBuffer); const etag = `"${hash.digest('hex')}"` if (ifNoneMatch === etag) { ctx.status = 304 } else { ctx.set('Etag', etag) ctx.body = fileBuffer } }
三、 浏览器缓存
1.Cookies
- MDN 定义: 是服务器发送到用户浏览器并报讯在本地的一小块数据,他会在浏览器下次想对立服务器再次发送申请时被携带并发送到服务器上。
利用场景:
- 会话状态治理【用户登陆状态,购物车,游戏分数或其余须要记录的信息】
- 个性化设置(如用户自定义设置、主题等)
- 浏览器行为跟踪(如跟踪剖析用户行为等)
- cookie 的读取与写入:
class Cookie { getCookie: (name) => { const reg = new RegExp('(^| )'+name+'=([^;]*)') let match = document.cookie.match(reg); // [全量,空格,value,‘;’] if(match) {return decodeURI(match[2])} } setCookie:(key,value,days,domain) => { // username=John Smith; expires=Thu, 18 Dec 2043 12:00:00 GMT; path=/ let d = new Date(); d.setTime(d.getTime()+(days*24*60*60*1000)); let expires = "; expires="+d.toGMTString(); let domain = domain ? '; domain='+domain : ''; document.cookie = name + '=' + value + expires + domain + '; path=/' } deleteCookie: (name: string, domain?: string, path?: string)=> { // 删除cookie,只须要将工夫设置为过期工夫,而无需删除cookie的value const d = new Date(0); domain = domain ? `; domain=${domain}` : ''; path = path || '/'; document.cookie = name + '=; expires=' + d.toUTCString() + domain + '; path=' + path; },}
- 存在的问题: 因为通过 Cookie 存储的数据,每次申请都会携带在申请头。对与一些数据是无需交给提交后端的,这个未免会带来额定的开销。
2.WebStorage API
浏览器能以一种比应用 Cookie 更直观的形式存储键值对
localStorage
为每一个给定的源(given origin)维持一个独立的存储区域,浏览器敞开,而后从新关上后数据依然存在。
sessionStorage
为每一个给定的源(given origin)维持一个独立的存储区域,该存储区域在页面会话期间可用(即只有浏览器处于关上状态,包含页面从新加载和复原)。
3.indexedDB 与 webSQL
webSQL
基本操作与理论数据库操作基本一致。
最终的数据去向,个别只是做长期存储和大型网站的业务运行存储缓存的作用,页面刷新后该库就不存在了。而其自身与关系数据库的概念比拟类似。
indexedDB
随着浏览器的性能一直加强,越来越多的网站开始思考,将大量数据贮存在客户端,这样能够缩小从服务器获取数据,间接从本地获取数据。现有的浏览器数据贮存计划,都不适宜贮存大量数据;
IndexedDB 是浏览器提供的本地数据库, 容许贮存大量数据,提供查找接口,还能建设索引。这些都是 LocalStorage 所不具备的。就数据库类型而言,IndexedDB 不属于关系型数据库(不反对 SQL 查问语句),更靠近 NoSQL 数据库。
四、应用程序缓存
Service Worker
在提及 Service Worker 之前,须要对 web Worker 有一些理解;
webWorker : Web Worker 是浏览器内置的线程所以能够被用来执行非阻塞事件循环的 JavaScript 代码。 js 是单线程,一次只能实现一件事,如果呈现一个简单的工作,线程就会被阻塞,重大影响用户体验, Web Worker 的作用就是容许主线程创立 worker 线程,与主线程同时进行。worker 线程只需负责简单的计算,而后把后果返回给主线程就能够了。简略的了解就是,worker 线程执行简单计算并且页面(主线程)ui 很晦涩,不会被阻塞。
Service Worker 是浏览器和网络之间的虚构代理。其解决了如何正确缓存往后网站资源并使其在离线时可用的问题。
Service Worker 运行在一个与页面 js 主线程独立的线程上,并且无权拜访 DOM 构造。他的 API 是非阻塞的,并且能够在不同的上下文之间发送和接管音讯。
他不仅仅提供离线性能,还能够做包含解决告诉、在独自的线程上执行沉重的计算等事务。Service Workers 十分弱小,因为他们能够管制网络申请,批改网络申请,返回缓存的自定义响应或者合成响应。
2.PWA
PWA,全称 Progressive web apps,即渐进式 Web 利用。PWA 技术次要作用为构建跨平台的 Web 应用程序,并使其具备与原生应用程序雷同的用户体验。
PWA 的外围: 最基本、最根本的,就是 Service Worker 以及在其外部应用的 Cache API。只有通过 Service Worker 与 Cache API,实现了对网站页面的缓存、对页面申请的拦挡、对页面缓存的操纵 。
为什么应用 PWA:
传统的 Web 存在的问题:
- 不足间接入口,须要记住他的域名,或者是保留在收藏夹,寻找起来不够不便;
- 依赖于网络。只有客户端处于断网的状态,整个 web 就处于瘫痪状态,客户端无奈应用;
- 无奈像 Native APP 推送音讯。
传统 Native APP 存在的问题:
- 须要装置与下载。哪怕只是应用 APP 的某个性能,也是须要全盘下载的;
- 开发成本高,个别须要兼容安卓与 IOS 零碎;
- 公布须要审核;
- 更新老本高……
PWA 的存在,就是为了解决以上问题所带来的麻烦:
劣势:
- 桌面入口,关上便捷;
- 离线可用,不必适度依赖网络;
- 装置不便;
- 一次性开发,无需审核,所有平台可用;
- 可能进行音讯推送
- Web App Manifest Web App Manifest(Web 利用程序清单)概括地说是一个以 JSON 模式集中书写页面相干信息和配置的文件。
{ "short_name": "User Mgmt", "name": "User Management", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" }, { "src": "logo192.png", "type": "image/png", "sizes": "192x192" }, { "src": "logo512.png", "type": "image/png", "sizes": "512x512" } ], "start_url": ".", // 调整网站的起始链接 "display": "standalone", // 设定网站提醒模式 : standalone 示意暗藏浏览器的UI "theme_color": "#000000", // 设定网站每个页面的主题色彩 "background_color": "#ffffff" // 設定背景顏色}
- ServiceWorker.js 代码
/* eslint-disable no-restricted-globals */// 确定哪些资源须要缓存const CACHE_VERSION = 0;const CACHE_NAME = 'cache_v' + CACHE_VERSION;const CACHE_URL = [ '/', 'index.html', 'favicon.ico', 'serviceWorker.js', 'static/js/bundle.js', 'manifest.json', 'users']const preCache = () => { return caches .open(CACHE_NAME) .then((cache) => { return cache.addAll(CACHE_URL) })}const clearCache = () => { return caches.keys().then(keys => { keys.forEach(key => { if (key !== CACHE_NAME) { caches.delete(key) } }) })}// 进行缓存self.addEventListener('install', (event) => { event.waitUntil( preCache() )})// 删除旧的缓存self.addEventListener('activated', (event) => { event.waitUntil( clearCache() )})console.log('hello, service wold');self.addEventListener('fetch', (event) => { console.log('request:', event.request.url) event.respondWith( fetch(event.request).catch(() => { // 优先网络申请,如果失败,则从缓存中拿资源 return caches.match(event.request) }) )})
- PWA 调试
当离线的时候仍然拿到缓存的资源,并且失常展现,能够看出资源被 serviceWorker 缓存。
借助开发者工具:
chrome devtools : chrome://inspect/#service-workers ,能够展现以后设施上激活和存储的 service worker
五、总结与思考
参考优良网站:
- 语雀: https://www.yuque.com/dashboard ;
- PWA 例子: https://mdn.github.io/pwa-examples/js13kpwa/