Liquid 进阶技巧
本教程面向已经熟悉 Liquid 基础语法,正在或将要负责主题级别工程的开发者。重点不在”Liquid 能做什么”,而在生产级主题中的可靠模式:如何处理复杂数据、如何控制性能、如何把代码组织得可维护。
阅读前置:
- 熟悉 Liquid 基础(assign、for、if、capture、render)
- 理解 Shopify 主题目录结构(layout、templates、sections、snippets)
- 推荐先阅读 Liquid 最佳实践
一、复杂数据结构处理
Liquid 没有原生的数组操作 API,处理多维或动态结构时需要借助字符串拼接与 split。
唯一值收集(去重)
从产品集合中收集所有唯一 tag:
{%- liquid
assign all_tags = ''
for product in collection.products
for tag in product.tags
unless all_tags contains tag
assign all_tags = all_tags | append: tag | append: '|'
endunless
endfor
endfor
assign tags_array = all_tags | split: '|' | compact
-%}
<ul class="tag-cloud">
{%- for tag in tags_array -%}
<li><a href="{{ collection.url }}/{{ tag | handle }}">{{ tag }}</a></li>
{%- endfor -%}
</ul>关键点:
- 用
|而非,作为分隔符,避免 tag 本身含逗号时拆分错误 compact过滤器移除空字符串- 这种模式对 collection.products.size > 200 会有性能压力,大集合应考虑用 metafield 预存
构建 JSON 输出
为前端 JavaScript 提供数据时,永远不要拼字符串。用 Shopify 的 json filter:
{%- liquid
assign products_data = ''
-%}
<script>
window.collectionData = {
"title": {{ collection.title | json }},
"handle": {{ collection.handle | json }},
"products_count": {{ collection.products.size }},
"products": [
{%- for product in collection.products limit: 12 -%}
{
"id": {{ product.id }},
"title": {{ product.title | json }},
"handle": {{ product.handle | json }},
"url": {{ product.url | json }},
"price": {{ product.price }},
"available": {{ product.available }},
"image": {{ product.featured_image | image_url: width: 600 | json }}
}{% unless forloop.last %},{% endunless %}
{%- endfor -%}
]
};
</script>关键点:
jsonfilter 自动处理引号、转义、Unicode- 价格保持整数(cents),前端再除以 100
forloop.last控制逗号
动态属性访问
Liquid 支持方括号语法访问对象属性:
{%- liquid
assign meta_key = 'specifications'
assign specs = product.metafields.custom[meta_key]
-%}
{%- for i in (1..3) -%}
{%- assign option_key = 'option' | append: i -%}
{%- if product[option_key] != blank -%}
<div class="option">{{ product[option_key] }}</div>
{%- endif -%}
{%- endfor -%}应用场景:
- 多语言字段从 metafield 按 locale 动态取值
- 配置驱动的列表渲染(同一个 section 服务多种类型)
- A/B 测试时根据用户分组选择不同内容字段
二、过滤器链式优化
过滤器可以链式组合,但顺序很重要——错误顺序导致结果不同或性能下降。
顺序原则
先过滤、再排序、最后限量:
{# 推荐:先 where 后 sort #}
{% assign featured_products = collection.products
| where: 'available'
| sort: 'created_at'
| reverse
| slice: 0, 6 %}{# 不推荐:先 sort 后 where,sort 浪费在被过滤掉的元素上 #}
{% assign featured_products = collection.products
| sort: 'created_at'
| reverse
| where: 'available'
| slice: 0, 6 %}虽然结果相同,前者在大集合(> 1000 产品)上明显更快。
多条件 where 链式
{% assign sale_featured = collection.products
| where: 'available'
| where: 'tags', 'featured'
| where: 'compare_at_price', '>', 0 %}每个 where 都是 O(n) 遍历。如果有 5000 产品,三个 where 链 = 15000 次比较。建议先用 paginate 或集合预筛限制范围。
字符串处理链
{%- liquid
assign description = product.description
| strip_html
| strip_newlines
| truncate: 160, '…'
| escape
-%}
<meta name="description" content="{{ description }}">strip_html 必须最先,否则 <p>…</p> 内的标点会被截断算错字数。
三、循环性能模式
tablerow 替代 for + 网格 CSS
需要固定列数的网格时,tablerow 比 for + CSS Grid 更简洁:
{% tablerow product in collection.products cols: 4 limit: 12 %}
{% render 'product-card', product: product %}
{% endtablerow %}生成的 HTML 是 <table> 结构,对静态展示性能略优于 CSS Grid。但缺乏响应式能力(不同断点切换列数),所以仅适合桌面端固定布局或邮件模板。
break 与 continue 早退
{%- liquid
assign featured_count = 0
for product in collection.products
unless product.available
continue
endunless
if product.tags contains 'featured'
assign featured_count = featured_count | plus: 1
render 'product-card', product: product
if featured_count >= 6
break
endif
endif
endfor
-%}break 在找到足够元素后立即退出,避免遍历整个集合。
数据预处理移出循环
循环内的重复计算是常见性能瓶颈:
{# 反例:循环内每次都重新计算 #}
{% for product in collection.products %}
{% if collection.products.size > 10 %}
{# size 在每次迭代中都被访问 #}
{% endif %}
{% endfor %}
{# 推荐:循环外预计算 #}
{%- liquid
assign total_count = collection.products.size
assign should_show_extra = false
if total_count > 10
assign should_show_extra = true
endif
-%}
{% for product in collection.products %}
{% if should_show_extra %}
{# 使用预计算的标志 #}
{% endif %}
{% endfor %}四、复杂条件逻辑
多重条件组合用单独的 assign 化简
{# 反例:深层嵌套难读 #}
{% if customer %}
{% if customer.orders.size > 0 %}
{% if customer.total_spent > 100000 %}
{% if customer.tags contains 'vip' %}
<!-- VIP 内容 -->
{% endif %}
{% endif %}
{% endif %}
{% endif %}
{# 推荐:扁平化布尔值 #}
{%- liquid
assign is_vip = false
if customer
and customer.orders.size > 0
and customer.total_spent > 100000
and customer.tags contains 'vip'
assign is_vip = true
endif
-%}
{% if is_vip %}
<!-- VIP 内容 -->
{% endif %}case / when 替代多分支 if
{%- liquid
case template
when 'product'
assign body_class = 'page--product'
when 'collection'
assign body_class = 'page--collection'
when 'cart'
assign body_class = 'page--cart'
when 'index'
assign body_class = 'page--home'
else
assign body_class = 'page--default'
endcase
-%}
<body class="{{ body_class }}">时间窗口判断
{%- liquid
assign now = 'now' | date: '%s' | times: 1
assign sale_start = settings.sale_start | date: '%s' | times: 1
assign sale_end = settings.sale_end | date: '%s' | times: 1
assign is_sale_active = false
if now >= sale_start and now <= sale_end
assign is_sale_active = true
endif
-%}
{% if is_sale_active %}
<div class="sale-banner">限时优惠中</div>
{% endif %}'now' | date: '%s' 返回 Unix 时间戳字符串,乘 1 转为数字才能比较。
五、字符串处理进阶
SKU 解析
按规则解码 SKU 提取分类信息:
{%- liquid
assign sku_parts = product.sku | split: '-'
assign category = sku_parts[0]
assign size = sku_parts[1]
assign color = sku_parts[2]
-%}
<div class="product-meta" data-category="{{ category }}" data-color="{{ color }}">动态 CSS 类名拼接
{%- liquid
assign css_classes = 'product-card'
unless product.available
assign css_classes = css_classes | append: ' is-sold-out'
endunless
if product.tags contains 'new'
assign css_classes = css_classes | append: ' is-new'
endif
if product.compare_at_price > product.price
assign css_classes = css_classes | append: ' is-on-sale'
endif
-%}
<div class="{{ css_classes }}">智能截断保持完整词
truncate 会在字符位置硬切,可能截掉半个单词。保持单词完整:
{%- liquid
assign desc = product.description | strip_html
if desc.size > 150
assign truncated = desc | truncate: 150, ''
assign words = truncated | split: ' '
assign safe_words = words | slice: 0, -1
assign final_desc = safe_words | join: ' ' | append: '…'
else
assign final_desc = desc
endif
-%}
<p>{{ final_desc }}</p>六、组件化与作用域
snippet 参数化设计
Liquid 没有真正的”函数”,但 {% render %} + 命名参数能达到类似效果:
{# snippets/price-display.liquid #}
{%- comment %}
显示价格组件
必需参数:
price (number) - 价格(cents)
可选参数:
compare_at_price (number) - 划线价(cents)
currency_code (string) - 货币代码,默认 cart.currency.iso_code
show_savings (boolean) - 是否显示节省金额,默认 false
css_class (string) - 自定义 class
{% endcomment -%}
{%- liquid
assign currency_code = currency_code | default: cart.currency.iso_code
assign show_savings = show_savings | default: false
assign css_class = css_class | default: 'price-display'
-%}
<div class="{{ css_class }}">
{%- if compare_at_price and compare_at_price > price -%}
<span class="price-display__sale">{{ price | money }}</span>
<span class="price-display__regular"><s>{{ compare_at_price | money }}</s></span>
{%- if show_savings -%}
{%- assign savings = compare_at_price | minus: price -%}
<span class="price-display__savings">省 {{ savings | money }}</span>
{%- endif -%}
{%- else -%}
<span class="price-display__regular">{{ price | money }}</span>
{%- endif -%}
</div>调用:
{% render 'price-display',
price: product.price,
compare_at_price: product.compare_at_price,
show_savings: true %}作用域隔离的好处
{% render %} 的作用域隔离让 snippet 真正可复用:
- snippet 内部不会被父模板变量”污染”
- 多次调用时变量不会互相影响
- 重构父模板时无需担心 snippet 副作用
唯一例外:product、section、block 等 Shopify 内置变量在 section 上下文中可自动透传到 snippet(取决于 Shopify 版本)。显式传参是更安全的做法。
Section block 循环
支持 block 的 section 可让运营人员在主题编辑器中自由增删模块:
{# sections/featured-collections.liquid #}
<div class="featured-collections">
{%- for block in section.blocks -%}
{%- case block.type -%}
{%- when 'collection' -%}
<div class="featured-collection" {{ block.shopify_attributes }}>
<h2>{{ block.settings.title }}</h2>
{%- if block.settings.collection != blank -%}
{%- render 'collection-preview',
collection: collections[block.settings.collection],
limit: block.settings.product_count -%}
{%- endif -%}
</div>
{%- when 'banner' -%}
<div class="featured-banner" {{ block.shopify_attributes }}>
<img src="{{ block.settings.image | image_url: width: 1200 }}"
alt="{{ block.settings.alt }}">
</div>
{%- endcase -%}
{%- endfor -%}
</div>
{% schema %}
{
"name": "Featured Collections",
"blocks": [
{
"type": "collection",
"name": "Collection",
"settings": [
{ "type": "text", "id": "title", "label": "标题" },
{ "type": "collection", "id": "collection", "label": "集合" },
{ "type": "range", "id": "product_count", "label": "产品数",
"min": 2, "max": 12, "step": 2, "default": 4 }
]
},
{
"type": "banner",
"name": "Banner",
"settings": [
{ "type": "image_picker", "id": "image", "label": "图片" },
{ "type": "text", "id": "alt", "label": "Alt 文本" }
]
}
],
"presets": [{ "name": "Featured Collections" }]
}
{% endschema %}block.shopify_attributes 是必加的——它让主题编辑器能识别并高亮可点击的 block。
七、国际化与本地化
翻译键的命名约定
按 命名空间.作用域.具体内容 三段式组织:
{
"products": {
"product": {
"add_to_cart": "加入购物车",
"sold_out": "已售罄",
"on_sale": "限时优惠"
},
"facets": {
"filter_by": "筛选条件",
"showing_count": "共 {{ count }} 件商品"
}
},
"cart": {
"general": {
"subtotal": "小计",
"free_shipping_remaining": "再加 {{ amount }} 包邮"
}
}
}调用:
{{ 'products.product.add_to_cart' | t }}
{{ 'products.facets.showing_count' | t: count: collection.products_count }}
{{ 'cart.general.free_shipping_remaining' | t: amount: remaining_amount | money }}货币地区化
{%- liquid
assign price_money = product.price | money
assign price_no_currency = product.price | money_without_currency
assign price_with_iso = product.price | money_with_currency
-%}
<span itemprop="price" content="{{ product.price | divided_by: 100.00 }}">
{{ price_money }}
</span>
<meta itemprop="priceCurrency" content="{{ cart.currency.iso_code }}">不同 filter 输出:
money:使用 Markets 配置的格式(如$9.99)money_without_currency:仅金额(9.99)money_with_currency:金额 + ISO 码(9.99 USD)
多语言内容回退
{%- liquid
assign current_locale = request.locale.iso_code
assign translated_title = product.metafields.translations[current_locale].title
assign final_title = translated_title | default: product.title
-%}
<h1>{{ final_title }}</h1>default filter 在变量为空或 false 时返回 fallback,是回退机制的标准写法。
八、调试技巧
输出对象结构
开发时输出对象的完整 JSON 结构,了解可用字段:
{% if settings.debug_mode %}
<pre style="background:#f4f4f4;padding:1em;font-size:11px;">
{{ product | json }}
</pre>
{% endif %}不要在生产环境保留——product | json 输出可能包含敏感字段。
性能埋点
{%- assign perf_start = 'now' | date: '%s%L' | times: 1 -%}
{# 待测代码块 #}
{%- for product in collection.products -%}
{# 复杂逻辑 #}
{%- endfor -%}
{%- assign perf_end = 'now' | date: '%s%L' | times: 1 -%}
{%- assign perf_diff = perf_end | minus: perf_start -%}
{% if settings.show_perf %}
<div class="perf-info">Block executed in {{ perf_diff }}ms</div>
{% endif %}%L 输出毫秒级时间戳。注意 Liquid 渲染时间通常 < 100ms,更精细的性能问题需要在客户端用 Performance API 测。
错误边界
Liquid 没有 try/catch,但可以用 default 与显式检查模拟错误处理:
{%- liquid
assign featured_handle = settings.featured_collection_handle
assign featured_collection = collections[featured_handle]
-%}
{%- if featured_collection and featured_collection.products.size > 0 -%}
{%- render 'collection-preview', collection: featured_collection -%}
{%- elsif collections.all.products.size > 0 -%}
{# 回退到全部产品 #}
{%- render 'collection-preview', collection: collections.all -%}
{%- else -%}
<p>暂无产品</p>
{%- endif -%}九、生产环境注意事项
Section 限制
每个 section 文件最大 1MB,schema JSON 最大 16KB。超出会导致主题保存失败。复杂 section 应拆分为多个独立 section 或用 snippet 抽取。
全局变量泄露
避免在 layout 中 assign 通用变量名(如 title、description、url)——会污染所有 section / snippet 的作用域。约定命名前缀(如 _layout_title)。
缓存失效
Shopify 主题渲染有页面缓存,但包含 customer 对象的内容不缓存。如果某个 section 引用了 customer.name,整个页面的缓存都会被绕过。
优化方法:把客户专属内容用 AJAX 异步加载,主页 HTML 保持纯静态。
渲染时间上限
单个模板渲染超过约 1.5 秒会触发 Shopify 的 timeout,返回空白或错误。性能压力最大的位置:
- 主页(多个 section 同时渲染)
- collection 页(产品列表 + filter)
- 含大量 metafield 查询的产品页
预防:避免 collections.all.products 在主页、用 paginate 限定产品列表、把复杂数据预存到 metafield。