前面讲了创建一个对象实例的方法单例模式Singleton Pattern, 创造多个产品的工厂模式(简单工厂模式 Simple Factory Pattern, 工厂方法模式 FactoryMothed Pattern抽象工厂模式 Abstract Factory Method),以及创建复杂对象的建造者模式 Builder Pattern, 这几几乎包含了产品创建的各个方方面,但是还有一种,那就是有自我创建能力的模式,这种模式能够创建出和自己相同或者相似的对象。生活中经常也会见到这方面的例子,比如蠕虫病毒的自我复制,细胞分裂以及自我繁殖,游戏角色的自我复制和分身等。

在软件开发中也会经常遇到这样的问题,最近在做项目的时候就碰到了这么一个需求,问卷调查试卷复用的问题。我们的系统用户组成是这样的,系统有一个超级管理员的角色,超级管理员可以干任何事情,系统中还接入了N多公司,每个公司有公司的管理员,公司管理员可以干超级管理员分配给该公司的相应权限, 那么这个问卷调查的需求是这样的:

1. 超级管理员可以创建问卷调查试卷,但是这个问卷调查的试卷不能直接使用,只能供各公司的管理员作为模板样例创建自己公司的问卷调查试卷。

2.各个公司的管理员可以创建自己公司的问卷调查试卷,仅供自己公司员工使用。

3.各个公司的管理员可以可以基于超级管理员创建的问卷调查试卷创建自己的模板,创建出来的问卷调查试卷仅供自己公司的员工使用。

4.公司管理员不能修改超级管理员创建的调查问卷试卷。

5. 超级管理员可以修改自己的问卷调查试卷, 并且不会影响各个公司之前根据问卷调查试卷创建出来的试卷。

需求用文字描述出来有点费劲,看下面这张图:

该怎么来实现呢? 这就是本文要讨论的主角原型模式Pototype Pattern ,也是最后一个创建型模式,原型模式就是为解决这类问题而生的:)。

一、原型模式的定义

原型模式(Prototype  Pattern):使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。原型模式是一种对象创建型模式

二、原型模式的结构

1、Prototype(抽象原型类):

它是声明克隆方法的接口,是所有具体原型类的公共父类,可以是抽象类也可以是接口,甚至还可以是具体实现类。

2、ConcretePrototype(具体原型类):

它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。

3、Client(客户类):

让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。由于客户类针对抽象原型类Prototype编程,因此用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或更换具体原型类都很方便。

三、原型模式的经典实现

原型模式的核心是克隆方法的实现:

1、通用实现方法

通用的克隆实现方法是在具体原型类的克隆方法中实例化一个与自身类型相同的对象并将其返回,并将相关的参数传入新创建的对象中,保证它们的成员属性相同:

public abstract class Pototype
{
public string Some { get; set; }
public abstract Pototype Clone();
} public class ConcretePototype : Pototype
{
public override Pototype Clone()
{
Pototype pototype = new ConcretePototype();
pototype.Some = this.Some; return pototype;
}
}

客户端调用:

static void Main(string[] args)
{
Structure.Pototype pototype = new ConcretePototype();
pototype.Some = "pototype";
Structure.Pototype clone = pototype.Clone();
Console.WriteLine("I'm old "+pototype.Some);
Console.WriteLine("I'm clone "+clone.Some); Console.ReadKey(); }

输出结果:

2、C#实现

C# 中有一个实现拷贝的方法MemberwiseClone,在克隆方法中我们用MemberwiseClone来实现克隆一个对象:

public class CsharpPototype : Pototype
{
public override Pototype Clone()
{
return this.MemberwiseClone() as Pototype;
}
}

客户端调用:

static void Main(string[] args)
{
Structure.Pototype pototype = new CsharpPototype();
pototype.Some = "pototype";
Structure.Pototype clone = pototype.Clone();
Console.WriteLine("I'm old "+pototype.Some);
Console.WriteLine("I'm clone "+clone.Some); Console.ReadKey();
}

客户端输出和上面一样, 这种方式实现的是浅拷贝。

深拷贝和浅拷贝, 浅拷贝如果原型对象的成员是值类型那么就将原型对象复制一份给克隆对象,如果是引用类型,只将原型对象的地址复制一份给克隆对象,这时克隆对象和原型对象在内存中指向同一个对象,修改其中的一个会影响另一个。深拷贝,则是将原型对象拷贝一份给克隆对象,克隆对象和原型对象在内存中有独立的存储空间,一个改动了不会影响另一个。

四、原型模式实例

现在我们弄明白了原型模式,现在就来实现开头提出的问卷调查试卷创建的需求,因为试卷的内容比较复杂这里我们是找出一些核心的能够说明问题的模型来演示这个例子。我们先将试卷的内容假定为字符串类型。

这里我们将超级管理员创建的试卷命名为SuperSurveyPaper。

这里SuperSurveyPaper 充当抽象原型类,CompanyASurveyPaper 和CompanyBSurveyPaper 充当具体原型类。

1、浅拷贝实现:

public abstract class SuperSurveyPaper
{
public string Name { get; set; }
public string Content { get; set; }
public abstract SuperSurveyPaper Clone();
}
public class CompanyASurveyPaper : SuperSurveyPaper
{
public override SuperSurveyPaper Clone()
{
return this.MemberwiseClone() as SuperSurveyPaper;
}
} public class CompanyBSurveyPaper : SuperSurveyPaper
{
public override SuperSurveyPaper Clone()
{
return this.MemberwiseClone() as SuperSurveyPaper;
}
}

客户端调用代码:

static void Main(string[] args)
{ PototypeInstance.SuperSurveyPaper pototype = new CompanyASurveyPaper();
pototype.Name = "SuperSurveyPaper ->Name";
pototype.Content = "SuperSurveyPaper -> Content";
PototypeInstance.SuperSurveyPaper clone = pototype.Clone();
Console.WriteLine("I'm old Name: " + pototype.Name);
Console.WriteLine("I'm old Content: " + pototype.Content);
Console.WriteLine("======================== ===========");
Console.WriteLine("I'm Clone Name: " + clone.Name);
Console.WriteLine("I'm Clone Content: " + clone.Content); Console.ReadKey();
}

输出结果:

这里也可以加入配置通过反射来创建原型对象

在app.config 中加入配置:

<appSettings>
<add key="Pototype" value="DesignPattern.Pototype.PototypeInstance.CompanyBSurveyPaper"/>
</appSettings>

调用段代码改成:

static void Main(string[] args)
{
PototypeInstance.SuperSurveyPaper pototype; var setting = ConfigurationSettings.AppSettings["Pototype"];
var obj = Type.GetType(setting);
if (obj == null) return;
pototype = Activator.CreateInstance(obj) as PototypeInstance.SuperSurveyPaper; if (pototype == null) return; pototype.Name = "SuperSurveyPaper ->Name";
pototype.Content = "SuperSurveyPaper -> Content";
PototypeInstance.SuperSurveyPaper clone = pototype.Clone();
Console.WriteLine("I'm old Name: " + pototype.Name);
Console.WriteLine("I'm old Content: " + pototype.Content);
Console.WriteLine("======================== ===========");
Console.WriteLine("I'm Clone Name: " + clone.Name);
Console.WriteLine("I'm Clone Content: " + clone.Content); Console.ReadKey();
}

输出结果和上面一样

2、深拷贝

如果现在的试卷内容不是一个简单的字符串了而是一个对象:

[Serializable]
public class SurveyPaperModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
[Serializable]
public abstract class SuperSurveyPaper
{
public string Name { get; set; }
public SurveyPaperModel Content { get; set; }
public abstract SuperSurveyPaper Clone();
}
[Serializable]
public class CompanyASurveyPaper : SuperSurveyPaper
{
public override SuperSurveyPaper Clone()
{
MemoryStream memoryStream = new MemoryStream();
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(memoryStream, this);
memoryStream.Position = 0;
return formatter.Deserialize(memoryStream) as SuperSurveyPaper;
}
}
[Serializable]
public class CompanyBSurveyPaper : SuperSurveyPaper
{
public override SuperSurveyPaper Clone()
{
MemoryStream memoryStream = new MemoryStream();
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(memoryStream, this);
memoryStream.Position = 0;
return formatter.Deserialize(memoryStream) as SuperSurveyPaper;
}
}

客户端调用:

static void Main(string[] args)
{
PototypeInstance.SuperSurveyPaper pototype; var setting = ConfigurationSettings.AppSettings["Pototype"];
var obj = Type.GetType(setting);
if (obj == null) return;
pototype = Activator.CreateInstance(obj) as PototypeInstance.SuperSurveyPaper; if (pototype == null) return; pototype.Name = "SuperSurveyPaper ->Name";
pototype.Content = new SurveyPaperModel { FirstName = "Design", LastName = "Pattern" };
PototypeInstance.SuperSurveyPaper clone = pototype.Clone();
Console.WriteLine("I'm old Name: " + pototype.Name);
Console.WriteLine("I'm old Content: " + pototype.Content.FirstName);
Console.WriteLine("I'm old Content: " + pototype.Content.LastName);
Console.WriteLine("======================== ===========");
Console.WriteLine("I'm Clone Name: " + clone.Name);
Console.WriteLine("I'm Clone Content: " + clone.Content.FirstName);
Console.WriteLine("I'm Clone Content: " + clone.Content.LastName);
Console.WriteLine("pototype==clone:" + clone.Equals(pototype)); Console.ReadKey();
}

输出结果:

五、原型模式的优点

  1. 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率。
  2. 扩展性较好,由于在原型模式中提供了抽象原型类,在客户端可以针对抽象原型类进行编程,而将具体原型类写在配置文件中,增加或减少产品类对原有系统都没有任何影响。
  3. 原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的工厂等级结构,而原型模式就不需要这样,原型模式中产品的复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品。
  4. 可以使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用(如恢复到某一历史状态),可辅助实现撤销操作。

六、原型模式的缺点

  1. 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时,需要修改源代码,违背了“开闭原则”。
  2. 在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦。

七、原型模式的使用场景

  1. 创建新对象成本较大(如初始化需要占用较长的时间,占用太多的CPU资源或网络资源),新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其成员变量稍作修改。
  2. 如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占用内存较少时,可以使用原型模式配合备忘录模式来实现。
  3. 需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便

八、扩展-原型管理器

原型管理器(Prototype Manager)是将多个原型对象存储在一个集合中供客户端使用,它是一个专门负责管理克隆对象的工厂,其中定义了一个集合用于存储原型对象,如果需要某个原型对象的一个克隆,可以在集合中找到该原型对象并克隆一个新对象。在原型管理器中针对抽象原型类进行编程,便于扩展。

还是应用上面的例子,现在加入B公司:

[Serializable]
public class SurveyPaperModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
[Serializable]
public abstract class SuperSurveyPaper
{
public string Name { get; set; }
public SurveyPaperModel Content { get; set; }
public abstract SuperSurveyPaper Clone();
}
[Serializable]
public class CompanyASurveyPaper : SuperSurveyPaper
{
public override SuperSurveyPaper Clone()
{
MemoryStream memoryStream = new MemoryStream();
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(memoryStream, this);
memoryStream.Position = 0;
return formatter.Deserialize(memoryStream) as SuperSurveyPaper;
}
}
[Serializable]
public class CompanyBSurveyPaper : SuperSurveyPaper
{
public override SuperSurveyPaper Clone()
{
MemoryStream memoryStream = new MemoryStream();
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(memoryStream, this);
memoryStream.Position = 0;
return formatter.Deserialize(memoryStream) as SuperSurveyPaper;
}
} public class PototypeManager
{
static IDictionary<string, SuperSurveyPaper> superSurveyPapers = new Dictionary<string, SuperSurveyPaper>();
static PototypeManager()
{
SuperSurveyPaper companyASurveyPaper = new CompanyASurveyPaper();
companyASurveyPaper.Name = "Company A";
companyASurveyPaper.Content= new SurveyPaperModel {FirstName="Michael",LastName="Du"}; SuperSurveyPaper companyBSurveyPaper = new CompanyBSurveyPaper();
companyBSurveyPaper.Name = "Company B";
companyBSurveyPaper.Content = new SurveyPaperModel { FirstName = "Kevin", LastName = "Durant" }; superSurveyPapers.Add("CompanyA", companyASurveyPaper);
superSurveyPapers.Add("CompanyB", companyBSurveyPaper);
}
private PototypeManager (){}
public SuperSurveyPaper GetSuperPaper(string key){
return superSurveyPapers[key].Clone();
}
public void RegisterSurveyPaper(string key, SuperSurveyPaper ssp){
superSurveyPapers.Add(key, ssp);
}
public static PototypeManager Instance
{
get{ return PototypeManagerInitializer.instance;}
}
private static class PototypeManagerInitializer
{
public static readonly PototypeManager instance=new PototypeManager();
}
}

这里的PototypeManager类使用了一个Singleton模式创建出来,在静态构造里初始化了原型对象,并将其注册在一个字典中,这个在项目中数据是从数据库中直接读取的。这个管理类还暴露了一个注册原型实例的方法,便于扩展和动态给管理器增加原型对象。在获取Clone对象的方法中直接将原型对象的一个Copy返回给客户程序。确保客户端得到的对象是一个全新的对象。

客户端调用代码:

static void Main(string[] args)
{
PototypeInstance.SuperSurveyPaper pototype1, pototype2, pototype3, pototype4; pototype1 = PototypeManager.Instance.GetSuperPaper("CompanyA");
pototype2 = PototypeManager.Instance.GetSuperPaper("CompanyA");
Console.WriteLine("I'm old Name: " + pototype1.Name);
Console.WriteLine("I'm old Content: " + pototype1.Content.FirstName);
Console.WriteLine("I'm old Content: " + pototype1.Content.LastName); Console.WriteLine("I'm Clone Name: " + pototype2.Name);
Console.WriteLine("I'm Clone Content: " + pototype2.Content.FirstName);
Console.WriteLine("I'm Clone Content: " + pototype2.Content.LastName);
Console.WriteLine("pototype1==pototype2:" + pototype2.Equals(pototype1));
Console.WriteLine("======================== ===========");
pototype3 = PototypeManager.Instance.GetSuperPaper("CompanyB");
pototype4 = PototypeManager.Instance.GetSuperPaper("CompanyB");
Console.WriteLine("I'm old Name: " + pototype3.Name);
Console.WriteLine("I'm old Content: " + pototype3.Content.FirstName);
Console.WriteLine("I'm old Content: " + pototype3.Content.LastName); Console.WriteLine("I'm Clone Name: " + pototype4.Name);
Console.WriteLine("I'm Clone Content: " + pototype4.Content.FirstName);
Console.WriteLine("I'm Clone Content: " + pototype4.Content.LastName);
Console.WriteLine("pototype3==pototype4:" + pototype4.Equals(pototype3));
Console.ReadKey();
}

输出结果:

模拟ctrl+c,ctrl+v

使用原型模式的“自我”复制能力,我们可以很容易的实现,创建副本和撤销副本的功能, 在控制台中我们输入c  替代ctrl+c,输入:z 替代ctrl+z  来模拟这个拷贝和撤销的过程,首先我们创建一个原型对象,每次按C的时候使用最后clone出来的对象再克隆新的对象,并把这些对象依次保存在一个list中,当按Z的时候我们依次在list中移除最后加入的对象直到起初创建的原型对象为止,  简单的客户端代码实现如下:

