Impact 的间距折叠:用 CSS 数学判断相邻 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。
值得借鉴的三点
- 用
+ *把信息传给下一个兄弟,绕开“元素读不到前一个兄弟的样式”这个限制。 - 用
calc、min、max把比较结果压成 0 或 1,再用乘法实现条件样式。 在 CSSif()普及之前,这种“布尔变量”很实用。Impact 还用同样的手法定义了--section-is-first,让第一个 section 为透明 header 预留顶部空间(assets/theme.css第 891 行)。 - 把“等于全局背景”统一归为 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。因为后面有回退值,所以页面上看不出问题。