前言
什么是 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 应该包含哪些内容呢?一般常用的有 padding
、margin
、font-size
、font-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 也无法面面俱到,只能是一定程度上的补充。参考常用的颜色值,最终我设置了红、橙、黄、绿、青、蓝、靛、紫、粉、冷灰、暖灰等几种色系。
其中每个颜色都有六个亮度值,分别用 -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 值的话,尺寸型命名就乏力了。我认为,凡是可以量化的属性,比如 padding
、margin
、font-size
、border-width
等,应该直接用数值表示,而对于不可以量化的属性,比如 box-shadow
,用尺寸型命名比较合适。
精简命名
大多数的 helpr 命名都是 CSS 属性的首字母缩写形式。比如 p
表示 padding
、m
表示 margin
、f-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-rounded
、rounded
等。我们也可以简化一下,比如直接用 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 类装饰的支离破碎。