常见模式和解决方案:先把 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,而不是留一个空盒子。
二、数组操作:用过滤器,不要用循环
新手最常见的写法是 for 套 if 挑数据。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,不用for套if; - 所有集合类循环外层有
{% paginate %}; - HTML 上下文
| escape,<script>里| json(且不额外加引号); - 富文本设置没有被
escape; - 金额一律走 money 系列过滤器,没有手写除法;
- 百分比计算用了浮点数,避免整数除法截断;
- 元字段取值前判断了
!= blank; - 折叠、弹窗、懒加载优先用原生
<details>/<dialog>/loading="lazy"; - 用户可见文案走
| t且键已建。
十、延伸阅读
- 自定义代码片段开发 — 把这些模式封装成可复用 snippet
- 主题开发实战要点 — 对象访问与性能的判断标准
- Liquid 过滤器 — 数组、字符串、金额过滤器全览
- 自定义分区开发 — 循环与 block 的配合
- Liquid 数据类型 —
blank、nil、空对象的区别 - Shopify 元字段与元对象 — 结构化内容的建模
十一、官方文档
小结:Liquid 没有运行时状态,所以”缓存""防抖""状态管理”这类模式在这里没有位置——真正高频的是另外几条。空值用
!= blank判断(布尔设置别用default);数组用where/map/find而不是for套if;集合循环必须paginate,因为for超过 50 条会静默丢数据;转义按上下文分——HTML 用escape、<script>里用json;金额只走 money 过滤器;交互先问能不能用<details>、<dialog>和loading="lazy"解决。