static List<PototypeInstance.SuperSurveyPaper> _list = new List<PototypeInstance.SuperSurveyPaper>();
static List<SurveyPaperModel> _listModel = new List<SurveyPaperModel>
{
new SurveyPaperModel{FirstName="Terry",LastName="Go"},
new SurveyPaperModel{FirstName="Ke",LastName="Be"},
new SurveyPaperModel{FirstName="Lebron",LastName="Jimes"},
new SurveyPaperModel{FirstName="Steve",LastName="Jo"},
new SurveyPaperModel{FirstName="Stive",LastName="Kurry"},
new SurveyPaperModel{FirstName="Henry",LastName="He"},
new SurveyPaperModel{FirstName="Kevin",LastName="Druant"},
new SurveyPaperModel{FirstName="Blue",LastName="Jhon"},
new SurveyPaperModel{FirstName="Jerry",LastName="Ma"},
new SurveyPaperModel{FirstName="Fred",LastName="Gao"},
};
static void Main(string[] args)
{
PototypeInstance.SuperSurveyPaper pototype1; pototype1 = PototypeManager.Instance.GetSuperPaper("CompanyB");
Console.WriteLine("I'm old Name: " + pototype1.Name);
Console.WriteLine("I'm old Content: " + pototype1.Content.FirstName);
Console.WriteLine("I'm old Content: " + pototype1.Content.LastName);
_list.Add(pototype1);
while (true)
{
var key = Console.ReadKey(); switch (key.Key)
{
case ConsoleKey.C:
var pototypeLastInstance = _list.Last<PototypeInstance.SuperSurveyPaper>();
var cloneFromPototypeLastInstance = pototypeLastInstance.Clone();
cloneFromPototypeLastInstance.Name = "Version " + _list.Count;
Random rd = new Random();
cloneFromPototypeLastInstance.Content = _listModel[rd.Next(0, _listModel.Count)];
_list.Add(cloneFromPototypeLastInstance);
PrintList(_list);
break;
case ConsoleKey.Z:
if (_list.Count > 1)
_list.RemoveAt(_list.Count - 1);
PrintList(_list);
break;
case ConsoleKey.Q:
return;
}
}
Console.ReadKey();
}
static void PrintList(List<PototypeInstance.SuperSurveyPaper> list)
{
Console.WriteLine("=========");
var pototpe = list.Last();
Console.WriteLine("History:" + pototpe.Name);
Console.WriteLine("I'm Firstname: " + pototpe.Content.FirstName);
Console.WriteLine("I'm LastName: " + pototpe.Content.LastName);
}

客户端输出:

好了,到这里设计模式的创建型模式就全部讨论完了。下面接着讨论结构型模式。

