learn.lianglianglee.com/专栏/白话设计模式 28 讲(完)/19 访问模式:一千个读者一千个哈姆雷特.md.html
2022-05-11 19:04:14 +08:00

903 lines
28 KiB
HTML
Raw Permalink 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>19 访问模式:一千个读者一千个哈姆雷特.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="/专栏/白话设计模式 28 讲(完)/00 生活中的设计模式:启程之前,请不要错过我.md.html">00 生活中的设计模式:启程之前,请不要错过我.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/01 监听模式:坑爹的热水器.md.html">01 监听模式:坑爹的热水器.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/02 适配模式:身高不够鞋来凑.md.html">02 适配模式:身高不够鞋来凑.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/03 状态模式:人与水的三态.md.html">03 状态模式:人与水的三态.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/04 单例模式:你是我生命的唯一.md.html">04 单例模式:你是我生命的唯一.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/05 职责模式:我的假条去哪了.md.html">05 职责模式:我的假条去哪了.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/06 中介模式:找房子问中介.md.html">06 中介模式:找房子问中介.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/07 代理模式:帮我拿一下快递.md.html">07 代理模式:帮我拿一下快递.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/08 装饰模式:你想怎么穿就怎么穿.md.html">08 装饰模式:你想怎么穿就怎么穿.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/09 工厂模式:你要拿铁还是摩卡.md.html">09 工厂模式:你要拿铁还是摩卡.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/10 迭代模式:下一个就是你了.md.html">10 迭代模式:下一个就是你了.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/11 组合模式:自己组装电脑.md.html">11 组合模式:自己组装电脑.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/12 构建模式:想要车还是庄园.md.html">12 构建模式:想要车还是庄园.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/13 克隆模式:给你一个分身术.md.html">13 克隆模式:给你一个分身术.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/14 策略模式:怎么来不重要,人到就行.md.html">14 策略模式:怎么来不重要,人到就行.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/15 命令模式:大闸蟹,走起!.md.html">15 命令模式:大闸蟹,走起!.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/16 备忘模式:好记性不如烂笔头.md.html">16 备忘模式:好记性不如烂笔头.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/17 享元模式:颜料很贵必须充分利用.md.html">17 享元模式:颜料很贵必须充分利用.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/18 外观模式:学妹别慌,学长帮你.md.html">18 外观模式:学妹别慌,学长帮你.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/白话设计模式 28 讲(完)/19 访问模式:一千个读者一千个哈姆雷特.md.html">19 访问模式:一千个读者一千个哈姆雷特.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/20 生活中的设计模式:与经典设计模式的不解渊源.md.html">20 生活中的设计模式:与经典设计模式的不解渊源.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/21 生活中的设计模式:那些未完待续的设计模式.md.html">21 生活中的设计模式:那些未完待续的设计模式.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/22 深入解读过滤器模式:制作一杯鲜纯细腻的豆浆.md.html">22 深入解读过滤器模式:制作一杯鲜纯细腻的豆浆.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/23 深入解读对象池技术:共享让生活更便捷.md.html">23 深入解读对象池技术:共享让生活更便捷.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/24 深入解读回调机制:把你技能亮出来.md.html">24 深入解读回调机制:把你技能亮出来.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/25 谈谈我对设计模式的理解.md.html">25 谈谈我对设计模式的理解.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/26 谈谈我对设计原则的思考.md.html">26 谈谈我对设计原则的思考.md.html</a>
</li>
<li>
<a href="/专栏/白话设计模式 28 讲(完)/27 谈谈我对项目重构的看法.md.html">27 谈谈我对项目重构的看法.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>19 访问模式:一千个读者一千个哈姆雷特</h1>
<p>【故事剧情】</p>
<blockquote>
<p>光阴似箭,转眼间作为 IT 狗的 Tony 已在职场上混迹快五年了都说五年一个瓶颈Tony 能否跳出这个瓶颈他心里也没底但他总觉得该留下点什么了。Tony 喜欢写博客,经常把自己对行业的看法及对应用到的技术的总结写成文章分享出来,这一习惯从大二开始一直坚持了下来,目前已经写了不少原创文章了。</p>
<p>喜欢写作的人都有一个共同的梦想就是希望有一天能写出一本书。Tony 也一样,出一本畅销书是隐藏在他内心的一个梦想,时刻有一种声音在呼唤着他!这也是他能一直坚持写作的动力,正好在这五年的一个拐点,他该行动了!</p>
<p>Tony 真的动笔了,写起了他酝酿已久的一个主题《从生活的角度解读设计模式》,文章一经发表,便收到了很多读者的好评,同是技术圈的朋友评价:能抓住模式的核心思想、深入浅出,很有见地!做产品和设计的朋友评价:配图非常有趣,文章很有层次感!那些 IT 圈外的朋友则评价:技术的内容一脸懵圈,但故事很精彩,像是看小说或是故事集!真是<strong>一千个读者一千个哈姆雷特</strong>啊。</p>
</blockquote>
<p><img src="assets/2b6e3ca0-88d0-11e8-950d-3f19ed380d25.jpg" alt="enter image description here" /></p>
<h3>用程序来模拟生活</h3>
<p>Tony 的书是以完全一样的内容呈现给他们,但他的那些朋友却因为专业和工作性质的不同,看到了不同的内容和角度。我们用程序来模拟一下这个场景。</p>
<p>源码示例:</p>
<pre><code class="language-python">from abc import ABCMeta, abstractmethod
# 引入ABCMeta和abstractmethod来定义抽象类和抽象方法
class DesignPatternBook:
&quot;《从生活的角度解读设计模式》一书&quot;
def getName(self):
return &quot;《从生活的角度解读设计模式》&quot;
class Reader(metaclass=ABCMeta):
&quot;访问者,也就是读者&quot;
@abstractmethod
def read(self, book):
pass
class Engineer(Reader):
def read(self, book):
print(&quot;技术狗读&quot; + book.getName() + &quot;一书后的感受:能抓住模式的核心思想,深入浅出,很有见地!&quot;)
class ProductManager(Reader):
&quot;产品经理&quot;
def read(self, book):
print(&quot;产品经理读&quot; + book.getName() + &quot;一书后的感受:配图非常有趣,文章很有层次感!&quot;)
class OtherFriend(Reader):
&quot;IT圈外的朋友&quot;
def read(self, book):
print(&quot;IT圈外的朋友读&quot; + book.getName() + &quot;一书后的感受:技术的内容一脸蒙蔽,但故事很精彩,像是看小说或是故事集!&quot;)
</code></pre>
<p>测试代码:</p>
<pre><code class="language-python">def testBook():
book = DesignPatternBook()
fans = [Engineer(), ProductManager(), OtherFriend()];
for fan in fans:
fan.read(book)
</code></pre>
<p>输出结果:</p>
<pre><code>技术狗读《从生活的角度解读设计模式》一书后的感受:能抓住模式的核心思想,深入浅出,很有见地!
产品经理读《从生活的角度解读设计模式》一书后的感受:配图非常有趣,文章很有层次感!
IT 圈外的朋友读《从生活的角度解读设计模式》一书后的感受:技术的内容一脸蒙蔽,但故事很精彩,像是看小说或是故事集!
</code></pre>
<h3>从剧情中思考访问模式</h3>
<p>在上面的示例中,同样内容的一本书,不同类型的读者看到了不同的内容,读到了不同的味道。这里读者和书是两类事物,他们虽有联系,却是比较弱的联系,因此我我们将其分开处理,这种方式在程序中叫<strong>访问者模式</strong>,也可简称为访问模式。这里的读者就是访问者,书就是被访问的对象,阅读是访问的行为。</p>
<h4>访问模式</h4>
<blockquote>
<p>Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.</p>
<p>封装一些作用于某种数据结构中各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。</p>
</blockquote>
<p>访问模式的核心思想在于:可以在不改变数据结构的前提下定义作用于这些元素的新操作。将数据结构和操作(或算法)进行解耦,而且能更方便地拓展新的操作。</p>
<h3>访问模式的模型抽象</h3>
<h4>代码框架</h4>
<p>上面的示例代码还是相对比较粗糙,我们可以对它进行进一步的重构和优化,抽象出访问模式的框架模型。</p>
<pre><code class="language-python">from abc import ABCMeta, abstractmethod
# 引入ABCMeta和abstractmethod来定义抽象类和抽象方法
class DataNode(metaclass=ABCMeta):
&quot;数据结构类&quot;
def accept(self, visitor):
&quot;接受访问者的访问&quot;
visitor.visit(self)
class Visitor(metaclass=ABCMeta):
&quot;访问者&quot;
@abstractmethod
def visit(self, data):
&quot;对数据对象的访问操作&quot;
pass
class ObjectStructure:
&quot;数据结构的管理类,也是数据对象的一个容器,可遍历容器内的所有元素&quot;
def __init__(self):
self.__datas = []
def add(self, dataElement):
self.__datas.append(dataElement)
def action(self, visitor):
&quot;进行数据访问的操作&quot;
for data in self.__datas:
visitor.visit(data)
</code></pre>
<p>这里 Visitor 的访问方法只有一个 visit(),是因为 Python 不支持方法的重载。在一些强类型的语言(如 Java、C++)中,应该有多个方法,针对每一个 DataNode 子类定义一个重载方法。</p>
<h4>类图</h4>
<p>上面的代码框架是访问模式的关键类的实现,访问模式的类图表示如下:</p>
<p><img src="assets/baf83ae0-88d2-11e8-bee1-4fab1cb0896f.jpg" alt="enter image description here" /></p>
<p>DataNode 是数据结点可接受accept访问者的访问如上面示例中的 DesignPatternBookDataNodeA 和 DataNodeB 是它的具体实现类。Visitor 是访问者类可访问visit具体的对象如上面示例中的 Reader。ObjectStructure 是数据结构的管理类,也是数据对象的一个容器,可遍历容器内的所有元素。</p>
<h4>基于框架的实现</h4>
<p>有了上面的代码框架之后,我们要实现示例代码的功能就会更简单了。最开始的示例代码假设它为 version 1.0,那么再看看基于框架的 version 2.0 吧。</p>
<pre><code class="language-python">class DesignPatternBook:
&quot;《从生活的角度解读设计模式》一书&quot;
def getName(self):
return &quot;《从生活的角度解读设计模式》&quot;
class Engineer(Visitor):
def visit(self, book):
print(&quot;技术狗读&quot; + book.getName() + &quot;一书后的感受:能抓住模式的核心思想,深入浅出,很有见地!&quot;)
class ProductManager(Visitor):
&quot;产品经理&quot;
def visit(self, book):
print(&quot;产品经理读&quot; + book.getName() + &quot;一书后的感受:配图非常有趣,文章很有层次感!&quot;)
class OtherFriend(Visitor):
&quot;IT圈外的朋友&quot;
def visit(self, book):
print(&quot;IT圈外的朋友读&quot; + book.getName() + &quot;一书后的感受:技术的内容一脸蒙蔽,但故事很精彩,像是看小说或是故事集!&quot;)
</code></pre>
<p>测试代码也得相应改动一下:</p>
<pre><code class="language-python">def testVisitBook():
book = DesignPatternBook()
objMgr = ObjectStructure()
objMgr.add(book)
objMgr.action(Engineer())
objMgr.action(ProductManager())
objMgr.action(OtherFriend())
</code></pre>
<p>自己跑一下,会发现输出结果和之前的是一样的。</p>
<h3>模型说明</h3>
<h4>设计要点</h4>
<ul>
<li><strong>访问者Visitor</strong> 负责对数据结点进行访问和操作。</li>
<li><strong>数据结点DataNode</strong> 即要被操作的数据对象。</li>
<li><strong>对象结构ObjectStructure</strong> 数据结构的管理类,也是数据对象的一个容器,可遍历容器内的所有元素。</li>
</ul>
<h4>优缺点</h4>
<h5><strong>访问模式的优点</strong></h5>
<ul>
<li>将数据和操作(算法)分离、降低了耦合度。将有关元素对象的访问行为集中到一个访问者对象中,而不是分散在一个个的元素类中,类的职责更加清晰。</li>
<li>增加新的访问操作很方便。使用访问者模式,增加新的访问操作就意味着增加一个新的具体访问者类,实现简单,无须修改源代码,符合“开闭原则”。</li>
<li>让用户能够在不修改现有元素类层次结构的情况下,定义作用于该层次结构的操作。</li>
</ul>
<h5><strong>访问模式的缺点</strong></h5>
<ul>
<li>增加新的元素类很困难。在访问者模式中,每增加一个新的元素类都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”的要求。</li>
<li>破坏数据对象的封装性。访问者模式要求访问者对象访问并调用每一个元素对象的操作,这意味着元素对象有时候必须暴露一些自己的内部操作和内部状态,否则无法供访问者访问。</li>
</ul>
<h3>实战应用</h3>
<p>在宠物界中,猫和狗历来就是一对欢喜冤家!假如宠物店中有 N 只猫和 M 只狗我们要进行下面这3个操作</p>
<ul>
<li>在这些宠物中雌猫、雄猫、雌狗、雄狗的数量分别是多少。</li>
<li>猫的平均体重和狗的平均体重分别是多少。</li>
<li>年龄最大的猫和狗。</li>
</ul>
<p>这个时候,如果要在猫和狗的对象上添加这些操作,将会增加非常多的方法而污染原有的对象;而且这些操作的拓展性也将非常差,这时访问模式是解决这个问题的最好方法,具体的实现如下。</p>
<p><strong>源码示例:</strong></p>
<pre><code class="language-python">class Animal(DataNode):
&quot;动物类&quot;
def __init__(self, name, isMale, age, weight):
self.__name = name
self.__isMale = isMale
self.__age = age
self.__weight = weight
def getName(self):
return self.__name
def isMale(self):
return self.__isMale
def getAge(self):
return self.__age
def getWeight(self):
return self.__weight
class Cat(Animal):
&quot;&quot;
def __init__(self, name, isMale, age, weight):
super().__init__(name, isMale, age, weight)
def speak(self):
print(&quot;miao~&quot;)
class Dog(Animal):
&quot;&quot;
def __init__(self, name, isMale, age, weight):
super().__init__( name, isMale, age, weight)
def speak(self):
print(&quot;wang~&quot;)
class GenderCounter(Visitor):
&quot;性别统计&quot;
def __init__(self):
self.__maleCat = 0
self.__femaleCat = 0
self.__maleDog = 0
self.__femalDog = 0
def visit(self, data):
if isinstance(data, Cat):
if data.isMale():
self.__maleCat += 1
else:
self.__femaleCat += 1
elif isinstance(data, Dog):
if data.isMale():
self.__maleDog += 1
else:
self.__femalDog += 1
else:
print(&quot;Not support this type&quot;)
def getInfo(self):
print(str(self.__maleCat) + &quot;只雄猫,&quot; + str(self.__femaleCat) + &quot;只雌猫,&quot;
+ str(self.__maleDog) + &quot;只雄狗,&quot; + str(self.__femalDog) + &quot;只雌狗。&quot;)
class WeightCounter(Visitor):
def __init__(self):
self.__catNum = 0
self.__catWeight = 0
self.__dogNum = 0
self.__dogWeight = 0
def visit(self, data):
if isinstance(data, Cat):
self.__catNum +=1
self.__catWeight += data.getWeight()
elif isinstance(data, Dog):
self.__dogNum += 1
self.__dogWeight += data.getWeight()
else:
print(&quot;Not support this type&quot;)
def getInfo(self):
print(&quot;猫的平均体重是:%0.2fkg 狗的平均体重是:%0.2fkg&quot; %
((self.__catWeight / self.__catNum),(self.__dogWeight / self.__dogNum)))
class AgeCounter(Visitor):
def __init__(self):
self.__catMaxAge = 0
self.__dogMaxAge = 0
def visit(self, data):
if isinstance(data, Cat):
if self.__catMaxAge &lt; data.getAge():
self.__catMaxAge = data.getAge()
elif isinstance(data, Dog):
if self.__dogMaxAge &lt; data.getAge():
self.__dogMaxAge = data.getAge()
else:
print(&quot;Not support this type&quot;)
def getInfo(self):
print(&quot;猫的最大年龄是:&quot; + str(self.__catMaxAge) + &quot; 狗的最大年龄是:&quot; + str(self.__dogMaxAge))
</code></pre>
<p><strong>测试代码:</strong></p>
<pre><code class="language-python">def testAnimal():
animals = ObjectStructure()
animals.add(Cat(&quot;Cat1&quot;, True, 1, 5))
animals.add(Cat(&quot;Cat2&quot;, False, 0.5, 3))
animals.add(Cat(&quot;Cat3&quot;, False, 1.2, 4.2))
animals.add(Dog(&quot;Dog1&quot;, True, 0.5, 8))
animals.add(Dog(&quot;Dog2&quot;, True, 3, 52))
animals.add(Dog(&quot;Dog3&quot;, False, 1, 21))
animals.add(Dog(&quot;Dog4&quot;, False, 2, 25))
genderCounter = GenderCounter()
animals.action(genderCounter)
genderCounter.getInfo()
print()
weightCounter = WeightCounter()
animals.action(weightCounter)
weightCounter.getInfo()
print()
ageCounter = AgeCounter()
animals.action(ageCounter)
ageCounter.getInfo()
</code></pre>
<p><strong>输出结果:</strong></p>
<pre><code>1只雄猫2只雌猫2只雄狗2只雌狗。
猫的平均体重是4.07kg 狗的平均体重是26.50kg
猫的最大年龄是1.2 狗的最大年龄是3
</code></pre>
<p>使用访问模式后,代码结构是不是清爽了很多!</p>
<h3>应用场景</h3>
<ul>
<li>对象结构中包含的对象类比较少,而且这些类需要比较固定,很少改变,但经常需要在此对象结构上定义新的操作。</li>
<li>一个对象结构包含多个类型的对象,希望对这些对象实施一些依赖其具体类型的操作。在访问者中针对每一种具体的类型都提供了一个访问操作,不同类型的对象可以有不同的访问操作。</li>
<li>需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。访问者模式使得我们可以将相关的访问操作集中起来定义在访问者类中,对象结构可以被多个不同的访问者类所使用,将对象本身与对象的访问操作分离。</li>
</ul>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/白话设计模式 28 讲(完)/18 外观模式:学妹别慌,学长帮你.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/白话设计模式 28 讲(完)/20 生活中的设计模式:与经典设计模式的不解渊源.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":"70997ba5ad883cfa","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>