整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

校导李凡〡浏览器缓存那些事儿

算机科学中最难的两件事是缓存失效和命名。

-- Phil Karlton

今天我们来简要了解一下浏览器的缓存问题。

浏览器缓存是指当浏览器请求一个资源的时候,会在本地保存一份副本,当下一个请求来到的时候,如果是相同的URL,缓存会根据缓存机制决定是直接使用副本响应访问请求,还是向源服务器再次发送请求。

浏览器缓存的作用

1. 减少网络带宽消耗无论对于网站运营者或者用户,带宽都代表着金钱,过多的带宽消耗,只会便宜了网络运营商。当Web缓存副本被使用时,只会产生极小的网络流量,可以有效的降低运营成本。

2. 降低服务器压力给网络资源设定有效期之后,用户可以重复使用本地的缓存,减少对源服务器的请求,间接降低服务器的压力。同时,搜索引擎的爬虫机器人也能根据过期机制降低爬取的频率,也能有效降低服务器的压力。

3. 减少网络延迟,加快页面打开速度 带宽对于个人网站运营者来说是十分重要,而对于大型的互联网公司来说,可能有时因为钱多而真的不在乎。那Web缓存还有作用吗?答案是肯定的,对于最终用户,缓存的使用能够明显加快页面打开速度,达到更好的体验。

浏览器端的缓存规则

真正的浏览器工作的时候并不是将完整的内容保存在本地,各种浏览器都有不同的方式,譬如firefox是一种类似innodb的方式存储的key value 的模式,在地址栏中输入 about:cache 可以看见缓存的文件,chrome会把缓存的文件保存在一个叫User Data的文件夹下。

浏览器缓存有一套自己的规则,可以用来决定何时使用缓存,何时从服务器上获取资源,这些规则是在HTTP协议头和HTML页面的Meta标签中定义的。

1. 响应头明确说明,我不想被缓存,则不会被缓存;

2. 如果请求信息是需要认证或者安全加密的(如: HTTPS),相应内容也不会被缓存;

3. 缓存如果有以下表现,则认为是fresh新鲜的(无需检查源服务器,直接发送给客户端),则内容缓存直取,绕过源服务器。

★ 含有完整的过期时间和寿命控制头信息,并且内容仍在保鲜期内。

★ 缓存最近已展现,并且在不久前修改。

4. 若内容陈旧,则会要求源服务器做验证 validate ,或者告诉缓存其拷贝副本是否是OK的。

5. 特定情况下——例如,断网了,之前有过的响应缓存直取而不检查源服务器。 总而言之,新鲜度 freshness 和校验 validation 是确定缓存内容是否可用的两个维度。如果要展示的足够新,直接使用缓存;如果检测发现展示内容并未变化,则会直接使用缓存。下面两张图很清晰的描述了浏览器发起请求时决定是否使用缓存的机制:

浏览器缓存的控制

1. 使用 Meta 标签控制页面缓存

前端攻城狮可以在HTML页面的节点中加入标签,代码如下:

<meta http-equiv="Pragma" content="no-cache">

上述代码的作用是告诉浏览器当前页面不被缓存,每次访问都需要去服务器拉取。使用上很简单,但只有部分浏览器可以支持,而且所有缓存代理服务器都不支持,因为代理不解析HTML内容本身。所以并不推荐使用这种方式控制页面缓存。

2.使用 HTTP 协议头控制页面缓存

在HTTP请求和响应的消息报头中,常见的与缓存有关的消息报头有以下这些:

Cache-Control 与 Expires

Cache-Control 与 Expires 的作用一致,都是指明当前资源的有效期,控制浏览器是否直接从浏览器缓存取数据还是重新发请求到服务器取数据。只不过 Cache-Control 的选择更多,设置更细致,如果同时设置的话,其优先级高于 Expires。

Last-Modified/ETag 与 Cache-Control/Expires

配置 Last-Modified/ETag 的情况下,浏览器再次访问统一 URI 的资源,还是会发送请求到服务器询问文件是否已经修改,如果没有,服务器会只发送一个304回给浏览器,告诉浏览器直接从自己本地的缓存取数据;如果修改过那就整个数据重新发给浏览器。

