Skip to Content
🎉 探索 Shopify 的无限可能 结构化知识 + 实战案例,持续更新中...
Liquid 开发常用模式

常见模式和解决方案:先把 Liquid 自己的模式用对

网上大量”Shopify 常见模式”文章其实在教通用 JavaScript:手写 LRU 缓存、防抖节流、状态管理、错误边界。这些东西在 Liquid 里没有对应位置——Liquid 在服务端渲染一次就结束了,没有运行时状态,也没有需要你手动缓存的东西。

这一篇只收 Liquid 自己的高频模式:空值处理、数组操作、循环上限、转义、金额、元字段、交互。每一条都是真实会踩的。


一、空值与兜底:blank 是你最常用的判断

商家没填的设置、没上传的图、被删掉的菜单——空值在主题里是常态,不是异常。

{% if section.settings.heading != blank %} <h2>{{ section.settings.heading | escape }}</h2> {% endif %} {{ section.settings.button_label | default: 'Shop now' }}

三条规则:

判断空值用 != blank,不要用 != '' 有些设置类型的空值不是空字符串:inline_richtext 空值是空对象,资源型设置(product / collection / image)空值是 blank。用 != '' 在这些类型上判断不出来,会渲染出空标签。

default 过滤器对 false 也生效。 {{ false | default: true }} 得到 true——所以布尔型设置不要用 default 兜底,用 {% if %} 显式判断,否则商家关掉的开关会被你重新打开。

兜底要给可用的内容,不是空白。 logo 没设就退回店名文字,图片没有就用 placeholder_svg_tag,而不是留一个空盒子。


二、数组操作:用过滤器,不要用循环

新手最常见的写法是 forif 挑数据。Liquid 有一组数组过滤器专门干这个,更短也更快:

目的写法
筛选{% assign in_stock = collection.products | where: 'available', true %}
反向筛选{% assign not_gift = product.tags | reject: 'gift' %}
取字段{% assign titles = collection.products | map: 'title' %}
找单个{% assign hero = collection.products | find: 'handle', 'hero-tee' %}
排序{% assign sorted = collection.products | sort: 'price' %}
去重 / 计数{% assign n = product.tags | uniq | size %}

一个真实对比。用循环挑上架商品:

{% assign available_count = 0 %} {% for product in collection.products %} {% if product.available %} {% assign available_count = available_count | plus: 1 %} {% endif %} {% endfor %} <p>{{ available_count }} 件现货</p>

这段既啰嗦,又会撞上 50 条循环上限而算错。改成:

{% assign available_count = collection.products | where: 'available', true | size %} <p>{{ available_count }} 件现货</p>

注意 where 的边界:它按属性做相等匹配,不能做区间比较(“价格大于 100”这类要自己循环或改用其他数据源),也不支持嵌套属性路径。另外 contains 只能用于字符串,不能用来判断某个对象是否在数组里。


三、循环的 50 条上限:集合数据一律 paginate

{% for %} 最多迭代 50 次,超出部分静默丢失——页面看着正常,只是少了商品。这是最容易带上线的 bug。

{% paginate collection.products by 24 %} <div class="product-grid"> {% for product in collection.products %} {% render 'product-card', product: product %} {% endfor %} </div> {{ paginate | default_pagination }} {% endpaginate %}

不要用 limit 代替 paginate {% for product in collection.products limit: 50 %} 只是把”丢数据”变成”明确只要 50 条”,如果你的意图是展示全部,它同样是错的。limit 只适合真的只想要前 N 条的场景(比如”推荐 4 个商品”)。


四、转义:分清 HTML 上下文和 JS 上下文

这是安全相关、也最容易写错的一条。规则只有两句:

输出到 HTML 文本或属性里,用 | escape

输出到 <script> 里或 JS 变量里,用 | json

反例:

<script> var productTitle = "{{ product.title | escape }}"; </script>

商品名里出现引号或换行时,这段 JS 直接语法错误。正确写法:

<script> var productTitle = {{ product.title | json }}; </script>

| json 会输出带引号的合法 JS 字面量,所以外面不要再加引号。整个对象也能传:

<script> var productData = {{ product | json }}; </script>

还有两个上下文要注意:富文本设置(richtext)本身就是 HTML,不要再 escape,否则商家会看到一堆标签源码;URL 参数用 | url_encode,不是 escape


五、金额:永远走 money 过滤器

价格在 Liquid 里是货币最小单位的整数1000 表示 10.00)。直接输出就是错的。

{{ product.price | money }} {{ product.price | money_with_currency }} {{ product.price | money_without_trailing_zeros }}

而且不要自己做除法换算:无小数位货币(日元、韩元)的规则不同,货币格式还由商家在后台配置。任何时候要显示金额,都过 money 系列过滤器。

算折扣百分比时才需要数学过滤器:

{% liquid assign saved = product.compare_at_price | minus: product.price assign percent = saved | times: 100.0 | divided_by: product.compare_at_price | round %} {% if percent > 0 %} <span class="badge">-{{ percent }}%</span> {% endif %}

注意 times: 100.0 用浮点数——整数除法会把结果截断成 0。


六、元字段:结构化内容的标准取法

商品级的自定义信息属于元字段,不属于主题设置。取值方式:

{% assign care = product.metafields.custom.care_instructions %} {% if care != blank %} <div class="product__care">{{ care | metafield_tag }}</div> {% endif %}

metafield_tag 会按元字段的类型输出合适的标记(富文本、列表、日期等);只要纯文本时用 metafield_text取值前一定判断 != blank——不是所有商品都填了。


七、交互模式:先问能不能不用 JS

主题里大量”交互”其实原生元素就能做,而且自带无障碍支持和键盘操作:

需求原生方案不要
折叠面板 / FAQ<details> + <summary>自己写 class 切换
弹窗<dialog>手搓遮罩与焦点陷阱
图片懒加载loading="lazy"从 CDN 引懒加载库
横向滑动CSS scroll-snap引入轮播库
表单校验required / type="email" / pattern手写校验逻辑

一个折叠面板的完整实现:

{% for block in section.blocks %} <details class="faq__item" {{ block.shopify_attributes }}> <summary class="faq__question">{{ block.settings.question | escape }}</summary> <div class="faq__answer rte">{{ block.settings.answer }}</div> </details> {% endfor %}

零 JavaScript,键盘可操作,屏幕阅读器能正确朗读展开状态。真的需要 JS 时,用 {% javascript %} 写在自己的 section / block 里,不要引第三方库。


八、条件渲染:把判断提前,不要在循环里堆逻辑

{% liquid %} 标签适合把一组判断集中在开头,让下面的标记保持干净:

{% liquid assign show_badge = false if product.compare_at_price > product.price assign show_badge = true endif assign badge_label = 'products.product.on_sale' | t unless product.available assign show_badge = true assign badge_label = 'products.product.sold_out' | t endunless %} {% if show_badge %} <span class="product__badge">{{ badge_label }}</span> {% endif %}

两个语法提醒:Liquid 没有三元运算符,也不支持条件里用括号分组——复杂逻辑要么嵌套 if,要么像上面这样先算出一个标志位。


九、上线自检

  • 空值判断用 != blank;布尔设置不用 default 兜底;
  • 数组筛选/取字段/查找用 where / map / find,不用 forif
  • 所有集合类循环外层有 {% paginate %}
  • HTML 上下文 | escape<script>| json(且不额外加引号);
  • 富文本设置没有被 escape
  • 金额一律走 money 系列过滤器,没有手写除法;
  • 百分比计算用了浮点数,避免整数除法截断;
  • 元字段取值前判断了 != blank
  • 折叠、弹窗、懒加载优先用原生 <details> / <dialog> / loading="lazy"
  • 用户可见文案走 | t 且键已建。

十、延伸阅读


十一、官方文档


小结:Liquid 没有运行时状态,所以”缓存""防抖""状态管理”这类模式在这里没有位置——真正高频的是另外几条。空值用 != blank 判断(布尔设置别用 default);数组用 where/map/find 而不是 forif;集合循环必须 paginate,因为 for 超过 50 条会静默丢数据;转义按上下文分——HTML 用 escape<script> 里用 json;金额只走 money 过滤器;交互先问能不能用 <details><dialog>loading="lazy" 解决。

最后更新时间: