文章作者:jqpeng
原文链接: 知识图谱学习笔记(1)

知识图谱学习笔记第一部分,包含RDF介绍,以及Jena RDF API使用

知识图谱的基石:RDF

RDF(Resource Description Framework),即资源描述框架,其本质是一个数据模型(Data Model)。它提供了一个统一的标准,用于描述实体/资源。简单来说,就是表示事物的一种方法和手段。
enter description here

RDF序列化方法

RDF序列化的方式主要有:RDF/XML,N-Triples,Turtle,RDFa,JSON-LD等几种。

  1. RDF/XML,顾名思义,就是用XML的格式来表示RDF数据
  2. N-Triples,即用多个三元组来表示RDF数据集,是最直观的表示方法。在文件中,每一行表示一个三元组,方便机器解析和处理。开放领域知识图谱DBpedia通常是用这种格式来发布数据的。
  3. Turtle, [‘tɝtl] 应该是使用得最多的一种RDF序列化方式了。它比RDF/XML紧凑,且可读性比N-Triples好。
  4. RDFa,即“The Resource Description Framework in Attributes”,是HTML5的一个扩展,在不改变任何显示效果的情况下,让网站构建者能够在页面中标记实体,像人物、地点、时间、评论等等
  5. JSON-LD,即“JSON for Linking Data”,用键值对的方式来存储RDF数据

Example1 N-Triples:

<http://www.kg.com/person/1> <http://www.kg.com/ontology/chineseName> "罗纳尔多·路易斯·纳萨里奥·德·利马"^^string.
<http://www.kg.com/person/1> <http://www.kg.com/ontology/career> "足球运动员"^^string.
<http://www.kg.com/person/1> <http://www.kg.com/ontology/fullName> "Ronaldo Luís Nazário de Lima"^^string.
<http://www.kg.com/person/1> <http://www.kg.com/ontology/birthDate> "1976-09-18"^^date.
<http://www.kg.com/person/1> <http://www.kg.com/ontology/height> "180"^^int.
<http://www.kg.com/person/1> <http://www.kg.com/ontology/weight> "98"^^int.
<http://www.kg.com/person/1> <http://www.kg.com/ontology/nationality> "巴西"^^string.
<http://www.kg.com/person/1> <http://www.kg.com/ontology/hasBirthPlace> <http://www.kg.com/place/10086>.
<http://www.kg.com/place/10086> <http://www.kg.com/ontology/address> "里约热内卢"^^string.
<http://www.kg.com/place/10086> <http://www.kg.com/ontology/coordinate> "-22.908333, -43.196389"^^string.

Example2 Turtle:

@prefix person: <http://www.kg.com/person/> .
@prefix place: <http://www.kg.com/place/> .
@prefix : <http://www.kg.com/ontology/> .

person:1 :chineseName "罗纳尔多·路易斯·纳萨里奥·德·利马"^^string.
person:1 :career "足球运动员"^^string.
person:1 :fullName "Ronaldo Luís Nazário de Lima"^^string.
person:1 :birthDate "1976-09-18"^^date.
person:1 :height "180"^^int. 
person:1 :weight "98"^^int.
person:1 :nationality "巴西"^^string. 
person:1 :hasBirthPlace place:10086.
place:10086 :address "里约热内卢"^^string.
place:10086 :coordinate "-22.908333, -43.196389"^^string.

RDF的表达能力

RDF的表达能力有限,无法区分类和对象,也无法定义和描述类的关系/属性。RDF是对具体事物的描述,缺乏抽象能力,无法对同一个类别的事物进行定义和描述。就以罗纳尔多这个知识图为例,RDF能够表达罗纳尔多和里约热内卢这两个实体具有哪些属性,以及它们之间的关系。但如果我们想定义罗纳尔多是人,里约热内卢是地点,并且人具有哪些属性,地点具有哪些属性,人和地点之间存在哪些关系,这个时候RDF就表示无能为力了。

RDFS/OWL

RDFS/OWL本质上是一些预定义词汇(vocabulary)构成的集合,用于对RDF进行类似的类定义及其属性的定义。

RDFS/OWL序列化方式和RDF没什么不同,其实在表现形式上,它们就是RDF。其常用的方式主要是RDF/XML,Turtle。另外,通常我们用小写开头的单词或词组来表示属性,大写开头的表示类。数据属性(data property,实体和literal字面量的关系)通常由名词组成,而对象数据(object property,实体和实体之间的关系)通常由动词(has,is之类的)加名词组成。剩下的部分符合驼峰命名法。

轻量级的模式语言——RDFS

RDFS,即“Resource Description Framework Schema”,是最基础的模式语言。还是以罗纳尔多知识图为例,我们在概念、抽象层面对RDF数据进行定义。下面的RDFS定义了人和地点这两个类,及每个类包含的属性。

@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix : <http://www.kg.com/ontology/> .

### 这里我们用词汇rdfs:Class定义了“人”和“地点”这两个类。
:Person rdf:type rdfs:Class.
:Place rdf:type rdfs:Class.

### rdfs当中不区分数据属性和对象属性,词汇rdf:Property定义了属性,即RDF的“边”。

:chineseName rdf:type rdf:Property;    rdfs:domain :Person;    rdfs:range xsd:string .

:career rdf:type rdf:Property;    rdfs:domain :Person;    rdfs:range xsd:string .

:fullName rdf:type rdf:Property;    rdfs:domain :Person;    rdfs:range xsd:string .

:birthDate rdf:type rdf:Property;    rdfs:domain :Person;    rdfs:range xsd:date .

:height rdf:type rdf:Property;    rdfs:domain :Person;    rdfs:range xsd:int .

:weight rdf:type rdf:Property;    rdfs:domain :Person;    rdfs:range xsd:int .

:nationality rdf:type rdf:Property;    rdfs:domain :Person;    rdfs:range xsd:string .

:hasBirthPlace rdf:type rdf:Property;    rdfs:domain :Person;    rdfs:range :Place .

:address rdf:type rdf:Property;    rdfs:domain :Place;    rdfs:range xsd:string .

:coordinate rdf:type rdf:Property;    rdfs:domain :Place;    rdfs:range xsd:string .

RDFS几个比较重要,常用的词汇:

  1. rdfs:Class. 用于定义类
  2. rdfs:domain. 用于表示该属性属于哪个类别
  3. rdfs:range. 用于描述该属性的取值类型
  4. rdfs:subClassOf. 用于描述该类的父类
  5. rdfs:subProperty. 用于描述该属性的父属性

enter description here

Data层是我们用RDF对罗纳尔多知识图的具体描述,Vocabulary是我们自己定义的一些词汇(类别,属性),RDF(S)则是预定义词汇。从下到上是一个具体到抽象的过程。图中我们用红色圆角矩形表示类,绿色字体表示rdf:type,rdfs:domain,rdfs:range三种预定义词汇,虚线表示rdf:type这种所属关系。

RDFS的扩展——OWL

RDFS本质上是RDF词汇的一个扩展。后来人们发现RDFS的表达能力还是相当有限,因此提出了OWL。我们也可以把OWL当做是RDFS的一个扩展,其添加了额外的预定义词汇。

OWL,即“Web Ontology Language”,语义网技术栈的核心之一。OWL有两个主要的功能:

  1. 提供快速、灵活的数据建模能力。
  2. 高效的自动推理。

用OWL对罗纳尔多知识图进行语义层的描述:

@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix : <http://www.kg.com/ontology/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .

### 这里我们用词汇owl:Class定义了“人”和“地点”这两个类。
:Person rdf:type owl:Class.
:Place rdf:type owl:Class.

### owl区分数据属性和对象属性(对象属性表示实体和实体之间的关系)。词汇owl:DatatypeProperty定义了数据属性,owl:ObjectProperty定义了对象属性。
:chineseName rdf:type owl:DatatypeProperty;    rdfs:domain :Person;    rdfs:range xsd:string .

:career rdf:type owl:DatatypeProperty;    rdfs:domain :Person;    rdfs:range xsd:string .

:fullName rdf:type owl:DatatypeProperty;    rdfs:domain :Person;    rdfs:range xsd:string .

:birthDate rdf:type owl:DatatypeProperty;    rdfs:domain :Person;    rdfs:range xsd:date .

:height rdf:type owl:DatatypeProperty;    rdfs:domain :Person;    rdfs:range xsd:int .

:weight rdf:type owl:DatatypeProperty;    rdfs:domain :Person;    rdfs:range xsd:int .

:nationality rdf:type owl:DatatypeProperty;    rdfs:domain :Person;    rdfs:range xsd:string .

:hasBirthPlace rdf:type owl:ObjectProperty;    rdfs:domain :Person;    rdfs:range :Place .

:address rdf:type owl:DatatypeProperty;    rdfs:domain :Place;    rdfs:range xsd:string .

:coordinate rdf:type owl:DatatypeProperty;    rdfs:domain :Place;    rdfs:range xsd:string .

schema层的描述语言换为OWL后,层次图表示为:
enter description here

owl区分数据属性和对象属性(对象属性表示实体和实体之间的关系)。词汇owl:DatatypeProperty定义了数据属性,owl:ObjectProperty定义了对象属性。

上图中,数据属性用青色表示,对象属性由蓝色表示。

描述属性特征的词汇

  1. owl:TransitiveProperty. 表示该属性具有传递性质。例如,我们定义“位于”是具有传递性的属性,若A位于B,B位于C,那么A肯定位于C。
  2. owl:SymmetricProperty. 表示该属性具有对称性。例如,我们定义“认识”是具有对称性的属性,若A认识B,那么B肯定认识A。
  3. owl:FunctionalProperty. 表示该属性取值的唯一性。 例如,我们定义“母亲”是具有唯一性的属性,若A的母亲是B,在其他地方我们得知A的母亲是C,那么B和C指的是同一个人。
  4. owl:inverseOf. 定义某个属性的相反关系。例如,定义“父母”的相反关系是“子女”,若A是B的父母,那么B肯定是A的子女。

本体映射词汇(Ontology Mapping)

  1. owl:equivalentClass. 表示某个类和另一个类是相同的。
  2. owl:equivalentProperty. 表示某个属性和另一个属性是相同的。
  3. owl:sameAs. 表示两个实体是同一个实体。

RDFS,OWL推理的推理机(reasoner)

RDFS同样支持推理,由于缺乏丰富的表达能力,推理能力也不强。举个例子,我们用RDFS定义人和动物两个类,另外,定义人是动物的一个子类。此时推理机能够推断出一个实体若是人,那么它也是动物。OWL当然支持这种基本的推理,除此之外,凭借其强大的表达能力,我们能进行更有实际意义的推理。想象一个场景,我们有一个庞大数据库存储人物的亲属关系。里面很多关系都是单向的,比如,其只保存了A的父亲(母亲)是B,但B的子女字段里面没有A,可以推理得到B的子女A。

enter description here

RDF查询语言SPARQL

SPARQL即SPARQL Protocol and RDF Query Language的递归缩写,专门用于访问和操作RDF数据,是语义网的核心技术之一。W3C的RDF数据存取小组(RDF Data Access Working Group, RDAWG)对其进行了标准化。在2008年,SPARQL 1.0成为W3C官方所推荐的标准。2013年发布了SPARQL 1.1。相对第一个版本,其支持RDF图的更新,提供更强大的查询,比如:子查询、聚合操作(像我们常用的count)等等。

由两个部分组成:协议和查询语言。

  1. 查询语言很好理解,就像SQL用于查询关系数据库中的数据,XQuery用于查询XML数据,SPARQL用于查询RDF数据。
  2. 协议是指我们可以通过HTTP协议在客户端和SPARQL服务器(SPARQL endpoint)之间传输查询和结果,这也是和其他查询语言最大的区别。

一个SPARQL查询本质上是一个带有变量的RDF图,以我们之前提到的罗纳尔多RDF数据为例:

<http://www.kg.com/person/1> <http://www.kg.com/ontology/chineseName> "罗纳尔多·路易斯·纳萨里奥·德·利马"^^string.

查询SPARQL

<http://www.kg.com/person/1> <http://www.kg.com/ontology/chineseName> ?x.

SPARQL查询是基于图匹配的思想。我们把上述的查询与RDF图进行匹配,找到符合该匹配模式的所有子图,最后得到变量的值。就上面这个例子而言,在RDF图中找到匹配的子图后,将”罗纳尔多·路易斯·纳萨里奥·德·利马”和“?x”绑定,我们就得到最后的结果。简而言之,SPARQL查询分为三个步骤:

  1. 构建查询图模式,表现形式就是带有变量的RDF。
  2. 匹配,匹配到符合指定图模式的子图。
  3. 绑定,将结果绑定到查询图模式对应的变量上。

举例

如何查询所有数据

PREFIX : <http://www.kgdemo.com#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX owl: <http://www.w3.org/2002/07/owl#>
PREFIX xsd: <XML Schema>
PREFIX vocab: <http://localhost:2020/resource/vocab/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX map: <http://localhost:2020/resource/#>
PREFIX db: <http://localhost:2020/resource/>

SELECT * WHERE {
  ?s ?p ?o
}

SPARQL的部分关键词:

  1. SELECT, 指定我们要查询的变量。在这里我们查询所有的变量,用*代替。
  2. WHERE,指定我们要查询的图模式。含义上和SQL的WHERE没有区别。
  3. FROM,指定查询的RDF数据集。我们这里只有一个图,因此省去了FROM关键词。 PREFIX,用于IRI的缩写。

“周星驰出演了哪些电影”:

PREFIX : <http://www.kgdemo.com#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX owl: <http://www.w3.org/2002/07/owl#>
PREFIX xsd: <XML Schema>
PREFIX vocab: <http://localhost:2020/resource/vocab/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX map: <http://localhost:2020/resource/#>
PREFIX db: <http://localhost:2020/resource/>

SELECT ?n WHERE {
  ?s rdf:type :Person.
  ?s :personName '周星驰'.
  ?s :hasActedIn ?o.
  ?o :movieTitle ?n
}

使用Jena 构建知识图谱

Jena是Apache基金会旗下的开源Java框架,用于构建Semantic Web 和 Linked Data 应用。

下面简要的介绍下API,要使用jena,可以下载jar包或者使用maven(推荐),建议测试时下面的都加上:

    <dependency>    <groupId>org.apache.jena</groupId>    <artifactId>apache-jena-libs</artifactId>    <type>pom</type>    <version>3.7.0</version></dependency><dependency>    <groupId>org.apache.jena</groupId>    <artifactId>jena-sdb</artifactId>    <version>3.7.0</version></dependency><dependency>    <groupId>org.apache.jena</groupId>    <artifactId>jena-base</artifactId>    <version>3.7.0</version></dependency><dependency>    <groupId>org.apache.jena</groupId>    <artifactId>jena-fuseki-embedded</artifactId>    <version>3.7.0</version> <!-- Set the version --></dependency>
<!-- https://mvnrepository.com/artifact/org.apache.jena/jena-arq --><dependency>    <groupId>org.apache.jena</groupId>    <artifactId>jena-arq</artifactId>    <version>3.7.0</version></dependency>

Jena RDF API

首先,三元组(triple)组成的图称之为Model,这个图里的Node可以是resources(实体)、literals(文本)或者blank nodes。

一个三元组,在jena里称之为Statement,一个 statement 包含三部分::

  • the subject :实体
  • the predicate :属性
  • the object : 值

创建Model

// URI 定义
static String personURI    = "http://somewhere/JohnSmith";
static String fullName     = "John Smith";

// 创建一个空模型(KG)
Model model = ModelFactory.createDefaultModel();

// 创建一个resource(一个subject)
Resource johnSmith = model.createResource(personURI);

// 添加属性,这里的value是一个literals(文本)
 johnSmith.addProperty(VCARD.FN, fullName);

当然,你还可以使用链式API,为resource添加多个Property

// create the resource
//   and add the properties cascading style
Resource johnSmith
  = model.createResource(personURI)     .addProperty(VCARD.FN, fullName)     .addProperty(VCARD.N,                  model.createResource()                       .addProperty(VCARD.Given, givenName)                       .addProperty(VCARD.Family, familyName));

遍历Model

使用model.listStatements遍历statements,返回一个迭代器,使用hasNext判断是否还有数据,通过getSubject,getPredicate,getObject 获取三元组信息。

// list the statements in the Model
StmtIterator iter = model.listStatements();

// print out the predicate, subject and object of each statement
while (iter.hasNext()) {
    Statement stmt      = iter.nextStatement();  // get next statement
    Resource  subject   = stmt.getSubject();     // get the subject
    Property  predicate = stmt.getPredicate();   // get the predicate
    RDFNode   object    = stmt.getObject();      // get the object
    System.out.print(subject.toString());
    System.out.print(" " + predicate.toString() + " ");
    if (object instanceof Resource) {
       System.out.print(object.toString());
    } else {
        // object is a literal
        System.out.print(" \"" + object.toString() + "\"");
    }
    System.out.println(" .");
} 

运行结果:

http://somewhere/JohnSmith http://www.w3.org/2001/vcard-rdf/3.0#N 80aeb72e-ef9c-4879-807d-62daf3c13b72 .
http://somewhere/JohnSmith http://www.w3.org/2001/vcard-rdf/3.0#FN  "John Smith" .
80aeb72e-ef9c-4879-807d-62daf3c13b72 http://www.w3.org/2001/vcard-rdf/3.0#Family  "Smith" .
80aeb72e-ef9c-4879-807d-62daf3c13b72 http://www.w3.org/2001/vcard-rdf/3.0#Given  "John" .

保存为 RDF文件

可以使用model.write方便的把Model保存为rdf文件,write默认保存为XML格式

// now write the model in XML form to a file
model.write(System.out);



<rdf:RDF
  xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'
  xmlns:vcard='http://www.w3.org/2001/vcard-rdf/3.0#'
 >
  <rdf:Description rdf:about='http://somewhere/JohnSmith'>
    <vcard:FN>John Smith</vcard:FN>
    <vcard:N rdf:nodeID="A0"/>
  </rdf:Description>
  <rdf:Description rdf:nodeID="A0">
    <vcard:Given>John</vcard:Given>
    <vcard:Family>Smith</vcard:Family>
  </rdf:Description>
</rdf:RDF>

write还提供重载版本write( OutputStream out, String lang ),lang可以为”RDF/XML-ABBREV”, “N-TRIPLE”, “TURTLE”, (and “TTL”) and “N3”
我们来保存为常见的TURTLE:

model.write(System.out, "TURTLE");

结果:

<http://somewhere/JohnSmith>    <http://www.w3.org/2001/vcard-rdf/3.0#FN>            "John Smith" ;    <http://www.w3.org/2001/vcard-rdf/3.0#N>            [ <http://www.w3.org/2001/vcard-rdf/3.0#Family>                      "Smith" ;              <http://www.w3.org/2001/vcard-rdf/3.0#Given>                      "John"            ] .

jena还提供prefix功能,我们可以指定prefix来简化turtle,下面的代码将指定prefix,并保存到文件1.rdf里:

    model.setNsPrefix( "vCard", "http://www.w3.org/2001/vcard-rdf/3.0#" );    model.setNsPrefix( "rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#" );    try {        model.write(new FileOutputStream("1.rdf"),"TURTLE");    } catch (FileNotFoundException e) {        e.printStackTrace();    }

结果:

@prefix rdf:   <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix vCard: <http://www.w3.org/2001/vcard-rdf/3.0#> .

<http://somewhere/JohnSmith>    vCard:FN  "John Smith" ;    vCard:N   [ vCard:Family  "Smith" ;                vCard:Given   "John"              ] .

读取rdf

Mode的read(Reader reader, String base)方法,提供 读取RDF文件的功能:

    static final String inputFileName  = "1.rdf";
                              
    public static void main (String args[]) {
        // create an empty model
        Model model = ModelFactory.createDefaultModel();

        InputStream in = FileManager.get().open( inputFileName );
        if (in == null) {
            throw new IllegalArgumentException( "File: " + inputFileName + " not found");
        }
        
        // read the RDF/XML file
        model.read(in, "","TURTLE");
                    
        // write it to standard out
        model.write(System.out);            
    }

注意,read的时候,默认是读取XML,如果是其他格式,需要指定lang。

从模型读取Resouce

一个resouce都有一个唯一的URI,我们可以通过URI来获取对应的Resouce:
函数原型:

    /**    Return a Resource instance with the given URI in this model. <i>This method    behaves identically to <code>createResource(String)</code></i> and exists as    legacy: createResource is now capable of, and allowed to, reuse existing objects.<p>    Subsequent operations on the returned object may modify this model.   @return a resource instance   @param uri the URI of the resource*/Resource getResource(String uri) ;

获取到Resouce后,通过getRequiredProperty获取属性,如果一个属性包含多个值,可以使用listProperties获取。

 static final String inputFileName = "1.rdf";
    static final String johnSmithURI = "http://somewhere/JohnSmith";
    
    public static void main (String args[]) {
        // create an empty model
        Model model = ModelFactory.createDefaultModel();
       
        // use the FileManager to find the input file
        InputStream in = FileManager.get().open(inputFileName);
        if (in == null) {
            throw new IllegalArgumentException( "File: " + inputFileName + " not found");
        }
        
        // read the RDF/XML file
        model.read(new InputStreamReader(in), "");
        
        // retrieve the Adam Smith vcard resource from the model
        Resource vcard = model.getResource(johnSmithURI);

        // retrieve the value of the N property
        Resource name = (Resource) vcard.getRequiredProperty(VCARD.N)
                                        .getObject();
        // retrieve the given name property
        String fullName = vcard.getRequiredProperty(VCARD.FN)
                               .getString();
        // add two nick name properties to vcard
        vcard.addProperty(VCARD.NICKNAME, "Smithy")
             .addProperty(VCARD.NICKNAME, "Adman");
        
        // set up the output
        System.out.println("The nicknames of \"" + fullName + "\" are:");
        // list the nicknames
        StmtIterator iter = vcard.listProperties(VCARD.NICKNAME);
        while (iter.hasNext()) {
            System.out.println("    " + iter.nextStatement().getObject()
                                            .toString());
        }

        try {
            model.write(new FileOutputStream("1.rdf"));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

查询模型

可以通过listResourcesWithProperty查询包含Property的数据:

    ResIterator iter = model.listResourcesWithProperty(VCARD.FN);
        if (iter.hasNext()) {
            System.out.println("The database contains vcards for:");
            while (iter.hasNext()) {
                System.out.println("  " + iter.nextResource()
                                              .getRequiredProperty(VCARD.FN)
                                              .getString() );
            }
        } else {
            System.out.println("No vcards were found in the database");
        }        

通过listStatements(SimpleSelector)查询Statement:

        // select all the resources with a VCARD.FN property
        // whose value ends with "Smith"
        StmtIterator iter = model.listStatements(
            new 
                SimpleSelector(null, VCARD.FN, (RDFNode) null) {
                    @Override
                    public boolean selects(Statement s) {
                            return s.getString().endsWith("Smith");
                    }
                });
        if (iter.hasNext()) {
            System.out.println("The database contains vcards for:");
            while (iter.hasNext()) {
                System.out.println("  " + iter.nextStatement()
                                              .getString());
            }
        } else {
            System.out.println("No Smith's were found in the database");
        }     

模型合并

可以通过union合并两个模型:

enter description here
enter description here

合并后:
enter description here

来源


作者:Jadepeng
出处:jqpeng的技术记事本–http://www.cnblogs.com/xiaoqi
您的支持是对博主最大的鼓励,感谢您的认真阅读。
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

文章作者:jqpeng
原文链接: 新型前端开发工程师的三个境界 后端开发工程师如何快速转前端

初入软件开发这一行时,当时还没有前后端分离这个概念,所有的开发工程师既能写html,也能写后台服务,随着技术的发展,前后端分离成为趋势,目前团队不少人能熟悉的写java后台服务,却难以hold住前端页面的开发,前端页面开发成为瓶颈。针对这个情况,筹划了一个前端培训专题,让后端的同事可以通过学习快速掌握前端开发技能。

愿景

  • 培养全栈工程师,前后端均可以Hold住

前端技能梳理

我们把前端同事做的事情简单的梳理下,大概可以分为:

效果图 -> HTML还原

将UED设计的效果图还原为页面,这个也是以前狭义的UI完成的工作。梳理下这个工作需要的技能:

  • 熟悉HTML\CSS、熟悉常见布局,div+css
  • 熟悉浏览器兼容
  • 熟悉PS切图

随着前端UI框架的发展,当你使用bootstrap、elements、iview这类框架时,80%的功能开发可以不需要这一步,因此一个小团队有1个这样的工程师就OK了。

HTML->应用

单独的HTML是缺乏灵魂的,还需要绑定数据,这样才是一个完整的页面。在前后端未分离的时代,通常是后端基于前端还原的html来进行开发,通过模板技术绑定数据。而随着ajax的兴起,前端 MVVM框架的流行,前后端分离,数据绑定工作前移到前端,因此前端的职责之一就是调用后端的服务,并显示到页面上。

同样的,梳理下这个工作需要的技能:

  • 了解或者熟悉html
  • 熟悉HTTP
  • 基本的javascript应用
  • 熟悉一个js框架的应用,比如jq、vue.js

一个合格的后端,在熟悉javascript的情况下,可以很快掌握。

复杂的单页应用

现在流行一个词“大前端”,前端更大的挑战就是构建复杂的单页应用,比如易企秀的H5编辑器,单个页面里包含了非常多的功能和逻辑,这类页面有个特点:

  • 包含复杂的业务逻辑
  • 通常需要上千行的javascript代码
  • 需要良好的设计模式来组织和维护代码,MVC\MVVM等概念在前端运用

而随着技术的发展,javascript可以用来开发手机端app(react-native、weex),本质上来说还是开发复杂的单页应用。特别是使用vuex这类状态管理库时,如果懂的后端的数据库概念,可以事半功倍的理解其原理。

总结一下,开发复杂的单页应用,需要具备的技能:

  • 熟悉数据结构和算法
  • 熟悉常用的设计模式
  • OOP思维
  • 模块化开发
  • db思维
  • 熟悉javascript,熟悉es2015\es2017

一句话总结起来,复杂的前端应用开发所需要的技能,恰恰是后端开发所擅长的,只是编程语言从java、c#变成了javascript,仅此而已。

新型前后端一体化工程师的三个境界

怎么来评价一个人的前端能力,简单起见,划分为三个境界:

  • 第一层(必须具备)

    • 依葫芦画瓢
    • 可以根据还原的HTML或者UI框架,实现简单页面的开发和数据绑定
    • 熟悉HTML常见标签、CSS盒子模型、CSS优先级,常见布局
    • 会使用Vue.js/jquery,Iview、Element等工具库
  • 第二层(努力可以达到)

    • 可以熟练的开发单页应用
    • javascript了然于心,es2015\2016信手拈来
    • 熟悉Vue、React、angular、知道各自的优缺点,根据需要选择合理的方案
    • 跟踪前端发展趋势、不盲从、独立思考
  • 第三层(尽量追求,需要时间和积累)

    • 融会贯通,可以改造轮子、造新的轮子提升效率
    • 在公司、业界前端形成影响力

培训规划

最后来定一下培训的规划。

目标

  • 所有人达到第一层境界
  • 骨干需要达到第二层

培训内容

课时1:HTTP+HTML+CSS基础+常见布局+HTML5+CSS3

  • HTTP
    • HTTP get/post/put/delete
    • HTTP响应码
    • chrome F12 network使用
  • html块元素、内联元素、表单
  • CSS 与盒子模型
  • 响应式布局
  • H5语义标签,audio,canvas
  • CSS3动画

课时2:javascript 基础

  • 数据类型,数组、对象,表达式、条件、循环等
  • javascript常用对象
  • DOM编程
  • AJAX、jsonp
  • 正则、表单验证

课时3:javascript进阶

  • 深入js
    • 模块化、AMD,require.js
    • 作用域链
    • 原型链与继承
    • 闭包
    • OOP
  • es2015/2017
    • 箭头函数等新语法糖
  • TypeScript

课时4:项目框架应用 Vue.js +IView使用培训

  • Vue.js 渐进式理解
  • Vue.js 模板绑定
  • Vue.js 组件
  • Vue.js 单页应用
  • Vuex 状态管理
  • Vue Router
  • IView 组件库介绍
  • 项目案例讲解

课时5:基于Nodejs的前端新生态

  • NodeJs原理、历史、发展
  • webpack
  • less
  • 代码质量eslint

课时6: vue.js与手机app、微信小程序开发

  • 使用vue.js+weex开发手机app
  • 微信小程序开发

最后,欢迎大家拍砖和提出建议。


作者:Jadepeng
出处:jqpeng的技术记事本–http://www.cnblogs.com/xiaoqi
您的支持是对博主最大的鼓励,感谢您的认真阅读。
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

文章作者:jqpeng
原文链接: axios介绍与使用说明 axios中文文档

本周在做一个使用vuejs的前端项目,访问后端服务使用axios库,这里对照官方文档,简单记录下,也方便大家参考。

Axios 是一个基于 Promise 的 HTTP 库,可以用在浏览器和 node.js 中。github开源地址https://github.com/axios/axios

特性

  • 在浏览器中创建 XMLHttpRequests
  • 在 node.js 则创建 http 请求
  • 支持 Promise API
  • 支持拦截请求和响应
  • 转换请求和响应数据
  • 取消请求
  • 自动转换 JSON 数据
  • 客户端支持防御 XSRF

浏览器支持

支持Chrome、火狐、Edge、IE8+等浏览器

安装

使用 npm安装:

$ npm install axios

使用 bower:

$ bower install axios

或者直接使用 cdn:

<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

使用举例

执行 GET 请求

// 为给定 ID 的 user 创建请求
axios.get('/user?ID=12345')
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

// GET 参数可以放到params里(推荐)
axios.get('/user', {
    params: {
      ID: 12345
    }
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

// 还可以使用ECMAScript 2017里的async/await,添加 `async` keyword to your outer function/method.
async function getUser() {
  try {
    const response = await axios.get('/user?ID=12345');
    console.log(response);
  } catch (error) {
    console.error(error);
  }
}

async/await 是 ECMAScript 2017新提供的功能 ,Internet Explorer 和一些旧的浏览器并不支持

执行 POST 请求

axios.post('/user', {
    firstName: 'Fred',
    lastName: 'Flintstone'
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

执行多个并发请求

function getUserAccount() {
  return axios.get('/user/12345');
}

function getUserPermissions() {
  return axios.get('/user/12345/permissions');
}
axios.all([getUserAccount(), getUserPermissions()])
  .then(axios.spread(function (acct, perms) {
    // 两个请求现在都执行完成
  }));

axios API

可以通过向 axios 传递相关配置来创建请求

axios(config)
// 发送 POST 请求
axios({
  method: 'post',
  url: '/user/12345',
  data: {
    firstName: 'Fred',
    lastName: 'Flintstone'
  }
});
//  GET 请求远程图片
axios({
  method:'get',
  url:'http://bit.ly/2mTM3nY',
  responseType:'stream'
})
  .then(function(response) {
  response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'))
});
axios(url[, config])
// 发送 GET 请求(默认的方法)
axios('/user/12345');

请求方法的别名

为方便使用,官方为所有支持的请求方法提供了别名,可以直接使用别名来发起请求:

axios.request(config)
axios.get(url[, config])
axios.delete(url[, config])
axios.head(url[, config])
axios.post(url[, data[, config]])
axios.put(url[, data[, config]])
axios.patch(url[, data[, config]])
NOTE

在使用别名方法时, urlmethoddata 这些属性都不必在配置中指定。

并发

处理并发请求的助手函数

axios.all(iterable)
axios.spread(callback)

创建实例

可以使用自定义配置新建一个 axios 实例

axios.create([config])
const instance = axios.create({
  baseURL: 'https://some-domain.com/api/',
  timeout: 1000,
  headers: {'X-Custom-Header': 'foobar'}
});

实例方法

以下是可用的实例方法。指定的配置将与实例的配置合并

axios#request(config)
axios#get(url[, config])
axios#delete(url[, config])
axios#head(url[, config])
axios#post(url[, data[, config]])
axios#put(url[, data[, config]])
axios#patch(url[, data[, config]])

请求配置项

下面是创建请求时可用的配置选项,注意只有 url 是必需的。如果没有指定 method,请求将默认使用 get 方法。

{
  // `url` 是用于请求的服务器 URL
  url: "/user",

  // `method` 是创建请求时使用的方法
  method: "get", // 默认是 get

  // `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
  // 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL
  baseURL: "https://some-domain.com/api/",

  // `transformRequest` 允许在向服务器发送前,修改请求数据
  // 只能用在 "PUT", "POST" 和 "PATCH" 这几个请求方法
  // 后面数组中的函数必须返回一个字符串,或 ArrayBuffer,或 Stream
  transformRequest: [function (data) {
    // 对 data 进行任意转换处理

    return data;
  }],

  // `transformResponse` 在传递给 then/catch 前,允许修改响应数据
  transformResponse: [function (data) {
    // 对 data 进行任意转换处理

    return data;
  }],

  // `headers` 是即将被发送的自定义请求头
  headers: {"X-Requested-With": "XMLHttpRequest"},

  // `params` 是即将与请求一起发送的 URL 参数
  // 必须是一个无格式对象(plain object)或 URLSearchParams 对象
  params: {
    ID: 12345
  },

  // `paramsSerializer` 是一个负责 `params` 序列化的函数
  // (e.g. https://www.npmjs.com/package/qs, http://api.jquery.com/jquery.param/)
  paramsSerializer: function(params) {
    return Qs.stringify(params, {arrayFormat: "brackets"})
  },

  // `data` 是作为请求主体被发送的数据
  // 只适用于这些请求方法 "PUT", "POST", 和 "PATCH"
  // 在没有设置 `transformRequest` 时,必须是以下类型之一:
  // - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
  // - 浏览器专属:FormData, File, Blob
  // - Node 专属: Stream
  data: {
    firstName: "Fred"
  },

  // `timeout` 指定请求超时的毫秒数(0 表示无超时时间)
  // 如果请求话费了超过 `timeout` 的时间,请求将被中断
  timeout: 1000,

  // `withCredentials` 表示跨域请求时是否需要使用凭证
  withCredentials: false, // 默认的

  // `adapter` 允许自定义处理请求,以使测试更轻松
  // 返回一个 promise 并应用一个有效的响应 (查阅 [response docs](#response-api)).
  adapter: function (config) {
    /* ... */
  },

  // `auth` 表示应该使用 HTTP 基础验证,并提供凭据
  // 这将设置一个 `Authorization` 头,覆写掉现有的任意使用 `headers` 设置的自定义 `Authorization`头
  auth: {
    username: "janedoe",
    password: "s00pers3cret"
  },

  // `responseType` 表示服务器响应的数据类型,可以是 "arraybuffer", "blob", "document", "json", "text", "stream"
  responseType: "json", // 默认的

  // `xsrfCookieName` 是用作 xsrf token 的值的cookie的名称
  xsrfCookieName: "XSRF-TOKEN", // default

  // `xsrfHeaderName` 是承载 xsrf token 的值的 HTTP 头的名称
  xsrfHeaderName: "X-XSRF-TOKEN", // 默认的

  // `onUploadProgress` 允许为上传处理进度事件
  onUploadProgress: function (progressEvent) {
    // 对原生进度事件的处理
  },

  // `onDownloadProgress` 允许为下载处理进度事件
  onDownloadProgress: function (progressEvent) {
    // 对原生进度事件的处理
  },

  // `maxContentLength` 定义允许的响应内容的最大尺寸
  maxContentLength: 2000,

  // `validateStatus` 定义对于给定的HTTP 响应状态码是 resolve 或 reject  promise 。如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),promise 将被 resolve; 否则,promise 将被 rejecte
  validateStatus: function (status) {
    return status &gt;= 200 &amp;&amp; status &lt; 300; // 默认的
  },

  // `maxRedirects` 定义在 node.js 中 follow 的最大重定向数目
  // 如果设置为0,将不会 follow 任何重定向
  maxRedirects: 5, // 默认的

  // `httpAgent` 和 `httpsAgent` 分别在 node.js 中用于定义在执行 http 和 https 时使用的自定义代理。允许像这样配置选项:
  // `keepAlive` 默认没有启用
  httpAgent: new http.Agent({ keepAlive: true }),
  httpsAgent: new https.Agent({ keepAlive: true }),

  // "proxy" 定义代理服务器的主机名称和端口
  // `auth` 表示 HTTP 基础验证应当用于连接代理,并提供凭据
  // 这将会设置一个 `Proxy-Authorization` 头,覆写掉已有的通过使用 `header` 设置的自定义 `Proxy-Authorization` 头。
  proxy: {
    host: "127.0.0.1",
    port: 9000,
    auth: : {
      username: "mikeymike",
      password: "rapunz3l"
    }
  },

  // `cancelToken` 指定用于取消请求的 cancel token
  // (查看后面的 Cancellation 这节了解更多)
  cancelToken: new CancelToken(function (cancel) {
  })
}

响应结构

axios请求的响应包含以下信息:

{
  // `data` 由服务器提供的响应
  data: {},

  // `status`  HTTP 状态码
  status: 200,

  // `statusText` 来自服务器响应的 HTTP 状态信息
  statusText: "OK",

  // `headers` 服务器响应的头
  headers: {},

  // `config` 是为请求提供的配置信息
  config: {}
}

使用 then 时,会接收下面这样的响应:

axios.get("/user/12345")
  .then(function(response) {
    console.log(response.data);
    console.log(response.status);
    console.log(response.statusText);
    console.log(response.headers);
    console.log(response.config);
  });

在使用 catch 时,或传递 rejection callback 作为 then 的第二个参数时,响应可以通过 error 对象可被使用,正如在错误处理这一节所讲。

配置的默认值/defaults

你可以指定将被用在各个请求的配置默认值

全局的 axios 默认值

axios.defaults.baseURL = "https://api.example.com";
axios.defaults.headers.common["Authorization"] = AUTH_TOKEN;
axios.defaults.headers.post["Content-Type"] = "application/x-www-form-urlencoded";

自定义实例默认值

// 创建实例时设置配置的默认值
var instance = axios.create({
  baseURL: "https://api.example.com"
});

// 在实例已创建后修改默认值
instance.defaults.headers.common["Authorization"] = AUTH_TOKEN;

配置的优先级

配置会以一个优先顺序进行合并。

请求的config > 实例的 defaults 属性 > 库默认值:

// 使用由库提供的配置的默认值来创建实例
// 此时超时配置的默认值是 `0`
var instance = axios.create();

// 覆写库的超时默认值
// 现在,在超时前,所有请求都会等待 2.5 秒
instance.defaults.timeout = 2500;

// 为已知需要花费很长时间的请求覆写超时设置
instance.get("/longRequest", {
  timeout: 5000
});

拦截器

可以自定义拦截器,在在请求或响应被 thencatch 处理前拦截它们。

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    return config;
  }, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  });

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 对响应数据做点什么
    return response;
  }, function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
  });

如果你想在稍后移除拦截器,可以这样:

var myInterceptor = axios.interceptors.request.use(function () {/*...*/});
axios.interceptors.request.eject(myInterceptor);

可以为自定义 axios 实例添加拦截器

var instance = axios.create();
instance.interceptors.request.use(function () {/*...*/});

错误处理

axios.get("/user/12345")
  .catch(function (error) {
    if (error.response) {
      // 请求已发出,但服务器响应的状态码不在 2xx 范围内
      console.log(error.response.data);
      console.log(error.response.status);
      console.log(error.response.headers);
    } else {
      // Something happened in setting up the request that triggered an Error
      console.log("Error", error.message);
    }
    console.log(error.config);
  });

可以使用 validateStatus 配置选项定义一个自定义 HTTP 状态码的错误范围。

axios.get("/user/12345", {
  validateStatus: function (status) {
    return status < 500; // 状态码在大于或等于500时才会 reject
  }
})

取消请求

使用 cancel token 取消请求

Axios 的 cancel token API 基于cancelable promises proposal

可以使用 CancelToken.source 工厂方法创建 cancel token,像这样:

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function(thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // handle error
  }
});

axios.post('/user/12345', {
  name: 'new name'
}, {
  cancelToken: source.token
})

// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');

还可以通过传递一个 executor 函数到 CancelToken 的构造函数来创建 cancel token:

var CancelToken = axios.CancelToken;
var cancel;

axios.get("/user/12345", {
  cancelToken: new CancelToken(function executor(c) {
    // executor 函数接收一个 cancel 函数作为参数
    cancel = c;
  })
});

// 取消请求
cancel();

Note : 可以使用同一个 cancel token 取消多个请求

请求时使用 application/x-www-form-urlencoded

axios会默认序列化 JavaScript 对象为 JSON. 如果想使用 application/x-www-form-urlencoded 格式,你可以使用下面的配置.

浏览器

在浏览器环境,你可以使用 URLSearchParams API ::

const params = new URLSearchParams();
params.append('param1', 'value1');
params.append('param2', 'value2');
axios.post('/foo', params);

URLSearchParams不是所有的浏览器均支持

除此之外,你可以使用qs库来编码数据:

const qs = require('qs');
axios.post('/foo', qs.stringify({ 'bar': 123 }));

// Or in another way (ES6),

import qs from 'qs';
const data = { 'bar': 123 };
const options = {
  method: 'POST',
  headers: { 'content-type': 'application/x-www-form-urlencoded' },
  data: qs.stringify(data),
  url,
};
axios(options);

Node.js环境

在 node.js里, 可以使用 querystring module:

const querystring = require('querystring');
axios.post('http://something.com/', querystring.stringify({ foo: 'bar' }));

当然,同浏览器一样,你还可以使用 qs library.

Promises

axios 依赖原生的 ES6 Promise 实现而被支持.
如果你的环境不支持 ES6 Promise,你可以使用 polyfill.

TypeScript支持

axios 包含 TypeScript definitions.

import axios from "axios";
axios.get("/user?ID=12345");

相关资源

Credits

axios 受Angular.提供的$http service 启发而创建, 致力于以提供一个脱离于ng的 $http模块。

开源协议

采用MIT


作者:Jadepeng
出处:jqpeng的技术记事本–http://www.cnblogs.com/xiaoqi
您的支持是对博主最大的鼓励,感谢您的认真阅读。
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

文章作者:jqpeng
原文链接: Spring boot web程序static资源放在jar外部

spring boot程序的static目录默认在resources/static目录, 打包为jar的时候,会把static目录打包进去,这样会存在一些问题:

  • static文件过多,造成jar包体积过大
  • 临时修改不方便

查看官方文档,可以发现,static其实是可以外置的。

方法1 直接修改配置文件

spring.resources.static-locations=file:///E://resources/static

自定义Configuration方法

@Configuration
public class StaticResourceConfiguration extends WebMvcConfigurerAdapter {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations("file:/path/to/my/dropbox/");
    }
}

推荐使用方法1,安全无害

相关阅读:Spring Boot配置文件放在jar外部


作者:Jadepeng
出处:jqpeng的技术记事本–http://www.cnblogs.com/xiaoqi
您的支持是对博主最大的鼓励,感谢您的认真阅读。
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

文章作者:jqpeng
原文链接: 易企秀前端压缩源码分析与还原

你是否想知道易企秀炫酷的H5是如何实现的,原理是什么,本文会为你揭秘并还原压缩过的源代码。

易企秀是一款h5页面制作工具,因方便易用成为业界标杆。后续一个项目会用到类似易企秀这样的自定义H5的功能,因此首先分析下易企秀的前端代码,看看他们是怎么实现的,再取其精华去其糟粕。
由于代码较多,且是压缩处理过的,阅读和还原起来较为困难,不过可以先大概分析下原理,然后有针对性的看主要代码,并借助VS Code等工具对变量、函数进行重命名,稍微耐心一点就能大概还原源代码。

分析数据模型

前端分析第一步,看看易企秀的数据模型:

数据模型

dataList是页面配置信息,elemengts是页面元素的配置信息,obj是H5的配置信息,

加载流程分析

查看H5源代码,发现入口函数是:

 eqShow.bootstrap();

顺藤摸瓜,大概分析下,主要流程如下:

加载主要流程

主要的功能函数在eqxiu和window对象下面,其中的重点是parsePage、renderPage和app,下面一一来分析。

parsePage

先看主要代码(重命名后的),主要功能是为每一页生成一个section并appendTo(“.nr”),另外如果页面有特效,加载相关js库并执行,最后再renderPage。

function parsePage(dataList, response) {

        for (var pageIndex = 1; pageIndex <= dataList.length; pageIndex++) {
            // 分页容器
            $('<section class="main-page"><div class="m-img" id="page' + pageIndex + '"></div></section>').appendTo(".nr");

            if (10 == pageMode) {
                $("#page" + pageIndex).parent(".main-page").wrap('<div class="flip-mask" id="flip' + pageIndex + '"></div>'),
                    $(".main-page").css({
                        width: $(".nr").width() + "px",
                        height: $(".nr").height() + "px"
                    });
            }

            if (dataList.length > 1 && 14 != pageMode && !response.obj.property.forbidHandFlip) {
                if (0 == pageMode || 1 == pageMode || 2 == pageMode || 6 == pageMode || 7 == pageMode ||
                    8 == pageMode || 11 == pageMode || 12 == pageMode || 13 == pageMode || 14 == pageMode) {
                    $('<section class="u-arrow-bottom"><div class="pre-wrap"><div class="pre-box1"><div class="pre1"></div></div><div class="pre-box2"><div class="pre2"></div></div></div></section>')
                        .appendTo("#page" + pageIndex)
                } else if (3 == pageMode || 4 == pageMode || 5 == pageMode || 9 == pageMode || 10 == pageMode) {
                    $('<section class="u-arrow-right"><div class="pre-wrap-right"><div class="pre-box3"><div class="pre3"></div></div><div class="pre-box4"><div class="pre4"></div></div></div></section>')
                        .appendTo("#page" + pageIndex);
                }
            }

              ....
           renderPage(eqShow, pageIndex, dataList);

                // 最后一页
                if (pageIndex == dataList.length) {
                    eqxiu.app($(".nr"), response.obj.pageMode, dataList, response);
                    addEnabledClassToPageCtrl(response);
                }
            }

        }

        hasSymbols || addReportToLastPage(dataList, response);
    }

渲染页面和组件

parsePage搭建了页面框架,renderPage实现页面渲染。

rendepage里,核心代码是:

eqShow.templateParser("jsonParser").parse({
    def: dataList[pageIndex - 1],
    appendTo: "#page" + pageIndex,
    mode: "view",
    disEvent: disEvent
});

templateParser负责将页面上的elements还原为组件,因此这里核心是要了解下templateParser,大致还原的代码如下:

            var jsonTemplateParser = eqShow.templateParser("jsonParser", function () {

                function createContainerFunction(container) {
                    return function (key, value) {
                        container[key] = value
                    }
                }

                function wrapComp(element, mode) {
                    try {
                        var comp = components[("" + element.type).charAt(0)](element, mode)
                    } catch (e) {
                        return
                    }
                    if (comp) {
                        var elementContainer = $('<li comp-drag comp-rotate class="comp-resize comp-rotate inside" id="inside_' + element.id + '" num="' +
                                element.num + '" ctype="' + element.type + '"></li>'),
                            elementType = ("" + element.type).charAt(0);

                        if ("3" !== elementType && "1" !== elementType) {
                            elementContainer.attr("comp-resize", "")
                        }

                        // 组件类型
                        /**
                         *  2 文本
                         *  3 背景
                         *  9 音乐
                         *  v video
                         *  4 图片
                         *  h shape形状
                         *  p 图集
                         *  5 输入框
                         *  r radio
                         *  c checkbox
                         *  z 多选按钮
                         *  a 评分组件
                         *  b 留言板
                         *  6 提交按钮
                         */
                        switch (elementType) {
                            case "p":
                                elementContainer.removeAttr("comp-rotate");
                                break;
                            case "1":
                                elementContainer.removeAttr("comp-drag");
                                break;
                            case "2": // 文本
                                elementContainer.addClass("wsite-text");
                                break;
                            case "3":
                                // 背景
                                break;
                            case "x":
                                elementContainer.addClass("show-text");
                                break;
                            case "4":
                                // image
                                element.properties.imgStyle && $(comp).css(element.properties.imgStyle), elementContainer.addClass("wsite-image");
                                break;
                            case "n":
                                elementContainer.addClass("wsite-image");
                                break;
                            case "h":
                                elementContainer.addClass("wsite-shape")
                                break;
                            case "5":
                                elementContainer.removeAttr("comp-input");
                                break;
                            case "6":
                            case "8":
                                elementContainer.removeAttr("comp-button");
                                break;
                            case "v":
                                elementContainer.removeAttr("comp-video");
                                elementContainer.addClass("wsite-video");
                                if (element.properties && element.properties.lock) {
                                    elementContainer.addClass("alock")
                                }
                                break;
                            case "b":
                                elementContainer.removeAttr("comp-boards");
                                elementContainer.attr("min-h", 60),
                                    elementContainer.attr("min-w", 230);
                                break;
                            default:
                                break;
                        }

                        elementContainer.mouseenter(function () {
                                $(this).addClass("inside-hover")
                            }),
                            elementContainer.mouseleave(function () {
                                $(this).removeClass("inside-hover")
                            });

                        // edit或者非文本type,再套一层
                        if ("edit" === jsonTemplateParser.mode || "x" !== ("" + element.type).charAt(0)) {
                            var elementBoxContent = $('<div class="element-box-contents">'),
                                elementBox = $('<div class="element-box">').append(elementBoxContent.append(comp));
                            elementContainer.append(elementBox),
                                "5" !== ("" + element.type).charAt(0) && "6" !== ("" + element.type).charAt(0) && "r" !== element.type && "c" !== element.type && "a" !== element.type && "8" !== element.type && "l" !== element.type && "s" !== element.type && "i" !== element.type && "h" !== element.type && "z" !== element.type || "edit" !== mode || $(comp).before($('<div class="element" style="position: absolute; height: 100%; width: 100%;z-index: 1;">'))
                        }

                        // 文本类型,处理font
                        var k, eleFonts = element.fonts || element.css.fontFamily || element.fontFamily;
                        if ("2" === elementType || "x" === elementType) {
                            for (var content = element.content, font_pattern = /font-family:(.*?);/g, matchResults = [], fonts = []; null !== (matchResults = font_pattern.exec(content));)
                                fonts.push(matchResults[1].trim());
                            if (1 !== fonts.length || "defaultFont" !== fonts[0] && "moren" !== fonts[0] || (eleFonts = null),
                                eleFonts) {
                                if ("view" === jsonTemplateParser.mode && element.css.fontFamily && window.scene && (window.scene.publishTime || !mobilecheck() && !tabletCheck() || (k = "@font-face{font-family:" + element.css.fontFamily + ';src: url("' + element.properties.localFontPath + '") format("truetype");}',
                                        b(k))),
                                    "object" == typeof eleFonts && eleFonts.constructor === Object) {
                                    if (!jQuery.isEmptyObject(eleFonts))
                                        for (var q in eleFonts)
                                            u[q] || ("edit" === jsonTemplateParser.mode ? k = "@font-face{font-family:" + q + ";src: url(" + PREFIX_FILE_HOST + eleFonts[q] + ") format(woff);}" : window.scene && window.scene.publishTime && (k = "@font-face{font-family:" + q + ';src: url("' + PREFIX_S2_URL + "fc/" + q + "_" + element.sceneId + "_" + scene.publishTime + '.woff") format("woff");}'),
                                                b(k),
                                                u[q] = !0)
                                } else
                                    u[eleFonts] || ("edit" === jsonTemplateParser.mode ? k = "@font-face{font-family:" + eleFonts + ";src: url(" + PREFIX_FILE_HOST + element.preWoffPath + ") format(woff);}" : window.scene && window.scene.publishTime && (k = "@font-face{font-family:" + eleFonts + ';src: url("' + PREFIX_S2_URL + "fc/" + eleFonts + "_" + element.sceneId + "_" + scene.publishTime + '.woff") format("woff");}'),
                                        b(k),
                                        u[eleFonts] = !0);
                                "edit" === jsonTemplateParser.mode && localStorage.setItem("shoppingFontFamily", JSON.stringify(u))
                            }
                        }

                        // 处理css
                        if (element.css) {
                            var elementWidth = 320 - parseInt(element.css.left, 10);
                            elementContainer.css({
                                width: elementWidth
                            });
                            elementContainer.css({
                                width: element.css.width,
                                height: element.css.height,
                                left: element.css.left,
                                top: element.css.top,
                                zIndex: element.css.zIndex,
                                bottom: element.css.bottom,
                                transform: element.css.transform
                            });
                            if (0 === element.css.boxShadowSize || "" + element.css.boxShadowSize == "0") {
                                element.css.boxShadow = "0px 0px 0px rgba(0,0,0,0.5)";
                                if ("edit" !== jsonTemplateParser.mode && "x" === ("" + element.type).charAt(0)) {
                                    return elementContainer.append(comp),
                                        elementContainer.find(".element-box").css({
                                            borderStyle: element.css.borderStyle,
                                            borderWidth: element.css.borderWidth,
                                            borderColor: element.css.borderColor,
                                            borderTopLeftRadius: element.css.borderTopLeftRadius,
                                            borderTopRightRadius: element.css.borderTopRightRadius,
                                            borderBottomRightRadius: element.css.borderBottomRightRadius,
                                            borderBottomLeftRadius: element.css.borderBottomLeftRadius,
                                            boxShadow: element.css.boxShadow,
                                            backgroundColor: element.css.backgroundColor,
                                            opacity: element.css.opacity,
                                            width: "100%",
                                            height: "100%",
                                            overflow: "hidden"
                                        }),
                                        elementContainer.find("img").css({
                                            width: "100%"
                                        }),
                                        elementContainer;
                                }
                            }

                            // Android 微信,图片,设置borderColor
                            isAndroid() &&
                                isWeixin() &&
                                "" + element.type == "4" &&
                                "0px" !== element.css.borderRadius &&
                                0 === element.css.borderWidth &&
                                element.properties.anim && (element.css.borderWidth = 1, element.css.borderColor = "rgba(0,0,0,0)");

                            var elementCss = $.extend(!0, {}, element.css);
                            delete elementCss.fontFamily,
                                elementBox.css(elementCss).css({
                                    width: "100%",
                                    height: "100%",
                                    transform: "none"
                                }),
                                elementBox.children(".element-box-contents").css({
                                    width: "100%",
                                    height: "100%"
                                }),
                                // 设置宽高
                                "4" !== ("" + element.type).charAt(0) &&
                                "n" !== ("" + element.type).charAt(0) &&
                                "p" !== ("" + element.type).charAt(0) &&
                                "h" !== ("" + element.type).charAt(0) && "t" !== ("" + element.type).charAt(0) &&
                                "z" !== ("" + element.type).charAt(0) &&
                                $(comp).css({
                                    width: element.css.width,
                                    height: element.css.height
                                }),
                                // w01 w02 设置lineHeight
                                ("w01" === element.type || "w02" === element.type) &&
                                $(comp).css({
                                    lineHeight: element.css.height + "px"
                                }),
                                // shape 类型
                                "h" === ("" + element.type).charAt(0) &&
                                ($(comp).find("g").length ?
                                    $(comp).find("g").attr("fill", element.css.color) :
                                    $(comp).children().attr("fill", element.css.color),
                                    elementBox.children(".element-box-contents").css("position", "relative"))
                        }
                        return elementContainer
                    }
                }

                /**
                 * 将element按zindex排序
                 */
                function sortElementsByZindex(elements) {
                    for (var i = 0; i < elements.length - 1; i++)
                        for (var j = i + 1; j < elements.length; j++)
                            if (parseInt(elements[i].css.zIndex, 10) > parseInt(elements[j].css.zIndex, 10)) {
                                var element = elements[i];
                                elements[i] = elements[j],
                                    elements[j] = element
                            }
                    for (var e = 0; e < elements.length; e++)
                        elements[e].css.zIndex = e + 1 + "";
                    return elements
                }

                function parseElements(pageDef, $edit_wrapper, mode) {
                    $edit_wrapper = $edit_wrapper.find(".edit_area");
                    var i, elements = pageDef.elements;
                    if (elements)
                        for (elements = sortElementsByZindex(elements),
                            i = 0; i < elements.length; i++)
                            if (elements[i].sceneId = pageDef.sceneId,
                                "" + elements[i].type == "3") {
                                // type == 3 
                                var component = components[("" + elements[i].type).charAt(0)](elements[i], $edit_wrapper);

                                // if is edit mode, dispatch edit event
                                "edit" === mode
                                    &&
                                    editEvents[("" + elements[i].type).charAt(0)] &&
                                    editEvents[("" + elements[i].type).charAt(0)](component, elements[i])
                            } else {
                                var comp = wrapComp(elements[i], mode);
                                if (!comp)
                                    continue;
                                $edit_wrapper.append(comp);

                                // invoke interceptors
                                for (var n = 0; n < interceptors.length; n++)
                                    interceptors[n](comp, elements[i], mode);

                                afterRenderEvents[("" + elements[i].type).charAt(0)] &&
                                    (
                                        afterRenderEvents[("" + elements[i].type).charAt(0)](comp, elements[i]),
                                        "edit" !== mode && (
                                            parseElementTrigger(comp, elements[i]),
                                            r(comp, elements[i])
                                        )
                                    ),

                                    "edit" === mode &&
                                    editEvents[("" + elements[i].type).charAt(0)] &&
                                    editEvents[("" + elements[i].type).charAt(0)](comp, elements[i])
                            }
                }

                function getEventHandlers() {
                    return editEvents
                }

                function getComponents() {
                    return components
                }

                function addInterceptor(interceptor) {
                    interceptors.push(interceptor)
                }

                function getInterceptors() {
                    return interceptors
                }
                var components = {},
                    editEvents = {},
                    afterRenderEvents = {},
                    interceptors = [],
                    _width = containerWidth = 320,
                    _height = containerHeight = 486,
                    p = 1,
                    s = 1,
                    parser = {
                        getComponents: getComponents,
                        getEventHandlers: getEventHandlers,
                        addComponent: createContainerFunction(components),
                        bindEditEvent: createContainerFunction(editEvents),
                        bindAfterRenderEvent: createContainerFunction(afterRenderEvents),
                        addInterceptor: addInterceptor,
                        getInterceptors: getInterceptors,
                        wrapComp: wrapComp,
                        disEvent: !1,
                        mode: "view",
                        parse: function (parseInfo) {
                            var edit_wrapper = $('<div class="edit_wrapper" data-scene-id="' + parseInfo.def.sceneId + '"><ul eqx-edit-destroy id="edit_area' + parseInfo.def.id + '" paste-element class="edit_area weebly-content-area weebly-area-active"></div>'),
                                mode = this.mode = parseInfo.mode;
                            // page 定义
                            this.def = parseInfo.def,
                                parseInfo.disEvent && (this.disEvent = !0),
                                "view" === mode && tplCount++;
                            // 页面容器
                            var pageContainer = $(parseInfo.appendTo);
                            return containerWidth = pageContainer.width(),
                                containerHeight = pageContainer.height(),
                                p = _width / containerWidth,
                                s = _height / containerHeight,
                                parseElements(parseInfo.def, edit_wrapper.appendTo($(parseInfo.appendTo)), mode)
                        }
                    };
                return parser
            });

上面的重点是parseElements,先把elements按zindex排序,然后逐个渲染。
注意,渲染是根据elementType,从components找到对应的组件,然后创建一个实例,因此这里要单独说下组件是如何定义的。

先看下一个组件的配置信息大概是这样,有id,css,type和动画等配置信息:

    {
    "id": 29,
    "css": {
        "top": 124.93546211843,
        "left": 62.967731059217,
        "color": "#676767",
        "width": 195,
        "height": 195,
        "zIndex": "1",
        "opacity": 1,
        "boxShadow": "0px 0px 0px rgba(0,0,0,0.5)",
        "transform": "rotateZ(45deg)",
        "lineHeight": 1,
        "paddingTop": 0,
        "borderColor": "rgba(255,255,255,1)",
        "borderStyle": "double",
        "borderWidth": 4,
        "borderRadius": "0px",
        "boxShadowSize": 0,
        "paddingBottom": 0,
        "backgroundColor": "rgba(252,230,238,0.16)",
        "borderRadiusPerc": 0,
        "boxShadowDirection": 0,
        "textAlign": "left",
        "borderBottomRightRadius": "0px",
        "borderBottomLeftRadius": "0px",
        "borderTopRightRadius": "0px",
        "borderTopLeftRadius": "0px"
    },
    "type": "2",
    "pageId": "24642",
    "content": "<div style=\"text-align: center;\"><br></div>",
    "sceneId": 8831293,
    "properties": {
        "anim": {
            "type": 4,
            "delay": 0.6,
            "countNum": 1,
            "duration": 1,
            "direction": 0
        },
        "width": 195,
        "height": 195
    }
}

jsonParser里用一个components对象存储组件,通过addComponent添加组件,key就是组件的type:

addComponent: createContainerFunction(components)
function createContainerFunction(container) {
                return function (key, value) {
                    container[key] = value
                }
            }

添加组件时,type 作为key,value为创建组件的函数:

// 添加组件1jsonTemplateParser.addComponent("1", function (element, mode) {    var comp = document.createElement("div");    if (comp.id = element.id,        comp.setAttribute("class", "element comp_title"),        // 设置组件content        element.content && (comp.textContent = element.content),        element.css) {        var item, elementCss = element.css;        for (item in elementCss)            comp.style[item] = elementCss[item]    }    if (element.properties.labels)        for (var labels = element.properties.labels, f = 0; f < labels.length; f++)            $('<a class = "label_content" style = "display: inline-block;">')            .appendTo($(comp))            .html(labels[f].title)            .css(labels[f].color)            .css("width", 100 / labels.length + "%");    return comp});

这样渲染组件时,根据element的类型就能找到createCompFunction,从而创建组件。

组件动画播放

H5之所以炫酷,很大一部分因为每个组件都有定制好的CSS3动画,我们这里来看看这些动画是如何执行的。

代码还是上一部分的代码,我们注意到组件渲染后,有一段代码;

  // invoke interceptorsfor (var n = 0; n < interceptors.length; n++)    interceptors[n](comp, elements[i], mode);

执行interceptors,这个interceptors可以通过addInterceptor注册拦截器,在组件渲染完成后会调用定义的拦截器,组件的动画就是这样来调用的。

        // 添加拦截器执行动画
        jsonTemplateParser.addInterceptor(function (comp, element, mode) {
            eqxCommon.animation(comp, element, mode, jsonTemplateParser.def.properties)
        });

我们发现,eqxiu通过addInterceptor注册了一个拦截器,该拦截器调用eqxCommon.animation执行组件动画,因此分析eqxCommon.animation就可以了解动画是如何实现的。

还是先看element里的定义:

     "properties": {        "anim": {            "type": 4,            "delay": 0.6,            "countNum": 1,            "duration": 1,            "direction": 0        },

我们看到,anim里定义了type,delay,duration等配置信息,可以设想播放动画无非就是解析这个配置,然后执行,其中type应该是对应的各种动画类型,分析代码吧,下面给出破解后的代码:

        // 动画播放序号
        var animIndex = 0;

        // 处理动画属性
        if (element.properties && element.properties.anim) {
            var anim = [];
            element.properties.anim.length ? anim = element.properties.anim : anim.push(element.properties.anim);
            var elementBox = $(".element-box", comp);
            elementBox.attr("element-anim", "");

            // 找出animations
            for (var animType, animTypes = [], anims = [], index = 0, animLength = anim.length; animLength > index; index++)
                if (null != anim[index].type &&
                    -1 != anim[index].type) {
                    animType = eqxCommon.convertType(anim[index]),
                        animTypes.push(animType),
                        anims.push(anim[index]);
                }

            if (properties && properties.scale)
                return;

            // 动画播放类型
            element.properties.anim.trigger ?
                comp.click(function () {
                    // 点击播放
                    playAnimation(elementBox, animType, element.properties.anim)
                }) :
                properties && properties.longPage ?
                playAnimation(elementBox, animTypes, anims, !0, element.css) // longpage
                :
                playAnimation(elementBox, animTypes, anims)
        }

上面的逻辑是先从element里找到anim,放入数组,然后再playAnimation。这里使用了convertType函数将数字type转换为真实的动画类型:

var convertType = function (a) {
        var animType, c, d = a.type;
        return "typer" === d && (animType = "typer"),
            0 === d && (animType = "fadeIn"),
            1 === d && (c = a.direction,
                0 === c && (animType = "fadeInLeft"),
                1 === c && (animType = "fadeInDown"),
                2 === c && (animType = "fadeInRight"),
                3 === c && (animType = "fadeInUp")),
            6 === d && (animType = "wobble"),
            5 === d && (animType = "rubberBand"),
            7 === d && (animType = "rotateIn"),
            8 === d && (animType = "flip"),
            9 === d && (animType = "swing"),
            2 === d && (c = a.direction,
                0 === c && (animType = "bounceInLeft"),
                1 === c && (animType = "bounceInDown"),
                2 === c && (animType = "bounceInRight"),
                3 === c && (animType = "bounceInUp")),
            3 === d && (animType = "bounceIn"),
            4 === d && (animType = "zoomIn"),
            10 === d && (animType = "fadeOut"),
            11 === d && (animType = "flipOutY"),
            12 === d && (animType = "rollIn"),
            13 === d && (animType = "lightSpeedIn"),
            14 === d && (animType = "bounceOut"),
            15 === d && (animType = "rollOut"),
            16 === d && (animType = "lightSpeedOut"),
            17 === d && (c = a.direction,
                0 === c && (animType = "fadeOutRight"),
                1 === c && (animType = "fadeOutDown"),
                2 === c && (animType = "fadeOutLeft"),
                3 === c && (animType = "fadeOutUp")),
            18 === d && (animType = "zoomOut"),
            19 === d && (c = a.direction,
                0 === c && (animType = "bounceOutRight"),
                1 === c && (animType = "bounceOutDown"),
                2 === c && (animType = "bounceOutLeft"),
                3 === c && (animType = "bounceOutUp")),
            20 === d && (animType = "flipInY"),
            21 === d && (animType = "tada"),
            22 === d && (animType = "jello"),
            23 === d && (animType = "flash"),
            26 === d && (animType = "twisterInDown"),
            27 === d && (animType = "puffIn"),
            28 === d && (animType = "puffOut"),
            29 === d && (animType = "slideDown"),
            30 === d && (animType = "slideUp"),
            24 === d && (animType = "flipInX"),
            25 === d && (animType = "flipOutX"),
            31 === d && (animType = "twisterInUp"),
            32 == d && (animType = "vanishOut"),
            33 == d && (animType = "vanishIn"),
            animType
    };

播放动画函数在playAnimation里:

 elementBox.css("animation", "");
                elementBox.css("animation", animTypes[animIndex] + " " + anims[animIndex].duration + "s ease " + anims[animIndex].delay + "s " +
                    (anims[animIndex].countNum ? anims[animIndex].countNum : ""));                 anims[animIndex].count && animIndex == anims.length - 1 && elementBox.css("animation-iteration-count", "infinite");
                    elementBox.css("animation-fill-mode", "both");

最后,如果有多个动画,在播放完成后继续播放下一个:

// 动画播放结束,播放下一个动画(一个组件可能有多个动画)
                elementBox.one("webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend", function () {
                    animIndex++;
                    playAnimation(elementBox, animTypes, anims);
                })

页面切换

由于是多页应用,因此涉及到页面切换,并且页面切换时还需要有对应的切换动画,改工作是由一个eqxiu对象来管理和实现的。

老套路,先看这块的配置吧,页面的配置在obj下面,其中pageMode定义了翻页效果:

"obj": {
    "id": 8831293,
    "name": "房产广告",
    "createUser": "1",
    "type": 103,
    "pageMode": 4,
    "image": {},
    "property": "{\"triggerLoop\":true,\"slideNumber\":true,\"autoFlipTime\":4,\"shareDes\":\"\",\"eqAdType\":1,\"hideEqAd\":false,\"autoFlip\":true,\"lastPageId\":604964}",
    "timeout": "",
    "timeout_url": "",
    "accessCode": null,
    "cover": "syspic/pageimg/yq0KA1UrbkOAV_yiAAFuhyGx9LE397.jpg",
    "bgAudio": "{\"url\":\"syspic/mp3/yq0KA1RHT3iAMXYOAAgPq1MjV9M930.mp3\",\"type\":\"3\"}",
    "isTpl": 0,
    "isPromotion": 0,
    "status": 1,
    "openLimit": 0,
    "startDate": null,
    "endDate": null,
    "updateTime": 1426045746000,
    "createTime": 1426572693000,
    "publishTime": 1426572693000,
    "applyTemplate": 0,
    "applyPromotion": 0,
    "sourceId": null,
    "code": "U903078B74Q5",
    "description": "房产广告",
    "sort": 0,
    "pageCount": 0,
    "dataCount": 0,
    "showCount": 44,
    "eqcode": "",
    "userLoginName": null,
    "userName": null
},

pagemode是这样定义的:

pagemodes = [{        id: 0,        name: "上下翻页"    }, {        id: 4,        name: "左右翻页"    }, {        id: 1,        name: "上下惯性翻页"    }, {        id: 3,        name: "左右惯性翻页"    }, {        id: 11,        name: "上下连续翻页"    }, {        id: 5,        name: "左右连续翻页"    }, {        id: 6,        name: "立体翻页"    }, {        id: 7,        name: "卡片翻页"    }, {        id: 8,        name: "放大翻页"    }, {        id: 9,        name: "交换翻页"    }, {        id: 10,        name: "翻书翻页"    }, {        id: 12,        name: "掉落翻页"    }, {        id: 13,        name: "淡入翻页"    }];

在renderpage结束后,调用eqxiu.app:

                  // 最后一页
                if (pageIndex == dataList.length) {
                    eqxiu.app($(".nr"), response.obj.pageMode, dataList, response);
                    addEnabledClassToPageCtrl(response);
                }

来分析eqxiu.app代码,通过pagemode,我们可以看出翻页打开分为上下翻页、左右翻页两个大类:

if ("8" == pageMode || "9" == pageMode) {
            transformTime = 0.7;
            timeoutDelay = 800;
        }
        // 上下翻页  上下惯性翻页 立体翻页 卡片翻页 放大翻页 上下连续翻页 上下连续翻页
        if (0 == pageMode || (1 == pageMode || (2 == pageMode || (6 == pageMode || (7 == pageMode || (8 == pageMode || (11 == pageMode || 12 == pageMode))))))) {
            /** @type {boolean} */
            upDownMode = true;
        } else {
            // 左右惯性翻页 左右翻页 左右连续翻页  翻书翻页
            if (3 == pageMode || (4 == pageMode || (5 == pageMode || 10 == pageMode))) {
                /** @type {boolean} */
                leftRightMode = true;
            }
        }

然后配置里有一个autoFlip,代表是否自动翻页,通过setInterval设置定时翻页任务:

        // 自动翻页
        if (response.obj.property.autoFlip) {
            // 自动翻页时间
            autoFlipTimeMS = 1000 * response.obj.property.autoFlipTime;
            setAndStartAutoFlip(autoFlipTimeMS);
        }    
    /**
     * 设置翻页时间间隔并启动翻页
     * @param {number} textStatus
     * @return {undefined}
     */
    function setAndStartAutoFlip(autoFlipTime) {
        autoFlipTime = autoFlipTime;
        pauseAutoFlip();
        startAutoFlip();
    }           /**
     * 启动自动翻页
     * @return {undefined}
     */
    function startAutoFlip() {
        // 通过setInterval
        autoFlipIntervalId = setInterval(function () {
            if (!(10 === self._scrollMode)) {
                if (!isTouching) {
                    nextPage();
                }
            }
        }, autoFlipTimeMS);
    }

默认情况下H5是支持touch滑动翻页的,这种滑动操作一般是监听相关事件,开始滑动、滑动中和滑动结束,为了同时支持移动端和PC端,还需要加上鼠标点击事件:

        var isTouch = false;
        self._$app.on("mousedown touchstart", function (e) {
            if (!self._isforbidHandFlip) {
                onTouchStart(e);
                isTouch = true;
            }
        }).on("mousemove touchmove", function (e) {
            if (!self._isforbidHandFlip) {
                if (isTouch) {
                    onTouchMove(e);
                }
            }
        }).on("mouseup touchend mouseleave", function (events) {
            if (!self._isforbidHandFlip) {
                onTouchEnd(events);
                /** @type {boolean} */
                isTouch = false;
            }
        });

翻页的核心无非就是判断位移是否超过特定的值,比如左右翻页X位移是否大于Y位移并且X的偏移量大于20。因此onTouchStart开始时,记录初始位置,onTouchMove时计算offset变化,按照pageMode执行对应的动画,onTouchEnd时判断位移是否足够,足够就切换页面,否则复位。

/**
         * 开始滑动
         * @param {Object} e
         * @return {undefined}
         */
        onTouchStart = function (e) {
            /** @type {boolean} */
            fa = false;
            if (isMobile) {
                if (e) {
                    e = event;
                }
            }
            if (!self._isDisableFlipPage) {
                self.$currentPage = self._$pages.filter(".z-current").get(0);
                if (!C) {
                    /** @type {null} */
                    self.$activePage = null;
                }
                if (self.$currentPage) {
                    if (completeEffect($(self.$currentPage))) {
                        isTouching = true;
                        isCursorAtEnd = false;
                        ignoreEvent = true;
                        offsetX = 0;
                        offsetY = 0;
                        if (e && "mousedown" == e.type) {
                            currentPageX = e.pageX;
                            currentPageY = e.pageY;
                        } else if (e && "touchstart" == e.type) {
                            currentPageX = e.touches ? e.touches[0].pageX : e.originalEvent.touches[0].pageX;
                            currentPageY = e.touches ? e.touches[0].pageY : e.originalEvent.touches[0].pageY;
                        }
                        self.$currentPage.classList.add("z-move");
                        setAttribute(self.$currentPage.style, "Transition", "none");
                        if ("12" == self._scrollMode) {
                            /** @type {number} */
                            self.$currentPage.style.zIndex = 3;
                        }
                    }
                }
            }
        };

        /**
         * 滑动处理
         * @param {Object} e
         * @return {undefined}
         */
        onTouchMove = function (e) {
            if (isMobile) {
                if (e) {
                    e = event;
                }
            }
            if (isTouching) {
                if (self._$pages.length > 1) {
                    if (e && "mousemove" == e.type) {
                        offsetX = e.pageX - currentPageX;
                        offsetY = e.pageY - currentPageY;
                    } else {
                        if (e) {
                            if ("touchmove" == e.type) {
                                offsetX = (e.touches ? e.touches[0].pageX : e.originalEvent.touches[0].pageX) - currentPageX;
                                offsetY = (e.touches ? e.touches[0].pageY : e.originalEvent.touches[0].pageY) - currentPageY;
                            }
                        }
                    }
                    if (!fa) {
                        if (Math.abs(offsetX) > 20 || Math.abs(offsetY) > 20) {
                            /** @type {boolean} */
                            fa = true;
                        }
                    }

                    switch (self._scrollMode + "") {
                        case "0":
                        case "1":
                        case "2":
                        case "15":
                            //上下翻页
                            upDownFlip();
                            break;
                        case "3":
                        case "4":
                            // 左右翻页
                            leftRightFlip();
                            break;
                        case "5":
                            // 左右连续翻页
                            leftRightLoopFlip();
                            break;
                        case "7":
                            cardFlip();
                            break;
                        case "8":
                            scaleUpFlip();
                            break;
                        case "9":
                            switchFlip();
                            break;
                        case "11":
                            //上下连续翻页
                            upDownContinuousFlip();
                            break;
                        case "12":
                            //掉落翻页
                            dropFlip();
                            break;
                        case "13":
                        case "14":
                            //淡入翻页
                            fadeFlip();
                            break;
                        default:
                            break;
                    }
                }
            }
        };


        /**
         *  滑动结束
         * @param {?} e
         * @return {undefined}
         */
        onTouchEnd = function (e) {
            if (isTouching && completeEffect($(self.$currentPage))) {
                isTouching = false;
                if (self.$activePage) {
                    self._isDisableFlipPage = true;
                    var ease;
                    ease = "6" == self._scrollMode || "7" == self._scrollMode ? "cubic-bezier(0,0,0.99,1)" : "12" == self._scrollMode ? "cubic-bezier(.17,.67,.87,.13)" : "linear";
                    self.$currentPage.style.webkitTransition = "-webkit-transform " + transformTime + "s " + ease;
                    self.$activePage.style.webkitTransition = "-webkit-transform " + transformTime + "s " + ease;
                    self.$currentPage.style.mozTransition = "-moz-transform " + transformTime + "s " + ease;
                    self.$activePage.style.mozTransition = "-moz-transform " + transformTime + "s " + ease;
                    self.$currentPage.style.transition = "transform " + transformTime + "s " + ease;
                    self.$activePage.style.transition = "transform " + transformTime + "s " + ease;

                    // 完成翻页
                    if ("0" == self._scrollMode || ("2" == self._scrollMode || ("1" == self._scrollMode || "15" == self._scrollMode))) {
                        endUpDownFlip();
                    } else if ("4" == self._scrollMode || "3" == self._scrollMode) {
                        // 左右翻页
                        endLeftRightFlip();
                    } else if ("5" == self._scrollMode) {
                        //左右连续翻页
                        endLeftRightContinueFlip();
                    } else if ("6" == self._scrollMode) {
                        //立体翻页
                        endCubeFlip();
                    } else if ("7" == self._scrollMode) {
                        //卡片翻页
                        endCardFlip();
                    } else if ("8" == self._scrollMode) {
                        //放大翻页
                        endScaleUpFlip();
                    } else if ("9" == self._scrollMode) {
                        //交换翻页
                        endSwitchFlip();
                    } else if ("11" == self._scrollMode) {
                        //上下连续翻页
                        endUpDownContinueFlip();
                    } else if ("12" == self._scrollMode) {
                        //掉落翻页
                        endDropFlip();
                    } else if ("13" == self._scrollMode || "14" == self._scrollMode) {
                        //淡入翻页
                        endFadeFlip();
                    } 

                    /** @type {number} */
                    var pageIndex = $(self.$activePage).find(".m-img").attr("id").replace("page", "") - 1;
                    if (self._pageData[pageIndex].properties) {
                        if (self._pageData[pageIndex].properties.longPage) {
                            $(document).trigger("clearTouchPos");
                        }
                    }
                    $(self.$activePage).find("li.comp-resize").each(function (dataAndEvents) {
                        /** @type {number} */
                        var i = 0;
                        for (; i < self._pageData[pageIndex].elements.length; i++) {
                            if (self._pageData[pageIndex].elements[i].id == parseInt($(this).attr("id").substring(7), 10)) {
                                eqxCommon.animation($(this), self._pageData[pageIndex].elements[i], "view", self._pageData[pageIndex].properties);
                                var r20 = getComp(self._pageData[pageIndex].elements[i].id);
                                eqxCommon.bindTrigger(r20, self._pageData[pageIndex].elements[i]);
                            }
                        }
                    });
                    /** @type {number} */
                    var i = 0;
                    for (; i < self._pageData.length; i++) {
                        if (self._pageData[i].effObj) {
                            /** @type {boolean} */
                            self._pageData[i].effObj.pause = true;
                        }
                    }
                    if (self._pageData[pageIndex].effObj) {
                        self._pageData[pageIndex].effObj.startPlay();
                    }
                    eqShow.setPageHis(self._pageData[pageIndex].id);
                } else {
                    self.$currentPage.classList.remove("z-move");
                }
            }
            C = false;
        };

然后再来看自动翻页nextPage

  /**
     * 启动自动翻页
     * @return {undefined}
     */
    function startAutoFlip() {
        // 通过setInterval
        autoFlipIntervalId = setInterval(function () {
            if (!(10 === self._scrollMode)) {
                if (!isTouching) {
                    nextPage();
                }
            }
        }, autoFlipTimeMS);
    }

自动翻页比较简单,模拟滑动操作,当位移足够时就可以自动翻页了:

/**
     *  上一页
     * @param {number} direction
     * @return {undefined}
     */
    function prePage(direction) {
        if (!(leftRightMode && 2 == direction || upDownMode && 1 == direction)) {
            if ("10" != self._scrollMode) {
                var offset = 0;
                // 开启滑动
                onTouchStart();
                // 定时器,增加offset,模拟滑动
                var poll = setInterval(function () {
                    offset += 2;
                    if ("0" == self._scrollMode || ("1" == self._scrollMode || ("2" == self._scrollMode || ("6" == self._scrollMode || ("7" == self._scrollMode || ("8" == self._scrollMode || ("11" == self._scrollMode || ("12" == self._scrollMode || ("13" == self._scrollMode || ("14" == self._scrollMode || "15" == self._scrollMode)))))))))) {
                        // 纵向翻页,增加y
                        offsetY = offset;
                    } else {
                        if ("3" == self._scrollMode || ("4" == self._scrollMode || ("5" == self._scrollMode || "9" == self._scrollMode))) {
                            // 横向翻页,增加x
                            offsetX = offset;
                        }
                    }
                    // 触发move操作,模拟滑动
                    onTouchMove();
                    if (offset >= 21) {
                        // 位移超过20,
                        clearInterval(poll);
                        // 停止滑动,完成翻页
                        onTouchEnd();
                    }
                }, 1);
            } else {
                // 翻书
                $(document).trigger("bookFlipPre");
            }
        }
    }

    /**
     * 下一页,逻辑和prePage类似
     * @param {number} direction
     * @return {undefined}
     */
    function nextPage(direction) {
        if (!(leftRightMode && 2 == direction || upDownMode && 1 == direction)) {
            if ("10" != self._scrollMode) {
                u = false;
                var offset = 0;
                if ("block" == $("body .boards-panel").css("display")) {
                    $("body .boards-panel").hide();
                    $("body .z-current").show();
                }
                onTouchStart();
                var poll = setInterval(function () {
                    offset -= 2;
                    if ("0" == self._scrollMode || ("1" == self._scrollMode || ("2" == self._scrollMode || ("6" == self._scrollMode || ("7" == self._scrollMode || ("8" == self._scrollMode || ("11" == self._scrollMode || ("12" == self._scrollMode || ("13" == self._scrollMode || ("14" == self._scrollMode || "15" == self._scrollMode)))))))))) {
                        offsetY = offset;
                    } else {
                        if ("3" == self._scrollMode || ("4" == self._scrollMode || ("5" == self._scrollMode || "9" == self._scrollMode))) {
                            offsetX = offset;
                        }
                    }
                    onTouchMove();
                    if (-21 >= offset) {
                        clearInterval(poll);
                        onTouchEnd();
                        if (!triggerLoop) {
                            if (!self.$activePage) {
                                clearInterval(autoFlipIntervalId);
                            }
                        }
                    }
                }, 1);
            } else {
                $(document).trigger("bookFlipNext");
            }
        }
    }

总结

上面是花了大概一天多的时间阅读代码的成果,总结经验就是阅读代码先分析大的流程,再层层递进分析一些细节,就能一步一步接近真相。

另外,阅读压缩过的代码,可以借助VS Code,善用F2重命名,修改的越多,越接近本来的代码:)


作者:Jadepeng
出处:jqpeng的技术记事本–http://www.cnblogs.com/xiaoqi
您的支持是对博主最大的鼓励,感谢您的认真阅读。
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

文章作者:jqpeng
原文链接: Spring boot自定义启动字符画(banner)

spring boot项目启动时会打印spring boot的ANSI字符画,可以进行自定义。

如何自定义

实现方式非常简单,我们只需要在Spring Boot工程的/src/main/resources目录下创建一个banner.txt文件,然后将ASCII字符画复制进去,就能替换默认的banner了。

█████████████████████████████████████████████████████████████████████████████████████████████████████

 █████╗ ██╗██╗   ██╗██╗    ███████╗ █████╗  █████╗ ███████╗
██╔══██╗██║██║   ██║██║    ██╔════╝██╔══██╗██╔══██╗██╔════╝
███████║██║██║   ██║██║    ███████╗███████║███████║███████╗
██╔══██║██║██║   ██║██║    ╚════██║██╔══██║██╔══██║╚════██║
██║  ██║██║╚██████╔╝██║    ███████║██║  ██║██║  ██║███████║
╚═╝  ╚═╝╚═╝ ╚═════╝ ╚═╝    ╚══════╝╚═╝  ╚═╝╚═╝  ╚═╝╚══════╝

█████████████████████████████████████████████████████████████████████████████████████████████████████

如何生成字符画

如果让我们手工的来编辑这些字符画,显然是一件非常困难的差事。

正好刚接触jhipster,发现有一个 generator-jhipster-banner插件,可以生成banner.

首先安装:

npm install -g generator-jhipster-banner

使用:

yo jhipster-banner.

没有安装yo的,先安装:

npm install -g yo

按提示输入文本和选择颜色即可。

其他方法:

http://patorjk.com/software/taag
http://www.network-science.de/ascii/
http://www.degraeve.com/img2txt.php

彩蛋:永不宕机佛祖

${AnsiColor.BRIGHT_YELLOW}
////////////////////////////////////////////////////////////////////
//                          _ooOoo_                               //
//                         o8888888o                              //
//                         88" . "88                              //
//                         (| ^_^ |)                              //
//                         O\  =  /O                              //
//                      ____/`---'\____                           //
//                    .'  \\|     |//  `.                         //
//                   /  \\|||  :  |||//  \                        //
//                  /  _||||| -:- |||||-  \                       //
//                  |   | \\\  -  /// |   |                       //
//                  | \_|  ''\---/''  |   |                       //
//                  \  .-\__  `-`  ___/-. /                       //
//                ___`. .'  /--.--\  `. . ___                     //
//              ."" '<  `.___\_<|>_/___.'  >'"".                  //
//            | | :  `- \`.;`\ _ /`;.`/ - ` : | |                 //
//            \  \ `-.   \_ __\ /__ _/   .-` /  /                 //
//      ========`-.____`-.___\_____/___.-`____.-'========         //
//                           `=---='                              //
//      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^        //
//            佛祖保佑       永不宕机     永无BUG                  //
////////////////////////////////////////////////////////////////////

作者:Jadepeng
出处:jqpeng的技术记事本–http://www.cnblogs.com/xiaoqi
您的支持是对博主最大的鼓励,感谢您的认真阅读。
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

文章作者:jqpeng
原文链接: gitbook安装与使用,并使用docker部署

本文简单介绍如何安装并使用gitbook,最后如何使用docker构建书籍镜像。

1. 前置条件

需要Nodejs环境,安装npm,国内用户再安装cnpm

npm install -g cnpm --registry=https://registry.npm.taobao.org

2. 安装gitbook

cnpm install -g gitbook-cli
gitbook -V 
CLI version: 2.3.2
Installing GitBook 3.2.3
gitbook@3.2.3 ..\AppData\Local\Temp\tmp-20544doJtj1hfVp40\node_modules\gitbook
├── escape-string-regexp@1.0.5
├── escape-html@1.0.3
 。。。。
GitBook version: 3.2.3

3. gitbook使用

3.1 生成目录和图书结构

mkdir docker-start
gitbook init
warn: no summary file in this book
info: create README.md
info: create SUMMARY.md
info: initialization is finished

编辑SUMMARY.md,输入:

* [简介](README.md)
* [1.Docker入门](chapter1/README.md)
 - [1.1 什么是Docker](chapter1/section1.md)
 - [1.2 Docker基本概念](chapter1/section2.md)
 - [1.3 安装Docker](chapter1/section3.md)
 - [1.4 使用Docker镜像](chapter1/section4.md)
 - [1.5 操作容器](chapter1/section5.md)
 - [1.6 访问仓库](chapter1/section6.md)
 - [1.6 数据管理](chapter1/section7.md)
* [2.使用Docker部署web应用](chapter2/README.md)
 - [2.1 编写DockerFile](chapter2/section1.md)
 - [2.2 编写web应用](chapter2/section2.md)
 - [2.3 构建镜像](chapter2/section3.md)
 - [2.4 运行web应用](chapter2/section4.md)
 - [2.5 分享镜像](chapter2/section5.md)
* [结束](end/README.md)

再次执行:

gitbook init
info: create chapter1/README.md
info: create chapter1/section1.md
info: create chapter1/section2.md
info: create chapter1/section3.md
info: create chapter1/section4.md
info: create chapter1/section5.md
info: create chapter1/section6.md
info: create chapter1/section7.md
info: create chapter2/README.md
info: create chapter2/section1.md
info: create chapter2/section2.md
info: create chapter2/section3.md
info: create chapter2/section4.md
info: create chapter2/section5.md
info: create end/README.md
info: create SUMMARY.md
info: initialization is finished

3.2 生成图书

使用:

gitbook serve .
Live reload server started on port: 35729
Press CTRL+C to quit ...

info: 7 plugins are installed
info: loading plugin "livereload"... OK
info: loading plugin "highlight"... OK
info: loading plugin "search"... OK
info: loading plugin "lunr"... OK
info: loading plugin "sharing"... OK
info: loading plugin "fontsettings"... OK
info: loading plugin "theme-default"... OK
info: found 16 pages
info: found 15 asset files
info: >> generation finished with success in 4.0s !

Starting server ...
Serving book on http://localhost:4000

访问 http://localhost:4000 ,就可以看到图书了

enter description here

编辑生成的md,gitbook会自动Restart,

enter description here

在当前目录下,会生成一个_book目录 ,里面是生成的静态html,可以发布到服务器直接使用。

4. 使用docker发布gitbook书籍

首先 将_book目录里的内容拷贝到一个新目录。

然后编写Dockerfile

FROM nginx
WORKDIR /usr/share/nginx/html
ADD . /usr/share/nginx/html
EXPOSE 80

build:

docker build -t docker-start-web .
Sending build context to Docker daemon  4.766MB
Step 1/4 : FROM nginx
 ---> 3f8a4339aadd
Step 2/4 : WORKDIR /usr/share/nginx/html
Removing intermediate container a4232f4b6b62
 ---> 91a66299ecad
Step 3/4 : ADD . /usr/share/nginx/html
 ---> 9a9fef80da3b
Step 4/4 : EXPOSE 80
 ---> Running in 59f2b829aba6
Removing intermediate container 59f2b829aba6
 ---> b92c92688046
Successfully built b92c92688046
Successfully tagged docker-start-web:latest

执行:

docker run -p 4000:80 --name docker-start-web -d docker-start-web
f91cf4446b3746c665476b3dd214446a941d838fa9a3ad47680190bb08c9aa48

访问服务器ip:4000就可以查看到了。


作者:Jadepeng
出处:jqpeng的技术记事本–http://www.cnblogs.com/xiaoqi
您的支持是对博主最大的鼓励,感谢您的认真阅读。
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

文章作者:jqpeng
原文链接: 使用Spring访问Mongodb的方法大全——Spring Data MongoDB查询指南

1.概述

Spring Data MongoDB 是Spring框架访问mongodb的神器,借助它可以非常方便的读写mongo库。本文介绍使用Spring Data MongoDB来访问mongodb数据库的几种方法:

  • 使用Query和Criteria类
  • JPA自动生成的查询方法
  • 使用@Query 注解基于JSON查询

在开始前,首先需要引入maven依赖

1.1 添加Maven的依赖

如果您想使用Spring Data MongoDB,则需要将以下条目添加到您的pom.xml文件中:

<dependency><groupId>org.springframework.data</groupId><artifactId>spring-data-mongodb</artifactId><version>1.9.6.RELEASE</version>
</dependency>

版本根据需要选择。

2.文档查询

使用Spring Data来查询MongoDB的最常用方法之一是使用Query和Criteria类 , 它们非常接近本地操作符。

2.1 is查询

在以下示例中 - 我们正在寻找名为Eric的用户。

我们来看看我们的数据库:

[{    "_id" : ObjectId("55c0e5e5511f0a164a581907"),    "_class" : "org.baeldung.model.User",    "name" : "Eric",    "age" : 45},{    "_id" : ObjectId("55c0e5e5511f0a164a581908"),    "_class" : "org.baeldung.model.User",    "name" : "Antony",    "age" : 55}
}]

让我们看看查询代码:

Query query = new Query();
query.addCriteria(Criteria.where("name").is("Eric"));
List<User> users = mongoTemplate.find(query, User.class);

如预期的那样,这个逻辑返回:

{"_id" : ObjectId("55c0e5e5511f0a164a581907"),"_class" : "org.baeldung.model.User","name" : "Eric","age" : 45
}

2.2 正则查询

正则表达式是一个更灵活和强大的查询类型。这使用了一个使用MongoDB $ regex的标准,该标准返回适用于这个字段的这个正则表达式的所有记录。

它的作用类似于startingWith,endingWith操作 - 让我们来看一个例子。

寻找名称以A开头的所有用户,这是数据库的状态:

[{    "_id" : ObjectId("55c0e5e5511f0a164a581907"),    "_class" : "org.baeldung.model.User",    "name" : "Eric",    "age" : 45},{    "_id" : ObjectId("55c0e5e5511f0a164a581908"),    "_class" : "org.baeldung.model.User",    "name" : "Antony",    "age" : 33},{    "_id" : ObjectId("55c0e5e5511f0a164a581909"),    "_class" : "org.baeldung.model.User",    "name" : "Alice",    "age" : 35}
]

我们来创建查询:

Query query = new Query();
query.addCriteria(Criteria.where("name").regex("^A"));
List<User> users = mongoTemplate.find(query,User.class);

这运行并返回2条记录:

[{    "_id" : ObjectId("55c0e5e5511f0a164a581908"),    "_class" : "org.baeldung.model.User",    "name" : "Antony",    "age" : 33},{    "_id" : ObjectId("55c0e5e5511f0a164a581909"),    "_class" : "org.baeldung.model.User",    "name" : "Alice",    "age" : 35}
]

下面是另一个简单的例子,这次查找名称以c结尾的所有用户:

Query query = new Query();
query.addCriteria(Criteria.where("name").regex("c$"));
List<User> users = mongoTemplate.find(query, User.class);

所以结果是:

{"_id" : ObjectId("55c0e5e5511f0a164a581907"),"_class" : "org.baeldung.model.User","name" : "Eric","age" : 45
}

2.3 LT和GT

$ lt(小于)运算符和$ gt(大于)。

让我们快速看一个例子 - 我们正在寻找年龄在20岁到50岁之间的所有用户。

数据库是:

[{    "_id" : ObjectId("55c0e5e5511f0a164a581907"),    "_class" : "org.baeldung.model.User",    "name" : "Eric",    "age" : 45},{    "_id" : ObjectId("55c0e5e5511f0a164a581908"),    "_class" : "org.baeldung.model.User",    "name" : "Antony",    "age" : 55}
}

构造查询:

Query query = new Query();
query.addCriteria(Criteria.where("age").lt(50).gt(20));
List<User> users = mongoTemplate.find(query,User.class);

结果 - 年龄大于20且小于50的所有用户:

{"_id" : ObjectId("55c0e5e5511f0a164a581907"),"_class" : "org.baeldung.model.User","name" : "Eric","age" : 45
}

2.4 结果排序

Sort用于指定结果的排序顺序。

首先 - 这里是现有的数据:

[{    "_id" : ObjectId("55c0e5e5511f0a164a581907"),    "_class" : "org.baeldung.model.User",    "name" : "Eric",    "age" : 45},{    "_id" : ObjectId("55c0e5e5511f0a164a581908"),    "_class" : "org.baeldung.model.User",    "name" : "Antony",    "age" : 33},{    "_id" : ObjectId("55c0e5e5511f0a164a581909"),    "_class" : "org.baeldung.model.User",    "name" : "Alice",    "age" : 35}
]

执行排序后:

Query query = new Query();
query.with(new Sort(Sort.Direction.ASC, "age"));
List<User> users = mongoTemplate.find(query,User.class);

这是查询的结果 - 很好地按年龄排序:

[{    "_id" : ObjectId("55c0e5e5511f0a164a581908"),    "_class" : "org.baeldung.model.User",    "name" : "Antony",    "age" : 33},{    "_id" : ObjectId("55c0e5e5511f0a164a581909"),    "_class" : "org.baeldung.model.User",    "name" : "Alice",    "age" : 35},{    "_id" : ObjectId("55c0e5e5511f0a164a581907"),    "_class" : "org.baeldung.model.User",    "name" : "Eric",    "age" : 45}
]

2.5 分页

我们来看一个使用分页的简单例子。

这是数据库的状态:

[{    "_id" : ObjectId("55c0e5e5511f0a164a581907"),    "_class" : "org.baeldung.model.User",    "name" : "Eric",    "age" : 45},{    "_id" : ObjectId("55c0e5e5511f0a164a581908"),    "_class" : "org.baeldung.model.User",    "name" : "Antony",    "age" : 33},{    "_id" : ObjectId("55c0e5e5511f0a164a581909"),    "_class" : "org.baeldung.model.User",    "name" : "Alice",    "age" : 35}
]

现在,查询逻辑,只需要一个大小为2的页面:

final Pageable pageableRequest = new PageRequest(0, 2);
Query query = new Query();
query.with(pageableRequest);

结果 :

[{    "_id" : ObjectId("55c0e5e5511f0a164a581907"),    "_class" : "org.baeldung.model.User",    "name" : "Eric",    "age" : 45},{    "_id" : ObjectId("55c0e5e5511f0a164a581908"),    "_class" : "org.baeldung.model.User",    "name" : "Antony",    "age" : 33}
]

为了探索这个API的全部细节,这里是Query和Criteria类的文档。

3.生成的查询方法(Generated Query Methods)

生成查询方法是JPA的一个特性,在Spring Data Mongodb里也可以使用。

要做到2里功能,只需要在接口上声明方法即可,

public interface UserRepository 
  extends MongoRepository<User, String>, QueryDslPredicateExecutor<User> {...
}

3.1 FindByX

我们将通过探索findBy类型的查询来简单地开始 - 在这种情况下,通过名称查找:

List<User> findByName(String name);

与上一节相同 2.1 - 查询将具有相同的结果,查找具有给定名称的所有用户:

List<User> users = userRepository.findByName("Eric");

3.2 StartingWith and endingWith.

下面是操作过程的一个简单例子:

List<User> findByNameStartingWith(String regexp);

List<User> findByNameEndingWith(String regexp);

实际使用这个例子当然会非常简单:

List<User> users = userRepository.findByNameStartingWith("A");
List<User> users = userRepository.findByNameEndingWith("c");

结果是完全一样的。

3.3 Between

类似于2.3,这将返回年龄在ageGT和ageLT之间的所有用户:

List<User> findByAgeBetween(int ageGT, int ageLT);
List<User> users = userRepository.findByAgeBetween(20, 50);

3.4 Like和OrderBy

让我们来看看这个更高级的示例 - 为生成的查询组合两种类型的修饰符。

我们将要查找名称中包含字母A的所有用户,我们也将按年龄顺序排列结果:

List<User> users = userRepository.findByNameLikeOrderByAgeAsc("A");

结果:

[{    "_id" : ObjectId("55c0e5e5511f0a164a581908"),    "_class" : "org.baeldung.model.User",    "name" : "Antony",    "age" : 33},{    "_id" : ObjectId("55c0e5e5511f0a164a581909"),    "_class" : "org.baeldung.model.User",    "name" : "Alice",    "age" : 35}
]

4. JSON查询方法

如果我们无法用方法名称或条件来表示查询,那么我们可以做更低层次的事情 - 使用@Query注解。

通过这个注解,我们可以指定一个原始查询 - 作为一个Mongo JSON查询字符串。

4.1 FindBy

让我们先从简单的,看看我们是如何将是一个通过查找类型的方法第一:

@Query("{ 'name' : ?0 }")
List<User> findUsersByName(String name);

这个方法应该按名称返回用户 - 占位符?0引用方法的第一个参数。

4.2 $regex

让我们来看一个正则表达式驱动的查询 - 这当然会产生与2.2和3.2相同的结果:

@Query("{ 'name' : { $regex: ?0 } }")
List<User> findUsersByRegexpName(String regexp);

用法也完全一样:

List<User> users = userRepository.findUsersByRegexpName("^A");
List<User> users = userRepository.findUsersByRegexpName("c$");

4.3. $ lt和$ gt

现在我们来实现lt和gt查询:

@Query("{ 'age' : { $gt: ?0, $lt: ?1 } }")
List<User> findUsersByAgeBetween(int ageGT, int ageLT);

5. 结论

在本文中,我们探讨了使用Spring Data MongoDB进行查询的常用方法。

本文示例可以从 spring-data-mongodb这里下载。

本文参考A Guide to Queries in Spring Data MongoDB


作者:Jadepeng
出处:jqpeng的技术记事本–http://www.cnblogs.com/xiaoqi
您的支持是对博主最大的鼓励,感谢您的认真阅读。
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

文章作者:jqpeng
原文链接: kgtemp文件转mp3工具

kgtemp文件是酷我音乐软件的缓存文件,本文从技术层面探讨如何解密该文件为mp3文件,并通过读取ID3信息来重命名。

备注:针对老版本的酷我音乐生效,新版本不支持!!!

kgtemp解密

kgtemp文件前1024个字节是固定的包头信息,解密方案详细可以参见(http://www.cnblogs.com/KMBlog/p/6877752.html):

class Program{    static void Main(string[] args)    {
        byte[] key={0xAC,0xEC,0xDF,0x57};        using (var input = new FileStream(@"E:\KuGou\Temp\236909b6016c6e98365e5225f488dd7a.kgtemp", FileMode.Open, FileAccess.Read))        {            var output = File.OpenWrite(@"d:\test.mp3");//输出文件            input.Seek(1024, SeekOrigin.Begin);//跳过1024字节的包头            byte[] buffer = new byte[key.Length];            int length;            while((length=input.Read(buffer,0,buffer.Length))>0)            {                for(int i=0;i<length;i++)                {                    var k = key[i];                    var kh = k >> 4;                    var kl = k & 0xf;                    var b = buffer[i];                    var low = b & 0xf ^ kl;//解密后的低4位                    var high = (b >> 4) ^ kh ^ low & 0xf;//解密后的高4位                    buffer[i] = (byte)(high << 4 | low);                }                output.Write(buffer, 0, length);            }            output.Close();        }        Console.WriteLine("按任意键退出...");        Console.ReadKey();    }}

这样解密出来就是mp3文件了

读取ID3信息

解密出来的文件还需要手动命名,不是很方便,可以读取ID3V1信息重命名文件。
ID3V1比较简单,它是存放在MP3文件的末尾,用16进制的编辑器打开一个MP3文件,查看其末尾的128个顺序存放字节,数据结构定义如下:
char Header3; /标签头必须是”TAG”否则认为没有标签/
char Title[30]; /标题/
char Artist[30]; /作者/
char Album[30]; /专集/
char Year4; /出品年代/
char Comment[30]; /备注/
char Genre; /类型,流派/

解析代码比较简单,注意中文歌曲用GBK编码就可以了:

  private static Mp3Info FormatMp3Info(byte[] Info, System.Text.Encoding Encoding)    {        Mp3Info myMp3Info = new Mp3Info();        string str = null;        int i;        int position = 0主要代码jia,; //循环的起始值        int currentIndex = 0; //Info的当前索引值
        //获取TAG标识        for (i = currentIndex; i < currentIndex + 3; i++)        {            str = str + (char)Info[i];            position++;        }        currentIndex = position;        myMp3Info.identify = str;
        //获取歌名        str = null;        byte[] bytTitle = new byte[30]; //将歌名部分读到一个单独的数组中        int j = 0;        for (i = currentIndex; i < currentIndex + 30; i++)        {            bytTitle[j] = Info[i];            position++;            j++;        }        currentIndex = position;        myMp3Info.Title = ByteToString(bytTitle, Encoding);
        //获取歌手名        str = null;        j = 0;        byte[] bytArtist = new byte[30]; //将歌手名部分读到一个单独的数组中        for (i = currentIndex; i < currentIndex + 30; i++)        {            bytArtist[j] = Info[i];            position++;            j++;        }        currentIndex = position;        myMp3Info.Artist = ByteToString(bytArtist, Encoding);
        //获取唱片名        str = null;        j = 0;        byte[] bytAlbum = new byte[30]; //将唱片名部分读到一个单独的数组中        for (i = currentIndex; i < currentIndex + 30; i++)        {            bytAlbum[j] = Info[i];            position++;            j++;        }        currentIndex = position;        myMp3Info.Album = ByteToString(bytAlbum, Encoding);
        //获取年        str = null;        j = 0;        byte[] bytYear = new byte[4]; //将年部分读到一个单独的数组中        for (i = currentIndex; i < currentIndex + 4; i++)        {            bytYear[j] = Info[i];            position++;            j++;        }        currentIndex = position;        myMp3Info.Year = ByteToString(bytYear, Encoding);
        //获取注释        str = null;        j = 0;        byte[] bytComment = new byte[28]; //将注释部分读到一个单独的数组中        for (i = currentIndex; i < currentIndex + 25; i++)        {            bytComment[j] = Info[i];            position++;            j++;        }        currentIndex = position;        myMp3Info.Comment = ByteToString(bytComment, Encoding);
        //以下获取保留位        myMp3Info.reserved1 = (char)Info[++position];        myMp3Info.reserved2 = (char)Info[++position];        myMp3Info.reserved3 = (char)Info[++position];
        //        return myMp3Info;    }

转换小工具

写了一个小工具,来进行转换

装换工具

下载地址:https://pan.baidu.com/s/1o7FIsPk

PS:上面只读取了IDV1,部分歌曲可能不存在
可以下载@缤纷 提供的程序,增加了ID3V2的支持:
https://files.cnblogs.com/files/gxlxzys/kgtemp文件转mp3工具.zip


作者:Jadepeng
出处:jqpeng的技术记事本–http://www.cnblogs.com/xiaoqi
您的支持是对博主最大的鼓励,感谢您的认真阅读。
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

文章作者:jqpeng
原文链接: 使用SpringBoot开发REST服务

本文介绍如何基于Spring Boot搭建一个简易的REST服务框架,以及如何通过自定义注解实现Rest服务鉴权

搭建框架

pom.xml

首先,引入相关依赖,数据库使用mongodb,同时使用redis做缓存

注意,这里没有使用tomcat,而是使用undertow



    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter</artifactId>    </dependency>
    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-test</artifactId>        <scope>test</scope>    </dependency>
    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-web</artifactId>        <exclusions>            <exclusion>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-starter-tomcat</artifactId>            </exclusion>        </exclusions>    </dependency>    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-undertow</artifactId>    </dependency>
    <!--redis支持-->    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-data-redis</artifactId>    </dependency>
    <!--mongodb支持-->    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-data-mongodb</artifactId>    </dependency>
  • 引入spring-boot-starter-web支持web服务
  • 引入spring-boot-starter-data-redis 和spring-boot-starter-data-mongodb就可以方便的使用mongodb和redis了

配置文件

profiles功能

为了方便 区分开发环境和线上环境,可以使用profiles功能,在application.properties里增加
spring.profiles.active=dev

然后增加application-dev.properties作为dev配置文件。

mondb配置

配置数据库地址即可

spring.data.mongodb.uri=mongodb://ip:port/database?readPreference=primaryPreferred

redis配置

spring.redis.database=0  
# Redis服务器地址
spring.redis.host=ip
# Redis服务器连接端口
spring.redis.port=6379  
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8  
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1  
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=8  
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=0  
# 连接超时时间(毫秒)
spring.redis.timeout=0  

数据访问

mongdb

mongdb访问很简单,直接定义接口extends MongoRepository即可,另外可以支持JPA语法,例如:

@Component
public interface UserRepository extends MongoRepository<User, Integer> {
public User findByUserName(String userName);
}

使用时,加上@Autowired注解即可。

@Component
public class AuthService extends BaseService {
@AutowiredUserRepository userRepository;}

Redis访问

使用StringRedisTemplate即可直接访问Redis

@Component
public class BaseService {@Autowiredprotected MongoTemplate mongoTemplate;
@Autowiredprotected StringRedisTemplate stringRedisTemplate;
}

储存数据:

.stringRedisTemplate.opsForValue().set(token_key, user.getId()+"",token_max_age, TimeUnit.SECONDS);

删除数据:

stringRedisTemplate.delete(getFormatToken(accessToken,platform));

Web服务

定义一个Controller类,加上RestController即可,使用RequestMapping用来设置url route

@RestController
public class AuthController extends BaseController {
@RequestMapping(value = {"/"}, produces = "application/json;charset=utf-8", method = {RequestMethod.GET, RequestMethod.POST})@ResponseBodypublic String main() {    return "hello world!";}

}

现在启动,应该就能看到hello world!了

服务鉴权

简易accessToken机制

提供登录接口,认证成功后,生成一个accessToken,以后访问接口时,带上accessToken,服务端通过accessToken来判断是否是合法用户。

为了方便,可以将accessToken存入redis,设定有效期。

        String token = EncryptionUtils.sha256Hex(String.format("%s%s", user.getUserName(), System.currentTimeMillis()));    String token_key = getFormatToken(token, platform);    this.stringRedisTemplate.opsForValue().set(token_key, user.getId()+"",token_max_age, TimeUnit.SECONDS);

拦截器身份认证

为了方便做统一的身份认证,可以基于Spring的拦截器机制,创建一个拦截器来做统一认证。

public class AuthCheckInterceptor implements HandlerInterceptor {
}

要使拦截器生效,还需要一步,增加配置:

@Configuration
public class SessionConfiguration extends WebMvcConfigurerAdapter {
@AutowiredAuthCheckInterceptor authCheckInterceptor;
@Overridepublic void addInterceptors(InterceptorRegistry registry) {    super.addInterceptors(registry);    // 添加拦截器    registry.addInterceptor(authCheckInterceptor).addPathPatterns("/**");}
}

自定义认证注解

为了精细化权限认证,比如有的接口只能具有特定权限的人才能访问,可以通过自定义注解轻松解决。在自定义的注解里,加上roles即可。

/**
 *  权限检验注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthCheck {
/** *  角色列表 * @return */String[] roles() default {};
}

检验逻辑:

  • 只要接口加上了AuthCheck注解,就必须是登陆用户
  • 如果指定了roles,则除了登录外,用户还应该具备相应的角色。
    String[] ignoreUrls = new String[]{
            "/user/.*",
            "/cat/.*",
            "/app/.*",
            "/error"
    };
 public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler) throws Exception {

        // 0 检验公共参数
        if(!checkParams("platform",httpServletRequest,httpServletResponse)){
            return  false;
        }

        // 1、忽略验证的URL
        String url = httpServletRequest.getRequestURI().toString();
        for(String ignoreUrl :ignoreUrls){
            if(url.matches(ignoreUrl)){
                return true;
            }
        }

        // 2、查询验证注解
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        // 查询注解
        AuthCheck authCheck = method.getAnnotation(AuthCheck.class);
        if (authCheck == null) {
            // 无注解,不需要
            return true;
        }

        // 3、有注解,先检查accessToken
        if(!checkParams("accessToken",httpServletRequest,httpServletResponse)){
            return  false;
        }
        // 检验token是否过期
        Integer userId = authService.getUserIdFromToken(httpServletRequest.getParameter("accessToken"),
                httpServletRequest.getParameter("platform"));
        if(userId==null){
            logger.debug("accessToken timeout");
            output(ResponseResult.Builder.error("accessToken已过期").build(),httpServletResponse);
            return false;
        }

        // 4、再检验是否包含必要的角色
        if(authCheck.roles()!=null&&authCheck.roles().length>0){
            User user = authService.getUser(userId);
            boolean isMatch = false;
            for(String role : authCheck.roles()){
                if(user.getRole().getName().equals(role)){
                    isMatch =  true;
                    break;
                }
            }
            // 角色未匹配,验证失败
            if(!isMatch){
                return false;
            }
        }

        return true;
    }

服务响应结果封装

增加一个Builder,方便生成最终结果

public class ResponseResult {

    public static class Builder{
        ResponseResult responseResult;

        Map<String,Object> dataMap = Maps.newHashMap();

        public Builder(){
            this.responseResult = new ResponseResult();
        }

        public Builder(String state){
            this.responseResult = new ResponseResult(state);
        }


        public static Builder newBuilder(){
           return new Builder();
        }

        public static Builder success(){
            return new Builder("success");
        }

        public static Builder error(String message){
            Builder builder =  new Builder("error");
            builder.responseResult.setError(message);
            return builder;
        }

        public  Builder append(String key,Object data){
            this.dataMap.put(key,data);
            return this;
        }

        /**
         *  设置列表数据
         * @param datas 数据
         * @return
         */
        public  Builder setListData(List<?> datas){
            this.dataMap.put("result",datas);
            this.dataMap.put("total",datas.size());
            return this;
        }

        public  Builder setData(Object data){
            this.dataMap.clear();
            this.responseResult.setData(data);
            return this;
        }

        boolean wrapData = false;

        /**
         * 将数据包裹在data中
         * @param wrapData
         * @return
         */
        public  Builder wrap(boolean wrapData){
            this.wrapData = wrapData;
            return this;
        }

        public String build(){

            JSONObject jsonObject = new JSONObject();
            jsonObject.put("state",this.responseResult.getState());
            if(this.responseResult.getState().equals("error")){
                jsonObject.put("error",this.responseResult.getError());
            }
            if(this.responseResult.getData()!=null){
                jsonObject.put("data", JSON.toJSON(this.responseResult.getData()));
            }else  if(dataMap.size()>0){
                if(wrapData) {
                    JSONObject data = new JSONObject();
                    dataMap.forEach((key, value) -> {
                        data.put(key, value);
                    });
                    jsonObject.put("data", data);
                }else{
                    dataMap.forEach((key, value) -> {
                        jsonObject.put(key, value);
                    });
                }
            }
            return jsonObject.toJSONString();
        }

    }

    private String state;
    private Object data;
    private String error;


    public String getError() {
        return error;
    }

    public void setError(String error) {
        this.error = error;
    }

    public ResponseResult(){}

    public ResponseResult(String rc){
        this.state = rc;
    }

    /**
     * 成功时返回
     * @param rc
     * @param result
     */
    public ResponseResult(String rc, Object result){
        this.state = rc;
        this.data = result;
    }

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

}

调用时可以优雅一点

    @RequestMapping(value = {"/user/login","/pc/user/login"}, produces = "application/json;charset=utf-8", method = {RequestMethod.GET, RequestMethod.POST})
    @ResponseBody
    public String login(String userName,String password,Integer platform) {
        User user = this.authService.login(userName,password);
        if(user!=null){
            //  登陆
            String token = authService.updateToken(user,platform);
            return ResponseResult.Builder                 .success()
                    .append("accessToken",token)
                    .append("userId",user.getId())
                    .build();
        }
        return ResponseResult.Builder.error("用户不存在或密码错误").build();
    }
    protected String error(String message){
        return  ResponseResult.Builder.error(message).build();
    }

    protected String success(){
        return  ResponseResult.Builder
                .success()
                .build();
    }

    protected String successDataList(List<?> data){
        return ResponseResult.Builder
                .success()
                .wrap(true) // data包裹
                .setListData(data)
                .build();
    }

作者:Jadepeng
出处:jqpeng的技术记事本–http://www.cnblogs.com/xiaoqi
您的支持是对博主最大的鼓励,感谢您的认真阅读。
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。