【设计模式】原型模式 Pototype Pattern的更多相关文章

  1. C#设计模式——原型模式(Prototype Pattern)

    一.概述 在软件开发中,经常会碰上某些对象,其创建的过程比较复杂,而且随着需求的变化,其创建过程也会发生剧烈的变化,但他们的接口却能比较稳定.对这类对象的创建,我们应该遵循依赖倒置原则,即抽象不应该依 ...

  2. 设计模式——原型模式(Prototype Pattern)

    原型模式:用原型实例制定创建对象的种类,并且通过拷贝这些原型创建新的对象. UML 图: 原型类: package com.cnblog.clarck; /** * 原型类 * * @author c ...

  3. Net设计模式实例之原型模式( Prototype Pattern)

    一.原型模式简介(Brief Introduction) 原型模式(Prototype Pattern):用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象. Specify the kin ...

  4. 乐在其中设计模式(C#) - 原型模式(Prototype Pattern)

    原文:乐在其中设计模式(C#) - 原型模式(Prototype Pattern) [索引页][源码下载] 乐在其中设计模式(C#) - 原型模式(Prototype Pattern) 作者:weba ...

  5. 设计模式系列之原型模式(Prototype Pattern)——对象的克隆

    说明:设计模式系列文章是读刘伟所著<设计模式的艺术之道(软件开发人员内功修炼之道)>一书的阅读笔记.个人感觉这本书讲的不错,有兴趣推荐读一读.详细内容也可以看看此书作者的博客https:/ ...

  6. 深入浅出设计模式——原型模式(Prototype Pattern)

    模式动机在面向对象系统中,使用原型模式来复制一个对象自身,从而克隆出多个与原型对象一模一样的对象.在软件系统中,有些对象的创建过程较为复杂,而且有时候需要频繁创建,原型模式通过给出一个原型对象来指明所 ...

  7. 二十四种设计模式:原型模式(Prototype Pattern)

    原型模式(Prototype Pattern) 介绍用原型实例指定创建对象的种类,并且通过拷贝这个原型来创建新的对象.示例有一个Message实体类,现在要克隆它. MessageModel usin ...

  8. 【设计模式】Java设计模式 - 原型模式

    [设计模式]Java设计模式 - 原型模式 不断学习才是王道 继续踏上学习之路,学之分享笔记 总有一天我也能像各位大佬一样 原创作品,更多关注我CSDN: 一个有梦有戏的人 准备将博客园.CSDN一起 ...

  9. 10. 星际争霸之php设计模式--原型模式

    题记==============================================================================本php设计模式专辑来源于博客(jymo ...

随机推荐

  1. ES 10 - Elasticsearch的索引别名和索引模板

    目录 1 索引模板概述 1.1 什么是索引模板 1.2 索引模板中的内容 1.3 索引模板的用途 2 创建索引模板 3 查看索引模板 4 删除索引模板 5 模板的使用建议 5.1 一个index中不能 ...

  2. RabbitMq在CentOs7下的完整安装步骤,带你踩坑

    1.前言 因为公司项目中用的RabbitMq来做消息处理,自己以前没有接触过,所以想自学一下.然额,光安装就花了6.7个小时才搞定,中间还换过一个版本,综合国内外博客才最终将所有安装中遇到的问题解决掉 ...

  3. 深度解密Go语言之关于 interface 的10个问题

    目录 1. Go 语言与鸭子类型的关系 2. 值接收者和指针接收者的区别 方法 值接收者和指针接收者 两者分别在何时使用 3. iface 和 eface 的区别是什么 4. 接口的动态类型和动态值 ...

  4. Asp.Net Core 轻松学-多线程之Task快速上手

    前言     Task是从 .NET Framework 4 开始引入的一项基于队列的异步任务(TAP)模式,从 .NET Framework 4.5 开始,任何使用 async/await 进行修饰 ...

  5. 14 ,CSS 文字与文本

    1.CSS 中长度与颜色 2.CSS 中的文字属性 3.CSS 中的文本属性 14.1 CSS 中长度与颜色 长度单位 说明 in 英寸 cm 公分 mm 公里 cm 以目前字体高度为单位 ex 以小 ...

  6. cesium 之加载地形图 Terrain 篇(附源码下载)

    前言 cesium 官网的api文档介绍地址cesium官网api,里面详细的介绍 cesium 各个类的介绍,还有就是在线例子:cesium 官网在线例子,这个也是学习 cesium 的好素材. 内 ...

  7. Android注解框架实战-ButterKnife

    文章大纲 Android注解框架介绍 ButterKnife实战 项目源码下载   一.框架介绍 为什么要用注解框架?  在Android开发过程中,我们经常性地需要操作组件,操作方法有findVie ...

  8. 史上最全的springboot导出pdf文件

    最近项目有一个导出报表文件的需求,我脑中闪过第一念头就是导出pdf(产品经理没有硬性规定导出excel还是pdf文件),于是赶紧上网查看相关的资料,直到踩了无数的坑把功能做出来了才知道其实导出exce ...

  9. ASP.NET Core 共享第三方依赖库部署的正常打开方式

    曾经: 写了一篇: ASP.Net Core on Linux (CentOS7) 共享第三方依赖库部署 当第二次想做相同的事,却遇上了Bug,于是有了第二篇: ASP.NET Core 共享第三方依 ...

  10. 使用Swiper轮播插件引起的探索

    提到Swiper轮播插件,小伙伴们应该不会感到陌生.以前我主要在移动端上使用,PC端使用较少. 注:这里需要注意的是,在PC端和移动端使用Swiper是不同的 官方给的版本有三个,分别是Swiper2 ...