Skip to Content
🎉 探索 Shopify 的无限可能 结构化知识 + 实战案例,持续更新中...
Liquid 开发自定义片段

Liquid 自定义代码片段开发:snippet 是函数,不是模板碎片

写 snippet 的人分两种:一种把它当”剪切出去的 HTML 片段”,另一种把它当带参数的函数。前者写出来的 snippet 换个地方用就崩,因为它偷偷依赖了调用处的变量;后者写出来的能被 theme-check 校验、能在编辑器里自动补全参数。

区别只在两件事:理解 {% render %} 的隔离作用域,以及给 snippet 写 LiquidDoc。这一篇就讲这两件事,剩下的都是它们的推论。


一、{% render %} 是隔离作用域

这是最重要的一条。{% render %} 渲染的 snippet 看不到调用处的任何变量,除了你显式传进去的参数(以及全局对象如 settingsshoproutes)。

{% assign badge_text = '新品' %} {% render 'badge' %}

上面的 badge snippet 里 badge_text空的。必须显式传:

{% render 'badge', text: '新品' %}

这个限制不是麻烦,是特性。它保证了:

  • snippet 的行为只由参数决定,不会因为调用处改了个变量名就静默出错;
  • 你能一眼看出一个 snippet 需要什么,不用全局搜索它依赖了哪些变量;
  • 同一个 snippet 在不同 section 里表现一致。

被废弃的 {% include %} 恰好相反:它共享调用处的作用域,还能反向修改外部变量。所以老代码里的 include 一旦出问题极难排查——新代码一律用 render


二、LiquidDoc:给 snippet 一个真正的接口

参数传错在 Liquid 里默认是静默的:拼错的参数名不会报错,缺失的必填参数也不会警告,页面就是少了一块。LiquidDoc 用来解决这个问题。

在 snippet 文件最顶部{% doc %} 声明接口,支持三个标签:@description(说明用途)、@param(声明参数)、@example(用法示例)。

文件snippets/responsive-image.liquid

{% doc %} @description 输出带 srcset 的响应式图片,首屏图请把 priority 设为 true。 @param {image} image - 要渲染的图片对象 @param {string} sizes - CSS sizes 属性值 @param {string} [alt] - 替代文本,默认取图片自带的 alt @param {boolean} [priority] - 是否为首屏关键图,默认 false @param {number} [max_width] - 最大输出宽度,默认 1600 @example {% render 'responsive-image', image: product.featured_image, sizes: '(min-width: 990px) 50vw, 100vw', priority: true %} {% enddoc %} {% liquid assign width = max_width | default: 1600 assign image_alt = alt | default: image.alt if priority assign loading_mode = 'eager' assign fetch_mode = 'high' else assign loading_mode = 'lazy' assign fetch_mode = 'auto' endif %} {% if image != blank %} {{ image | image_url: width: width | image_tag: widths: '360, 540, 720, 900, 1200, 1600', sizes: sizes, loading: loading_mode, fetchpriority: fetch_mode, alt: image_alt }} {% endif %}

四个要点:

类型写在花括号里,可选参数用方括号包住参数名。 {image} image 是必填的 image 对象,{string} [alt] 是可选字符串。theme-check 会据此校验调用处:漏传必填参数、传了未声明的参数,都会报出来。

@example 不只是文档。 编辑器的悬浮提示和补全会用它,团队里其他人不用读实现就知道怎么调。

可选参数的默认值用 | default: 兜底。 LiquidDoc 只声明”可选”,不提供默认值,实际默认值仍要在代码里写。

多行赋值用 {% liquid %} 比连写五行 {% assign %} 干净,也是现在的主流写法。注意 Liquid 里没有三元运算符,条件必须用 {% if %}


三、示例二:参数化的商品卡片

snippet 最典型的用途就是这种”到处都要用、每处略有不同”的组件:

文件snippets/product-card.liquid

{% doc %} @description 商品卡片,用于集合页、推荐位与搜索结果。 @param {product} product - 商品对象 @param {string} [image_sizes] - 图片 sizes 值,默认按三列网格 @param {boolean} [show_vendor] - 是否显示品牌名,默认 false @param {boolean} [priority] - 首屏卡片可设为 true @example {% render 'product-card', product: product, show_vendor: true %} {% enddoc %} {% liquid assign card_sizes = image_sizes | default: '(min-width: 990px) 33vw, 50vw' assign has_discount = false if product.compare_at_price > product.price assign has_discount = true endif %} <article class="product-card"> <a class="product-card__link" href="{{ product.url }}"> {% if product.featured_image != blank %} {% render 'responsive-image', image: product.featured_image, sizes: card_sizes, priority: priority, max_width: 900 %} {% else %} {{ 'product' | placeholder_svg_tag: 'product-card__placeholder' }} {% endif %} <h3 class="product-card__title">{{ product.title | escape }}</h3> </a> {% if show_vendor and product.vendor != blank %} <p class="product-card__vendor">{{ product.vendor | escape }}</p> {% endif %} <p class="product-card__price"> {% if has_discount %} <s class="product-card__price-was">{{ product.compare_at_price | money }}</s> {% endif %} <span class="product-card__price-now">{{ product.price | money }}</span> </p> {% unless product.available %} <p class="product-card__sold-out">{{ 'products.product.sold_out' | t }}</p> {% endunless %} </article> {% stylesheet %} .product-card__title { margin-block: 0.5rem 0.25rem; font-size: 0.95rem; } .product-card__price-was { opacity: 0.6; } {% endstylesheet %}

