如何编写通用的 Helper Class

时间:2022-04-16 05:25:13

如何编写通用的 Helper Class

Github: https://github.com/nzbin/snack-helper

Docs: https://nzbin.github.io/snack-helper

前言

什么是 helper ?任何框架都不是万能的,而业务需求却是多种多样,很多时候我们只需要更改组件的部分属性,而 helper 就是调整细节的工具。我在之前的文章《如何编写轻量级 CSS 框架》中也举过例子,我们完全没必要因为几个属性的不同而重新编写新组件。大部分的 helper 都是一个类对应一个 CSS 属性,属于最细小的类。通过工作的实践总结,我觉得编写一套简单易用、通俗易懂的 helper 非常重要。本文的目的就是探讨 helper 的组成部分、编写方式以及如何精简 helper 的命名。

组件与零件

详细介绍如何编写 helper 之前,先说一下我对于组件以及零件的看法。在之前编写轻量级 CSS 框架的时候,我们是以组件的方式开发。而编写 helper 更像是开发一个零件,因为 helper 的属性单一,而且多个 helper 可以形成一个组件。比如下面的例子:

假设有 .boxes 组件

.boxes {
border: 1px solid #eee;
border-radius: 5px;
margin-bottom: 15px;
overflow: hidden;
}

假设有如下 helper

.b-1 {
border: 1px solid #eee !important;
}
.r-5{
border-radius: 5px !important;
}
.m-b-15{
margin-bottom: 15px !important;
}
.overflow-hidden {
overflow: hidden !important;
}

.boxes = .b-1 + .r-5 + .m-b-15 + .overflow-hidden

我是一个模型爱好者,这样的组合方式让我想到了寿屋的 HEXA GEAR 系列模型,这个系列的特点是“零件+零件=组件、组件+组件=骨架、骨架+骨架=素体、素体+武装=机体”。

在编写 helper 的时候,基于以上想法,我在思考是否可以把 helper 拆分的足够精细,这样它就可以自成一体形成一个框架,也就是“零件+零件=组件、组件+组件=框架”。令人遗憾的是,我的想法已经被人实践,前几天浏览 GitHub 时发现了相关的项目 tailwindcss,这个框架就是以 helper 为基础,通过属性叠加的方式添加样式。

组件式框架和零件式框架是两种完全不同的思想,难分伯仲,各有优缺点。

Helper 的组成部分

一套完整的 helper 应该包含哪些内容呢?一般常用的有 paddingmarginfont-sizefont-weight 等。为了编写更为通用的 helper,我们需要更细致的划分。虽然我们并没有打算把它写成一个框架,但是我们希望 helper 的功能足够强大。通过对比和思考,我将 helper 暂时划分成以下几个模块:

  • Colors(颜色,包括 bg-color 及 text-color)
  • Paddings(内边距序列)
  • Margins(外边距序列)
  • Typography(排版,包括 font-size 及 font-weight)
  • Border(边框线)
  • Radius(圆角)
  • Shadow(阴影)
  • Size(尺寸,包括 height 及 width)
  • Gutters(栅格间距序列)
  • Alignment(主要是 vertical-align)
  • ...

和之前编写轻量级框架一样,我们同样使用 Sass 预编译器。helper 类几乎都是 Sass 循环生成的,所以源代码看上去很精简。

颜色变量

因为颜色稍微特殊一点,我将颜色与其它内容分开单独介绍。在编写轻量级框架的时候,我也定义了常用的一些颜色,但是面对特殊需求时略显单一,所以我们需要使用 helper 扩充颜色集群。但是颜色是一个无法量化的概念,所以再强大的 helper 也无法面面俱到,只能是一定程度上的补充。参考常用的颜色值,最终我设置了红、橙、黄、绿、青、蓝、靛、紫、粉、冷灰、暖灰等几种色系。

如何编写通用的 Helper Class

其中每个颜色都有六个亮度值,分别用 -lightest-lighter-light-dark-darker-darkest 表示,此处有参考 tailwindcss 的颜色命名。这些颜色都是通过 Sass 的颜色函数生成的。以灰色为例,Sass 代码如下:

$gray:#999;
$gray-light:lighten($gray, 15%);
$gray-lighter:lighten($gray, 25%);
$gray-lightest:lighten($gray, 35%);
$gray-dark:darken($gray, 15%);
$gray-darker:darken($gray, 25%);
$gray-darkest:darken($gray, 35%);

这些颜色序列看上去很像一套马克笔,不过马克笔灰色系更丰富,包括冷灰、暖灰、蓝灰、绿灰。

其中背景色的循环方式如下,为了便于循环,我们定义了一个 color map,然后用 @each 方法循环。

