[译]关于HTML语义与前端架构

原文:About HTML semantics and front-end architecture
作者:Nicolas Gallagher
翻译:Mark Qin

本文收集了一些我喜欢的想法、经验、理念,以及在过去一年中所尝试的创意。它涵盖了HTML语义、组件、前端架构的方法、class命名模式,以及HTTP压缩等内容。

我们永远都不该停止探索
而我们一切探索的终点
将是回到我们启程的地方
并且是平生第一次了解这个地方

托·斯·艾略特 —— “小吉丁”

关于语义

语义研究的是标志与符号之间的关系,以及它们所代表的意义。在语言学中,它主要是研究这些标志(如单词,短语,或者声音)在语言中的意义。而在前端开发领域,语义主要涉及的是HTML元素、属性和属性值(包括像Microdata这样的扩展)所约定的意义。这些在规范中常用的正式约定语义,可以帮助程序(以及后来参与开发的人)更好地理解一个网站各方面的信息。然而,即使这些元素、属性和属性值的语义是正式化的,它们依然得服从于开发者的适应程度以及共同选择的结果。这使得正式的约定语义也可能会在今后被修改(而这正是HTML设计原则之一)。

区分不同类型的HTML语义

遵守编写“语义化的HTML”这个原则,是现代专业前端开发的基础之一。绝大多数的语义都与当前或预期的内容性质有关(如:h1元素,lang属性,type属性的email值,Microdata)。

然而,并非所有的语义都需要以内容为导向。类名不能“无语义”。不管是用什么名字命名,它们都必须要有意义与目的。类名的语义可以和那些HTML元素不同。我们可以借助HTML元素、某些HTML属性、Microdata等所具有的“全局性”语义,然后利用网站或应用的“局部性”特定语义加以区分,这些特定语义通常包含在属性值中,比如class属性。

尽管在HTML5规范的class属性这一章节中重申了这个假定的“最佳实践”…

…鼓励开发者使用class属性值描述实际内容,而不是描述期望展现的内容。

…并没有什么内在的原因非这样做不可。事实上,当这种方法在大型网站或者应用中运用时,它往往会成为一种障碍。

  • HTML元素和其它属性已经提供了内容层的语义
  • 对于机器或访问者来说,类名所能透露的有用的语义信息非常少,甚至没有。除非它是已经约定的那一小部分名称(机器同样可读) —— Mircoformats
  • 类名的主要用途是成为CSS和JavaScript的钩子。如果你不需要为你的页面添加表现和行为,那么你或许不必在你的HTML里添加类名
  • 类名应该为开发者传达有用的信息。当你阅读一个DOM片段时,它将有助于理解某个类名的具体作用。尤其是在多人协作的开发团队里,与HTML组件打交道的可不光只有前端开发者。

举一个非常简单的例子:

<div class="news">
    <h2>News</h2>
    [news content]
</div>

当内容还不明显的时候,这个类名news不能告诉你任何事情。它没有向你提供关于这个组件整体结构的信息,而且一旦内容不再是“新闻”时,使用这个类名就显得非常不妥。类名的语义过分贴近内容,架构既不容易扩展,也不容易为其他开发人员所用。

与内容无关的类名

从某个设计模式的结构与功能中提取类名的语义是一种更好的方法。那些类名与内容无关的组件可重用性更高

我们不应该害怕让各层之间的关系变得清晰而明确(这里应该是指结构层、内容层等,译者注),而不是用类名严格地反应明确的内容。这样做不会使类名“无语义”,这只是表明它们的语义并不取决于内容。我们也不应该害怕使用额外的HTML元素,只要它们能帮助你创建更强壮、更灵活且更具重用性的组件。这样做不会使HTML变得“无语义”,这仅仅意味着你标记内容所使用的元素数量超过了最小值而已。

前端架构

组件、模板、面向对象的体系结构的目的是能够开发出一种数量有限的可重复使用的组件,它可以在一定范围内包含不同的内容类型。在大型的应用程序中,对类名语义来说最重要的事情是,能够用实用主义服务于它们的主要目的 —— 提供有意义的、灵活的、可重复使用的表现或行为的钩子供开发者使用。

可重用且可组合的组件

总的来说,可扩展的HTML/CSS必须依赖HTML中的class,以便创建可重用的组件。一个灵活的、可重用的组件,既不依赖DOM树中的某一部分,也不需要使用特定类型的元素。它应该能适应不同的容器,并且可以很容易地更换主题。如果有必要,额外的HTML元素(超出标记内容所必须的元素之外的元素)可以让组件更加强壮。Nicole Sullivan所说的media object就是一个很好的例子。

避免用类型选择器支持class,可以让组件更容易合并。下面这个例子中,btn组件与uilist组件不易于合并。问题在于.btn的权重比.uilist a要小(这将覆盖任何共享属性)。而且ulist组件需要锚点作为子节点。

.btn { /* styles */ }
.uilist { /* styles */ }
.uilist a { /* styles */ }
<nav class="uilist">
    <a href="#">Home</a>
    <a href="#">About</a>
    <a class="btn" href="#">Login</a>
</nav>

一种让uilist组件与其它组件轻松组合的方法是,uilist的子级DOM元素用class来添加样式。尽管这会降低权重,但是它的主要好处在于,它为你提供了处理子节点的任何结构样式的选择权。

.btn { /* styles */ }
.uilist { /* styles */ }
.uilist-item { /* styles */ }
<nav class="uilist">
    <a class="uilist-item" href="#">Home</a>
    <a class="uilist-item" href="#">About</a>
    <span class="uilist-item">
        <a class="btn" href="#">Login</a>
    </span>
</nav>

JavaScript专用类

使用某种形式的JavaScript专用类,可以降低因组件样式或结构的改变导致JavaScript失效的风险。我已经找到了一种非常有效的方法,那就是专为JavaScript的钩子使用一种特定的类——js-*——不要在这个类名上添加任何描述。

<a href="/login" class="btn btn-primary js-login"></a>

在你修改组件的结构或样式的时候,可能会不经意间对那些必要的JavaScript行为和复杂的功能造成影响,用这种方法的话,可以降低这种可能性。

组件修改器

组件常常会有一些变体,它们与基本组件只有细微的差别。比如,不同的背景色或者边框。主要有两种创建这些组件变体的模式。我将它们称为“单类名”模式和“多类名”模式。

单类名模式

.btn, .btn-primary { /* 按钮模板样式 */ }
.btn-primary { /* 主按钮的特殊样式 */ }
<button class="btn">Default</button>
<button class="btn-primary">Login</button>

多类名模式

.btn { /* 按钮模板样式 */ }
.btn-primary { /* 主按钮的特殊样式 */ }
<button class="btn">Default</button>
<button class="btn btn-primary">Login</button>

如果你使用预处理程序,你可以用Sass的@extend功能,以减少一些在使用“单类名”模式时所涉及的维护工作。然而,即使有预处理程序的帮忙,我依然倾向于使用“多类名”模式,并在HTML中修改类名。

我发现这是一种更具扩展性的模式。比如,要实现一个基本的btn组件,并增加5种类型的按钮与3种额外的尺寸。用“多类名”模式的话只要9个class就可以搞定,用“单类名”模式则需要24个class。

如果需要的话,它也更容易让上下文环境适应组件。你可能想对出现在其它组件中的任一btn做一些细节调整。

/* “多类名”样式调整 */
.thing .btn { /* 相应的样式调整 */ }

/* “单类名”样式调整 */
.thing .btn,
.thing .btn-primary,
.thing .btn-danger,
.thing .btn-etc { /* 相应的样式调整 */ }

“多类名”模式意味着,你只需要用一个单独的组件内部选择器,便可以改变所有类型的btn元素的样式。“单类名”模式意味着,你必须顾及所有可能的按钮类型,并在创造一个新的按钮变体时调整这个选择器。

结构化的类名

当创建一个组件时——并为之添加了“主题”——其中一些class被用来区分各个组件,一些class被当做组件的修改器,其它的class则被用来关联DOM节点,它们一起被包含在一个较大的抽象组件中。

很难去判断btn(组件)、btn-primary(修改器)、brn-group(组件)和btn-group-item(组件子对象)之间的关系,这是因为这些名字不能清晰地表现class的目的。没有一致的模式。

在过去的一年中,我一直在尝试命名模式,目的是能帮助我快速理解在一个DOM片段中节点的表象之间的关系,而不用为此来回切换HTML、CSS与JS文件拼凑网站的架构。这种模式主要受到BEM系统的命名方法的影响,但被改编成一种我认为更容易浏览的形式。

t-template-name
t-template-name--modifier-name
t-template-name__sub-object
t-template-name__sub-object--modifier-name

component-name
component-name--modifier-name
component-name__sub-object
component-name__sub-object--modifier-name

is-state-type

js-action-name
js-component-type

我将一些结构当做抽象的“模板”来处理,其它的则视为更清晰的组件(通常建立在“模板”上)。但是这种区分并非总是必要的。

这仅仅是我目前发现的一种有用的命名模式。命名模式可以采用任何形式。但这种命名模式的好处在于消除了模糊的类名,只依赖(单)连接符,或者下划线,或者是驼峰格式。

原始文件大小和HTTP压缩的注意事项

任何关于模块化与可扩展的CSS的讨论都会谈及对文件大小与“膨胀”的担心。Nicole Sullivan的言论中经常会提到文件大小的存储(以及维护改进),并提到了像Facebook这样的公司采用这种方法的经历。进一步的,我想我会分享我在预处理输出时的HTTP压缩效果,以及大量使用HTML类的一些事情。

当Twitter Bootstrap刚刚问世的时候,我重写了已编译的CSS,以便更好地与手动操作的文件比较大小。在最小化所有的文件之后,手动操作的CSS文件比预处理程序输出的小10%。但是当所有的文件都通过gzip压缩后,预处理程序输出的CSS文件比手动操作的小了5%。

这强调了比较HTTP压缩后文件大小的重要性,因为减少的文件大小并不能说明全部问题。它暗示了有经验的CSS开发者在用预处理程序时不必太过关注编译后的CSS中一定程度的重复,因为它将在HTTP压缩后变得更小。通过预处理程序处理更易于维护的CSS代码所带来的好处,要胜过关注原始CSS和压缩后输出的CSS的美观或文件大小。

在另一个实验中,我从线上扒了一个60KB的HTML文件(由很多可重用的组件组成),并删除了它的每一个class属性。这样处理之后,文件大小减小到25KB。当原始文件与扒下来的文件都通过gzip压缩后,它们的大小分别变为7.6KB和6KB——只相差1.6KB。自由使用class所导致的实际文件大小的结果已经不值得再去强调了。

我是如何学会停止担忧的…

多年来,许多技术熟练的开发人员的经验,已经改变了大型网站和应用程序的开发方式。尽管如此,对于个人来说,放弃“语义化的HTML”的观念,意味着将由内容来决定类名(即使非要这么做,也只能作为最后的手段来使用),这通常需要你开发完一个大型的应用程序后,才能够强烈地意识到这种做法不靠谱的一面。你必须准备好抛弃老观念,寻找替代方案,甚至重新审视以前已被你摒弃的方法。

一旦你开始写大型的网站和应用,你和其他人不仅必须去维护它,而且还得积极地迭代改进。你很快会发现,尽管你尽了最大的努力,你的代码仍然开始变得越来越难以维护。已经有一些人提出了他们自己的方法来解决这些问题:Nicole的博客和面向对象的CSS项目,Jonathan Snook的可扩展的模块化结构CSS,以及Yandex(俄罗斯最大的搜索引擎,译者注)开发的块级元素修改器。这些方法值得我们花时间去探索。

当你选择了以减少编写和编辑CSS的时间为目的的方式来编写HTML和CSS时,那么如果你想改变它们的样式,你就必须接受花更多的时间去改变HTML元素的class。无论是前端还是后端开发者 —— 任何人都可以重新排列预建的“乐高积木”,这将是相当实用的;事实证明,没有人会CSS魔法。

前端开发 | , , | 3 条评论

CSS3模拟Chrome浏览器

最近做了一件看起来有点无聊的事情,就是用浏览器中运行的HTML与CSS生成一个浏览器。话不多说,先来欣赏下这个无聊的作品(因为一时找不到Chrome的原装图标,又懒得抠图,所以用到图标的地方我都空在那了,各位先将就着看吧):

浏览器Demo >>

这个山寨Chrome在技术上没什么高深的东西,都是一些基本的CSS3的使用,只有浏览器标签的实现方法值得一提。

一、梯形标签的实现方法

Chrome作为当代文艺青年的必备利器,犀利的标签是必不可少的:

可以看出,Chrome标签的基本特点是“梯形、有圆角、宽度自适应”。对于这样的标签,虽说用图片可以轻松搞定,但用CSS3实现的话无疑更具文艺范儿(不支持CSS3的前辈级浏览器暂时就不考虑了)。

1、 :before 与 :after

比起使用背景图片的常规方法,用CSS3实现梯形标签,最大的优点是HTML非常简洁:

<ul class="tabs">
    <li>新标签页</li>
    <li class="selected">百度一下,你就知道</li>
    <li>新标签页</li>
</ul>

是不是太简单了点?不用怀疑,这些已经足够了。梯形的两端用伪元素 :before 与 :after 可以轻松搞定。说白了,就是用这两个伪元素代替常规方法中无意义的空容器(这里插一句伪元素的小问题,因为有不少人会认为 :before应该叫伪类,::before才叫伪元素,关于这个疑惑,W3C有详细解释):

.tabs li:before,
.tabs li:after {
     width: 16px;
     height: 24px;
     content: " ";
     border: 1px solid #3b5c95;
}

2、定位

定位很简单,只需将这两个伪元素绝对定位到标签的两端即可,当然,要先声明 li 为相对定位,同时要注意各个元素之间的 z-index 关系。:

.tabs li {
     display: inline-block;
     position: relative;
     z-index: 0;
}
.tabs li:before,
.tabs li:after {
     position: absolute;
     z-index: 3;
}
.tabs li:before {
     left: -12px;
}
.tabs li:after {
     right: -12px;
}

3、变形

伪元素变形是整个浏览器标签实现过程中最重要的环节。方法是用CSS3中的transform,使前后两个伪元素分别呈不同的角度变形:

.tabs li:before {
    -o-transform: skew(-22deg);
    -ms-transform: skew(-22deg);
    -moz-transform: skew(-22deg);
    -webkit-transform: skew(-22deg);
    transform: skew(-22deg);
}
.tabs li:after {
    -o-transform: skew(22deg);
    -ms-transform: skew(22deg);
    -moz-transform: skew(22deg);
    -webkit-transform: skew(22deg);
    transform: skew(22deg);
}

到这里,模拟Chrome标签的工作基本上算是完成了。剩下的无非是照着Chrome浏览器填充渐变色、微调一些宽高位移而已,就不再赘述了。请看单独的Chrome浏览器标签Demo

这个山寨浏览器标签有一个地方和原版有出入。在原版中,前面(左边)的标签总是盖着后面(右边)一个标签,但因为我是使用 inline-block 布局的,所以在文档结构中后面的元素总会覆盖前面的元素,我既不能一个一个地加 z-index,又不想使用 float:right 破坏结构,便随它去了(为了防止露馅,在demo中我只写3个标签)。如果哪位有好方法,一定得告诉我!

