Liquid 自定义代码片段开发:snippet 是函数,不是模板碎片
写 snippet 的人分两种:一种把它当”剪切出去的 HTML 片段”,另一种把它当带参数的函数。前者写出来的 snippet 换个地方用就崩,因为它偷偷依赖了调用处的变量;后者写出来的能被 theme-check 校验、能在编辑器里自动补全参数。
区别只在两件事:理解 {% render %} 的隔离作用域,以及给 snippet 写 LiquidDoc。这一篇就讲这两件事,剩下的都是它们的推论。
一、{% render %} 是隔离作用域
这是最重要的一条。{% render %} 渲染的 snippet 看不到调用处的任何变量,除了你显式传进去的参数(以及全局对象如 settings、shop、routes)。
{% 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.index、forloop.first、forloop.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 与 theme block 的分工
- 常见模式和解决方案 — 空值、数组过滤、转义等 Liquid 模式
- 主题开发实战要点 — 性能与对象访问的判断标准
- Liquid 过滤器 —
image_url、money、default等 - 布局模板开发 — 组件级资源在主题里的位置
- 性能优化 — 渲染成本与资源加载
九、官方文档
小结:把 snippet 当带参数的函数写。
{% render %}是隔离作用域——它看不见调用处的变量,所以每个依赖都必须显式传参,这正是它比废弃的include可靠的原因。然后给每个 snippet 写{% doc %}:声明@param(可选参数用[])后,theme-check 才能查出拼错和漏传的参数,否则这类错误永远是静默的。需要商家配置的东西做成 theme block,纯技术复用留给 snippet,两者常见的组合是 block 暴露设置、内部 render snippet 干活。