$color-list:(
'gray':$gray,
'brown':$brown,
'red':$red,
'orange':$orange,
'yellow':$yellow,
'green':$green,
'teal':$teal,
'blue':$blue,
'indigo':$indigo,
'purple':$purple,
'pink':$pink
); @each $name,$color in $color-list {
.bg-#{$name} {
background-color: $color;
}
.bg-#{$name}-light {
background-color: lighten($color, 15%);
}
.bg-#{$name}-lighter {
background-color: lighten($color, 25%);
}
.bg-#{$name}-lightest {
background-color: lighten($color, 35%);
}
.bg-#{$name}-dark {
background-color: darken($color, 15%);
}
.bg-#{$name}-darker {
background-color: darken($color, 25%);
}
.bg-#{$name}-darkest {
background-color: darken($color, 35%);
}
}

命名策略

理所当然,我又提到了命名策略。在编写轻量级框架的时候,我也着重讨论了类命名策略以及比较了一些框架的命名方式。无论是框架还是 helper,类命名都决定了其易用性,而且会影响使用者的习惯,所以我会从简洁、直观、易用等几个角度命名。不过 helper 的命名比较简单,因为几乎大多数都是单一的 CSS 样式,所以命名策略基本都是对 CSS 属性的抽象与简化。

数字型命名 VS. 尺寸型命名

我在工作中接触过两种 helper 序列的表示方法,一种是常见的数字型,另一种是尺寸型。以 padding 为例:

数字型

.p-5 {
padding: 5px !important;
}
.p-10 {
padding: 10px !important;
}
.p-15 {
padding: 15px !important;
}
.p-20 {
padding: 20px !important;
}
.p-25 {
padding: 25px !important;
}

尺寸型

.p-xs {
padding: 5px !important;
}
.p-sm {
padding: 10px !important;
}
.p-md {
padding: 15px !important;
}
.p-lg {
padding: 20px !important;
}
.p-xl {
padding: 25px !important;
}

虽然在实际应用时,尺寸型写法并没有什么不妥,但很明显它的扩展性很差,而且不直观。作为例子,我只写了五个数值,但如果我们希望添加更多的 padding 值的话,尺寸型命名就乏力了。我认为,凡是可以量化的属性,比如 paddingmarginfont-sizeborder-width 等,应该直接用数值表示,而对于不可以量化的属性,比如 box-shadow,用尺寸型命名比较合适。

精简命名

大多数的 helpr 命名都是 CSS 属性的首字母缩写形式。比如 p 表示 paddingm 表示 marginf-s 表示 font-size 等。这符合我们期望的简洁直观的要求。但也不能唯缩写论,所有的命名都用缩写,因为有些属性的缩写会重复,而且有些缩写之后就不知道具体含义了。我们可以沿用之前的规则,可以量化的属性都用缩写,不可以量化的属性用简化的全称(比如 box-shadow 可以替换为 shadow)。

以 padding 循环为例:

@for $counter from 0 through 6 {
.p-#{ $counter * 5 } {
padding: ($counter * 5px) !important;
}
.p-t-#{ $counter * 5 } {
padding-top: ($counter * 5px) !important;
}
.p-r-#{ $counter * 5 } {
padding-right: ($counter * 5px) !important;
}
.p-b-#{ $counter * 5 } {
padding-bottom: ($counter * 5px) !important;
}
.p-l-#{ $counter * 5 } {
padding-left: ($counter * 5px) !important;
}
}

对于其它几个 helper 与此类似,循环也很简单。

关于 Margin 负值

margin 的 helper 相比其它来说比较特殊,因为它有负值,所以我们必须考虑如何表示负值。有些框架用 n (negtive)表示负值。比如 m-{t,r,b,l}-n-* 的形式:

.m-t-n-5 {
margin-top: -5px !important;
}
.m-r-n-5 {
margin-right: -5px !important;
}
.m-b-n-5 {
margin-bottom: -5px !important;
}
.m-l-n-5 {
margin-left: -5px !important;
}

我觉得完全可以简化一步,用 - 表示负值,简单易懂,如下:

.m-t--5 {
margin-top: -5px !important;
}
.m-r--5 {
margin-right: -5px !important;
}
.m-b--5 {
margin-bottom: -5px !important;
}
.m-l--5 {
margin-left: -5px !important;
}

虽然这种命名方式很简洁,但看上去和其它 helper 不太统一。

关于圆角

圆角的 CSS 属性名为 border-radius,如果直接简写的话和 border-right 就重复了,参见其它框架的表示方法有 corner-roundedrounded 等。我们也可以简化一下,比如直接用 r 表示,既可以代表  rounded 也可以代表 radius,一举两得。这样的表示方法应该不会有歧义,毕竟在我们的脑海中,r 表示半径算是一个根深蒂固的概念。Sass 代码如下:

@for $counter from 0 through 10 {
.r-#{ $counter } {
border-radius: ($counter * 1px) !important;
}
.r-t-l-#{ $counter } {
border-top-left-radius: ($counter * 1px) !important;
}
.r-t-r-#{ $counter } {
border-top-right-radius: ($counter * 1px) !important;
}
.r-b-r-#{ $counter } {
border-bottom-right-radius: ($counter * 1px) !important;
}
.r-b-l-#{ $counter } {
border-bottom-left-radius: ($counter * 1px) !important;
}
}