Cache-Control/Expires 则不同,如果检测到本地的缓存还是有效的时间范围内,浏览器直接使用本地副本,不会发送任何请求。两者一起使用时,Cache-Control/Expires 的优先级要高于 Last-Modified/ETag。即当本地副本根据 Cache-Control/Expires 发现还在有效期内时,则不会再次发送请求去服务器询问修改时间(Last-Modified)或实体标识(Etag)了。

一般情况下,使用 Cache-Control/Expires 会配合 Last-Modified/ETag 一起使用,因为即使服务器设置缓存时间, 当用户点击“刷新”按钮时,浏览器会忽略缓存继续向服务器发送请求,这时 Last-Modified/ETag 将能够很好利用304,从而减少响应开销。

Last-Modified与ETag

你可能会觉得使用 Last-Modified 已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要 Etag(实体标识)呢?HTTP1.1 中 Etag 的出现主要是为了解决几个 Last-Modified 比较难解决的问题:

★ Last-Modified 标注的最后修改只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,它将不能准确标注文件的新鲜度。

★ 如果某些文件会被定期生成,当有时内容并没有任何变化,但 Last-Modified 却改变了,导致文件没法使用缓存。

★ 有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形。

Etag 是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符,能够更加准确的控制缓存。Last-Modified 与ETag 是可以一起使用的,服务器会优先验证 ETag,一致的情况下,才会继续比对 Last-Modified,最后才决定是否返回304。

浏览器缓存机制在实际开发中的应用

服务器端可以设置缓存规则,但在服务器不能掌控的地方也许会出现一些意外。

1. 缓存会被挤出。

所谓缓存被挤出,因为浏览器的缓存空间是有限的,所有网站希望缓存的文件都塞在里面,形成一个先进先出的队列。所以你即使设置了20年的缓存时间,也基本上是不可能保证有那么久的生命期。而且用户也有可能主动清除浏览器缓存,某些系统清理软件也可能清理浏览器缓存。

2. 文件有可能在运营商服务器上被劫持。

第二个问题是用户的宽带运营商为了提高速度,可能会在自己某节点服务器上缓存你的文件(比如 style.css?v1),好处是当用户请求这个文件的时候,运营商无需来你的服务器上请求文件,而自己直接就给出了。 如果你的 querys string 更新了(style.css?v2),按照HTTP规范,这理应被视作一个新的文件。

但是运营商仍然可能会拿自己的缓存,而不是遵循规范。有点可恶对不对?这就是我们在用户量极大的情况下侦查到的情况,所以,为了保证更新的文件确定能下发到所有的用户,我们最好是在资源文件改变时,使用前端构建工具修改文件名,而不是仅修改 query string。

以下是业界公认的最佳规则:

对于动态生成的html页面使用HTTP头:Cache-Control: no-cache

对于静态html页面使用HTTP头:Last-Modified

其他所有的文件类型都设置Expires头,并且在文件内容有所修改的时候修改文件名

作者简介:李凡,英文名Levy,校导Web前端工程师,热衷钻研前端技术,对单页应用开发和前端工程化有着浓厚的兴趣。



在前面

今年国庆假期终于可以憋在家里了不用出门了,不用出去看后脑了,真的是一种享受。这么好的光阴怎么浪费,睡觉、吃饭、打豆豆这怎么可能(耍多了也烦),完全不符合我们程序员的作风,赶紧起来把文章写完。

这篇文章比较基础,在国庆期间的业余时间写的,这几天又完善了下,力求把更多的前端所涉及到的关于文件上传的各种场景和应用都涵盖了,若有疏漏和问题还请留言斧正和补充。

自测读不读

以下是本文所涉及到的知识点,break or continue ?

  • 文件上传原理
  • 最原始的文件上传
  • 使用 koa2 作为服务端写一个文件上传接口
  • 单文件上传和上传进度
  • 多文件上传和上传进度
  • 拖拽上传
  • 剪贴板上传
  • 大文件上传之分片上传
  • 大文件上传之断点续传
  • node 端文件上传

原理概述

原理很简单,就是根据 http 协议的规范和定义,完成请求消息体的封装和消息体的解析,然后将二进制内容保存到文件。

我们都知道如果要上传一个文件,需要把 form 标签的enctype设置为multipart/form-data,同时method必须为post方法。

那么multipart/form-data表示什么呢?

multipart互联网上的混合资源,就是资源由多种元素组成,form-data表示可以使用HTML Forms 和 POST 方法上传文件,具体的定义可以参考RFC 7578。

multipart/form-data 结构

看下 http 请求的消息体



  • 请求头:

Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryDCntfiXcSkPhS4PN 表示本次请求要上传文件,其中boundary表示分隔符,如果要上传多个表单项,就要使用boundary分割,每个表单项由———XXX开始,以———XXX结尾。

  • 消息体- Form Data 部分

每一个表单项又由Content-Type和Content-Disposition组成。

Content-Disposition: form-data 为固定值,表示一个表单元素,name 表示表单元素的 名称,回车换行后面就是name的值,如果是上传文件就是文件的二进制内容。

Content-Type:表示当前的内容的 MIME 类型,是图片还是文本还是二进制数据。

解析

客户端发送请求到服务器后,服务器会收到请求的消息体,然后对消息体进行解析,解析出哪是普通表单哪些是附件。

可能大家马上能想到通过正则或者字符串处理分割出内容,不过这样是行不通的,二进制buffer转化为string,对字符串进行截取后,其索引和字符串是不一致的,所以结果就不会正确,除非上传的就是字符串。

不过一般情况下不需要自行解析,目前已经有很成熟的三方库可以使用。

至于如何解析,这个也会占用很大篇幅,后面的文章在详细说。

最原始的文件上传

使用 form 表单上传文件

在 ie时代,如果实现一个无刷新的文件上传那可是费老劲了,大部分都是用 iframe 来实现局部刷新或者使用 flash 插件来搞定,在那个时代 ie 就是最好用的浏览器(别无选择)。

DEMO



这种方式上传文件,不需要 js ,而且没有兼容问题,所有浏览器都支持,就是体验很差,导致页面刷新,页面其他数据丢失。

HTML

 <form method="post" action="http://localhost:8100" enctype="multipart/form-data">

        选择文件:
            <input type="file" name="f1"/> input 必须设置 name 属性,否则数据无法发送<br/>
<br/>
            标题:<input type="text" name="title"/><br/><br/><br/>

        <button type="submit" id="btn-0">上 传</button>

</form>

复制代码

文件上传接口

服务端文件的保存基于现有的库koa-body结合 koa2实现服务端文件的保存和数据的返回。

在项目开发中,文件上传本身和业务无关,代码基本上都可通用。

在这里我们使用koa-body库来实现解析和文件的保存。

koa-body 会自动保存文件到系统临时目录下,也可以指定保存的文件路径。



然后在后续中间件内得到已保存的文件的信息,再做二次处理。

  • ctx.request.files.f1 得到文件信息,f1为input file 标签的 name
  • 获得文件的扩展名,重命名文件

NODE

/**
 * 服务入口
 */
var http = require('http');
var koaStatic = require('koa-static');
var path = require('path');
var koaBody = require('koa-body');//文件保存库
var fs = require('fs');
var Koa = require('koa2');

var app = new Koa();
var port = process.env.PORT || '8100';

var uploadHost= `http://localhost:${port}/uploads/`;

app.use(koaBody({
    formidable: {
        //设置文件的默认保存目录,不设置则保存在系统临时目录下  os
        uploadDir: path.resolve(__dirname, '../static/uploads')
    },
    multipart: true // 开启文件上传,默认是关闭
}));

//开启静态文件访问
app.use(koaStatic(
    path.resolve(__dirname, '../static') 
));

//文件二次处理,修改名称
app.use((ctx) => {
    var file = ctx.request.files.f1;//得道文件对象
    var path = file.path;
    var fname = file.name;//原文件名称
    var nextPath = path+fname;
    if(file.size>0 && path){
        //得到扩展名
        var extArr = fname.split('.');
        var ext = extArr[extArr.length-1];
        var nextPath = path+'.'+ext;
        //重命名文件
        fs.renameSync(path, nextPath);
    }
    //以 json 形式输出上传文件地址
    ctx.body = `{
        "fileUrl":"${uploadHost}${nextPath.slice(nextPath.lastIndexOf('/')+1)}"
    }`;
});

