强缓存?协商缓存?node给你答案

1,866 阅读5分钟

前言

相信做前端的小伙伴们都用过浏览器的缓存机制。当我们在项目优化时,我们都不会忘记了使用缓存机制对静态资源进行缓存。

浏览器缓存机制设计出来就是为了提高网页加载速度和减少对服务器请求次数。它可以帮助我们在本地缓存一些静态资源,以便我们再次访问时可以直接从本地拿到这些静态资源。

浏览器缓存机制两种:

  1. 强缓存: 也可以叫浏览器缓存,它是浏览器自身对于资源进行缓存。
  2. 协商缓存:也可以叫HTTP缓存,它则需要浏览器和服务器之间通过 HTTP 协议进行的缓存控制。

那接下来,我就用代码的形式来和大家详细的介绍介绍这两种缓存机制。

强缓存

首先我们要知道,强缓存是指浏览器在请求资源时,直接从本地缓存中获取资源,不需要在向服务器发送请求。也就等于浏览器在第一次访问时,强制的帮我们把某些资源就留下来了。这样浏览器再次访问就可以直接在本地读取资源并展示了。

强缓存主要依赖于 HTTP 头中的 Expires 和 Cache-Control 字段。 下面我就以这两个字段来解析强缓存的过程。

Cache-Control

Cache-Control 字段是用来控制缓存行为的指令集合。它有许多常见的指令:

  • max-age:表示资源在缓存中的最大存储时间,单位为秒。
  • no-cache:表示每次请求都要向服务器验证资源的有效性,也就是不缓存。
  • private:允许浏览器使用缓存。
  • public: 客户端和代理服务器都可缓存。

我们用 node 模仿出一个缓存的接口,来展示如何用Cache-Control字段来开启强缓存,代码如下:

const http = require('http')
const server = http.createServer((req, res) => {
    res.writeHead(200, {
        'Cache-Control': 'max-age=10',
        "Access-Control-Allow-Origin": "*"
    })
    res.end('我要缓存')
})
server.listen(8080, () => {
    console.log('服务已启动在', server.address());
})

如上述代码,我们在请求头里写了Cache-Control': 'max-age=10', 这意味着,我们在十秒之内再次请求该接口,浏览器则会命中缓存。效果如下:

图片.png

Expires

Expires 字段是在服务器响应头中设置的一个时间戳,表示资源的过期时间。当浏览器再次请求该资源时,会先检查本地缓存中的 Expires 字段与当前时间的比较,如果未过期,则直接使用本地缓存,否则重新向服务器请求资源。

我们拿到上述代码,这样改写一下:

const http = require('http')
const server = http.createServer((req, res) => {
    const expires = new Date();
    expires.setDate(expires.getDate() + 7);
    res.setHeader('Expires', expires.toUTCString());
    res.writeHead(200, {
        "Access-Control-Allow-Origin": "*"
    })
    res.end('我要缓存')
})
server.listen(8080, () => {
    console.log('服务已启动在', server.address());
})

我们再来看看请求该接口,我们也可以开启强缓存,效果如下:

图片.png

协商缓存

协商缓存是指浏览器在请求资源时,先向服务器发送一个验证请求,由服务器判断是否需要返回资源的内容。该缓存策略主要依赖的是 HTTP 头中的 Last-Modified 和 ETag 字段。

Last-Modified

Last-Modified 字段是在服务器响应头中设置的资源最后修改的时间。当浏览器再次请求该资源时,会将该字段的值通过 If-Modified-Since 字段发送给服务器,服务器根据这个值判断资源是否发生了变化。如果未发生变化,则返回 304 状态码,浏览器则会继续使用本地缓存;如果发生了变化,则返回新的资源内容。

我们同样用一段node代码来和小伙伴们看看它是如何开启的:

const http = require('http')
const path = require('path')
const fs = require('fs')
const mime = require('mime')

const server = http.createServer((req, res) => {
    let filePath = path.resolve(__dirname, path.join('www/test.txt', req.url))
    if (fs.existsSync(filePath)) {
        const stats = fs.statSync(filePath) // fs.statSync判断当前资源是文件还是路径
        if(fs.existsSync(filePath)) {
            const { ext } = path.parse(filePath)
            const stats = fs.statSync(filePath)
            let status = 200
            const timeStamp = req.headers['if-modified-since']
            if(timeStamp && Number(timeStamp) === stats.mtimeMs) {
                //资源在后端没有修改过
                status = 304
            }
            res.writeHead(status, {
                'Content-Type': mime.getType(ext),
                'Last-Modified': stats.mtimeMs, // 协商缓存的响应头
                "Access-Control-Allow-Origin":'*'
            })
           if(status === 200) {
            const fileStream = fs.createReadStream(filePath) // 把文件读取成流类型
            fileStream.pipe(res) // 将文件流流入响应流对象中
           }else {
               res.end()
           }
        }
    }else {
        res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' })
        res.end('<h1> Not Found</h1>')
    }
})
server.listen(8080, () => {
    console.log('服务已启动');
})

我们来看上述这一段代码,我们用的node读取了一个文件的最后修改时间,将它输出给前端,当前端再次请求时,我们仅需要判断请求头带过来的if-modified-since 与文件的最后修改时间是否相同。效果如下:

图片.png

ETag

ETag 字段是在服务器响应头中设置的资源的唯一标识符,通常为根据资源内容生成的哈希值。当浏览器再次请求该资源时,会将该字段的值通过 If-None-Match 字段发送给服务器,服务器根据这个值判断资源是否发生了变化。与 Last-Modified 类似,如果未发生变化,则返回 304 状态码;如果发生了变化,则返回新的资源内容。

我们来将上述代码修改一下:

const http = require('http')
const path = require('path')
const url = require('url')
const fs = require('fs')
const mime = require('mime')
const checksum = require('checksum')
const server = http.createServer((req, res) => {
    const srcUrl = url.parse(`${req.url}`)
    const resPath = 'www/test.txt'
    const { ext } = path.parse(resPath)
    res.setHeader('Access-Control-Allow-Origin', '*')
    if (!fs.existsSync(resPath)) {
        res.writeHead(404, { "Content-Type": 'text/html' })
        return res.end('<h1>404 Not Found<h1>')
    }

    checksum.file(resPath, (err, sum) => {
        const resStream = fs.createReadStream(resPath)
        sum = `"${sum}"`
        if (req.headers['if-none-match'] === sum) {
            res.writeHead(304, {
                'Content-Type': mime.getType(ext),
                etag: sum
            })
            res.end()

        } else {
            res.writeHead(200, {
                'Content-Type': mime.getType(ext),
                etag: sum
            })
            resStream.pipe(res)
        }
    })
})
server.listen(8080, () => {
    console.log('服务已启动');
})

看上述代码,我们可以看出,其实Etag的用法和Last-Modified的用法差不多,只是换了一个http响应头而已。运行效果如下: image.png

结语

讲到这里,相信小伙伴们也理解了浏览器缓存,我们也可以自己动手试试哦。