关于node.js:用代码实践Web缓存

34次阅读

共计 4461 个字符,预计需要花费 12 分钟才能阅读完成。

Web 缓存是能够主动保留常见文档正本的 HTTP 设施。当 Web 申请到达缓存时,如果本地有“已缓存的正本”,就能够从本地存储设备而不是原始服务器中提取这个文档。

下面是《HTTP 权威指南》中对 Web 缓存的定义,缓存的益处次要有以下几点:

  1. 缩小了冗余数据的传输;
  2. 缩小了客户端的网络申请,也升高了原始服务器的压力;
  3. 升高了时延,页面加载更快。

总结一下就是省流量,省带宽,还贼快。那么缓存是如何工作的呢?客户端和服务端是如何协调缓存的时效性的呢?上面咱们用代码来一步一步揭晓缓存的工作原理。

一、浏览器缓存

当咱们在浏览器地址栏敲入 localhost:8080/test.txt 并回车时,咱们是向指定的服务端发动对 text.txt 文件的申请,

服务端在接管到这个申请之后,找到了这个文件并筹备返回给客户端,并通过设置 Cache-ControlExpires两个 response header 通知客户端这个文件要缓存下来,在过期之前别跟我要了。

首先咱们看一下我的项目目录:

|-- Cache
    |-- index.js
    |-- assets
        |-- index.html
        |-- test.txt

具体实现代码如下:

<!-- index.html -->
...
<a href="./test.txt">test.txt</a>
...
// index.js
const http = require('http');
const path = require('path');
const fs = require('fs');

http.createServer((req, res) => {const requestUrl = path.join(__dirname, '/assets', path.normalize(req.url));
    fs.stat(requestUrl, (err, stats) => {if (err || !stats.isFile) {res.writeHead(404, 'Not Found');
            res.end();} else {const readStream = fs.createReadStream(requestUrl);
            const maxAge = 10;
            const expireDate = new Date(new Date().getTime() + maxAge * 1000).toUTCString();
            res.setHeader('Cache-Control', `max-age=${maxAge}, public`);
            res.setHeader('Expires', expireDate);
            readStream.pipe(res);
        }
    });
}).listen(8080);

Cache-ControlExpires这个两个 response header 又代表什么意思呢?Cache-Control:max-age=500示意设置缓存存储的最大周期为 500 秒,超过这个工夫缓存被认为过期。Expires:Tue, 23 Feb 2021 01:23:48 GMT示意在 Tue, 23 Feb 2021 01:23:48 GMT 这个日期之后文档过期。

启动 server 后,在浏览器拜访localhost:8080/index.html,这时是第一次拜访,没有缓存,所以服务器返回残缺的资源。

咱们点击超链接拜访test.txt

因为是第一次拜访,所以没有缓存,这个时候咱们点击返回按钮回到index.html

发现不同了吗?这个时候 NetWork 中 Size 曾经变成了disk cache,阐明命中了浏览器缓存,也就是强缓存,这个时候再点击超链接拜访test.txt,如果在设置的过期工夫 10s 以内,就能看到命中浏览器缓存,如果超过 10s,就会从新从服务器获取资源。

这里阐明一点,浏览器的后退后退按钮会始终从缓存中读取资源,而疏忽设置的缓存规定。也就是说方才如果我从 localhost:8080/test.txt 页面通过浏览器返回按钮回到 localhost:8080/index.html 页面,会发现不论过多久 Network 都是 disk cache,同样再点击浏览器后退按钮进入localhost:8080/test.txt 页面,哪怕超过设置的过期工夫也还是 from disk cache。

留神 Cache-Control 的优先级大于 Expires,因为时差起因还有服务端工夫和客户端工夫可能不统一会导致Expires 判断缓存有效性不精确。然而 Expires 兼容 http1.0,Cache-Control兼容到 http1.1,所以个别还是两个都设置。

二、协商缓存

下面咱们设置过缓存时限后,如果缓存过期了怎么办呢?你可能会说,过期了就从新从服务端获取资源啊。然而也有可能缓存工夫过期了,然而资源并没有变动,所以咱们还要引入其余的策略来解决这种状况,那就是协商缓存也就是弱缓存。

咱们梳理一下协商缓存的流程:

