从上一篇《前端动画实现与原理分析》,我们从 Performance 进行动画的性能分析,并根据 Performance 分析来优化动画。但,前端不仅仅是实现流畅的动画。ToB 项目会经常与数据的保存、渲染打交道。例如开发中,为了提高用户体验,遇到了一些场景,其实就是在利用某些手段,来进行性能优化。
那不免引发思考,我们从缓存与数据存储来思考,该如何优化?
是什么?
Http 缓存其实就是浏览器保存通过 HTTP 获取的所有资源,
是浏览器将网络资源存储在本地的一种行为。
请求的资源在哪里?
Expires相反,时间是相对于请求的时间?? Cache-control
last-Modified : 表示本地文件最后修改的日期,浏览器会在请求头中带上 if-Modified-since(也是上次返回的 Last-Modified 的值),服务器会将这个值与资源修改的时间进行匹配,如果时间不一致,服务器会返回新的资源,并且将 Last-modified 值更新,并作为响应头返回给浏览器。如果时间一致,表示资源没有更新,服务器会返回 304 状态,浏览器拿到响应状态码后从本地缓存中读取资源。
http 1.1 中, 服务器通过 Etag 来设置响应头缓存标示。Etag 是由服务器来生成的。
第一次请求时,服务器会将资源和 ETag 一并返回浏览器,浏览器将两者缓存到本地缓存中。
第二次请求时,浏览器会将 ETag 的值放到 If-None-Match 请求头去访问服务器,服务器接收请求后,会将服务器中的文件标识与浏览器发来的标识进行比对,如果不同, 服务器会返回更新的资源和新的 Etag,如果相同,服务器会返回 304 状态码,浏览器读取缓存。
?? 思考为什么有了 Last-Modified 这一对儿,还需要 Etag 来标识是否过期进行命中协商缓存
?? 如果 Etag 与 Last-Modified 同时存在,服务器会先检查 ETag,然后在检查 Last-Modified, 最终确定是返回 304 或 200。
测试环境: 利用 Koa,搭建一个 node 服务,用来测试如何命中强缓存还是协商缓存
当 index.js 没有配置任何关于缓存的配置时, 无论怎么刷新 chrome,都没有缓存机制的;
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')}) /** * 命中协商缓存 * 设置 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 } }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 更直观的方式存储键值对
为每一个给定的源(given origin)维持一个独立的存储区域,浏览器关闭,然后重新打开后数据仍然存在。
为每一个给定的源(given origin)维持一个独立的存储区域,该存储区域在页面会话期间可用(即只要浏览器处于打开状态,包括页面重新加载和恢复)。
基本操作与实际数据库操作基本一致。
最终的数据去向,一般只是做临时存储和大型网站的业务运行存储缓存的作用,页面刷新后该库就不存在了。而其本身与关系数据库的概念比较相似。
随着浏览器的功能不断增强,越来越多的网站开始考虑,将大量数据储存在客户端,这样可以减少从服务器获取数据,直接从本地获取数据。现有的浏览器数据储存方案,都不适合储存大量数据;
IndexedDB 是浏览器提供的本地数据库, 允许储存大量数据,提供查找接口,还能建立索引。这些都是 LocalStorage 所不具备的。就数据库类型而言,IndexedDB 不属于关系型数据库(不支持 SQL 查询语句),更接近 NoSQL 数据库。
在提及 Service Worker 之前,需要对 web Worker 有一些了解;
webWorker : Web Worker 是浏览器内置的线程所以可以被用来执行非阻塞事件循环的 JavaScript 代码。 js 是单线程,一次只能完成一件事,如果出现一个复杂的任务,线程就会被阻塞,严重影响用户体验, Web Worker 的作用就是允许主线程创建 worker 线程,与主线程同时进行。worker 线程只需负责复杂的计算,然后把结果返回给主线程就可以了。简单的理解就是,worker 线程执行复杂计算并且页面(主线程)ui 很流畅,不会被阻塞。
Service Worker 是浏览器和网络之间的虚拟代理。其解决了如何正确缓存往后网站资源并使其在离线时可用的问题。
Service Worker 运行在一个与页面 js 主线程独立的线程上,并且无权访问 DOM 结构。他的 API 是非阻塞的,并且可以在不同的上下文之间发送和接收消息。
他不仅仅提供离线功能,还可以做包括处理通知、在单独的线程上执行繁重的计算等事务。Service Workers 非常强大,因为他们可以控制网络请求,修改网络请求,返回缓存的自定义响应或者合成响应。
?? PWA,全称 Progressive web apps,即渐进式 Web 应用。PWA 技术主要作用为构建跨平台的 Web 应用程序,并使其具有与原生应用程序相同的用户体验。
?? PWA 的核心: 最根本、最基本的,就是 Service Worker 以及在其内部使用的 Cache API。只要通过 Service Worker 与 Cache API,实现了对网站页面的缓存、对页面请求的拦截、对页面缓存的操纵 。
为什么使用 PWA:
传统的 Web 存在的问题:
传统 Native APP 存在的问题:
PWA 的存在,就是为了解决以上问题所带来的麻烦:
优势:
{ "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" // 設定背景顏色}/* 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) }) )})当离线的时候依然拿到缓存的资源,并且正常展示,可以看出资源被 serviceWorker 缓存。
借助开发者工具:
chrome devtools : chrome://inspect/#service-workers ,可以展示当前设备上激活和存储的 service worker
参考优秀网站: