本文我们学习Liquid的基础知识,主要包含如下内容:
- Liquid是什么?
- 学习Liquid及其定界符
- 学习比较运算符
- 使用逻辑运算符
- 数据类型
- 对空白的控制
学习完本文,读者将会对Liquid的逻辑有更深的了解,也能 GET 到操作各种类型数据的运算符以及Liquid删除烦人的空白字符的方法等知识。通过学习如何使用逻辑运算符以及操作句柄属性,我们能学到生成各类动态特性的知识,有助于未来编写高质量的复杂代码。
Liquid是什么?
上一篇文章中,我们对Shopify有了一定的了解。学习了Shopify是什么,如何创建Shopify伙伴账号以及如何管理主题。最后,我们还探讨了一下主题的文件结构及其所包含的目录,基础文件放在Layout目录中,读者可能也注意到了其中大部分文件的后缀名都是.liquid。那么 Liquid 究竟是什么样的存在呢?
Liquid是一个由Shopify联合创始人及CEO Tobias Lütke所创建的开源项目。Liquid作为一种模板语言,它变量连接着Shopify店铺的数据与主题中的HTML内容,让我们可以将静态模板页面变成动态、强大、效果很好的电商网站。可以把Liquid元素看成占位符,在文件内代码译、发往浏览器时会填充相应的数据。
自2006年起,Liquid不断地成长、演化。时至今日,有多个web应用依赖于Liquid,Shopify是其中之一,表明对Liquid的需求在不断增长。学习Liquid是技术得以精进很好途径,其语法易学,很快我们就能掌握Liquid并使用它创建复杂的功能。
学习Liquid及其定界符
分辨Liquid有两种方式,一种是通过其文件后缀名.liquid。作为一种模板语言,Liquid文件是静态与动态内容的结合体。
- 我们使用HTML编写的元素称之为静态内容,不论访问什么页面它们的内容保持不变。
- 而以Liquid编写的元素称之为动态内容,其内容会随所访问页面的变化而变化。
虽然浏览器可以很快速地处理HTML代码,但并不能处理、解析Liquid代码。可以把Shopify URL提交到浏览器拆解成5个逻辑步骤:
- Shopify服务端判断你所要访问的是哪一个店铺。
- 根据我们当前请求信息的页面类型,Shopify尝试从在线主题目录中定位、选取相应的Liquid模板。
- 在成功确定所请求的Liquid模板后,Shopify服务端开始将占位符替换为平台存储的实际数据。
- 在Shopify完成占位符并执行所选取模板中的逻辑后,浏览器会接收到所编译的HTML文件。
- 现在浏览器接收到了响应的HTML文件,会开始处理该文件并加载所需的资源,包括JavaScript、样式表和图片等。
另一种辨别Liquid文件和代码的方式是其两个定界符:
- {{ }} 双大括号用于表示在等待一个输出。等待输出的一个Liquid示例代码如下:
Our collection name is {{ collection.title }}
上面一行中,我们看到一段字符串,后面接{{ collection.title }}。可以看出collection.title被双大括号所包裹,表明代码的结果是输出内容。在Shopify服务端处理了Liquid代码并返回浏览器可以处理的内容后,我们会收到下面一段结果:
Our collection name is Winter Shoes
- {% %}大括号加百分号,在我们希望执行某种逻辑时使用。
上例中,我们可以使用{{ collection.title }}的结果来获取集合的名称。那如果想要显示集合的描述,而出于某种原因,collection description并未返回任何内容呢?我们会得到一段不完整的信息:
Our collection description is
为确保这一情况不会发生,我们需要使用Liquid逻辑运算符和比较运算符结合一些Shopify的数据类型来检查该数据值是否存在。
学习比较运算符
通过Liquid,我们可以访问7种比较运算符,可以组合起来创建代码所需的某种逻辑流。下面来逐步了解:
- == 运算符让我们可以检测所比较的元素是否与某个值相等:
123{% if collection.title == "Winter Shoes" %}Winter Shows collection found!{% endif %}
如果collection.title严格等于字符串“Winter Shows”,则逻辑返回true,我们就会看到这条消息。否则逻辑运算返回false。注意只有字符串精确匹配才会返回true,包括大小写也应一致。 - != 运算符与前一个运算符相似。不同之处在于该运算符检测所比较的元素是否与某个值不相等:
123{% if collection.title != "Winter Shows" %}Winter Shows collection is not found!{% endif %}
与前面的例子一样,我们使用collection.title,但在本例中我们检测集合名是否与字符串“Winter Shows”不相等。如果结果是collection.title与字符串不相应,返回true,这时就会显示消息。否则,逻辑运算返回false,隐藏消息。 - > 运算符用于检测比较值是否大于被比较值:
123{% if collection.all_products_count > 25 %}The collection has more than 25 products!{% endif %}
此处我们在检查集合的产品数是否大于25个。如果是则返回true,我们就能看到这段消息。反之返回false,会隐藏消息。 - 类似上例,这里我们也是判断集合中产品的数量,但这里使用 < 运算符,在比较值小于被比较值时返回true:
123{% if collection.all_products_count < 25 %}The collection has less than 25 products!{% endif %}
如果集合中的产品数少于25,逻辑语句返回true,我们就可以看这段消息。反之则返回false,消息被隐藏。
我们对< 和 >运算符有了基本的了解,但如果使用下面语句表示什么意思呢:
123{% if collection.all_products_count > "25" %}The collection has more than 25 products!{% endif %}
前面说到,> 用于检测比较值是否大于被比较值。是的话返回true,显示消息。但运行这段代码会返回false,消息不显示。为什么呢?
仔细看会发现这里的右值中使用了双引号,表示是在和字符串值进行比较,而collection.all_products_count返回的是数值。前面提到,比较运算符仅在满足运算符所要求的精确条件时都会返回true。本例中要显示消息必须满足两个条件。
第一个条件是两个值必须为同一数据类型,也就是说不能像上面这样混用数据类型。如果去除右值中的引号,就会将字符串变成数值类型。
第二个条件是左值应大于右值。满足了这两个条件后,逻辑语句会返回true,消息就会显示。 - >= 让我们可以检测左值是否大于等于右侧:
123{% if collection.all_products_count >= 25 %}The collection has more or an exact number of products as the comparing value!{% endif %}
因为两个值的数据类型相同,如果产品数大于等于右侧的话,逻辑语句返回true,我们就可以看其中的消息。反之则返回false,消息不显示。 - <= 运算符让我们可以检测左值是否小于等于右值:
123{% if collection.all_products_count <= 25 %}The collection has less or an exact number of products as the comparing value!{% endif %}
类似前面的比较元素,我们检测集合的产品总数是否小于等于右值。如果条件满足返回true,显示消息。反之则返回false,消息不显示。
使用逻辑运算符
除比较运算符外,我们还可以使用两个逻辑运算符,用于组合多个条件来形成复杂语句。可以分成以下两组:
- or 运算符可以设置多个条件,需满足其中的一个条件:
123{% if collection.title == "Winter Shoes" or collection.all_products_count > 25 %}The collection name is Winter Shows, or the collection contains more than 25 products!{% endif %}
在上例中,我们检查集合的名称是否等于Winter Shoes或是集合中是否包含25个以上的产品。如果满足其中的一个条件上,返回true,显示其中的消息。反之则返回false,消息不显示。 - 类似地,and运算符也可以设置多个条件。但仅所在所有条件均满足时才返回true:
123{% if collection.title == "Winter Shoes" and collection.all_products_count > 25 %}The collection name is Winter Shoes, and the collection contains more than 25 products!{% endif %}
本例中,我们通过查看集合名是否等于Winter Shoes以及集合中是否有25个以上的产品。如果两具条件均满足,返回true,显示消息。否则返回false,消息不显示。
我们现在创建具有多个比较值的条件语句。但是要注意我们的条件语句是从右开始执行的。无法通过括号来改变执行顺序,因为在Liquid中括号是非法字符,会导致程序执行失败。 - contains是我们要讲的最后一个运算符。这个运算符与前面的其它运算符不同,它不进行值的比较,而是查看字符串是否包含某个子字符串:
123{% if collection.title contains "Christmas" %}Our Christmas collection is stocked and ready to go!{% endif %}
在上面的代码中,我们查看collection.title(返回字符串)中是否包含单词Christmas,如若包含,显示中间的消息。注意contains是区分大小写的,仅在子字符串严格匹配字符串中的内容时才返回true。
我们可以使用contains来搜索字符串是否包含某个子字符串或者查看某个字符串是否在字符串数组中。但无法使用它来查看某个对象是否位于对象数组中。contains只能用来搜索字符串。
我们已经学习到了很多有关逻辑运算符和比较运算符的知识。但是只学了两个值进行比较。那如果检测某个值,比如集合描述是否存在呢?因为我们不知道collection.description会返回什么内容,就无法以现有的知识执行检测。要完成这一任务,首先我们要学习Liquid中能够使用的各种数据类型。
数据类型
至此我们讲到了两种数据类型,也即字符串和数值类型。但在Liquid中,有6种不同的数据类型:
- Strings(字符串)
- Number(数值)
- Boolean(布尔)
- Nil(空)
- Array(数组)
- EmptyDrop
Strings
字符串是用于表示文件的一种数据类型。因为字符串可能包含字母、数字或特殊字符,所以应当使用双引号进行包裹:
1 2 3 |
{% if product.title contains "Book" or product.title contains "2021" %} We have found a product that contains the word Book or the product that contains the word 2021. {% endif %} |
上例中,我们查看产品标题中是否含有字符串“Book” 或 “2021”,如果包含,则显示消息。
Number
数值类型无需使用引号,使用它来表示如下两种类型的数值数据:
- 浮点数(float)为包含小数点的数字。
- 整数(integer或int)则为不包含小数点的数字。
123{% if product.price > 25 and product.price < 3500 %}The number of products in a collection is greater than 25, and the product's price is lower than 3500.{% endif %}
上例中,我们检测产品价格是否大于25,同时又小于3500。如果两个条件都为真,则显示消息。记住这里使用的是and运算符,所以两个条件必须都为真,如果其中之一为假,就不会显示消息。
注意用于比较的两个数字,25 或 3500,都没有使用引号。否则就会变成字符串,那就会像之前那样无法和product.price所返回的值进行比较。
Boolean
布尔是一种值为true或false的数据类型:
1 2 3 |
{% if customer.accepts_marketing == true %} The customer has signed up for our newsletter! {% endif %} |
从例子中可以看出,类似数值类型,布尔值不使用引号。如果客户订阅了邮件,customer.accepts_marketing对象就会为真,消息也会显示。
Nil
Nil是一种代码无返回结果,返回空值的数据类型。因为nil不返回任何值,我们可以用它来检测语句是否为真。但应当注意我们只能使用nil来检测值是否存在。不能使用nil来检测值的内容:
1 2 3 |
{% if customer %} Welcome {{ customer.name }}! {% endif %} |
上例中,我们使用条件语句查询访问店铺的客户是否注册了账户。注意并没有在customer对象后添加任何运算符或数据类型。其原因是nil是一种特殊的空值。它不是一个内容为nil的字符串或一个变量,并不需要任何视觉上的表现。
我们可以将Liquid中的值分成两类:
- 在条件语句中使用时默认为true的数据类型视为真类型。注意即使用是空值,如字符串,默认也视为真。
- 另一种是返回false的数据类型,视为假类型。只有false和nil默认返回false。
因为nil被看成是假,用户没有注册店铺账号的话就不会显示消息。有注册条件语句就返回true,消息正常显示。
虽然nil用户很大,但并不是万能的。读者如果记得的话,前面我们使用collection.description 来查看集合的描述内容。
1 |
Our collection description is {{ collection.description }}. |
如果集合描述中定义了内容,消息就会正确显示。但如果没有定义集合描述,那么collection.description会返回空,并且我们会得到一条不完整的消息:
1 |
Out collection description is. |
我们说过仅能使用nil来检测某一元素如对象是否存在。它无法检测其中的内容,什么意思呢?
1 2 3 |
{% if collection.description %} <h3>{{ collection.description }}</h3> {% endif %} |
本例中,我们把元素封装在条件语句中,使用nil数据类型来检测collection.description实例是否存在。但可以看到这不是我们所希望的结果:
1 |
<h3></h3> |
因为nil只检测元素是否存在,本例中条件语句发现collection.description虽然为空,但却是存在的。还记得Liquid中除了false和nil默认都为真吗?也就是说即使是空字符串,也被看成是真。出于这一原因,虽然我们的collection.descriptio实例为空且返回空字符串,其结果为真。因此条件语句所包含的代码会显示出来。
要解决这一问题,我们不检测其值是事存在,而是检测值是否为空:
1 2 3 |
{% if collection.description != blank %} <h3>The collection description element will show only if its content is not empty.<h3> {% endif %} |
在本例中,我们对blank使用了等号参数来检测集合描述是否为空。如果条件语句的结果是collection.description不为空,所定义的消息就会显示。否则,条件语句会返回false,其中的消息就不会显示。
Array
数组是一种包含元素列表的数据结构,通常元素类型相同。注意我们不能仅使用Liquid初始化数组,但可以将字符串分解成子字符串数组,可通过如下两种方式访问数据:
- 第一个访问数组中数据的方式是通过直接访问每一项。因为前面提到不能仅通过Liquid初始化数组,本例中我们使用product.tags,它会返回一个字符串数组:
1"learning", "with", "packt", "is", "awesome!"
要访问其中的字符串,我们可以对product.tags使用方括号 []以及下标位访问各个子项:
12345<p>{{ product.tags[0] }}</p><p>{{ product.tags[1] }}</p><p>{{ product.tags[2] }}</p><p>{{ product.tags[3] }}</p><p>{{ product.tags[4] }}</p>
注意数组的下标以 0开头,因此我们使用product.tags[0]来访问数组中的第一个元素。在将代码提交给Liquid服务端后,我们收到如下的字符串列表:
12345<p>learning</p><p>with</p><p>packt</p><p>is</p><p>awesome!</p>
虽然这个方法可产生结果,但仅在我们知道数组的内容以及所需获取元素的位置时才有用。如果我们希望输出整体内容,又不编写大量代码的话,就需要使用其它方法。 - 访问数组中的所有元素,我们需要遍历数组中的每一项并输出其内容:
123{% for tag in product.tags %}<p>{{ tag }}</p>{% endfor %}
我们可以使用for标签来重复执行一个代码块,遍历数组中的所有值并进行输出:
12345<p>learning</p><p>with</p><p>packt</p><p>is</p><p>awesome!</p>
可以看出,运行for循环的结果与前面访问每一项的结果完全相同。
虽然对数组使用循环快速输出比单独调用数组中的每一项更为容易,但应该了解这两种方法,有助于在稍后实现复杂功能。
EmptyDrop
最后一种数据类型是EmptyDrop,在访问此前已删除对象或句柄进行禁用时会得到这一结果。
在学习最后这个数据类型之前,我们需要先学习什么是句柄,如果如何找到它。
查找句柄
句柄是小写的页面标题,使用中间杠(–)来替换特殊字符及空格。类似在数组中使用位置索引来访问子项内容,我们可以使用页面句柄来访问Liquid对象的属性。
现在知道了什么是页面句柄,我们试着在店铺中创建一个页面将其放入上下文,实操下刚刚学习的知识:
- 新建页面首先我们需要通过https://my-store-name.myshopify.com/admin访问店铺,然后使用注册的Shopify伙伴账号登录。也可以直接访问Shopify伙伴网站https://www.shopify.com/partners,使用上一篇文章所注册的账号进行登录,再点击左侧边栏的Store按钮,接着点击店铺名旁的Log in按钮。
- 成功登录店铺后,点击左侧边栏Sales channels之下的Online Store展开其中的菜单,然后点击Pages,就会进入到店铺的Pages页面:
- 进入后,因为这是一个新店铺,还没有内容,仅能看到一个Add page按钮。否则我们会看到店铺中可以访问的所有页面。点击Add page按钮创建第一个页面。
- 在进入新建页面的流程后,会进入一个页面,其中可定义新页面的名称、描述、可见性等内容并进行模板的选择。本例中我们仅需输入标题,在Visibility中选择Visible,Content字段当前可以留空。标题我们输入Learning about the page handle,确保选择了Visible,然后就可以点击Save按钮了:
- 创建好页面后,我们可以点击页面下方的Edit website SEO按钮查看页面句柄,它会展开、显示该页面的SEO信息,可以看到如下编辑信息:
前面说到句柄是小写的页面标题,使用中间杠(–)来替换特殊字符及空格。这里页面句柄为learning-about-the-page-handle。
既然我们已经学习了页面句柄是什么以及如何进行管理,下面就继续讲解EmptyDrop数据类型剩下的学习。
EmptyDrop数据类型
前文说过EmptyDrop是由于访问已删除或已禁用对象的属性所导致的。注意EmptyDrop类似nil,不是一个文本为EmptyDrop的字符串或者变量,因此没有什么视觉表现。
我们可以把所访问的对象名变成复数,后接中括号([]) 或点号标记(.)来访问对象的句柄:
1 2 |
<h1>{{ pages.learning-about-the-page-handle.title }}</h1> \ <h1>{{ pages["learning-about-the-page-handle"].title}}</h1> |
虽然不论选取哪种标记法结果都相同,但很有必要提出,因为目的不同,在本文后面会讲到:
1 2 |
<h1>Learning about the page handle</h1> <h1>Learning about the page handle</h1> |
我们学习了如何访问对象并通过页面句柄读取其属性。但如果进入到管理后台并通常将Visibility选择切换为Hidden,禁用掉之前所创建的页面呢?这样就会产生EmptyDrop:
1 2 |
<h1></h1> <h1></h1> |
EmptyDrop仅有一个名为empty的属性,总是返回真。要避免这一个问题,我们可以创建条件语句来检测EmptyDrop是否为empty:
1 2 3 |
{% if pages["learning-about-the-page-handle"].title != empty %} <div class="tester">{{ pages["learning-about-the-page-handle"].title }}</div> {% endif %} |
如果该对象不等于empty(对EmptyDrop总是会返回真),条件语句中的代码会进行渲染。否则如果等于empty,所查找的对象为empty,代码不进行渲染。
对空白的控制
上一部分中我们学习了如何使用带有变量数据类型的条件语句来确保总是接收正确的值。但即使用这种条件语句,一些值可能携带多余的空白:
1 2 3 4 |
Collection info: {% if collection %} The collection's name is {{ collection.title }} ! {% endif %} |
本例中,我们创建一个在集合对象存在时返回true的条件语句,以保障不会出现不完整的消息。虽然结果看上去是对的,但查看页面元素的话,会发现结果并不完美:
1 2 |
Collection info: The collection's name is Winter Shoes ! |
从例中可以看出,我们成功地获取了集合信息。但是,在Liquid所处理得到的消息周围存在多余的空格。虽然不是所有的Liquid代码都会输出HTML,默认每一行Liquid代码都会生成一行。
1 2 3 4 |
Collection info: {%- if collection -%} The collection's name is {{ collection.title -}} ! {%- endif -%} |
通过像上面代码块中这样引入中间杠,我们成功地移除了条件语句所渲染的两个空行:
1 2 |
Collection info: The collection's name is Winter Shoes! |
注意条件语句内两端所添加的中间杠删除掉了两边的空白。但我们仅在collection.title的闭合括弧端加了一个中间杠,用于移除右侧的空格。如果在左侧加添加了中间杠,会删除掉 is 与集合名词之间的空格。
小结
本文中我们学习了什么是Liquid、Liquid的基础知识,并通过学习如何阅读和编写其语句来识别Liquid代码。我们对Liquid的所有逻辑和比较运算符建立了了解,结合文中所讲解的所有Liquid数据类型,可以帮助我们仅接收到所希望得到的数据。在不断学习的过程中,这会变量越来越基础。
最后,我们学习了如何单独创建、访问每个页面,对于后续文章的学习会非常有用,在后续文章中我们会更深入地学习本文中出现的对象。
知识巩固
- 如果希望输出为结果我们该使用哪种定界符?
- 以下条件语句会产生什么结果,为什么?
123{% if collection.all_products_count > "20" %}The number of products in a collection is greater than 20!{% endif %} - 访问属性内子项的两个方法是什么?
- 使用句柄访问对象的正确方式是什么?
- 以下代码块中存在的两具问题是什么?
123{% if customer != nil %}Welcome {{- customer.name -}} !{% endif %}