使用内联 CSS 自定义属性和 calc() 创建高效的无限实用程序辅助函数

Avatar of Andy Ford
Andy Ford

DigitalOcean 为您的旅程的每个阶段提供云产品。 立即开始使用 $200 免费额度!

我最近编写了一个非常基本的 Sass 循环,它输出多个填充和边距实用程序类。 没什么特别的,只是一个包含 11 个间距值的 Sass 地图,循环以在每侧创建填充和边距类。 正如我们将在下面看到的,这有效,但最终会产生大量 CSS。 我们将对其进行重构以使用 CSS 自定义属性 并使系统更加精简。

这是原始 Sass 实现

$space-stops: (
  '0': 0,
  '1': 0.25rem,
  '2': 0.5rem,
  '3': 0.75rem,
  '4': 1rem,
  '5': 1.25rem,
  '6': 1.5rem,
  '7': 1.75rem,
  '8': 2rem,
  '9': 2.25rem,
  '10': 2.5rem,
);

@each $key, $val in $space-stops {
  .p-#{$key} {
    padding: #{$val} !important;
  }
  .pt-#{$key} {
    padding-top: #{$val} !important;
  }
  .pr-#{$key} {
    padding-right: #{$val} !important;
  }
  .pb-#{$key} {
    padding-bottom: #{$val} !important;
  }
  .pl-#{$key} {
    padding-left: #{$val} !important;
  }
  .px-#{$key} {
    padding-right: #{$val} !important;
    padding-left: #{$val} !important;
  }
  .py-#{$key} {
    padding-top: #{$val} !important;
    padding-bottom: #{$val} !important;
  }

  .m-#{$key} {
    margin: #{$val} !important;
  }
  .mt-#{$key} {
    margin-top: #{$val} !important;
  }
  .mr-#{$key} {
    margin-right: #{$val} !important;
  }
  .mb-#{$key} {
    margin-bottom: #{$val} !important;
  }
  .ml-#{$key} {
    margin-left: #{$val} !important;
  }
  .mx-#{$key} {
    margin-right: #{$val} !important;
    margin-left: #{$val} !important;
  }
  .my-#{$key} {
    margin-top: #{$val} !important;
    margin-bottom: #{$val} !important;
  }
}

这非常有效。 它输出我们需要的全部实用程序类。 但是,它也会很快膨胀。 在我的情况下,它们大约有 8.6kb 未压缩,压缩后不到 1kb。(Brotli 为 542 字节,gzip 为 925 字节。)

由于它们极度重复,因此压缩效果很好,但我仍然无法摆脱这种感觉,即所有这些类都是多余的。 另外,我还没有进行任何小/中/大断点,而这些断点对于这类辅助类来说是相当典型的。

以下是一个经过设计示例,展示了添加小/中/大类后的响应式版本。 我们将重用之前定义的 $space-stops 地图并将重复的代码放入 mixin 中

@mixin finite-spacing-utils($bp: '') {
    @each $key, $val in $space-stops {
        .p-#{$key}#{$bp} {
            padding: #{$val} !important;
        }
        .pt-#{$key}#{$bp} {
            padding-top: #{$val} !important;
        }
        .pr-#{$key}#{$bp} {
            padding-right: #{$val} !important;
        }
        .pb-#{$key}#{$bp} {
            padding-bottom: #{$val} !important;
        }
        .pl-#{$key}#{$bp} {
            padding-left: #{$val} !important;
        }
        .px-#{$key}#{$bp} {
            padding-right: #{$val} !important;
            padding-left: #{$val} !important;
        }
        .py-#{$key}#{$bp} {
            padding-top: #{$val} !important;
            padding-bottom: #{$val} !important;
        }

        .m-#{$key}#{$bp} {
            margin: #{$val} !important;
        }
        .mt-#{$key}#{$bp} {
            margin-top: #{$val} !important;
        }
        .mr-#{$key}#{$bp} {
            margin-right: #{$val} !important;
        }
        .mb-#{$key}#{$bp} {
            margin-bottom: #{$val} !important;
        }
        .ml-#{$key}#{$bp} {
            margin-left: #{$val} !important;
        }
        .mx-#{$key}#{$bp} {
            margin-right: #{$val} !important;
            margin-left: #{$val} !important;
        }
        .my-#{$key}#{$bp} {
            margin-top: #{$val} !important;
            margin-bottom: #{$val} !important;
        }
    }
}

@include finite-spacing-utils;

@media (min-width: 544px) {
    @include finite-spacing-utils($bp: '_sm');
}

@media (min-width: 768px) {
    @include finite-spacing-utils($bp: '_md');
}

@media (min-width: 1024px) {
    @include finite-spacing-utils($bp: '_lg');
}

这大约有 41.7kb 未压缩(Brotli 约为 1kb,gzip 约为 3kb)。 它仍然压缩得很好,但有点荒谬。

我知道可以使用 data-* 属性 从 CSS 内部引用 [attr() 函数,所以我很好奇是否可以使用 calc()attr() 结合在一起,通过 data-* 属性创建动态计算的间距实用程序辅助函数 - 例如 data-m="1"data-m="1@md" - 然后在 CSS 中执行类似于 margin: calc(attr(data-m) * 0.25rem) 的操作(假设我正在使用一个以 0.25rem 间隔递增的间距刻度)。 这将非常强大。

但这个故事的结局是:不,你(目前)无法attr() 与除 content 属性之外的任何属性一起使用。 真可惜。 但在搜索 attr()calc() 信息时,我发现 Simon Rigét 在 Stack Overflow 上发表的这篇有趣的评论 建议将 CSS 变量直接设置在内联样式属性中。 哈哈!

因此可以执行类似于 <div style="--p: 4;"> 的操作,然后在 CSS 中

:root {
  --p: 0;
}

[style*='--p:'] {
  padding: calc(0.25rem * var(--p)) !important;
}

style="--p: 4;" 示例的情况下,你将有效地得到 padding: 1rem !important;

… 现在你有一个可无限扩展的间距实用程序类怪物辅助函数。

以下是 CSS 中可能的样子

:root {
  --p: 0;
  --pt: 0;
  --pr: 0;
  --pb: 0;
  --pl: 0;
  --px: 0;
  --py: 0;
  --m: 0;
  --mt: 0;
  --mr: 0;
  --mb: 0;
  --ml: 0;
  --mx: 0;
  --my: 0;
}

[style*='--p:'] {
  padding: calc(0.25rem * var(--p)) !important;
}
[style*='--pt:'] {
  padding-top: calc(0.25rem * var(--pt)) !important;
}
[style*='--pr:'] {
  padding-right: calc(0.25rem * var(--pr)) !important;
}
[style*='--pb:'] {
  padding-bottom: calc(0.25rem * var(--pb)) !important;
}
[style*='--pl:'] {
  padding-left: calc(0.25rem * var(--pl)) !important;
}
[style*='--px:'] {
  padding-right: calc(0.25rem * var(--px)) !important;
  padding-left: calc(0.25rem * var(--px)) !important;
}
[style*='--py:'] {
  padding-top: calc(0.25rem * var(--py)) !important;
  padding-bottom: calc(0.25rem * var(--py)) !important;
}

[style*='--m:'] {
  margin: calc(0.25rem * var(--m)) !important;
}
[style*='--mt:'] {
  margin-top: calc(0.25rem * var(--mt)) !important;
}
[style*='--mr:'] {
  margin-right: calc(0.25rem * var(--mr)) !important;
}
[style*='--mb:'] {
  margin-bottom: calc(0.25rem * var(--mb)) !important;
}
[style*='--ml:'] {
  margin-left: calc(0.25rem * var(--ml)) !important;
}
[style*='--mx:'] {
  margin-right: calc(0.25rem * var(--mx)) !important;
  margin-left: calc(0.25rem * var(--mx)) !important;
}
[style*='--my:'] {
  margin-top: calc(0.25rem * var(--my)) !important;
  margin-bottom: calc(0.25rem * var(--my)) !important;
}

这非常像上面的第一个 Sass 循环,但没有 11 次循环 - 而且它是无限的。 它大约有 1.4kb 未压缩,Brotli 为 226 字节,gzip 为 284 字节。

如果你想将它扩展到断点,不幸的是你不能将“@”字符放在 CSS 变量名中(尽管奇怪的是允许表情符号和其他 UTF-8 字符)。 因此,你可能可以设置类似于 p_smsm_p 的变量名。 你需要添加一些额外的 CSS 变量和一些媒体查询来处理这一切,但它不会像使用 Sass for 循环创建的传统 CSS 类名那样呈指数级增长。

以下是等效的响应式版本。 我们将再次使用 Sass mixin 来减少重复

:root {
  --p: 0;
  --pt: 0;
  --pr: 0;
  --pb: 0;
  --pl: 0;
  --px: 0;
  --py: 0;
  --m: 0;
  --mt: 0;
  --mr: 0;
  --mb: 0;
  --ml: 0;
  --mx: 0;
  --my: 0;
}

@mixin infinite-spacing-utils($bp: '') {
    [style*='--p#{$bp}:'] {
        padding: calc(0.25rem * var(--p#{$bp})) !important;
    }
    [style*='--pt#{$bp}:'] {
        padding-top: calc(0.25rem * var(--pt#{$bp})) !important;
    }
    [style*='--pr#{$bp}:'] {
        padding-right: calc(0.25rem * var(--pr#{$bp})) !important;
    }
    [style*='--pb#{$bp}:'] {
        padding-bottom: calc(0.25rem * var(--pb#{$bp})) !important;
    }
    [style*='--pl#{$bp}:'] {
        padding-left: calc(0.25rem * var(--pl#{$bp})) !important;
    }
    [style*='--px#{$bp}:'] {
        padding-right: calc(0.25rem * var(--px#{$bp})) !important;
        padding-left: calc(0.25rem * var(--px)#{$bp}) !important;
    }
    [style*='--py#{$bp}:'] {
        padding-top: calc(0.25rem * var(--py#{$bp})) !important;
        padding-bottom: calc(0.25rem * var(--py#{$bp})) !important;
    }
    [style*='--m#{$bp}:'] {
        margin: calc(0.25rem * var(--m#{$bp})) !important;
    }
    [style*='--mt#{$bp}:'] {
        margin-top: calc(0.25rem * var(--mt#{$bp})) !important;
    }
    [style*='--mr#{$bp}:'] {
        margin-right: calc(0.25rem * var(--mr#{$bp})) !important;
    }
    [style*='--mb#{$bp}:'] {
        margin-bottom: calc(0.25rem * var(--mb#{$bp})) !important;
    }
    [style*='--ml#{$bp}:'] {
        margin-left: calc(0.25rem * var(--ml#{$bp})) !important;
    }
    [style*='--mx#{$bp}:'] {
        margin-right: calc(0.25rem * var(--mx#{$bp})) !important;
        margin-left: calc(0.25rem * var(--mx#{$bp})) !important;
    }
    [style*='--my#{$bp}:'] {
        margin-top: calc(0.25rem * var(--my#{$bp})) !important;
        margin-bottom: calc(0.25rem * var(--my#{$bp})) !important;
    }
}

@include infinite-spacing-utils;

@media (min-width: 544px) {
    @include infinite-spacing-utils($bp: '_sm');
}

@media (min-width: 768px) {
    @include infinite-spacing-utils($bp: '_md');
}

@media (min-width: 1024px) {
    @include infinite-spacing-utils($bp: '_lg');
}

这大约有 6.1kb 未压缩,Brotli 为 428 字节,gzip 为 563 字节。

我认为编写像 <div style="--px:2; --my:4;"> 这样的 HTML 代码是否赏心悦目或具有良好的开发人员人体工程学… 不,不是特别。 但是这种方法在您(由于某种原因)需要极少量的 CSS 或根本不需要外部 CSS 文件的情况下可行吗? 是的,我当然认为可行。

值得在此指出的是,在内联样式中分配的 CSS 变量不会泄漏。 它们仅作用于当前元素,不会全局更改变量的值。 感谢上帝! 我到目前为止发现的一个奇怪之处是,DevTools(至少在 Chrome、Firefox 和 Safari 中)不会在“计算”样式选项卡中报告使用此技术的样式。

还需要提及的是,我使用了传统的 paddingmargin 属性,以及 -top-right-bottom-left,但你可以使用等效的 逻辑属性,如 padding-blockpadding-inline。 甚至可以通过选择性地混合和匹配逻辑属性和传统属性来减少一些字节。 我设法将 Brotli 压缩到 400 字节,gzip 压缩到 521 字节。

其他用例

这似乎最适合那些处于(线性)增量尺度上的东西(这就是为什么填充和边距似乎是一个很好的用例),但我可以看到这可能适用于网格系统中的宽度和高度(列号和/或宽度)。 **也许** 用于排版刻度(但可能不适合)。

我非常关注文件大小,但可能还有其他一些我没想到的用途。 也许**你**不会以这种方式编写代码,但一个关键的 CSS 工具可能会重构代码以使用这种方法。

深入研究

当我深入研究时,我发现 Ahmad Shadeed 在 2019 年的博客文章 中讨论了将 calc() 与内联样式中的 CSS 变量分配相结合,特别是用于头像大小。 Miriam Suzanne 在 2019 年发表在 Smashing Magazine 上的文章 没有使用 calc(),但分享了一些使用内联样式中的变量分配可以做到的惊人事情。