Skip to Content
🎉 探索 Shopify 的无限可能 结构化知识 + 实战案例,持续更新中...
进阶教程高级Liquid模板

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>

关键点

  • json filter 自动处理引号、转义、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 副作用

唯一例外:productsectionblock 等 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 通用变量名(如 titledescriptionurl)——会污染所有 section / snippet 的作用域。约定命名前缀(如 _layout_title)。

缓存失效

Shopify 主题渲染有页面缓存,但包含 customer 对象的内容不缓存。如果某个 section 引用了 customer.name,整个页面的缓存都会被绕过。

优化方法:把客户专属内容用 AJAX 异步加载,主页 HTML 保持纯静态。

渲染时间上限

单个模板渲染超过约 1.5 秒会触发 Shopify 的 timeout,返回空白或错误。性能压力最大的位置:

  • 主页(多个 section 同时渲染)
  • collection 页(产品列表 + filter)
  • 含大量 metafield 查询的产品页

预防:避免 collections.all.products 在主页、用 paginate 限定产品列表、把复杂数据预存到 metafield。

延伸阅读

最后更新时间: