|
| 1 | +## 浏览器缓存策略之node实战 |
| 2 | +浏览器缓存是web开发中减少服务器压力相当常见也是相当有效的一种方式,那么本文会总结浏览器缓存的主要策略,并且最后会给出一个带缓存的node服务器代码实践。 |
| 3 | +### 浏览器缓存策略讲解 |
| 4 | +以下讲解全部基于服务器端支持缓存策略,若不支持,则每次都会想服务端发起请求,即不存在缓存 |
| 5 | +第一次访问某个网站的时候: |
| 6 | + |
| 7 | +浏览器会缓存这个响应的所有信息 |
| 8 | + |
| 9 | +之后再次访问这个网站的时候:会判断是否命中强缓存(根据expiress和cache-control),如果命中强缓存,会直接读取之前缓存过的资源,如果未命中强缓存,那么执行弱缓存策略,回头看一下我们之前缓存过的请求头中,Etag和Last-modified两个字段,我们的浏览器会将这两个缓存过的头放在请求头中,但是这个时候Etag变成了If-none-match,Last-modified变成了If-modified-since,服务器收到请求后会根据这两个字段判断请求的资源是否新鲜(即是否有改动),如果新鲜,返回304,如果不新鲜,则获取新资源,返回200. |
| 10 | + |
| 11 | +浏览器把缓存类型根据是否需要向服务器发起请求分为两种:**强缓存**和**弱缓存(协商缓存)** |
| 12 | + |
| 13 | + 缓存类型 |获取资源方式| 状态码| 是否发送请求到服务器| |
| 14 | +--------- | --------- | --------|---------| |
| 15 | +强缓存 |从缓存取 | 200(from disk cache) | 否,直接从缓存取| |
| 16 | +弱缓存 | 从缓存去取 |304(not modified)| 是,通过服务器来告诉浏览器缓存是否可用 | |
| 17 | +------------ |
| 18 | + |
| 19 | +### 强缓存详解 |
| 20 | +* expires,这是http1.0时的规范;它的值为一个绝对时间的GMT格式的时间字符串,如Mon, 10 Jun 2015 21:31:12 GMT,如果发送请求的时间在expires之前,那么本地缓存始终有效,否则就会发送请求到服务器来获取资源 |
| 21 | +* cache-control:max-age=number,这是http1.1时出现的header信息,主要是利用该字段的max-age值来进行判断,它是一个相对值;资源第一次的请求时间和Cache-Control设定的有效期,计算出一个资源过期时间,再拿这个过期时间跟当前的请求时间比较,如果请求时间在过期时间之前,就能命中缓存,否则就不行;cache-control除了该字段外,还有下面几个比较常用的设置值: |
| 22 | + * no-cache:不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。 |
| 23 | + * no-store:直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。 |
| 24 | + * public:可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。 |
| 25 | + * private:只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。 |
| 26 | + |
| 27 | + ps:如果cache-control与expires同时存在的话,cache-control的优先级高于expires |
| 28 | +### 弱缓存详解 |
| 29 | +弱缓存都是由服务器来确定缓存资源是否可用的,所以客户端与服务器端要通过某种标识来进行通信,从而让服务器判断请求资源是否可以缓存访问,这主要涉及到下面两组header字段,这两组搭档都是成对出现的,即第一次请求的响应头带上某个字段(Last-Modified或者Etag),则后续请求则会带上对应的请求字段(If-Modified-Since或者If-None-Match),若响应头没有Last-Modified或者Etag字段,则请求头也不会有对应的字段。 |
| 30 | +* Last-Modified/If-Modified-Since |
| 31 | + * 浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在respone的header加上Last-Modified的header,这个header表示这个资源在服务器上的最后修改时间 |
| 32 | + * 浏览器再次跟服务器请求这个资源时,在request的header上加上If-Modified-Since的header,这个header的值就是上一次请求时返回的Last-Modified的值 |
| 33 | + * 服务器再次收到资源请求时,根据浏览器传过来If-Modified-Since和资源在服务器上的最后修改时间判断资源是否有变化,如果没有变化则返回304 Not Modified,但是不会返回资源内容;如果有变化,就正常返回资源内容。当服务器返回304 Not Modified的响应时,response header中不会再添加Last-Modified的header,因为既然资源没有变化,那么Last-Modified也就不会改变,这是服务器返回304时的response header |
| 34 | + * 浏览器收到304的响应后,就会从缓存中加载资源 |
| 35 | + * 如果协商缓存没有命中,浏览器直接从服务器加载资源时,Last-Modified的Header在重新加载的时候会被更新,下次请求时,If-Modified-Since会启用上次返回的Last-Modified值 |
| 36 | +* Etag/If-None-Match:这两个值是由服务器生成的每个资源的唯一标识字符串,只要资源有变化就这个值就会改变;其判断过程与Last-Modified/If-Modified-Since类似,与Last-Modified不一样的是,当服务器返回304 Not Modified的响应时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化。 |
| 37 | +### Etag相对Last-modified的区别 |
| 38 | +* 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET; |
| 39 | +* 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能 检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒); |
| 40 | + |
| 41 | + ps:浏览器会优先验证Etag,在一致的情况下,才会继续判断Last-modified。 |
| 42 | +### 实战 |
| 43 | +浏览器缓存主要是需要服务端去做处理,那我们这里大致介绍一下服务端的请求头设置,以及弱缓存判断是否新鲜的方式: |
| 44 | +```javascript |
| 45 | +setFreshHeaders(stat, res) { |
| 46 | + //关于缓存可以在https://www.cnblogs.com/wonyun/p/5524617.html参考 |
| 47 | + //从stat里拿到信息 |
| 48 | + const lastModified = stat.mtime.toUTCString(); |
| 49 | + //强缓存 |
| 50 | + if (this.enableExpires) { |
| 51 | + const expireTime = (new Date(Date.now() + this.maxAge * 1000)).toUTCString(); |
| 52 | + res.setHeader('Expires', expireTime); |
| 53 | + } |
| 54 | + if (this.enableCacheControl) { |
| 55 | + res.setHeader('Cache-Control', `public, max-age=${this.maxAge}`); |
| 56 | + } |
| 57 | + //弱缓存 |
| 58 | + if (this.enableLastModified) { |
| 59 | + res.setHeader('Last-Modified', lastModified); |
| 60 | + } |
| 61 | + if (this.enableETag) { |
| 62 | + res.setHeader('ETag', this.generateETag(stat)); |
| 63 | + } |
| 64 | + } |
| 65 | +``` |
| 66 | +这里的stat是可以通过fs.stat()这个api取到资源文件的相关信息,res是传入的response对象。 |
| 67 | +关于Etag的组成方式,通过查询mdn得到了这样的答案 |
| 68 | + |
| 69 | +我们这里使用的是最后修改事件戳的哈希值,实际上用文件内容的哈希会更加准确(原因参见etag和last-modified区别第一条) |
| 70 | +```javascript |
| 71 | +//设置etag的一种简单方式 |
| 72 | + generateETag(stat) { |
| 73 | + const mtime = stat.mtime.getTime().toString(16); |
| 74 | + const size = stat.size.toString(16); |
| 75 | + return `W/"${size}-${mtime}"`; |
| 76 | + } |
| 77 | +``` |
| 78 | +那么当服务器受到带有if-modified-since和if-none-match的请求时,会去判断资源是否新鲜,判断代码如下: |
| 79 | +```javascript |
| 80 | +isFresh(reqHeaders, resHeaders) { |
| 81 | + const noneMatch = reqHeaders['if-none-match']; |
| 82 | + const lastModified = reqHeaders['if-modified-since']; |
| 83 | + if (!(noneMatch || lastModified)) return false; |
| 84 | + if(noneMatch && (noneMatch !== resHeaders['etag'])) return false; |
| 85 | + if(lastModified && lastModified !== resHeaders['last-modified']) return false; |
| 86 | + return true; |
| 87 | + } |
| 88 | +``` |
| 89 | +可以参考[完整代码](https://github.com/coderzzp/how2-learn-nodejs/tree/master/node-static-server),测试一下刚才所说过的强弱缓存的策略,同时也学习一下用node如何写一个带缓存的静态服务器 |
| 90 | + |
| 91 | + 我们在测试页面强缓存时不要用F5刷新,这样并不会命中强缓存,建议新开或者使用前后键来刷新页面 |
| 92 | + |
| 93 | + |
| 94 | + [参考问题](https://webmasters.stackexchange.com/questions/25342/headers-to-prevent-304-if-modified-since-head-requests) |
| 95 | + |
| 96 | + |
0 commit comments