看了这篇文章,浏览器缓存一定能记住

807 阅读5分钟

关于浏览器缓存方面的知识,也看了好几篇文章了。大家提到也总能说几个关键词出来,但是说到如何使用可能又不够确定了。因此我在这通过实操记录下来,方便更好的理解和记忆。

首先,常说的浏览器缓存的两种情况强缓存协商缓存,优先级较高的是强缓存,当强缓存命中失败时才会走协商缓存。

强缓存

强缓存是不需要发送http请求的,当资源命中强缓存时,直接从缓存中获取,响应状态返回200,打开控制台查看Size也不显示资源大小,而是告诉我们来自缓存。

强缓存

强缓存

强缓存如何实现呢,本文后端代码都通过Koa来演示

1、Expires

设置过期时间,第一次请求以后响应头中设置Expires

router.get('/img/1.jpg', ctx => {
    const file = fs.readFileSync(path.resolve(__dirname,`.${ctx.request.path}`))
    ctx.set({
        'Expires': new Date((+new Date() + 1000*60*60*24*3))
    })
    ctx.body = file
})

将过期时间设置为三天后,这样在缓存未过期下就不需要再重新请求服务端了。但是Expires存在问题,就是客户端的时间和服务端时间可能是不一致的,比如你把电脑时间设置成三天甚至一年后,缓存就无效了。

2、Cache-Control

HTTP1.1新增了Cache-Control字段,Cache-Control优先级高于Expires,两者同时使用时会忽略Expires(Expires保留的作用是向下兼容),Cache-Control字段属性值比较灵活。

2.1 max-age

max-age指定资源的有效时间,单位是秒。以下是demo2,Cache-Control:max-age=0,同时设置Expires。测试可以看到图片始终不会缓存,也体现出了优先级的问题。

// demo2
router.get('/img/2.jpg', ctx => {
    const file = fs.readFileSync(path.resolve(__dirname,`.${ctx.request.path}`))
    ctx.set({
        'Expires': new Date((+new Date() + 1000*60*60*24*3)),
        'Cache-Control': 'max-age=0'
    })
    ctx.body = file
})

再测试demo3,这样图片的缓存时间就是10秒

// demo3
router.get('/img/3.jpg', ctx => {
    const file = fs.readFileSync(path.resolve(__dirname,`.${ctx.request.path}`))
    ctx.set({
        'Cache-Control': 'max-age=10'
    })
    ctx.body = file
})

2.2 s-maxage

s-maxagemax-age类似,不同的地方s-maxage是针对代理服务器的

// demo4
router.get('/img/4.jpg', ctx => {
    const file = fs.readFileSync(path.resolve(__dirname,`.${ctx.request.path}`))
    ctx.set({
        'Expires': new Date((+new Date() + 1000*60*60*24*3)),
        'Cache-Control': 's-maxage=10'
    })
    ctx.body = file
})

测试发现图片无法缓存

2.3 private和public

和前面两个属性相关,private对应资源可以被浏览器缓存,public表示资源既可以被浏览器缓存,也可以被代理服务器缓存。 默认值是private,相当于设置了max-age的情况;当设置了s-maxage属性,就表示只能被代理服务器缓存。

这里请看demo5

// demo5
router.get('/img/5.jpg', ctx => {
    const file = fs.readFileSync(path.resolve(__dirname,`.${ctx.request.path}`))
    ctx.set({
        'Cache-Control': 'private'
    })
    ctx.body = file
})

测试发现图片始终不会缓存,所以private对应max-age的默认值应该是0。但是private真的和max-age=0完全相同吗,我又写了个栗子测试。

//demo6
router.get('/img/6.jpg', ctx => {
    const file = fs.readFileSync(path.resolve(__dirname,`.${ctx.request.path}`))
    ctx.set({
        'Expires': new Date((+new Date() + 1000*60*60*24*3)),
        'Cache-Control': 'private'
    })
    ctx.body = file
})

测试发现图片会被缓存,说明private情况下,不会让Expires失效。

2.4 no-store和no-cache

no-store比较暴力,不适用任何缓存机制,直接向服务器发起请求,下载完整资源。

no-cache跳过强缓存,也就是Expiresmax-age等都无效了,直接请求服务器,确认资源是否过期,也就是进入协商缓存的阶段。协商缓存中有两个关键字段,Last-ModifiedEtag

协商缓存

1、Last-Modified和If-Modified-Since

