聊聊浏览器缓存

2019-07-27  本文已影响0人  cd2001cjm

一.引言

对于前端工程化,浏览器缓存是一个很重要的知识点。

浏览器缓存是提升加载性能的重要手段,但如果使用不当,反而会造成副作用,

比如:脚本发布了但客户端没加载最新的

下面就对浏览器缓存一探究竟

二. 浏览器缓存类型

浏览器缓存类型分两种:

协商缓存:每次读取缓存时,先到服务器端去验证一下是否有改变,如果有就获取新的,没有从缓存中读取,响应code为304

强制缓存:只要缓存没过有效期,就强制读取缓存,响应code为200。该类缓存必定是要持久化到disk的。Firefox中并没有对强制缓存进行进一步的分类。但谷歌浏览器的处理略有不同,分为两种:一种是(from disk cache),另一种是(from memory cache)

谷歌浏览器HTTP请求流程

从流程图可以看到,http请求最重要的类就是HttpCache,请求发送前,会先验证是否命中缓存

如果命中,就会发起httpCacheTransaction,去读取缓存信息。当然在读取前会判断是否要进行缓存有效性的验证。如果有变化就更新缓存

未命中,就会发起httpNetworkTransaction去服务端获取,然后根据响应头去判断是否要缓存,以及何种形式的缓存。如果缓存就写入磁盘。

谷歌浏览器的缓存模式有13种,具体可参考http_cache_transaction.cc。

常见的有三种:无缓存,强制缓存,协商缓存

三. HTTP缓存指令

Header信息分为request header和response header。

请求缓存指令:会影响请求发送时,是否要缓存。

响应缓存指令:会影响接受到响应时,是否要缓存。

Pragma,Expires是HTTP1.0的字段,在HTTP1.1中已经被Cache-Control取代,不做讨论。

3.1,请求缓存指令

从谷歌代码中可以看到(注意注释),Cache-Control相关的有两个值:

no-cache:如果请求包含这些请求头中的一个,那么避免重用我们的缓存副本(如果有的话)

max-age=0:如果请求包含这些请求头中的一个,则强制缓存副本(如果有)将在重用前重新验证

3.2,响应缓存指令

Cache-control:nostore 

响应内容不会写入disk_cache

Cache-control:max-age=3600(1小时)

    写入disk_cache,1小时内再次访问该url,强制从缓存中读取。基础时间是第一次访问时,响应中返回的Date

Cache-control:no-cache 

再次访问的时候,会去服务端验证

3.3,对于协商缓存,去服务端的验证过程是什么,或者说我们要验证什么。

简单来说就是验证服务器脚本是否改变,改变了就加载新的,分为下面三个步骤:

a,响应头信息是否改变

响应头的改变会影响浏览器对缓存的处理。

比如:响应头由【缓存】->【不缓存】,那么对已有缓存也要根据情况进行相应的删除替换处理

b,资源的最后修改时间是否改变

  首次访问的时候,响应会返回Last-Modified,再次访问时,该字段会放在请求中的If-Modified-Since,然后在服务端比较文件的最后修改时间,以确认资源是否发生变化。

这只是时间维度的比较,但如果内容没变,只是时间改变了呢?

为了解决内容对比问题,就有了下面的Etag

c,内容是否改变

  首次访问的时候,响应同时会返回Etag,再次访问时,该字段会放在请求中的If-None-Match,然后在服务端比较是否一致。

Etag真的能解决文件内容一致性的问题么?

3.4 ,Etag

Etag 是URL的Entity Tag,用于标示URL对象是否改变。标准概念网上资料很多,这里不做累述。

根据Etag的定义,我们期待的是:当文件内容改变的时候,浏览器能根据Etag去加载最新的资源文件。

是不是真的能达到期望的效果,完全取决于中间件对Etag的算法实现。

我们看看Nginx的Etag生成代码:

ngx_int_t

ngx_http_set_etag(ngx_http_request_t *r)

{

    ngx_table_elt_t          *etag;

    ngx_http_core_loc_conf_t  *clcf;

    clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);

    if (!clcf->etag) {

        return NGX_OK;

    }

    etag = ngx_list_push(&r->headers_out.headers);

    if (etag == NULL) {

        return NGX_ERROR;

    }

    etag->hash = 1;

    ngx_str_set(&etag->key, "ETag");

    etag->value.data = ngx_pnalloc(r->pool, NGX_OFF_T_LEN + NGX_TIME_T_LEN + 3);

    if (etag->value.data == NULL) {

        etag->hash = 0;

        return NGX_ERROR;

    }

    etag->value.len = ngx_sprintf(etag->value.data, "\"%xT-%xO\"",

                                  r->headers_out.last_modified_time,

                                  r->headers_out.content_length_n)

                      - etag->value.data;

    r->headers_out.etag = etag;

    return NGX_OK;

}

void

ngx_http_weak_etag(ngx_http_request_t *r)

{

    size_t            len;

    u_char          *p;

    ngx_table_elt_t  *etag;

    etag = r->headers_out.etag;

    if (etag == NULL) {

        return;

    }

    if (etag->value.len > 2

        && etag->value.data[0] == 'W'

        && etag->value.data[1] == '/')

    {

        return;

    }

    if (etag->value.len < 1 || etag->value.data[0] != '"') {

        r->headers_out.etag->hash = 0;

        r->headers_out.etag = NULL;

        return;

    }

    p = ngx_pnalloc(r->pool, etag->value.len + 2);

    if (p == NULL) {

        r->headers_out.etag->hash = 0;

        r->headers_out.etag = NULL;

        return;

    }

    len = ngx_sprintf(p, "W/%V", &etag->value) - p;

    etag->value.data = p;

    etag->value.len = len;

}


通过上面的代码我们可以看到nginx的算法:

强Etag=最后修改时间+”-”+内容长度 (16进制)

弱Etag=“\W”+ 强Etag

当我们发布新文件的时候,最后修改时间都会发生变化。基本能满足项目需要。

但通过该算法,我们也能看到,当内容没变最后修改时间变了,就不会命中缓存的问题依然没有得到解决。仅仅判断内容长度还是不够的。

所以对于有能力扩展中间件的公司,都会自行实现Etag算法,目前看来基于文件MD5码是个不错的选择

四,写在最后

通常在项目中,多数都是对响应指令进行控制。

缓存机制完全是依赖浏览器和中间件的实现,本文的分析是基于谷歌和nginx,不代表所有浏览器和中间件完全一致。切记!

上一篇下一篇

猜你喜欢

热点阅读