EN
Shopify 知识库 · 概念

Impact 的间距折叠:用 CSS 数学判断相邻 section 背景是否相同

Impact 7.2.0 如何用 Liquid 把背景色转成数字哈希,借相邻兄弟选择器传给下一个 section,再用 calc、min、max 算出布尔值来决定顶部间距,避免同背景 section 出现双倍留白。
历史资料
请结合文中的适用版本和来源阅读。

本篇基于 Impact 7.2.0,涉及的文件是 snippets/section-spacing-collapsing.liquid 和 assets/theme.css。

问题

Shopify 主题的 section 由商家在 Theme Editor 里自由排列,每个 section 通常都有上下内边距。于是会出现两种情况:

  • 两个背景相同的 section 相邻:上一个的底部间距加上下一个的顶部间距,看起来是双倍留白。
  • 两个背景不同的 section 相邻:每个色块都需要完整的内边距,否则内容会贴着色块边缘。

正确的规则是“背景相同就只保留一份间距,不同就两边都保留”。难点在于每个 section 是独立渲染的,Liquid 拿不到上一个 section 的背景;CSS 选择器也不能比较两个元素的样式值。

常见的做法有两种:用 JS 在页面加载后逐个比较 section 的背景色,或者让商家手动调每个 section 的上下间距。Impact 两种都没有用,只靠 Liquid 和 CSS 就解决了。

第一步:把背景色变成数字

每个 section 开头会渲染这个 snippet。它先把背景色做 md5,再逐个字符转成数字拼起来:

{%- assign background_hash = background | md5 | split: '' -%}

{%- assign background_numeric_hash = 0 -%}
{%- for item in background_hash -%}
  {%- assign background_numeric_hash = item | times: 1 | prepend: background_numeric_hash -%}
{%- endfor -%}

md5 的结果是 32 位十六进制字符串。times: 1 让数字字符保持原值,a 到 f 变成 0,拼接后得到一长串数字。严格来说这不是好的哈希,但足以让不同颜色得到不同的数。

以下三种情况不计算哈希,值保持为 0:

  • 没有设置背景;
  • 背景是透明色 rgba(0,0,0,0);
  • 背景等于全局设置 settings.background。

也就是说,所有“跟随页面背景”的 section 共享同一个值 0。整个方案能成立,靠的就是这一点。

第二步:同时写给自己和下一个兄弟

<style>
  #shopify-section-{{ section.id }} {
    --section-background-hash: {{ background_numeric_hash }};
  }

  #shopify-section-{{ section.id }} + * {
    --previous-section-background-hash: {{ background_numeric_hash }};
  }
</style>

section 看不到自己前面的兄弟,但可以用相邻兄弟选择器 + * 把自己的值推给后一个兄弟。这样每个 section 身上都有两个变量:一个是自己的背景值,一个是前一个 section 的背景值。

第三步:在 CSS 里把“是否不同”算成 0 或 1

assets/theme.css 第 2030–2034 行:

[role="main"] .shopify-section {
  --hash-difference: calc(var(--section-background-hash) - var(--previous-section-background-hash, -1));
  --hash-difference-abs: max(var(--hash-difference), -1 * var(--hash-difference));
  --background-differs-from-previous: max(0, min(var(--hash-difference-abs, 1), 1));
}
  • 两个值相减,再用 max(x, -x) 取绝对值。这是 abs() 普及之前的写法。
  • 用 max(0, min(x, 1)) 把结果限制在 0 到 1 之间。两个哈希都是整数,只要不相等,差的绝对值至少是 1,所以结果只可能是 0 或 1,相当于一个布尔值。
  • 第一个 section 前面没有兄弟,拿不到 --previous-section-background-hash,回退值 -1 让它必然被判为“不同”,所以首个 section 总会有顶部间距。

第四步:用布尔值乘间距

assets/theme.css 第 2074–2077 行:

.section {
  padding-block-start: calc(var(--background-differs-from-previous) * var(--calculated-section-spacing-block-start));
  padding-block-end: var(--calculated-section-spacing-block-end);
}

底部间距一直保留,顶部间距只在背景和上一个不同时才出现。两个同背景的 section 相邻时,中间只剩上一个的底部间距,正好是一份。

两个延伸细节

盒装 section。 非全宽、又设置了背景的 section,在宽度 ≥700px 时会显示成带圆角和阴影的卡片,四周露出页面背景。卡片外面是页面背景,所以对它的邻居来说,它的表现应该和“跟随页面背景”的 section 一样。snippet 为此加了一段媒体查询,把自己的值和推给下一个的值都改成 0:

{%- if section_boxed and block_margin_collapsing != true -%}
  @media screen and (min-width: 700px) {
    #shopify-section-{{ section.id }} { --section-background-hash: 0; }
    #shopify-section-{{ section.id }} + * { --previous-section-background-hash: 0; }
  }
{%- endif -%}

同一个 section 在手机上是全宽色块,在桌面上是卡片,判定结果随断点自动切换,不需要 JS。卡片的上外边距也乘了同一个布尔值(assets/theme.css 第 2094 行)。

主动退出折叠。 hot-spots section 传入 block_margin_collapsing: true,snippet 改用 'now' | date: '%N' 生成一个几乎不会和别人重复的值,强制让它与前后都被判为“不同”。

覆盖面

在 sections/ 目录中,有 41 个 section 渲染了这个 snippet。没有渲染的主要是 header、footer、抽屉、弹窗和客户账户页等不参与正文排版的 section。

值得借鉴的三点

  1. 用 + * 把信息传给下一个兄弟,绕开“元素读不到前一个兄弟的样式”这个限制。
  2. 用 calc、min、max 把比较结果压成 0 或 1,再用乘法实现条件样式。 在 CSS if() 普及之前,这种“布尔变量”很实用。Impact 还用同样的手法定义了 --section-is-first,让第一个 section 为透明 header 预留顶部空间(assets/theme.css 第 891 行)。
  3. 把“等于全局背景”统一归为 0,所有默认背景的 section 就会自动折叠,商家完全不用操心。

局限

  • 数值精度。 哈希是一个约 33 位的十进制数,而 CSS 数值是浮点数,只能精确保留 15 到 16 位有效数字。两个不同颜色的哈希如果前 15 到 16 位恰好相同,相减会得到 0,被误判为“相同”。这种情况概率很低,但并非不可能;a 到 f 全部变成 0 也损失了信息量。更稳的做法是只取 md5 的前几位,或者直接把 RGB 分量编码成一个较小的整数。
  • 只比较直接相邻的兄弟。 前一个 section 的值只推给紧挨着的那个元素。如果中间夹着一个被 :empty 隐藏的空 section,空 section 自己不会输出这段 <style>,后面的 section 拿不到前一个可见 section 的值,会回退为 -1,即使两者背景相同也会多出顶部间距。
  • 变量名拼写错误。 assets/theme.css 第 2095 行写的是 --section-outer-spacin-block-end,少了一个 g。因为后面有回退值,所以页面上看不出问题。