/**
 * http server
 */
var server = http.createServer(app.callback());
server.listen(port);
console.log('demo1 server start ......   ');
复制代码

CODE

https://github.com/Bigerfe/fe-learn-code/

果我们测试一个版本时遇到Bug,然后等开发改了以后及时验证,开发可能会好心提醒一句,你先清一下浏览器缓存再测,是不是经常碰到这种情况?所以我们在测试中要经常和缓存打交道,既然是老朋友了,那我们干脆来好好认识一下它。

缓存基本上是对网页上经常访问的资源的存储和重用。由于存储是在快速访问的地方完成的,因此导航速度更快,这也提高了网站和应用程序的性能。这就是为什么 HTTP 缓存对于想要优化用户体验并因此增加收入的企业来说是必不可少的工具。

每一毫秒都很重要

如果你被问到一个网站最让你烦恼的是什么,你可能会回答说这是一个网站加载速度很慢。而且有调查显示:70%的消费者表示,网页加载的速度会直接影响他们在线购买的意愿。网页的缓慢加载和呈现会影响用户体验,从而导致失去客户参与和销售的机会。

由于我们生活在即时性的时代,因此在虚拟世界中逃脱的任何事物都是失败的代名词。百度和其他搜索引擎在对搜索进行排名时不会推荐缓慢的网站,想象一下用户在等待查看和浏览网站时的体验和时间消耗。

在访问量大的时候,使网站更快的有效方法是使用缓存。你应该听说过缓存,至于它是什么,以及如何工作的,这就是接下来要谈的内容。

什么是 HTTP 缓存?

为了更好地理解什么是HTTP缓存,首先你得知道HTTP是什么。简而言之HTTP是一种基于文本的应用层传输协议,我们一般称之为“超文本传输协议”,基于该协议可以传输如图像、图形、URL、 HTML 文本和脚本之类,那么这些的存储和重用,那这样留下备份就被称为缓存,从而防止每次访问页面时都下载它们。缓存的主要目的是通过重用以前的响应消息来满足当前的请求来提高通信性能。

HTTP 缓存是如何工作的?

缓存的工作方式如下:


  1. 网站页面向源服务器请求资源。
  2. 系统检查缓存以查看是否已存储资源的副本。
  3. 如果资源已缓存,那么资源将从缓存中传送。
  4. 如果未缓存资源,则会导致缓存丢失,并且文件将在其源服务器中访问。
  5. 缓存资源后,会一直优先使用该资源,直到过期或清除缓存。

缓存的类型

缓存的类型是根据内容的存储位置定义的。

  • 浏览器缓存 -也叫客户端缓存, 是指存储在客户端中的缓存,一般提到缓存我们首先想到的就是此类。所有浏览器都有一个本地存储,通常用于检索以前访问的资源。这种类型的缓存是私有的,因为存储的资源不共享。客户端缓存可以存储网页的静态资源,例如CSS、JavaScript、图片等,以及一些动态内容,例如AJAX请求的响应。
  • 反向代理服务器缓存 - 也称为网关缓存,它是一个单独的独立层,此存储位于客户端和应用程序之间。它缓存客户端发出的请求并将其发送到应用程序,并对从应用程序发送到客户端的响应执行相同的操作。如果再次请求资源,缓存会在到达应用程序之前返回响应。它也是一个共享缓存,但由服务器而不是用户共享。
  • 代理服务器缓存 - 此存储(也称为中间缓存)在客户端和源服务器之间的代理服务器上完成。这是一种共享缓存,因为它由多个客户端使用,通常由提供程序维护。
  • 应用程序缓存 - 此存储是在应用程序中完成的。它允许开发人员指定浏览器应该缓存哪些文件,并使用户即使在离线时也可以使用它们。
  • CDN缓存 -CDN缓存是一种分布式缓存技术,通过将数据缓存在全球各地的CDN节点上,加速内容的传输和访问。CDN缓存通常用于存储静态资源,例如图片、视频、音频等,以减少数据传输的延迟和带宽消耗。

