Http缓存机制(强制缓存、协商缓存)及不同浏览器中刷新方式采坑小记

1,571 阅读5分钟

http缓存机制分为两种:强制缓存和协商缓存。并且在不同刷新方式下和浏览器环境下对策略的采取会有所区别。

强制缓存

初次请求,服务器返回资源并在响应中携带cache-control头。
如果服务端觉得这个资源可以被缓存的话就加cache-control,如果服务端觉得这个资源没法被缓存或者不适合被缓存,则不加cache-control。
再次请求,先看本地缓存的时间判断是否靠谱,如果是的话就在本地找这个资源,不会请求服务器。 时间长了,缓存失效了,就去请求服务器,然后重新返回资源和cache-control

cache-control头可以设置如下字段:

  • max-age:缓存的最大过期时间(秒)(常见)
  • no-cache:不用强制缓存,交给服务端来处理,服务端怎么做不管 (常见)
  • no-store:不用强制缓存,而且不用服务端的一些缓存措施,让服务端简单粗暴地将资源再翻过来就行 (不常见)
  • private:只允许最终用户做缓存
  • public:允许中间的一些路由 、代理做缓存

协商缓存

服务端来判断某个资源能不能用缓存的内容,如果能,304。 服务端判断客户端资源,是否和服务端资源一样(如果一样,没必要再来一份) 一致返回304,否则返回200和最新的资源

资源返回时,会携带EtagLast-Modified两个响应头。

  • Etag,资源的指纹标识
  • Last-Modified,资源的最后修改时间

当再次请求此资源时,浏览器会在请求头中通过If-None-MatchIf-Modified-Since将之前请求资源获得的这两个字段再次发给服务器,供服务器判断。

  • If-None-Match:Etag
  • If-Modified-Since:Last-Modified

二者都存在的时候优先采用Etag进行判断,因为Etag是根据文件内容来的,而Last-Modified只能精确到秒。如果资源被重复生成,而内容不变,则Etag更精确(如果资源一秒生成一次,一秒生成一次,内容不变,用Last-Modified去对比的话每次都会过期不断获取新的资源,Etag是根据内容计算的)

缓存策略

向服务器请求资源,首先看本地有没有缓存,如果有的话,先通过强制缓存(cache-control:max-age或者expires)判断本地缓存资源能否继续使用;如果能的话,直接冲本地读取资源即可;否则向服务器请求,服务器通过If-None-Match和If-Modified-Since请求头判断资源能否继续使用;如果能,响应中设置304状态码,继续使用本地资源,否则,重新返回服务器端的资源。 image.png

刷新页面对http缓存的影响

刷新页面可以分为三种:

  • 正常操作 : 地址栏输入url ,跳转链接,前进后退等 ; 两种缓存都有效
  • 手动刷新 : F5 ,点击刷新按钮,右击菜单刷新 ; 仅协商缓存有效
  • 强制刷新 : ctrl +f5 cmd + r ; 两种缓存策略都无效,重新请求资源

实操

使用express框架编写一个简单的返回静态资源的服务端程序

// config.js 配置
const path = require('path');

module.exports = {
    port: 9527, // 服务端口
    root: process.cwd(), // 根目录
    static: path.resolve(process.cwd(), "static"), // 静态资源目录
    maxAge: 10, // 强制缓存时间
    enableEtag: true, // 是否使用Etag进行判断
    enableLastModified: true, // 是否使用Last-Modified进行判断
}
// cache.js 中间件,处理缓存相关
const {maxAge, enableEtag, enableLastModified, root, static} = require("../../config/config");
const path = require('path');
const etag = require('etag');
const fs = require('fs')

const mimes = { // MIME类型
    css: 'text/css',
    less: 'text/css',
    gif: 'image/gif',
    html: 'text/html',
    ico: 'image/x-icon',
    jpeg: 'image/jpeg',
    jpg: 'image/jpeg',
    js: 'text/javascript',
    json: 'application/json',
    pdf: 'application/pdf',
    png: 'image/png',
    svg: 'image/svg+xml',
    swf: 'application/x-shockwave-flash',
    tiff: 'image/tiff',
    txt: 'text/plain',
    wav: 'audio/x-wav',
    wma: 'audio/x-ms-wma',
    wmv: 'video/x-ms-wmv',
    xml: 'text/xml',
}

function handleCache(req, res, next){
    const extName = path.extname(req.url); // 取请求后缀名,没有就是空, 有就是 “.jpg”之类
    if(mimes.hasOwnProperty(extName.slice(1))){
        res.setHeader('Cache-Control', `public,max-age=${maxAge}`);
        if(!enableEtag && !enableLastModified){
            // 不使用协商缓存
            res.statusCode = 200;
        }
        const {url, headers} = req; // req.url: /后面的内容
        const filePath = path.join(static, url);

        if (enableLastModified) {
            // 使用last-modified进行协商缓存
            const lastModified = headers['if-modified-since'];
            const mtime = fs.statSync(filePath).mtime.toUTCString();
            res.setHeader('Last-Modified', mtime);
            res.statusCode = lastModified === mtime ? 304 : 200;
        }
        if (enableEtag) {
            // 使用etag进行协商缓存
            const reqEtag = headers['if-none-match'];
            // 使用同步方法读取响应内容,计算 etag
            const resEtag = etag(fs.readFileSync(filePath));
            res.setHeader('ETag', resEtag);
            res.statusCode = reqEtag === resEtag ? 304 : 200;
        }
    }
    next();
   
}

module.exports = handleCache
// server.js 启动服务
const express = require('express');
const events = require('events');
const fs = require('fs');
const path = require('path');
const {port, static} = require('../config/config')

const app = express();
const route = express.Router();
const userRouter = require('./router/user');
const fileRouter = require('./router/file');
const cache = require('./cache/cache')

const bodyParser = require('body-parser')
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

app.all('*',function (req, res, next) { // 跨域
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With');
    res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
    if (req.method === 'OPTIONS') {
        res.send(200);
    }
    else {
        next();
    }
});

app.use(cache);
// 设置静态资源目录
app.use(express.static(static))

app.listen(port, () => {
    console.log('server start');
})

在静态资源目录下随便放个什么文件,我就搞了个图片timg3.jpg
启动服务,在浏览器目录中输入http://localhost:9527/timg3.jpg, 可以获得图片。

image.png 第一次是200,并且可以获得缓存的相关响应头(红框)

在max-age规定的秒数内,再次请求图片,发现这里是直接使用本地的图片

image.png
点击浏览器刷新按钮,按照刷新规则,这里是只采用协商缓存,跳过强缓存。结果却是是采用了协商缓存,返回304。

image.png
如果细心一些会发现,此时浏览器发送的请求中带了一个Cache-Control头,其中将max-age设置为0,可就是说资源默认就是陈旧的,所以跳过了强缓存,直接使用协商缓存。

image.png

然后我研究了一下,发现这个情况在chrome和火狐中是相同的,刷新url设置max-age=0跳过强缓存。但是在IE(1909版本)中没有此现象,点击刷新按钮后没有设置max-age=0,强缓存依然生效,只有在max-age之后才进行协商缓存。

image.png