缓存&PWA实践
缓存&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
五、总结与思考
参考优秀网站:
缓存&PWA实践的更多相关文章
- Guava Cache,Java本地内存缓存使用实践
Guava Cache,网上介绍很多,我就不赘述了. 分享一篇好的文章: Guava Cache内存缓存使用实践-定时异步刷新及简单抽象封装 Google Guava 3-缓存 在原作者基础上,我做了 ...
- Spring Boot缓存应用实践
缓存是最直接有效提升系统性能的手段之一.个人认为用好用对缓存是优秀程序员的必备基本素质. 本文结合实际开发经验,从简单概念原理和代码入手,一步一步搭建一个简单的二级缓存系统. 一.通用缓存接口 1.缓 ...
- 基于微前端qiankun的多页签缓存方案实践
作者:vivo 互联网前端团队- Tang Xiao 本文梳理了基于阿里开源微前端框架qiankun,实现多页签及子应用缓存的方案,同时还类比了多个不同方案之间的区别及优劣势,为使用微前端进行多页签开 ...
- squid3.5缓存代理实践记录
准备: 两台服务器,一台web,一台squid缓存代理 squid机域名:www.dannylinux.top web机IP:12.1.1.1 1.版本: [root@danny squid]# sq ...
- vue项目缓存最佳实践
需求 在开发vue的项目中有遇到了这样一个需求:一个视频列表页面,展示视频名称和是否收藏,点击进去某一项观看,可以收藏或者取消收藏,返回的时候需要记住列表页面的页码等状态,同时这条视频的收藏状态也需要 ...
- [http] http缓存机制
原文链接:http://my.oschina.net/leejun2005/blog/369148 1.缓存的分类 缓存分为服务端侧(server side,比如 Nginx.Apache)和客户端侧 ...
- ASP.NET Core缓存静态资源
背景 缓存样式表,JavaScript或图像文件等静态资源可以提高您网站的性能.在客户端,总是从缓存中加载一个静态文件,这样可以减少对服务器的请求数量,从而减少获取页面及其资源的时间.在服务器端,由于 ...
- PWA初体验
一.前言 现在市面上的Native APP成千上万个,各种应用商店里面的APP琳琅满目.原生的APP下载到手机上之后,用户就可以获取一个方便的入口,体验上也十分顺畅.但是再好的事物难免有点缺点: 1 ...
- Nginx缓存服务
Nginx缓存服务 1.缓存常见类型 2.缓存配置语法 3.缓存配置实践 4.缓存清理实践 5.部分页面不缓存 6.缓存日志记录统计 通常情况下缓存是用来减少后端压力, 将压力尽可能的往前推, 减少后 ...
随机推荐
- 有关表单autocomplete = "off" 失效问题解决方案
一.autocomplete介绍 autocomplete是Html5中的新属性.该属性规定输入字段是否应该启用自动完成功能.自动完成允许浏览器预测对字段的输入.当用户在字段开始键入的时候,浏览器基于 ...
- 项目需求与分析--NABCD模型
合作项目特点NABCD分析结果: 特点:便捷 N(Need 需求):在大学期间内,我们通常会有许多不用的课本或书籍或者其他东西,堆积起来又没有地方放,想卖出去就要建一个群,十分麻烦,开发该软件用户可直 ...
- linux安装sbt
1.官网下载tgz sbt - Download (scala-sbt.org) 2.解压 tar zxvf sbt-0.13.5.tgz -C /opt/scala/ 3.建立启动sbt脚本 /*选 ...
- CentOS 7.9 网络配置
vi /etc/sysconfig/network-scripts/ifcfg-ens33 (45条消息) CentOS 7.9 网络配置_$青的博客-CSDN博客_centos7.9网卡配置
- 访问控制protected是不同包中对子类可见,什么意思?
2.2 以下例子说明:protected是不同包中对子类可见,对非子类不可见. 例1.2.2.a:---本例为正常用法. package p1;public class A { protecte ...
- PAT1018 锤子剪刀布
大家应该都会玩"锤子剪刀布"的游戏:两人同时给出手势,胜负规则如图所示: 现给出两人的交锋记录,请统计双方的胜.平.负次数,并且给出双方分别出什么手势的胜算最大. 输入格式: 输入 ...
- 如何基于 ZEGO SDK 实现 Flutter 一对一音视频聊天应用?
之前的文章发布了ZEGO SDK实现Android端音视频通话应用的开发教程,不少开发者反馈很实用,能不能也出一版Flutter的教程. 有求必应,这不小编来了- 我们封装了ZEGO Flutter ...
- BootstrapBlazor-ValidateForm 表单验证组件
原文链接:https://www.cnblogs.com/ysmc/p/16082279.html 故名思意,这个组件的作用我就不再多说了,配合 AutoGenerateColumnAttribute ...
- mpvue下拉刷新
1 开启下拉刷新 在想要下拉刷新的页面的 main.json 里,添加: { "navigationBarTitleText": "页面标题", "e ...
- 初识gradle, idea+springboot Demo
写在前面; 使用maven管理写过几个springboot的系统, 此篇博客纯属记录整理学习的过程. 另外, 源码分享地址在最后. Java: 1.8.0_281 tomcat: 1.8 IDE: I ...