二、小结

最后,有两个地方需要略作说明。

一个是变形角度问题。上面代码中看起来怪怪的22°,其实不是我的本意。我开始是写20°的(我一直是完美的整数控…),但当我将标签放到蓝色背景上时,发现在Chrome浏览器中有点小瑕疵,衔接的地方总有一个白点。但调整到22°时,白点就消失了。如下图:

除了同为Webkit内核的Safari中不出意外地存在这个问题外,Firefox 10与Opera 11.6在20°时都表现完美,大家稍微留意下。

还有一个问题是关于CSS3中transition的。大家可以打开自己的Chrome浏览器看一下,当鼠标滑过Chrome浏览器标签时是有一个过渡动画效果的。我开始天真的认为用transition可以轻松搞定,但事实证明我错了。甚至一怒之下,我用了最霸道的方法写transition:

* {
    -o-transition: all .3s ease-in-out;
    -moz-transition: all .3s ease-in-out;
    -webkit-transition: all .3s ease-in-out;
    transition: all .3s ease-in-out;
}

分析后发现,问题出在背景上。我用了radial-gradient径像渐变作为:hover时的背景:

.tabs li:hover {
    background-image: -o-radial-gradient(circle,#dbe9f9 0,#bcd7f6 60%);
    background-image: -ms-radial-gradient(circle,#dbe9f9 0,#bcd7f6 60%);
    background-image: -moz-radial-gradient(circle,#dbe9f9 0,#bcd7f6 60%);
    background-image: -webkit-radial-gradient(circle,#dbe9f9 0,#bcd7f6 60%);
    background-image: radial-gradient(circle,#dbe9f9 0,#bcd7f6 60%);
}

radial-gradient属于background-image,而W3C关于transition的文档中明确说明transition是支持background-image的,并且是“only gradients”。但经测试,不管是linear-gradient还是radial-gradient,只要是从一个gradient背景向另一个gradient背景进行transition,都不起效果。gradient背景向普通的background-color则可以顺利过渡,反之不行。不知是我写的不对还是我理解错误(测试浏览器:Chrome 16.0 /Firefox 10.0.2/Opera 11.6.1/Safari 5.1.2)。

这件无聊的事情就先做到这了,有兴趣的朋友可以试着写一个,或者帮我继续完善它。

前端开发 | 抢沙发

通用选择符“ * ”与前端的“矫情”

晚上看了Pual Irish的一篇名为* { box-sizing: border-box } FTW的文章。我不知道标题中的”FTW”是什么意思,遂搜索之,发现其释意为:“For The Win的英文缩写,网络用词,一般用来表达对某一事物喜欢到极致的心情。”于是大惊!惊的倒不是 border-box 的使用,而是最前面那个 * 选择符(当然,这种全面的使用border-box确实也够惊人的,但与本篇主旨无关,所以暂且先按下不表)。

在众前端的眼中,通用选择符 * 一直以来都是个不祥之物,因为江湖盛传此君是个顶级的性能杀手。还记得*{ padding:0; margin:0;}么?当年讨论css reset的时候,有很多人反对这种写法的一个原因就是说 * 有严重的性能隐患。而如今,Pual Irish偏偏对它“FTW”。当然,这个“FTW”多半是送给border-box的,对于 * 的使用,Pual Irish是这样说的:

You might get up in arms about the universal * selector. Apparently you’ve heard its slow. Firstly, it’s not. It is as fast as h1 as a selector. It can be slow when you specifically use it like .foo > *, so don’t do that. Aside from that, you are not allowed to care about the performance of * unless you concatenate all your javascript, have it at the bottom, minify your css and js, gzip all your assets, and losslessly compress all your images. If aren’t getting 90+ Page Speed scores, its way too early to be thinking about selector optimization.

我简单地翻译了一下:

你可能对通用选择符 * 充满敌意。显然你早就听说过它的缓慢。首先,它并不缓慢。作为一个选择符,它的速度和h1标签一样迅速。除非当你像 .foo > * 这样具体使用时它才可能变慢,所以千万不要这样使用。除此之外,你根本不需要担心 * 的性能问题,除非你已经并置了所有的javascript文件,将它们放在文档底部,压缩了你的css与js文件,并将你所有的资源都gzip压缩,同时已经无损压缩你所有的图片。如果你的页面在Page Speed中的得分没有90分以上,那么此时考虑选择器的性能就显得为时过早了。

读完这段话后恍然大悟:原来是我们“矫情”了!竟然为了一个莫须有的原因就与 * 选择符老死不相往来,甚至有时本末倒置地将性能优化的重点放在选择器上,实在是不该。这不是个案,这样的“矫情”还有不少。最典型的要数对table的使用问题了。因为久闻table恶名或是曾被table搞地死去活来,所以不管什么情况都拒绝使用它,然后煞费苦心地用各种标签模拟table,并引以为荣,这不是“矫情”么?还有一个例子是关于CSS Sprites的,即刻意追求将整个站点所有的背景图片全部聚合在一张图上,不这么做还不行。诚然,出发点是好的,可是真的有必要这样做么?且不说这样做对性能的提升到底有多大,单单整那么一张庞然的聚合图您累不累啊,还有很多更重要的事情等着我们去做呐!这也可以说是一种“矫情”吧!

这样例子应该还有很多,比如float的过度使用?比如对多层嵌套的坚决排斥?我一时也想不了那么多了,欢迎大家吐槽补充。

上面所说的这些“矫情”,也许是由于对某些知识点理解还不够透彻,也许是完美主义在作祟,但不管怎么样,该补的要补,该抛的要抛,这种“矫情”要不得。诸君还是酌情放下这份“执着”吧!善哉。

前端开发 | 2 条评论

你好,2012。

2012如期而至。这不是艾默里奇的灾难电影,也不是玛雅文明的世纪预言。对我而言,它只是又一个会转瞬即逝的十二个月的简称。这无疑是极其不浪漫的比喻,但我并不是在试图消除对末日的恐惧,而是在无奈地总结与冷静地展望。

时光荏苒,白驹过隙,日月如梭,光阴似箭……当我们总是发出诸如此类的廉价的感慨时,我们是否已经变得麻木?这个问题也许每年都会被提起,甚至连问题本身也麻木了,但在这个被末日危言所渲染的年份,它似乎再次变得沉重起来。我隐隐开始怀疑,所谓的世界末日,是不是指我们这些被时间这锅温水慢慢煮着的青蛙再也跳不起来的日子?

总结,展望,再总结,再展望,年复一年,日复一日。这个过程像极了尼采所说的“永恒轮回”,但我却不敢断言它到底是不是。米兰·昆德拉在《不能承受的生命之轻》中这样写道:

“永恒轮回之说从反面肯定了生命一旦永远消逝,便不再回复,似影子一般,了无分量,未灭先亡,即使它是残酷,美丽,或是绚烂的,这份残酷、美丽和绚烂都没有任何意义。我们对它不必太在意,它就像是十四世纪非洲部落之间的一次战争,尽管在这期间有三十万黑人在难以描绘的凄惨中死去,也丝毫改变不了世界的面目。”

我说的已不单单是“总结与展望”的反复,而是我们对待时间的态度。如果我们在有限的时间轴上真的陷入了“永恒轮回”,那么很多事情就会变得了无意义,如鸿毛一般,最终变成我们“不能承受的生命之轻”。

跳出“永恒轮回”,认真对待每一天,还有比这更实在的新年愿望么?

你好,2012。

散文随笔 | 1 条评论