我们用 -full 表示 100%,其它框架也基本如此,稍后再谈论 r-100% 这种形式的可行性及问题所在。

.r-full {
border-radius: 100%
}
.r-t-l-full {
border-top-left-radius: 100%
}
.r-t-r-full {
border-top-right-radius: 100%
}
.r-b-r-full {
border-bottom-right-radius: 100%
}
.r-b-l-full {
border-bottom-left-radius: 100%
}

同样的,高度和宽度的 100% 数值也用 -full 表示,循环方式类似。

关于阴影

我们在之前反复提到了阴影属于非量化的属性,所以只能使用尺寸型命名法,当然用数字也不是不可以,一会儿再详细说明。先看源代码:

.shadow-xs{
box-shadow:0 1px 5px 1px rgba(0,0,0,.15);
}
.shadow-sm{
box-shadow:0 2px 10px 2px rgba(0,0,0,.15);
}
.shadow-md{
box-shadow:0 3px 20px 3px rgba(0,0,0,.15);
}
.shadow-lg{
box-shadow:0 4px 30px 4px rgba(0,0,0,.15);
}
.shadow-xl{
box-shadow:0 5px 40px 5px rgba(0,0,0,.15);
}

整体而言,比较简洁,不过阴影的数值我是粗略添加的,实际情况要做调整。说点题外话,我个人觉得对于非量化的属性本身而言,或许用处就不大,因为这些属性能够满足业务需求的可能微乎其微,但是它仍然是不可缺少的一部分。所以说“通用的” helper 并不一定通用。

关于强度表示法

通过 font-weight 说一下关于强度的表示法,font-weight 的 CSS 属性本身就有两种表示法,一种是直接文字命名,比如 .f-s-thin , .f-s-normal, .f-s-bold 等,另一种是比较直接的 100 ~ 900 数值型表示法。以我个人观点,我更倾向于数值型表示法,简单直观,并没有歧义,也算是约定俗成的规定吧。font-weight 的循环比较简单,而且数值有限,我们可以直接写出从 100 ~ 900 的所有 helper。其它类似的 helper  也可以用 100 ~ 900 表示强度,比如颜色。

需要注意的是,编写 helper 时一定要对数值型、尺寸型、强度型命名做好归类与统一,切记毫无章法地胡乱使用。

类命名中的特殊字符

对于 r-100% 或者 w-100% 这样的写法是可以的,但是在定义 CSS 时要进行字符转义,比如

.r-100\% {
border-radius: 100%
}

使用方式如下

<div class="r-100%"></div>

但是这种写法总给人怪怪的感觉,而且输入时要按 shift + %,不太方便,所以暂时只作为参考。

另外需要说明一点,我们可以通过特殊字符定义百分数,比如:

.w-50 {
width: 50px;
}
.w\:50 {
width: 50%
}

通过约定的这种规则,我们就可以为 helper 添加栅格系统了。不过这只是暂时的想法,毕竟我们已经有一套轻量级 CSS 框架了。

序列数量

因为 helper 是循环生成的,所以循环的数量决定了 helper 的丰富度。那么循环的数量多少合适呢?这是所有 helper 最难统一的地方。不可否认,helper 的数量越多,通用性越强,也就越灵活。任何事物都有两面性,虽然 helper 越多越好,但是数量太多会造成文件臃肿。目前我写的 helper 的文件体积几乎和之前的轻量级框架差不多,某种程度上来说确实在向“零件化”的框架发展。另一方面,其实 helper 并没有必要写的太全面,很多数值存在冗余。

简单来说,对于有限值的 helper 就可以全部写出,比如对其方式、font-weight 等。而对于任意数值的 helper 来说,我们需要选择常用的一些数值,比如 padding、margin 等属性,基本 1~50 px 之间就可以了,而圆角 1~20 px 足矣。不能量化的属性比如阴影就完全看个人喜好了,我觉得五个尺寸就差不多。对于实在特殊的需求也只能特殊对待了。

演示

现在我们测试一下我们所写的 helper 是不是能够满足一般需求,比如一个带有圆角阴影的用户卡片,如下:

See the Pen snack-helper-test by Zongbin (@nzbin) on CodePen.

这个实例全部是用 helper 完成的,可惜这套 helper 没有栅格系统,所以布局并不灵活,但是结合之前的轻量级框架,会显示出它强大的功能。

总结

编写 helper 比编写框架要容易的多,但简单易用、通俗易懂的 helper 还需要严谨的思考,详细的 helper 可以参见 GitHub 源码。虽然我一直声称没有打算把 helper 写成一个框架,但随着细节的追加与调整,比如添加栅格系统,这个通用的 helper 已经趋向于一个“零件化”的框架了。至于组件式框架和零件式框架哪个更好,这是一个很难选择的问题。但是我更倾向于组件与零件的结合,因为我不希望整个 HTML 文件被冗长的 CSS 类装饰的支离破碎。