learn.lianglianglee.com/专栏/透视HTTP协议/20 生鲜速递:HTTP的缓存控制.md.html
2022-05-11 18:57:05 +08:00

1031 lines
29 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>20 生鲜速递HTTP的缓存控制.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/透视HTTP协议/00 开篇词To Be a HTTP Hero.md.html">00 开篇词To Be a HTTP Hero.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/01 时势与英雄HTTP的前世今生.md.html">01 时势与英雄HTTP的前世今生.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/02 HTTP是什么HTTP又不是什么.md.html">02 HTTP是什么HTTP又不是什么.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/03 HTTP世界全览与HTTP相关的各种概念.md.html">03 HTTP世界全览与HTTP相关的各种概念.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/04 HTTP世界全览与HTTP相关的各种协议.md.html">04 HTTP世界全览与HTTP相关的各种协议.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/05 常说的“四层”和“七层”到底是什么?“五层”“六层”哪去了?.md.html">05 常说的“四层”和“七层”到底是什么?“五层”“六层”哪去了?.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/06 域名里有哪些门道?.md.html">06 域名里有哪些门道?.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/07 自己动手搭建HTTP实验环境.md.html">07 自己动手搭建HTTP实验环境.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/08 键入网址再按下回车,后面究竟发生了什么?.md.html">08 键入网址再按下回车,后面究竟发生了什么?.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/09 HTTP报文是什么样子的.md.html">09 HTTP报文是什么样子的.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/10 应该如何理解请求方法?.md.html">10 应该如何理解请求方法?.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/11 你能写出正确的网址吗?.md.html">11 你能写出正确的网址吗?.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/12 响应状态码该怎么用?.md.html">12 响应状态码该怎么用?.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/13 HTTP有哪些特点.md.html">13 HTTP有哪些特点.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/14 HTTP有哪些优点又有哪些缺点.md.html">14 HTTP有哪些优点又有哪些缺点.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/15 海纳百川HTTP的实体数据.md.html">15 海纳百川HTTP的实体数据.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/16 把大象装进冰箱HTTP传输大文件的方法.md.html">16 把大象装进冰箱HTTP传输大文件的方法.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/17 排队也要讲效率HTTP的连接管理.md.html">17 排队也要讲效率HTTP的连接管理.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/18 四通八达HTTP的重定向和跳转.md.html">18 四通八达HTTP的重定向和跳转.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/19 让我知道你是谁HTTP的Cookie机制.md.html">19 让我知道你是谁HTTP的Cookie机制.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/透视HTTP协议/20 生鲜速递HTTP的缓存控制.md.html">20 生鲜速递HTTP的缓存控制.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/21 良心中间商HTTP的代理服务.md.html">21 良心中间商HTTP的代理服务.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/22 冷链周转HTTP的缓存代理.md.html">22 冷链周转HTTP的缓存代理.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/23 HTTPS是什么SSLTLS又是什么.md.html">23 HTTPS是什么SSLTLS又是什么.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/24 固若金汤的根本(上):对称加密与非对称加密.md.html">24 固若金汤的根本(上):对称加密与非对称加密.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/25 固若金汤的根本(下):数字签名与证书.md.html">25 固若金汤的根本(下):数字签名与证书.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/26 信任始于握手TLS1.2连接过程解析.md.html">26 信任始于握手TLS1.2连接过程解析.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/27 更好更快的握手TLS1.3特性解析.md.html">27 更好更快的握手TLS1.3特性解析.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/28 连接太慢该怎么办HTTPS的优化.md.html">28 连接太慢该怎么办HTTPS的优化.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/29 我应该迁移到HTTPS吗.md.html">29 我应该迁移到HTTPS吗.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/30 时代之风HTTP2特性概览.md.html">30 时代之风HTTP2特性概览.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/31 时代之风HTTP2内核剖析.md.html">31 时代之风HTTP2内核剖析.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/32 未来之路HTTP3展望.md.html">32 未来之路HTTP3展望.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/33 我应该迁移到HTTP2吗.md.html">33 我应该迁移到HTTP2吗.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/34 Nginx高性能的Web服务器.md.html">34 Nginx高性能的Web服务器.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/35 OpenResty更灵活的Web服务器.md.html">35 OpenResty更灵活的Web服务器.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/36 WAF保护我们的网络服务.md.html">36 WAF保护我们的网络服务.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/37 CDN加速我们的网络服务.md.html">37 CDN加速我们的网络服务.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/38 WebSocket沙盒里的TCP.md.html">38 WebSocket沙盒里的TCP.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/39 HTTP性能优化面面观.md.html">39 HTTP性能优化面面观.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/40 HTTP性能优化面面观.md.html">40 HTTP性能优化面面观.md.html</a>
</li>
<li>
<a href="/专栏/透视HTTP协议/结束语 做兴趣使然的Hero.md.html">结束语 做兴趣使然的Hero.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>20 生鲜速递HTTP的缓存控制</h1>
<p>缓存Cache是计算机领域里的一个重要概念是优化系统性能的利器。</p>
<p>由于链路漫长,网络时延不可控,浏览器使用 HTTP 获取资源的成本较高。所以,非常有必要把“来之不易”的数据缓存起来,下次再请求的时候尽可能地复用。这样,就可以避免多次请求 - 应答的通信成本,节约网络带宽,也可以加快响应速度。</p>
<p>试想一下,如果有几十 K 甚至几十 M 的数据,不是从网络而是从本地磁盘获取,那将是多么大的一笔节省,免去多少等待的时间。</p>
<p>实际上HTTP 传输的每一个环节基本上都会有缓存,非常复杂。</p>
<p>基于“请求 - 应答”模式的特点,可以大致分为客户端缓存和服务器端缓存,因为服务器端缓存经常与代理服务“混搭”在一起,所以今天我先讲客户端——也就是浏览器的缓存。</p>
<h2>服务器的缓存控制</h2>
<p>为了更好地说明缓存的运行机制,下面我用“生鲜速递”作为比喻,看看缓存是如何工作的。</p>
<p>夏天到了,天气很热。你想吃西瓜消暑,于是打开冰箱,但很不巧,冰箱是空的。不过没事,现在物流很发达,给生鲜超市打个电话,不一会儿,就给你送来一个 8 斤的沙瓤大西瓜,上面还贴着标签:“保鲜期 5 天”。好了,你把它放进冰箱,想吃的时候随时拿出来。</p>
<p>在这个场景里,“生鲜超市”就是 Web 服务器,“你”就是浏览器,“冰箱”就是浏览器内部的缓存。整个流程翻译成 HTTP 就是:</p>
<ol>
<li>浏览器发现缓存无数据,于是发送请求,向服务器获取资源;</li>
<li>服务器响应请求,返回资源,同时标记资源的有效期;</li>
<li>浏览器缓存资源,等待下次重用。</li>
</ol>
<p><img src="assets/a1968821f214df4a3ae16c9b30f99a5b.png" alt="img" /></p>
<p>你可以访问实验环境的 URI “/20-1”看看具体的请求 - 应答过程。</p>
<p><img src="assets/dfd2d20670443a782443fc3193ae1cd8.png" alt="img" /></p>
<p>服务器标记资源有效期使用的头字段是“<strong>Cache-Control</strong>”,里面的值“<strong>max-age=30</strong>”就是资源的有效时间,相当于告诉浏览器,“这个页面只能缓存 30 秒,之后就算是过期,不能用。”</p>
<p>你可能要问了,让浏览器直接缓存数据就好了,为什么要加个有效期呢?</p>
<p>这是因为网络上的数据随时都在变化,不能保证它稍后的一段时间还是原来的样子。就像生鲜超市给你快递的西瓜,只有 5 天的保鲜期,过了这个期限最好还是别吃,不然可能会闹肚子。</p>
<p>“Cache-Control”字段里的“max-age”和上一讲里 Cookie 有点像,都是标记资源的有效期。</p>
<p>但我必须提醒你注意,这里的 max-age 是“<strong>生存时间</strong>”(又叫“新鲜度”“缓存寿命”,类似 TTLTime-To-Live时间的计算起点是响应报文的创建时刻即 Date 字段,也就是离开服务器的时刻),而不是客户端收到报文的时刻,也就是说包含了在链路传输过程中所有节点所停留的时间。</p>
<p>比如服务器设定“max-age=5”但因为网络质量很糟糕等浏览器收到响应报文已经过去了 4 秒,那么这个资源在客户端就最多能够再存 1 秒钟,之后就会失效。</p>
<p>“max-age”是 HTTP 缓存控制最常用的属性,此外在响应报文里还可以用其他的属性来更精确地指示浏览器应该如何使用缓存:</p>
<ul>
<li>no_store<strong>不允许缓存</strong>,用于某些变化非常频繁的数据,例如秒杀页面;</li>
<li>no_cache它的字面含义容易与 no_store 搞混,实际的意思并不是不允许缓存,而是<strong>可以缓存</strong>,但在使用之前必须要去服务器验证是否过期,是否有最新的版本;</li>
<li>must-revalidate又是一个和 no_cache 相似的词,它的意思是如果缓存不过期就可以继续使用,但过期了如果还想用就必须去服务器验证。</li>
</ul>
<p>听的有点糊涂吧。没关系,我拿生鲜速递来举例说明一下:</p>
<ul>
<li>no_store买来的西瓜不允许放进冰箱要么立刻吃要么立刻扔掉</li>
<li>no_cache可以放进冰箱但吃之前必须问超市有没有更新鲜的有就吃超市里的</li>
<li>must-revalidate可以放进冰箱保鲜期内可以吃过期了就要问超市让不让吃。</li>
</ul>
<p>你看,这超市管的还真多啊,西瓜到了家里怎么吃还得听他。不过没办法,在 HTTP 协议里服务器就是这样的“霸气”。</p>
<p>我把服务器的缓存控制策略画了一个流程图对照着它你就可以在今后的后台开发里明确“Cache-Control”的用法了。</p>
<p><img src="assets/8a67535620ab9c7764560363f83982b2.png" alt="img" /></p>
<h2>客户端的缓存控制</h2>
<p>现在冰箱里已经有了“缓存”的西瓜,是不是就可以直接开吃了呢?</p>
<p>你可以在 Chrome 里点几次“刷新”按钮,估计你会失望,页面上的 ID 一直在变,根本不是缓存的结果,明明说缓存 30 秒,怎么就不起作用呢?</p>
<p>其实不止服务器可以发“Cache-Control”头浏览器也可以发“Cache-Control”也就是说请求 - 应答的双方都可以用这个字段进行缓存控制,互相协商缓存的使用策略。</p>
<p>当你点“刷新”按钮的时候,浏览器会在请求头里加一个“<strong>Cache-Control: max-age=0</strong>”。因为 max-age 是“<strong>生存时间</strong>max-age=0 的意思就是“我要一个最最新鲜的西瓜”,而本地缓存里的数据至少保存了几秒钟,所以浏览器就不会使用缓存,而是向服务器发请求。服务器看到 max-age=0也就会用一个最新生成的报文回应浏览器。</p>
<p>Ctrl+F5 的“强制刷新”又是什么样的呢?</p>
<p>它其实是发了一个“<strong>Cache-Control: no-cache</strong>含义和“max-age=0”基本一样就看后台的服务器怎么理解通常两者的效果是相同的。</p>
<p><img src="assets/2fc3fa639f44b98d7c19d25604c65249.png" alt="img" /></p>
<p>那么,浏览器的缓存究竟什么时候才能生效呢?</p>
<p>别着急试着点一下浏览器的“前进”“后退”按钮再看开发者工具你就会惊喜地发现“from disk cache”的字样意思是没有发送网络请求而是读取的磁盘上的缓存。</p>
<p>另外,如果用[第 18 讲]里的重定向跳转功能,也可以发现浏览器使用了缓存:</p>
<pre><code>http://www.chrono.com/18-1?dst=20-1
</code></pre>
<p><img src="assets/f2a12669e997ea6dc0f2228bcaf65a06.png" alt="img" /></p>
<p>这几个操作与刷新有什么区别呢?</p>
<p>其实也很简单在“前进”“后退”“跳转”这些重定向动作中浏览器不会“夹带私货”只用最基本的请求头没有“Cache-Control”所以就会检查缓存直接利用之前的资源不再进行网络通信。</p>
<p>这个过程你也可以用 Wireshark 抓包,看看是否真的没有向服务器发请求。</p>
<h2>条件请求</h2>
<p>浏览器用“Cache-Control”做缓存控制只能是刷新数据不能很好地利用缓存数据又因为缓存会失效使用前还必须要去服务器验证是否是最新版。</p>
<p>那么该怎么做呢?</p>
<p>浏览器可以用两个连续的请求组成“验证动作”:先是一个 HEAD获取资源的修改时间等元信息然后与缓存数据比较如果没有改动就使用缓存节省网络流量否则就再发一个 GET 请求,获取最新的版本。</p>
<p>但这样的两个请求网络成本太高了,所以 HTTP 协议就定义了一系列“<strong>If</strong>”开头的“<strong>条件请求</strong>”字段,专门用来检查验证资源是否过期,把两个请求才能完成的工作合并在一个请求里做。而且,验证的责任也交给服务器,浏览器只需“坐享其成”。</p>
<p>条件请求一共有 5 个头字段,我们最常用的是“<strong>if-Modified-Since</strong>”和“<strong>If-None-Match</strong>”这两个。需要第一次的响应报文预先提供“<strong>Last-modified</strong>”和“<strong>ETag</strong>”,然后第二次请求时就可以带上缓存里的原值,验证资源是否是最新的。</p>
<p>如果资源没有变,服务器就回应一个“<strong>304 Not Modified</strong>”,表示缓存依然有效,浏览器就可以更新一下有效期,然后放心大胆地使用缓存了。</p>
<p><img src="assets/b239d0804be630ce182e24ea9e4ab237.png" alt="img" /></p>
<p>“Last-modified”很好理解就是文件的最后修改时间。ETag 是什么呢?</p>
<p>ETag 是“实体标签”Entity Tag的缩写<strong>是资源的一个唯一标识</strong>,主要是用来解决修改时间无法准确区分文件变化的问题。</p>
<p>比如,一个文件在一秒内修改了多次,但因为修改时间是秒级,所以这一秒内的新版本无法区分。</p>
<p>再比如,一个文件定期更新,但有时会是同样的内容,实际上没有变化,用修改时间就会误以为发生了变化,传送给浏览器就会浪费带宽。</p>
<p>使用 ETag 就可以精确地识别资源的变动情况,让浏览器能够更有效地利用缓存。</p>
<p>ETag 还有“强”“弱”之分。</p>
<p>强 ETag 要求资源在字节级别必须完全相符,弱 ETag 在值前有个“W/”标记,只要求资源在语义上没有变化,但内部可能会有部分发生了改变(例如 HTML 里的标签顺序调整,或者多了几个空格)。</p>
<p>还是拿生鲜速递做比喻最容易理解:</p>
<p>你打电话给超市,“我这个西瓜是 3 天前买的,还有最新的吗?”。超市看了一下库存,说:“没有啊,我这里都是 3 天前的。”于是你就知道了,再让超市送货也没用,还是吃冰箱里的西瓜吧。这就是“<strong>if-Modified-Since</strong>”和“<strong>Last-modified</strong>”。</p>
<p>但你还是想要最新的就又打电话“有不是沙瓤的西瓜吗超市告诉你都是沙瓤的Match于是你还是只能吃冰箱里的沙瓤西瓜。这就是“<strong>If-None-Match</strong>”和“<strong>弱 ETag</strong>”。</p>
<p>第三次打电话,你说“有不是 8 斤的沙瓤西瓜吗?”,这回超市给了你满意的答复:“有个 10 斤的沙瓤西瓜”。于是,你就扔掉了冰箱里的存货,让超市重新送了一个新的大西瓜。这就是“<strong>If-None-Match</strong>”和“<strong>强 ETag</strong>”。</p>
<p>再来看看实验环境的 URI “/20-2”。它为资源增加了 ETag 字段刷新页面时浏览器就会同时发送缓存控制头“max-age=0”和条件请求头“If-None-Match”如果缓存有效服务器就会返回 304</p>
<p><img src="assets/30965c97bb7433eabe10008fefaeb5f9.png" alt="img" /></p>
<p>条件请求里其他的三个头字段是“If-Unmodified-Since”“If-Match”和“If-Range”其实只要你掌握了“if-Modified-Since”和“If-None-Match”可以轻易地“举一反三”。</p>
<h2>小结</h2>
<p>今天我们学习了 HTTP 的缓存控制和条件请求,用好它们可以减少响应时间、节约网络流量,一起小结一下今天的内容吧:</p>
<ol>
<li>缓存是优化系统性能的重要手段HTTP 传输的每一个环节中都可以有缓存;</li>
<li>服务器使用“Cache-Control”设置缓存策略常用的是“max-age”表示资源的有效期</li>
<li>浏览器收到数据就会存入缓存,如果没过期就可以直接使用,过期就要去服务器验证是否仍然可用;</li>
<li>验证资源是否失效需要使用“条件请求”常用的是“if-Modified-Since”和“If-None-Match”收到 304 就可以复用缓存里的资源;</li>
<li>验证资源是否被修改的条件有两个“Last-modified”和“ETag”需要服务器预先在响应报文里设置搭配条件请求使用</li>
<li>浏览器也可以发送“Cache-Control”字段使用“max-age=0”或“no_cache”刷新数据。</li>
</ol>
<p>HTTP 缓存看上去很复杂,但基本原理说白了就是一句话:“没有消息就是好消息”,“没有请求的请求,才是最快的请求。”</p>
<h2>课下作业</h2>
<ol>
<li>Cache 和 Cookie 都是服务器发给客户端并存储的数据,你能比较一下两者的异同吗?</li>
<li>即使有“Last-modified”和“ETag”强制刷新Ctrl+F5也能够从服务器获取最新数据返回 200 而不是 304请你在实验环境里试一下观察请求头和响应头解释原因。</li>
</ol>
<p>欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。</p>
<p><img src="assets/1348aa2c81bd5d65ace3aa068b21044b.png" alt="unpreview" /></p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/透视HTTP协议/19 让我知道你是谁HTTP的Cookie机制.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/透视HTTP协议/21 良心中间商HTTP的代理服务.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997d0b28b03cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>