请看demo8

// demo8
router.get('/img/8.jpg', ctx => {
    const file = fs.readFileSync(path.resolve(__dirname,`.${ctx.request.path}`))
    const stats = fs.statSync(path.resolve(__dirname,`.${ctx.request.path}`))
    if(ctx.request.header['if-modified-since'] === stats.mtime.toUTCString()){
        return ctx.status = 304
    }
    ctx.set({
        'Cache-Control': 'no-cache',
        'Last-Modified': stats.mtime.toUTCString()
    })
    ctx.body = file
})

第一次请求后,在响应头中加入Last-Modified字段,返回资源的最后修改时间,下次请求时客户端请求头会带上If-Modified-Since的字段,里面的值就是之前响应头中Last-Modified的值,然后进行比较,如果没有变化,则返回304的状态码。

协商缓存

如果改造一下把'Cache-control': 'no-cache'去掉呢,测试后发现,在不清缓存的情况资源就变成强缓存了,请求头中的If-Modified-Since也没有了(加上也没用,因为不会发送http请求),导致文件更新就无法检测了。

2、 ETag和If-None-Match

请看demo9

// demo9
router.get('/img/9.jpg', ctx => {
    const file = fs.readFileSync(path.resolve(__dirname,`.${ctx.request.path}`))
    const stats = fs.statSync(path.resolve(__dirname,`.${ctx.request.path}`))
    if(ctx.request.header['if-none-match']){
        if(ctx.request.header['if-none-match'] === 'abc123'){
            return ctx.status = 304
        }
    }else if(ctx.request.header['if-modified-since'] === stats.mtime.toUTCString()){
        return ctx.status = 304
    }
    ctx.set({
        'Cache-Control': 'no-cache',
        'Last-Modified': stats.mtime.toUTCString(),
        'ETag': 'abc123'
    })
    ctx.body = file
})

Last-Modified类似,第一次请求以后,响应头中会增加ETag字段,ETag通过资源的内容生成一个标识符,下次请求在请求中增加If-None-Match字段,值就是之前响应头中ETag的值,然后进行比较。ETag可以说是对Last-Modified的一个补充,因为Last-Modified也是有不足的地方。举个栗子,Last-Modified中的时间是精确到秒的,如果同一秒内文件被修改了一次,下一次请求时,预期获取新资源而实际还是会走协商缓存。而ETag是基于资源内容的,所以会生成新的值,因此能达到预期效果。

补充:浏览器缓存的四个位置

  1. Service Worker Cache
  2. Memory Cache
  3. Disk Cache
  4. Push Cache

Service Worker Cache,这个很多人都听过,是PWA应用的重要实现机制,推荐资源:PWA应用实战

Memory CacheDisk Cache就是前面总结的,我们强缓存和协商缓存存放资源的位置。Memory Cache内存缓存,是效率最高的,当然内存资源也是昂贵的有限的,不可能都使用内存缓存,Disk Cache磁盘缓存,相对来说读取速度慢些。一般来说,大文件或者内存使用高的情况下,资源会被丢进磁盘。

Push Cache推送缓存,缓存的最后一道防线。是HTTP2中的内容,需要自行去了解。

总结几个点

1、协商缓存中有两组约定的字段,一是Last-ModifiedIf-Modified-Since;二是ETagIf-None-Match。也就是响应头中存在Last-Modified(或ETag),则下次请求的请求头中会自动增加If-Modified-Since(或If-None-Match)字段,至于是否走协商缓存取决于具体代码,比如触发条件一般就是比较同一组数据是否相同,同时因为第二组更加准确,所以优先级也更高。

2、Cache-Control: privateCache-Control: max-age=0,效果不完全相同,前者不会让Expires失效。

3、浏览器缓存机制总览,首先如果命中强缓存就直接使用;否则就进入协商缓存阶段,这里会产生http请求,通过协商缓存的两组规范,检查资源是否更新。没更新的话就返回304状态码,否则就重新获取资源并返回200状态码。

4、通过上面的演示可以看得出来,处理缓存的工作量主要在后端,然后在工作中,静态资源我们一般都是直接使用中间件来处理,比如笔者在Koa项目中的话,会用koa-static这个中间件。前端不需要写相关代码,后端用现成的轮子,因此缓存相关的知识就被抛弃了。

5、看完这些demo,相信你一定能记住,demo地址,也可以自己clone下来跑几遍试试,希望本文对你有帮助。