Tychlog

CSS语义思维

Linguistic

前一阵子在项目组中讲了一个关于CSS的Session,在讲之前我曾收到了许多意见,大部分是希望能讲讲CSS实用性的技术,比如盒模型,CSS3之类的。干货人人都喜欢,因为看得见摸得着,拿来就有用,但我最后还是决定讲一些”湿货“。因为在Code Diff的时候我发现了许多样式的问题不是由于不会写CSS导致的,而是由于在错误的地方使用了写在错误地方的样式。

其实CSS很简单,没有计算没有流程,只是一直描述,无论什么复杂的效果,你只要Google一下就知道怎么写了,甚至可以直接copy。但CSS又很复杂,一个元素的表现会受到它旁边的兄弟元素,也会受到内部的子元素影响,还会受到父元素影响,在这种多重影响下,一个元素的显示逻辑会变得错综复杂。有没有面对塌陷的块级元素而束手无策?无论怎么改它的属性就是得不到自己想要的,但看看似乎一模一样的示例程序却安然无恙,是不是恨得咬牙切齿?我想这就是本文所要解决的主要问题,让你学会如何优雅的写CSS。

泾渭分明 - 明确书写意图和表现语义

其实我们只要稍微接触过CSS,一定都学过盒模型,我假设你已经对margin、padding和border已经很熟悉。好了,我们经常会遇到一种情况,想让父元素和子元素之间有一些空间,比如布局的时候,container和content之间的留白。一般来说有两种方式,一种是给父元素加padding内边距,另一种是给子元素加margin外边距。现在想想,你是否慎重考虑过用哪一种方式?你是否明白两者的区别?如果你从未考虑过这些,现在考虑也不迟,这真的很重要,如果你真的想优雅的运用CSS,而不是被它耍的团团转,如果你真的想把CSS写出规模,那你就要认真的思考这里面的逻辑。

先说说父元素的container加上padding的方式,子元素的背景将不会延伸到空白区域,另一种给子元素加margin的方式则反之。但我想先抛下这个表象,看看此时加上padding的父元素的逻辑。它意味着,我创建了一个容器,并且该容器的内部有一些区域是被看不见的东西填充的,无论放入什么都不应该占据这个区域。在你的脑海中想象一个厚厚的玻璃瓶,它就好像是在说“虽然我是透明的,但是在这厚厚的玻璃之内才是你应该呆的地方,无论是什么只要你想放在瓶子里”。现在来看看子元素,是的,其背景也应该并且确实在内部,而不该占据那段空白的区域。如果是为子元素加上margin,这意味着我在容器中创建了某个东西,而这个东西有一个自己的地盘,容器内的其他东西都不能进入这里。

所以他们的本质区别在于其所表达的意义,也就是语义,一个是容器上的语义,另一个是容器内元素的语义,主语也不同,谓语意义也不同。那么这能说明什么呢,不得不提一下高级语言中的高级,这代表某个语言将机器语言封装的更好,接口更接近自然语言,其强大不言而喻。其实编程语言的发展轨迹就是不断把机器语言往自然语言翻译,让人们可以更容易的跟机器沟通,终极目标自然是人工智能,和机器用自然语言自由沟通。所以虽然代码或者说CSS的语法并没有什么变化,但我们应该在思维上清楚的区分代码片段的意图和语义来写CSS。

语言学家乔姆斯基曾构造过一句符合语法的话:

“Colorless green ideas sleep furiously”

意思是无色的绿色想法愤怒的睡觉。想法睡觉是违反常识的,无色的绿色是矛盾的,愤怒的睡觉是不合常理的。仅仅符合语法是不够的,仅仅不报错也是不够的,尤其是CSS这种描述性的语言很少会报错,所以我想说的是,其描述性质让我们有更多选择来实现我们所期望的效果,但我们却应该慎重,要让语义分明,而不是在有限的语法规则下任意妄为,那样写不出好的CSS代码。除了其本身的语法规则,我们必须自我约束,引入语义规则,不仅仅是为了提高可读性这样的理由,而是拉近人与机器的距离,让我们在思维上更加和代码契合,这样才能写出优秀的程序。