缓存头

在缓存头中,给出了用于定义缓存特征的指令。例如:


缓存控制

在缓存头中,可以为缓存提供以下标准指令:

  • private

内容被视为私有内容,因为只有一个用户有权访问。在这种情况下,私有内容可以由客户端的浏览器存储,但不能由中间缓存存储。

  • public

内容被视为公开内容,因为多个用户可以访问。内容可以由浏览器存储,也可以存储在客户端和服务器之间的其他缓存中。

  • max-age

定义在不在源服务器上重新验证的情况下可以缓存内容的最长时间。时间以秒为单位定义,最大值为一年(31,536,000 秒)。

  • no-store

无法缓存内容,因此请求始终发送到源服务器。传输机密数据时会标明此格式。

  • no-cache

必须在每个新请求中重新验证缓存的内容,这会使内容立即过时。在这种情况下,缓存会在释放存储的副本之前将请求发送到源服务器进行验证。

  • s-maxage

指示可以缓存内容的时间量,因此与 max-age 非常相似,但不同之处在于此选项仅适用于中间缓存,而不适用于浏览器。

  • expires

Expires 标头定义内容何时过期。在指定的时间之后,缓存的内容将被视为过时,因此请求将有权访问源服务器上的最新内容。

  • etag

Etag(实体标签)标头用于验证浏览器的缓存资源是否与源服务器上的缓存资源相同。也就是说,它验证客户端是否正在接收缓存内容的最新版本。此标头用作与网站上每个资源关联的唯一标识符。为了进行标识,网页服务器使用 Etag 值,该值在每次更改资源时都会修改。ETag 值是上次更新资源的日期和时间。

  • last-modified

Last-Modified 标头显示浏览器上次修改资源的时间,以及它是否应使用缓存的副本或下载最新版本。例如,当用户访问您的网站时,浏览器会存储页面的资源,因此当他们下次访问该网站时,服务器会检查文件自上次访问以来是否已更改。如果没有修改,服务器会向浏览器发送“304 not modified”响应,并使用缓存的副本。

  • vary

Vary 标头可以存储相同内容的不同版本,因此它用于请求缓存以在决定请求哪些内容之前检查其他标头。例如,当与 Accept-Encoding 标头一起使用时,该设置允许区分压缩和未压缩的内容,或者当与User-Agent 标头一起使用时,它会区分移动或桌面网站的版本。

缓存的好处

  • 减少延迟提升网页加载速度
  • 减少带宽消耗;
  • 减少网络流量;
  • 支持网页离线浏览;
  • 网站速度和性能提高。


测试人员在什么场景下要留意缓存

1.及时验证测试

这是最常见的一类需要注意缓存的情况,就是如果遇到一些小bug,需要开发立即改好后验证,那就可以等开发改完,测试这边需要把验证用到的浏览器缓存清除一次,因为很可能页面还用的之前的缓存数据;

2. 页面加载性能测试

缓存可以用于加速页面加载时间。测试人员需要验证页面加载性能,并确保缓存策略不会导致页面内容过时或不一致;

3. 数据一致性测试

当应用程序使用缓存来存储数据时,测试人员需要测试缓存对数据一致性的影响。验证数据在缓存和数据库之间的同步性,以及在数据更新时缓存的更新机制,这点也是很重要的;

4. 用户认证和授权测试

缓存可能包含用户的认证和授权信息。测试人员需要验证用户登录和注销时缓存的正确更新,以及对用户权限变更的处理,虽然这在登录模块中是比较次要的测试,但是对于安全性要求较高的系统还是比较重要的;

5. 并发性能测试

在高并发环境下,缓存的并发读写可能导致性能问题。测试人员需要验证缓存在高负载情况下的表现,确保它能够正确处理并发请求,这对于性能有要求的系统就极为重视。


总结

总之,HTTP缓存是Web性能优化的关键组成部分,也是测试工程师所要牢牢掌握的知识点,理解了http缓存有助于我们对web的架构有更深的认识,这样才能在测试中驾轻就熟。