当服务端第一次返回资源时,除了设置 Cache-ControlExpires响应头之外,还会设置 Last-Modified(资源更新工夫)和ETag(资源摘要或资源版本)两个响应头,别离代表资源的最近一次变更工夫和实体标签。当客户端没有命中强缓存时,会从新像服务端发动申请,并携带If-modified-SinceIf-None-Match两个申请头,服务端拿到这两个申请头会跟之前设置的 Last-ModifiedETag作比拟,如果不匹配,阐明缓存不可用,从新返回资源,反之阐明缓存无效,返回 304 响应码,告知缓存能够持续应用,并更新缓存无效工夫。

上面咱们看一下具体代码实现:

const http = require('http');
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');

// 生成 entity digest
function generateDigest(requestUrl) {
    let hash = '2jmj7l5rSw0yVb/vlWAYkK/YBwk';
    let len = 0;
    fs.readFile(requestUrl, (err, data) => {if (err) {console.error(error);
            throw new Error(err);
        } else {len = Buffer.byteLength(data, 'utf8');
            hash = crypto
                .createHash('sha1')
                .update(data, 'utf-8')
                .digest('base64')
                .substring(0, 27);
        }
    });
    return '"'+ len.toString(16) +'-'+ hash +'"';
}

// 响应文件
function responseFile(requestUrl, stats, res) {const readStream = fs.createReadStream(requestUrl);
    const maxAge = 10;
    const expireDate = new Date(new Date().getTime() + maxAge * 1000).toUTCString();
    res.setHeader('Cache-Control', `max-age=${maxAge}, public`);
    res.setHeader('Expires', expireDate);
    res.setHeader('Last-Modified', stats.mtime);
    res.setHeader('ETag', generateDigest(requestUrl));
    readStream.pipe(res);
}

// 判断新鲜度
function isFresh(requestUrl, stats, req) {const ifModifiedSince = req.headers['if-modified-since'];
    const ifNoneMatch = req.headers['if-none-match'];

    if (!ifModifiedSince && !ifNoneMatch) {
        // 如果没有相应的申请头,应该返回全新的资源
        return false;
    } else if (ifNoneMatch && ifNoneMatch !== generateDigest(requestUrl)) {
        // 如果 ETag 不匹配(资源内容产生扭转),示意缓存不陈腐
        return false;
    } else if (ifModifiedSince && ifModifiedSince !== stats.mtime.toString()) {
        // 如果资源更新工夫不匹配,示意缓存不陈腐
        return false;
    }
    return true;
}

http.createServer((req, res) => {const requestUrl = path.join(__dirname, '/assets', path.normalize(req.url));

    fs.stat(requestUrl, (err, stats) => {if (err || !stats.isFile) {res.writeHead(404, 'Not Found');
            res.end();} else {if (isFresh(requestUrl, stats, req)) {
                // 缓存陈腐,告知客户端没有缓存可用,不返回响应实体
                res.writeHead(304, 'Not Modified');
                res.end();} else {
                // 缓存不陈腐,从新返回资源
                responseFile(requestUrl, stats, res);
            }
        }
    });
}).listen(8080);

从代码中能够看到 ETagLast-Modified都是用于协商缓存的校验的,ETag基于实体标签,个别能够通过版本号,或者资源摘要来指定;Last-Modified则是基于资源的最初批改工夫。

这时拜访 localhost:8080/test.txt 文件,当命中强缓存后,期待 10s 钟,再次拜访,服务器返回304,而非200,表明协商缓存失效。

此时批改 test.txt 文件,再次拜访,服务器返回 200,页面展现最新的test.txt 文件内容。

总结一下:

  1. ETag能更准确地判断资源到底有没有变动,且优先级高于Last-Modified
  2. 基于摘要实现的 ETag 绝对较慢,更占资源;
  3. Last-Modified准确到秒,对亚秒级的资源更新的缓存新鲜度判断无能为力;
  4. ETag兼容到 http1.1Last-Modified 兼容到http1.0

留神:本文中通过超链接拜访 test.txt 是因为,如果间接在地址栏拜访该资源,浏览器会在 request headers 中设置cache-control:max-age=0,这样永远不会命中浏览器缓存。

本文测试浏览器:Chrome 版本 88.0.4324.192

参考:

  1. 《HTTP 权威指南》
  2. HTTP 缓存
  3. etag

正文完
 0