言归正传,究竟如何语义化CSS呢。之前有讲过OOCSS,其实面向对象的思想就是一种语义化,将虚拟的元素看作实际的对象,用常识来构造代码,就像是用户体验中的用户习惯一样,这是一种遍及全人类的用户习惯,让代码更友好,即所谓的优雅。

比如最常见的布局,我们一般都会有header、body和footer,这样无需任何说明和规则,谁都会理解,header在前,footer在后,中间是body。有趣的是table中的thead、tbody和tfoot,由于加载优化,我们即使把tfoot放在tbody前面,让其先加载,但显示的时候tfoot仍然会在tbody下面,由此可见语义化的重要,否则你一定会以为不是W3C的人弄错了标准,就是浏览器厂商搞出了Bug。虽然这是HTML标签不是CSS,但我觉得这个例子很恰当的说明了一点,显示逻辑是独立的,不应该和其他逻辑混为一谈,它应该只关心如何显示。

再比如,一个导航条一般分为左右两边,里面各有若干链接。你会如何命名?navbar-left和navbar-right是Bootstrap所使用的class,但当宽度减少,Responsive响应式的Bootstrap会让这些链接都收进下拉菜单中,左和右又从何谈起呢?同样是Bootstrap中的颜色命名就做的十分成熟,不是green、blue、red这些词汇,而是success、primary、warning这些词,它们的区别是表现化与语义化的区别,我喜欢把这个叫做显示逻辑。也就是说在class层面我们应该只关注元素(或者说对象)是用来做什么的(意图)以及它应该表示什么(语义)。这里三种颜色代表成功、重要和警告,至于我们是用绿、蓝和红来表示还是通过其他什么颜色甚至形状,应该在style层面写CSS属性表示。如此我们使用class时无需迷茫于表示成功应该用绿色还是红色,意图明确无误。回到导航栏的例子,我们应该使用main和sub之类的词语标识导航栏中重要的和次要的链接,即使在某些实现下,主要和次要没有区别也不用担心,那是该实现的显示逻辑,我们不应该横加干涉。

狡兔三窟 - 善用class与style多对多的复杂关系

说到class和style(样式属性),这又是一段混乱不清的关系。其实每当我们要实现一个样式,自觉或不自觉的都会考虑,是将元素和class一一对应,然后为元素上的class写style,还是用class和几条style对应之后在元素上搭配组合。前者的好处是每个class中的style不会影响其他class,但可用性十分的低,极端点说这已经不算是在编程了,而是在画页面;后者的好处可以灵活使用不同的样式搭配给元素使用,组合出元素需要的效果,但很难维护,牵一发动全身,样式很难测试,无法保证在你动了一条style之后,没有影响其他的地方。甚至最要命的是大部分CSS代码都是无意识的游走在这两种情况之间,所以我们要做的不是选择一种极端,而是找到一个平衡点,并使用一些方法使得我们的CSS既能灵活复用在各种元素上,又能易于维护,不致修了这破坏那的情况。

分组

首先,对于一个或一套元素的样式,我们应该有自己的创建原则,而不是想到哪写到哪。比如,我一般使用这样的步骤来创建元素:

  1. 结构

首先仅关注结构布局,以站点整体为基准,将元素抽象为一个或多个结构。

比如制作一个按钮,你也许会发现它的尺寸和导航上的链接一致,而内外边距以及display和overflow之类的属性又和页面容器一样,这时你就可以把按钮的结构拆成两个结构,一个是尺寸,一个是边界逻辑,这种情况时常发生,所以有的CSS理论认为应该把width和height这对尺寸属性和padding以及margin这对边距属性彻底的分开也是这个道理。其实并不是说它们4个放一起就决定不行,只是一般它们都有各自复用的价值,所以常常被拆开使用,归根揭底这是由显示逻辑决定的。

  1. 皮肤

一般由于设计统一性,我们可以借助设计指导,轻松的制作出标准皮肤。但要注意的是,皮肤指的不仅仅甚至不一定是颜色之类的。继续用Bootstrap举例,有5种基础颜色,但这并不是皮肤,5种颜色的语义不同,它们只是默认用了5种颜色,勉强可以看作默认皮肤,而真正的皮肤是theme,让按钮变得有立体感。还有一个误区就是认为哪几种属性是皮肤,哪几种不是。就像前面所说的,我们要在语义上进行划分。一个属性在某些意图下可能是结构,在另外的意图中可能是皮肤。Logo举个极端的例子,在一个Workshop中我用CSS和HTML制作了左边这样的Logo,Logo本身应该完全算是皮肤,因为一般来说就是一张图片,所以制作它的所有CSS属性都应该算作皮肤。下面的CSS代码中,无论是定位还是文字的处理,以及尺寸等,都是为了构造Logo,所以这些属性都是皮肤。

#logo i, #logo b {
  position: relative;
}
#logo i {
  left: -30px;
  transform: rotate(-30deg);
  font-size: 60px;
  letter-spacing: -11px;
  opacity: 0.3;
}
#logo b {
  top: -43px;
  left: 40px;
  font-size: 68px;
  word-break: break-all;
  width: 3px;
  line-height: 10px;
  text-indent: 6px;
}
  1. 状态

除了结构和皮肤,其实还有一类很容易被人忽视的样式,即使是设计人员也经常忘了它,那就是状态。最常见的是hover,鼠标触碰元素与否的状态,以及active,当前选中的标签,当前所在的分页等等。通常人们会把它划分到皮肤中,但你要明白我们划分的依据是意图,很显然,状态不是皮肤,仅仅是通常为表达某些状态时会使用一些颜色,毕竟颜色是最好的表示方法。它还与各种逻辑有着千丝万缕的联系,经常会使用JavaScript来加以控制,所以将状态单独分出来是极有必要的,JavaScript只需要更改一个class就可以实现状态的切换,而不是在执行的时候才想起来应该把哪里隐藏把哪里显示,或者是变个颜色出个动画之类的。

使用

接着,无论我们是已经把各种写好的CSS属性分好了组,还是正准备以这种方式开始写CSS,我先引入一个语言学的术语 语块(Lexical chunk),创造这一术语的 Michael Lewis 认为语言并非由传统语法和词汇组成,而是由多个词汇的预制语块组成。我们现在分好的组其实就是一个个CSS的语块,CSS本身就是这种预制功能,我们要做的就是把style语块化,然后在HTML中写上代表它的class名称,当HTML元素上的多个class组合在一起时就组成完整的语义。

比如表示步骤的元素,这是一个形状类似标签,一个挨一个的排列的元素。我将它的尺寸等属性抽出来命名为label,然后把布局的属性组命名为float-left,如果我的设计风格是扁平化的,我可以把相关的皮肤属性命名为flat,你知道虽然扁平化一般用不着特别的属性,但我也可以写一些强制去掉圆角和渐变背景的属性。作为步骤,会分为当前的步骤,之前的步骤,以及还没到的步骤,当前的步骤可以命名为current,或者is—active,后面的步骤不能点所以可以命名为is-disable,这些都是状态。而现在我们来看看当前步骤元素是什么样的

    <div class="flat label float-left is-active"></div>

总结成一句话就是“The flat label which float left is active”。这就是语义化的CSS。把组织好的语块像说话一样作用于元素上,对元素发出指令,让其变成想要的样式。而且你应该会发现,一组该元素所独有的属性,或者是尺寸之类的一般是主语,布局是动宾短语,而皮肤多是一些形容词,最后状态可以用表语。

总之,CSS作为一个描述性的语言,有很多人觉得不能算一种语言,但我反而觉得这是一种高级语言,因为更接近自然语言。当然其主要作用是在视觉渲染上,所以应该是一种受限的语言,可以看作是接近自然语言的子集。所以它的性质觉得了其重语义轻语法的特点,写CSS不能仅靠语法规则,一定要用上语义规则,最好是能和项目中所有人达成共识,CSS框架其实就是一种通用共识。