注意几处:

snippet 里可以 render 另一个 snippet。 上面的卡片复用了 responsive-image,图片逻辑只有一份。这是 snippet 最大的价值。

没有图片时用 placeholder_svg_tag 商家还没上传图的商品不该渲染出一个破图框,官方占位图是现成的。

缺货文案走 | t,并且记得在 locales/en.default.json 里建键——| t 只查表,不建表。


四、用 render ... for 渲染列表

需要对数组逐项渲染时,{% render %} 有个专门的 for 形式,比手写 {% for %}render 更清晰:

{% render 'product-card' for collection.products as product %}

在被渲染的 snippet 内部,除了 product,还能拿到 forloop 对象(forloop.indexforloop.firstforloop.last 等)。

但要注意 50 条上限for 循环最多迭代 50 次,超出部分静默丢失。集合类数据一律先 {% paginate %}

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

五、snippet、theme block 还是 section

三者经常被混用。判断标准是谁来决定它出现在哪、长什么样

谁决定内容有 schema商家能拖动用途
snippet开发者(通过参数)不能复用逻辑与结构:图片、卡片、价格
theme block商家(编辑器里配置)可增删排序的内容单元
section商家整条通栏模块

一句话:需要商家配置的东西不要做成 snippet,因为 snippet 没有 schema,商家看不见它。反过来,纯技术性的复用(怎么输出一张响应式图)不要做成 theme block,那只会在编辑器里给商家添乱。

常见的正确组合是:theme block 负责暴露设置,内部 render 一个 snippet 干实际的活。


六、五个常见错误

依赖调用处的变量。 因为 render 隔离作用域,这类 snippet 表现为”在 A 处好用、在 B 处空白”。写完后自问:这个 snippet 用到的每个值,是参数、全局对象,还是我以为能拿到的外部变量?

传 handle 而不是对象。 {% render 'card', product_handle: 'xxx' %} 然后在里面 all_products[product_handle] —— 多一次查询,且失去类型校验。资源型设置和循环变量本身就是对象,直接传对象。

不写 {% doc %} 没有它,theme-check 无法校验参数、编辑器无法补全,参数拼错永远静默。写 snippet 的第一件事就是写 doc 头。

{% stylesheet %} 里写 Liquid。 那里的 Liquid 不会被渲染。动态值输出到元素的 style="--x: ...",CSS 里用 var(--x)

把整段 JS 塞进 snippet 当”组件”。 交互逻辑用 {% javascript %} 写在自己的组件文件里,由平台合并;不要从 CDN 拉库,也不要在 snippet 里手写一套状态管理——原生 <details><dialog> 能解决的交互别用 JS。


七、上线自检

  • 每个 snippet 顶部有 {% doc %},含 @description、全部 @param(可选参数用 [])、@example
  • 所有可选参数在代码里用 | default: 给了兜底值;
  • snippet 内没有使用未声明为参数的外部变量;
  • 传的是对象而不是 handle;
  • 图片走 image_url + image_tag,首屏图 eager + fetchpriority: 'high'
  • 缺图时有 placeholder_svg_tag 兜底;
  • 集合类循环外层有 {% paginate %}
  • 用户可见文案走 | t 且键已加进 locales/en.default.json
  • {% stylesheet %} 内没有 Liquid;
  • {% render %} 而不是 {% include %}

八、延伸阅读


九、官方文档


小结:把 snippet 当带参数的函数写。{% render %} 是隔离作用域——它看不见调用处的变量,所以每个依赖都必须显式传参,这正是它比废弃的 include 可靠的原因。然后给每个 snippet 写 {% doc %}:声明 @param(可选参数用 [])后,theme-check 才能查出拼错和漏传的参数,否则这类错误永远是静默的。需要商家配置的东西做成 theme block,纯技术复用留给 snippet,两者常见的组合是 block 暴露设置、内部 render snippet 干活。

最后更新时间: