三种缓存方式,再也不用麻烦运维小哥哥了!!!

7,995 阅读8分钟

依然在学习node的艰辛过程中,最近学习了http相关的知识,学到了东西当然第一时间就来和大家分享分享,今天呢就教大家来看看利用node中的http模块去实现不同的缓存策略!!!

我们都知道,对于我们前端开发来说,缓存是一个十分重要的东西,即希望用户不能每次请求过来都要重复下载我们的页面内容,希望为用户节省流量,并且能提高我们页面的浏览流畅度,但是同时当我们修改了一个bug后,又希望线上能够及时更新,这时候就要求爷爷告奶奶让运维小哥哥帮我们刷新一下缓存了,那么有没有一些比较好的缓存策略可以针对我们修改bug又能不麻烦运维及时更新呢,今天我们就利用node来看一下后端中的缓存策略是如何设置的。

强制缓存

通常我们对于强制缓存的设置是服务端告诉客户端你刚刚已经请求过一次了,我们约定好十分钟内你再过来请求都直接读取缓存吧,意思也就是当客户端在十分钟内多次请求的话只有第一次会下载页面内容,其他的请求都是直接走缓存,不管我们页面在这期间有没有变化都不会影响客户端读取缓存。 那我们来看一下代码的实现

let http = require('http');
let path = require('path');
let fs = require('fs');
let url = require('url');
// 创建一个服务
let server = http.createServer();
// 监听请求
server.on('request',(req,res)=>{
    // 获取到请求的路径
    let {pathname,query} = url.parse(req.url,true);
    // 将路径拼接成服务器上对应得文件路径
    let readPath = path.join(__dirname, 'public',pathname);
    console.log(readPath)
    try {
        // 获取路径状态
        let statObj = fs.statSync(readPath);
        // 服务端设置响应头 Cache-Control 也就是缓存多久以秒为单位
        res.setHeader('Cache-Control','max-age=10');
        // 服务器设置响应头Expires 过期时间 获取当前时间加上刚刚设置的缓存秒数
        res.setHeader('Expires',new Date(Date.now()+10*1000).toGMTString());
        //判断如果路径是一件文件夹 就默认查找该文件下的index.html
        if(statObj.isDirectory()){
            let p = path.join(readPath,'index.html');
            console.log(p);
            // 判断是否有index.html 没有就返回404
            fs.statSync(p);
            // 创建文件可读流 并且pipe到响应res可写流中
            fs.createReadStream(p).pipe(res)
        }else{
            // 如果请求的就是一个文件 那么久直接返回
            fs.createReadStream(readPath).pipe(res)
        }
    } catch (error) {
        // 读取不到 返回404 
        console.log(error)
        res.setHeader('Content-Type','text/html;charset=utf8')
        res.statusCode = 404;
        res.end(`未发现文件`)
    }
})
// 监听3000端口
server.listen(3000)

通过上面代码测试我们会发现当我们在10秒内进行对同一文件的请求,那么我们浏览器就会直接走缓存 通过上图可以看到我们重复请求的时候我们会看到css变成from memory cache,我们也看到我们刚刚的响应头也被设置上了

协商缓存

上面的强制缓存我们就发现了 就是我们平时改完bug上线要苦苦等待的一个原因了,那么有没有其他的好的缓存处理方法呢,我们设想一下 假如我们能够知道我们文件有没有修改,假如我们修改了服务器就返回最新的内容假如没有修改 就一直默认缓存 ,这样是不是听起来十分的棒!那我们就想如果我们能够知道文件的最后修改时间是不是就可以实现了!

通过文件最后修改时间来缓存

let http = require('http');
let path = require('path');
let fs = require('fs');
let url = require('url');
let server = http.createServer();
server.on('request',(req,res)=>{
    // 获取到请求的路径
    let {pathname,query} = url.parse(req.url,true);
    // 将路径拼接成服务器上对应得文件路径
    let readPath = path.join(__dirname, 'public',pathname);
    try {
        // 获取路径状态
        let statObj = fs.statSync(readPath);
        // 为了方便测试 我们告诉客户端不要走强制缓存了
        res.setHeader('Cache-Control','no-cache');
        if(statObj.isDirectory()){
            let p = path.join(readPath,'index.html');
            let statObj = fs.statSync(p);
            // 我们通过获取到文件状态来拿到文件的最后修改时间 也就是ctime 我们把这个时间通过响应头Last-Modified来告诉客户端,客户端再下一次请求的时候会通过请求头If-Modified-Since把这个值带给服务端,我们只要判断这两个值是否相等,假如相等那么也就是说 文件没有被修改那么我们就告诉客户端304 你直接读缓存吧
            res.setHeader('Last-Modified',statObj.ctime.toGMTString());
            if(req.headers['if-modified-since'] === statObj.ctime.toGMTString()){
                res.statusCode = 304;
                res.end();
                return
            }
            // 修改了那么我们就直接返回新的内容
            fs.createReadStream(p).pipe(res)
        }else{
            res.setHeader('Last-Modified',statObj.ctime.toGMTString());
            if(req.headers['if-modified-since'] === statObj.ctime.toGMTString()){
                res.statusCode = 304;
                res.end();
                return
            }
            fs.createReadStream(readPath).pipe(res)
        }
    } catch (error) {
        console.log(error)
        res.setHeader('Content-Type','text/html;charset=utf8')
        res.statusCode = 404;
        res.end(`未发现文件`)
    }
})

server.listen(3000)

我们通过请求可以看到,当我们第一次请求过后,无论怎么刷新请求都是304 直接读取的缓存,假如我们在服务端把这个文件修改了 那么我们就能看到又能请求到最新的内容了,这就是我们通过协商缓存来处理的,我们通过获取到文件状态来拿到文件的最后修改时间 也就是ctime 我们把这个时间通过响应头Last-Modified来告诉客户端,客户端再下一次请求的时候会通过请求头If-Modified-Since把这个值带给服务端,我们只要判断这两个值是否相等,假如相等那么也就是说 文件没有被修改那么我们就告诉客户端304 你直接读缓存吧

通过文件内容来缓存

再再再再再假如我们在文件中删除了字符a然后又还原了,那么这时候保存我们的文件的修改时间其实也发生了变化,但是其实我们文件的真正内容并没有发生变化,所以这时候其实客户端继续走缓存也是可以的 ,我们来看看这样的缓存策略如何实现。

let http = require('http');
let path = require('path');
let fs = require('fs');
let url = require('url');
let crypto = require('crypto');
let server = http.createServer();
server.on('request',(req,res)=>{
    // 获取到请求的路径
    let {pathname,query} = url.parse(req.url,true);
    // 将路径拼接成服务器上对应得文件路径
    let readPath = path.join(__dirname, 'public',pathname);
    try {
        // 获取路径状态
        let statObj = fs.statSync(readPath);
        // 为了方便测试 我们告诉客户端不要走强制缓存了
        res.setHeader('Cache-Control','no-cache');
        if(statObj.isDirectory()){
            let p = path.join(readPath,'index.html');
            let statObj = fs.statSync(p);
            // 我们通过流把文件读取出来 然后对读取问来的内容进行md5加密 得到一个base64加密hash值
            let rs = fs.createReadStream(p);
            let md5 = crypto.createHash('md5');
            let arr = [];
            rs.on('data',(data)=>{
                arr.push(data);
                md5.update(data);
            })
            rs.on('end',(data)=>{
                let r = md5.digest('base64');
                // 然后我们将这个hash值通过响应头Etag传给客户端,客户端再下一次请求的时候会把上一次的Etag值通过请求头if-none-match带过来,然后我们就可以继续比对文件生成的hash值和上次产生的hash是否一样 如果一样说明文件内容没有发生变化 就告诉客户端304 读取缓存
                res.setHeader('Etag',r);
                if(req.headers['if-none-match']===r){
                    res.statusCode=304;
                    res.end();
                    return;
                }
                res.end(Buffer.concat(arr))
            })
        }else{
            let rs = fs.createReadStream(readPath);
            let md5 = crypto.createHash('md5');
            let arr = [];
            rs.on('data',(data)=>{
                arr.push(data);
                md5.update(data);
            })
            rs.on('end',(data)=>{
                let r = md5.digest('base64');
                res.setHeader('Etag',r);
                if(req.headers['if-none-match']===r){
                    res.statusCode=304;
                    res.end();
                    return;
                }
                res.end(Buffer.concat(arr))
            })
        }
    } catch (error) {
        console.log(error)
        res.setHeader('Content-Type','text/html;charset=utf8')
        res.statusCode = 404;
        res.end(`未发现文件`)
    }
})

server.listen(3000)

通过控制台我们可以看出来 请求头和响应头中都有我们上面所说的对应的值,但是从代码里我们也能看出来,我们每次在请求到来的时候都会把文件全部读取出来并且进行加密生产hash然后再做对比,这样其实十分的消耗性能,因此这种缓存方式也有他自己的缺点

总结

我们通过node来亲自实现了三种缓存方式,我们可以总结出每种缓存方式对应的实现:

  • 强制缓存 服务端设置响应头Cache-Control:max-age=xxx,并且设置Expires响应头过期时间,客户端自行判断是否读取缓存
  • 协商缓存 通过状态码304告诉客户端该走缓存
    • 修改时间:通过文件的最后修改时间判断该不该读取缓存,服务端设置响应头Last-Modified,客户端把上次服务端响应头中的Last-modified值通过if-modified-since 传递给服务端 , 服务端通过比较当前文件的修改时间和上次修改时间(上次传给客户端的值),如果相等那么说明文件修改时间没变也就是没变化
    • 文件内容:通过文件的内容来判断该不该读取缓存,服务端通过把文件内容读取出来,通过md5进行base64加密得出hash值,把这个值设置响应头Etag,客户端下一次请求通过if-none-match带过来,服务端再比对当前文件内容加密得出的hash值和上次是否一样,如果一样说明文件内容没有发生改变,这种方式是最准确的方式,但是也是最耗性能