【GISER&&前端优化】前端缓存的几种主流选择
这周遇到了一个新需求,产品反馈地图瓦片服务的图片资源没有Http缓存,每次移动地图范围都会向后台发处请求/响应数据,影响了客户端的地图加载体验。所以需要增加这样一种缓存:1)针对同一个请求资源地址URL,首次加载需要缓存数据,后续加载直接读取缓存;2)后台数据发生更新时,需要实时更新缓存;
在完成这个需求之前,我借机补习了一下前端的缓存体系:
一 HTTP缓存
提起前端缓存,首先第一反应就是浏览器自带的缓存机制,通过在Http报文头部中设置一些属性字段,告知浏览器对本次请求响应的资源进行缓存,之后对于相同URL的资源请求则直接从缓存中读取。这个思路没错,那具体应该如何实现呢?这就需要我们对这些属性进行简单介绍一下:
1 强缓存
首次请求资源A设置缓存,再次请求资源A时直接查找缓存,如果缓存命中,则不发送新的请求;否则发送新请求;
控制强缓存的主要属性字段:
【Expire】:HTTP1.0标准下的字段,指定一个日期或时间,在时间过期前执行上述逻辑,时间过期后重新执行
【Cache-Control】:
field description
private 仅客户端缓存
public 客户端和代理服务器缓存
max-age=xxx 缓存内容将在xxx秒后失效(相对请求时间)
s-max-age 仅适用于public共享缓存
no-cache 需要使用协商缓存来验证缓存(会发送新的请求)
no-store 所有内容都不缓存
must-revalidate
2 协商缓存
顾名思义,由服务端协助客户端实现缓存机制:即在资源B已缓存的前提下,再次请求资源B时,会继续想服务器端发送请求,通过服务器发送请求,获取资源B是否失效的信息,如果未失效,返回未失效Flag告知前端,从而在缓存中获取资源B,如果失效,则返回新的数据和报文缓存头设置;
常用的协商缓存包括:
【Cache-Control的must-revalidate】:标识每次必须向服务端请求验证资源
【Etag】-【if-none-match】:资源的唯一标识,即在第一次请求资源C时,会计算一个能够唯一标识当前资源C的tag值,并放在response的Etag属性字段中,当资源C缓存在前端后,后续的每一次请求资源C都会将tag值设置在请求request的if-none-match中,服务端根据tag值进行判断当前请求资源C是否发生变化,如变化这表示缓存未命中,返回新的资源C,否则返回缓存命中标识,直接返回304获取资源C在前端的缓存。
【last-modified】-【if-modified-since】:同上Etag,只不过此处一般以时间戳作为判断依据,服务端则需要获取资源C的文件最后修改时间(这个在JAVA中File提供了getLastModified方法),并将时间戳赋值给response中的last-modified属性字段,后续的每一次请求会将时间戳放在if-modified-since中,与后台的新时间戳对比,如果时间戳不同,则表示缓存未命中,返回新的资源C,否则返回缓存命中标识,前端获取缓存值;
3 实现
OK,梳理完上述的Http缓存机制后,我们很容易发现如果要满足需求(即客户端缓存数据,且能够在数据发生变化后请求返回新的数据,更新缓存),应当选择协商缓存。
策略选择好之后,实现还是比较简单的,我们这里以时间戳为例:
3.1) 后台在返回数据时增加时间戳判断:
Public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ //......
String filePath = ""; // File fileC = new File(filePath); long lastNew = fileC.lastModified(); String lastNewStr = Long.toString(lastNew); //获取当前资源的最后修改时间 String lastOldStr = request.getHeader("if-modified-since"); //获取当前资源缓存的最后修改时间 if(lastNewStr.equalsIgnoreCase(lastOldStr)){ Response.setStatus(304); //返回304 not modified }else{ //获取新的数据 } // ...... }
3.2) 验证效果
效果比较尴尬,因为在实现了这类缓存后,请求响应资源的效率提高并不明显,虽然实现了缓存,但是每次资源校验依然要经过一次完整的请求响应过程,其中的连接开销应该是耗时的主要部分,而非数据的传输。所以,只能临时更改策略,采用期限强缓存,后台如果更新数据,则需要客户端手动刷新缓存。
实现方法如下,采用Cache-Control的max-age,以一天时间为缓存期限:
Public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ //… response.setHeader("Cache-Control", "max-age=86400"); //… }
二 前端数据库缓存
在完成这个任务后,我突然想起来之前在完成矢量地图渲染时,为了提高前端中文字体加载效率,曾经使用过前端数据库indexDB作为缓存技术,这个和Http缓存又有什么不同呢?一般来说,前端数据库主要有一下几种:
1 cookie
常用场景:用户个人信息保存(用户名/密码)
存在问题:
容量:4KB
数据安全问题
2 localStorage/sessionStorage【键值对】:本地浏览器数据库,根据浏览器不同,缓存容量不同,一般5MB左右,适合存储一些常用结构简单的数据;
localStorage.setItem("key", "value")
localStorage.getItem("key")
localStorage.removeItem("key")
localStorage.clear()
优势场景:
兼容性好,易操作
缺点:
容量小,每个域名分配3-5M空间
3 indexedDB【结构化对象】:本地数据库存储,使用索引高效检索,异步处理;
优势场景:
a 异步操作,可大量读写数据而不阻塞浏览器主线程
b 支持事务
c 存储空间大(250MB->更多)
d 支持二进制存储ArrayBuffer和Blob
缺点:
操作比较繁琐复杂,不如localStorage方便
实现:
var databaseName = "test"
var version = "20191121"
var request = window.indexedDB.open(databaseName, version) //创建一个indexedDB数据库
具体操作略微有些复杂,需要熟悉相关API接口,此处就不作详细介绍了,具体可以参照阮一峰的教程;
三 比较
比较Http缓存和前端数据库缓存,暂时没有发现这些缓存有什么明显的差异,只是针对应用场景不同而已:
1)Http缓存一般能够缓存一些前端请求的一些公共资源,减少一些不必要的请求响应开销,提高web端的体验
2)而前端数据库缓存则能够缓存一些高频常用的业务数据,提高业务请求的数据返回速度,比如高德地图和百度地图: 【高德地图】:打开Chrome控制台的Application选项卡,可以查看IndexedDB中的数据,其中包含了road/region等矢量瓦片数据; 【百度地图】:打开IndexedDB空空如也,但是在LocalStorage中发现了一些POI搜索记录;并且百度地图的矢量数据服务利用了Cache-Control属性强缓存了矢量数据,从而减小实时的数据请求。