Shopify Liquid snippet 实作:render 参数、作用域与 LiquidDoc
Shopify Liquid snippet 适合把价格、图标、卡片等重复输出拆成可复用的小文件,但真正容易出错的地方不是“把代码放进 snippets 目录”,而是参数怎么传、变量在哪个作用域可见,以及怎样让后续维护者知道它需要什么数据。本文用 Dawn 的真实调用方式和一个通过 Theme Check 的小示例,把这三件事串起来。

snippet 和 render 分别解决什么问题
snippet 是 snippets 目录中的 Liquid 文件。它不会像 section、block 那样直接出现在主题编辑器里,而是由其他 Liquid 文件调用。适合放进去的通常是“有清晰输入、产生一段输出、会在多个位置复用”的内容,例如价格、评分、图标或商品卡片的局部结构。
render 则负责执行 snippet,并通过命名参数把数据传进去。Shopify 官方文档明确说明,render 创建的是隔离作用域:调用位置自己创建的局部变量不会自动进入 snippet,snippet 里创建的局部变量也不会泄漏回调用文件。这种限制看似多写几项参数,实际上能减少“换个页面就突然取不到变量”的隐性依赖。
先看 Dawn 中真实的价格调用链
我在本地 Dawn 仓库提交 258f00f 中检查了 sections/main-product.liquid 和 snippets/price.liquid。商品主区域没有复制整套价格 HTML,而是把当前商品和显示选项传给价格 snippet:
{% render 'price',
product: product,
use_variant: true,
show_badges: true,
price_class: 'price--large'
%}
这段调用里,左侧的 product、use_variant、show_badges 和 price_class 是 snippet 内部读取的参数名;冒号右侧才是调用位置提供的值。参数名不必和父级变量同名,但同名时更容易读懂。
Dawn 的 price.liquid 文件开头也列出了这些参数和调用示例。商品卡片 card-product.liquid 会调用同一个价格 snippet,却传入不同的显示选项。这正是 Shopify Liquid snippet 的典型价值:输出规则只维护一份,调用者明确决定本次需要哪些行为。
render 的隔离作用域怎样理解
假设 section 中先写了 assign sale_label = '促销',再执行 {% render 'price' %}。price snippet 不能仅凭父级存在这个局部变量就读取它;要使用它,应显式传入:
{% render 'price', product: product, sale_label: sale_label %}
反过来,snippet 内部的 assign 也不会改掉调用位置的同名局部变量。全局对象和当前模板可访问的对象另有规则,但在自定义 snippet 中仍建议把关键依赖写成参数。这样只看 render 这一行,就能判断该组件依赖什么。
实作:带 LiquidDoc 的商品价格提示
下面创建 snippets/product-price-note.liquid。它接收商品对象,并用可选布尔参数决定是否显示更高的划线价。示例没有替代 Dawn 完整价格组件的打算,只用于说明一条最小、可验证的参数链。
{% doc %}
Renders the selected variant price and an optional compare-at price.
@param {object} product - Product whose selected or first available variant supplies the price.
@param {boolean} [show_compare_at] - Whether to show a higher compare-at price.
@example
{% render 'product-price-note', product: product, show_compare_at: true %}
{% enddoc %}
{%- liquid
assign current_variant = product.selected_or_first_available_variant
assign current_price = current_variant.price | money
assign compare_at_price = current_variant.compare_at_price
-%}
<p class="product-price-note">
<span class="product-price-note__current">{{ current_price }}</span>
{%- if show_compare_at and compare_at_price > current_variant.price -%}
<s class="product-price-note__compare">{{ compare_at_price | money }}</s>
{%- endif -%}
</p>
{% doc %} 到 {% enddoc %} 是 LiquidDoc 文档区。它不参与店面输出,但能声明说明、参数类型、可选参数和示例。Shopify 的 Liquid VS Code 扩展可以据此提供悬停说明、补全和参数检查。对会长期维护的 snippet,这比只留一句“价格组件”更有用。
在商品模板或能取得 product 对象的 section 中,调用方式如下:
{% render 'product-price-note',
product: product,
show_compare_at: true
%}
这里有两个刻意的保护:先从商品取得 selected_or_first_available_variant,避免直接假设某个变体已被选中;只有划线价存在且高于当前价时才输出 <s>。正式项目还应根据现有主题的货币、税费、单位价格和无障碍规则决定是否复用 Dawn 的 price snippet,而不是盲目重写。
用 Shopify CLI 做 Theme Check
我把上述 snippet 放进一个最小主题结构,并从 section 真实调用它,然后使用本机 Shopify CLI 4.7.0 执行:
shopify theme check --path ./liquid-render-example
--output json
--fail-level error
--no-color
检查结果为 [],即 snippet 与调用文件没有 Theme Check offense。只校验单独的 snippet 时曾出现 OrphanedSnippet 警告,因为当时还没有文件引用它;补上真实 render 调用后警告消失。这也说明静态检查要尽量放在接近真实主题结构的上下文中。
如果需要进一步理解失败级别、warning 与 CI 退出码,可以继续看 Shopify Theme Check 使用教程。
四个常见问题
- 以为父级局部变量会自动可用。 render 是隔离作用域,关键变量应作为命名参数传入。
- 参数名传错但没有立即报错。 Liquid 中未定义值常表现为空,页面可能只是少一块内容。用 LiquidDoc、Theme Check 和真实商品数据一起检查。
- 继续使用旧的 include 思路。 Shopify 已弃用
include,新代码应使用render,并显式处理作用域。 - 在循环里层层 render。 snippet 本身可以复用,但深层嵌套和循环中的大量 render 会增加渲染开销。能在一次调用里完成的结构,不必为了“组件化”继续拆成多层。
snippet、section 和 block 怎么选
| 类型 | 主要用途 | 商家能否直接在主题编辑器配置 |
|---|---|---|
| snippet | 复用局部 Liquid 输出和逻辑 | 不能直接配置,由调用者传参 |
| section | 组成页面的大区域,包含 schema | 可以 |
| block | section 内可添加、排序或复用的内容单元 | 通常可以 |
如果内容需要商家添加、排序或调整设置,先考虑 section 或 block;如果问题是多个模板重复了一段输出,且输入可以通过参数说清楚,才优先考虑 snippet。关于三者在 Dawn 目录里的位置,可以接着看《Shopify Dawn 主题目录结构》;需要先理解编辑器中的层级,则参考《Shopify Section 与 Block 实战理解》。
完成前检查清单
- snippet 的必要输入都通过 render 参数显式传入;
- 可选参数在未传入时也有安全行为;
- LiquidDoc 与真实参数名、类型和示例一致;
- 用真实商品、变体和价格状态检查输出;
- Theme Check 无错误,并留意嵌套 render 的数量。
一个好用的 Shopify Liquid snippet 不只是“能被 render”,还应该让调用位置一眼看出输入,让文件内部不依赖隐含局部变量,并能通过文档和静态检查把错误提前暴露。先从价格或图标这样边界清楚的小组件开始,比一次拆完整张商品卡片更容易验证。




