好程序绝非偶然天成。这并不是说开发人员天生懒惰或者不值得信赖,而是因为独立工作 的时候,我们针对同一个问题能提出各种不同的解决方案。不同于走迷宫,解决问题几乎 不会只有一种方法。我们每个人的经验、观点和习惯各异,因此解决同一个问题的方式也不尽相同
后端开发有着各种规范,而前端已经发展到不再是随便切切图的时代,业务越来越多,工作流程越来越复杂。我们不再浪费时间把一些 Photoshop 设计稿 重构成 CMS 模板页面。因为逐渐把设计的环节转移到浏览器中,并书写响应式的网页 框架,所以在实现 CMS 的界面之前,我们往往已经开始编写所有的 HTML 和 CSS 代 码。要实现这个颠覆性的角色转变,就需要改变现有的开发流程。
这一部分将帮助我们探讨如何提高 HTML、CSS 和 JavaScript 的代码质量,编写类、设计 函数,以及声明接口。
HTML
当使用模板引擎时,人们宁可写一堆标签和CSS类名,最终,他们编写的代码如下:
<div id="header" class="clearfix">
<div id="header-screen" class="clearfix">
<div id="header-inner" class="container-12 clearfix">
<div id="nav-header" role="navigation">
<div class="region region-navigation">
<div class="block block-system block-menu">
<div class="block-inner">
<div class="content">
<ul class="menu">
<li class="first leaf">
<a href="/start">Get Started</a>
这只是一段简单的页面顶部,在使用CMS模板标记时,为了实现业务很有可能使用div乱炖,可能你也没注意过,通常会不止10层!
为更好地可维护性,可以使用BEM原则
这样写
<nav class="nav">
<ul class="nav__container">
<li class="nav__item">
<a href="/products" class="nav__link">
<ul class="nav__container--secondary">
<li class="nav__item--secondary">
<a href="/socks" class="nav__link--secondary">
尽管这还是相当冗长,但我要说的是,它的冗 余程度其实是恰到好处的。给每个元素都添加了相应的 CSS 类名之后,我们就不再需要依赖那些只为了样式标签而存在的 CSS 类名或元素的层级关系来决定视觉外观了。相比动 态标记,这个标记更清晰,并且我敢说,这也让标记的组织形式更“模块化”了。这个导 航可以作为网站的导航通用模板,不用改任何一个标记就可以在多处复用。因此,这种标 记并不是先等 CMS 创建完成再另外添加样式标记的,而是创建的同时就添加了样式标记, 然后整合到网站的整个导航系统中。
这背后的设计系统
CSS理论几乎和JavaScript框架一样多,但CSS理论更多的是阐述HTML和CSS之间的关系,而不是预编译的代码库
OOCSS方法
<div class="toggle simple">
<div class="toggle-control open">
<h1 class="toggle-title">Title 1</h1>
</div>
<div class="toggle-details open"> ... </div>
...
</div>
<button class="btn btn-primary btn-lg"></button>
OOCSS(http://oocss.org/)有两个主要的原则:分离结构和外观,以及分离容器和内容。
分离结构和外观,意味着将视觉特性定义为可复用的单元。前面那段简单的切换就是一个 简短的可复用性强的例子,可以套用很多不同的外观样式。例如,当前的“simple”皮肤 使用直角,而“complex”皮肤可能使用圆角,btn-primary规范按钮的颜色,btn-lg规范按钮的大小
分离容器和内容,指的是不再将元素位置作为样式的限定词。和在容器内标记的 CSS 类名 不同,我们现在使用的是可复用的 CSS 类名,如 toggle-title,它应用于相应的文本处理 上,而不管这个文本的元素是什么。这种方式下,如果没有应用别的 CSS 类名,你可以让 H1 标签以默认的样式呈现
这样的规范让开发者能设计出一套可经由「组合」而产生多种样式结构,让程式码更精简、便于管理与维护。
bootstrap 就是个很好的例子
SMACSS方法
同样以切换组件为例,按照 SMACSS(Scalable and Modular Architecture for CSS),模块化 架构的可扩展 CSS)方法,写出来的代码如下:
<div class="toggle toggle-simple container dark">
<div class="toggle-control is-active">
<h2 class="toggle-title">Title 1</h2>
</div>
<div class="toggle-details is-active">
...
</div>
...
</div>
-
基础
如果不添加 CSS 类名,标记会以什么外观呈现。
-
布局
把页面分成一些区域。
-
模块
设计中的模块化、可复用的单元。
-
状态
描述在特定的状态或情况下,模块或布局的显示方式。
-
主题
一个可选的视觉外观层,可以让你更换不同主题。
BEM方法
BEM(Block Element Modifier,块元素修饰符)写出的组件代码
<div class="toggle toggle--simple">
<div class="toggle__control toggle__control--active">
<h2 class="toggle__title">Title 1</h2>
</div>
<div class="toggle__details toggle__details--active">
...
</div>
...
</div>
-
块名
所属组件的名称。
-
元素
元素在块里面的名称。
-
修饰符
任何与块或元素相关联的修饰符
依照 Block、Element 和 Modifier 來命名,toggle__details–active 描述这里的 details 是元素,active 是修饰符,这个约定使得 CSS 类名非常清晰。使用双横杠 是为了避免块名被混淆为修饰符
CSS
CSS没有作用域的概念,我们写的CSS都是从全局作用域开始开发,一层层增加细节,当项目复杂起来,通常会出现以下问题
<body>
<div class="main">
<h2>I'm a Header</h2>
</div>
<div id="sidebar">
<h2>I'm a Sidebar Header</h2>
</div>
</body>
<style>
h2 {
font-size: 24px;
color: red;
}
#sidebar h2 {
font-size: 20px;
background: red;
color: white;
}
</style>
现在在sidebar增加一个日历,也有h2标签(如下),现在希望日历的h2标签背景色应该是白色,而不是从sidebar继承过来的红色,为此,要增加一个样式覆盖它
<body>
<div id="main">
<h2>I'm a Header</h2>
</div>
<div id="sidebar">
<h2>I'm a Sidebar Header</h2>
<div class="calendar">
<h2>I'm a Calendar Header</h2>
</div>
</div>
</body>
<style>
h2 {
font-size: 24px;
color: red;
}
#sidebar h2 {
font-size: 20px;
background: red;
color: white;
}
#sidebar .calendar h2 {
background: none;
color: red;
}
</style>
-
选择器优先级
无论你处理带 ID 的标签还是长选择器,重写一个选择器时,总是需要注意它的优先级。
-
颜色重置
要恢复到原来的 H2 颜色,我们必须再次指定样式,并且要覆盖当前的背景颜色。
-
位置依赖
现在我们的日历样式依赖于侧边栏的样式。如果将日历移到页首或者页尾,样式将会改变。
-
多重继承
现在这个 H2 的样式来源多达三个,这意味着只要改变主体或侧边栏的样式,都会影响到日历的呈现。
-
深层嵌套
日历控件里的日历条目可能还有其他的 H2,而它们也需要一个更具体的选择器,这样 一来,H2 的样式来源就增加到了四个。
如何优化?
上面介绍的三种设计方法可以拿出来分析一下了,让我们来快速看一下这些关键原则,并且了解它们是如何帮助 我们解决前面遇到的问题的。
OOCSS带来分离容器和内容的思想,我们要将日历看成一个组件,只关心日历样式实现,每处都只修改日历样式,而不是从 sidebar 选中到 日历,将日历与sidebar整体分开,分离结构和外观。
SMACSS将样式系统分为5个具体类别,在编写选择器时,需单独定义好layout(布局)、module(模块)、state(状态),theme(主题)需要哪个样式就给标签添哪个,更该模块样式通过子模块或者皮肤/主题修改,和OOCSS最显著的差异是使用皮肤和带is前缀的状态名
BEM为每个标记命名独一无二的CSS标识,这样会使每个 BEM 风格的 CSS 类名都可以 对应到某一组独属于该元素的 CSS 属性,而不会随着具体情境或选择器的使用而变化:
<body>
<div class="main">
<h2 class="content__title">"I'm a Header"</h2>
</div>
<div class="sidebar">
<h2 class="content__title--reversed">
"I'm a Sidebar Header"
</h2>
<div class="calendar">
<h2 class="calendar__title">"I'm a Calendar Header"</h2>
</div>
</div>
</body>
<style>
/* 组件文件夹 */
.content__title {
font-size: 24px;
color: red;
}
.content__title--reversed {
font-size: 20px;
background: red;
color: white;
}
.calendar__title {
font-size: 20px;
color: red;
}
/* 布局文件夹 */
.main {
float: left;
...
}
.sidebar {
float: right;
...
}
</style>
这就解决了刚才的由于依赖位置而造成 CSS 样式混乱的问题。
-
选择器优先级
把 ID 选择器改成 CSS 类名选择器是一个很好的开始,这样可以停止 CSS 优先级之间 的冲突问题,让每一个选择器的权重扁平化成“1”,我们就不再需要利用优先级较量出 “胜利者”来决定样式。
-
颜色重置
比降低权重更好的方法是对每一个元素使用唯一的选择器。这样你的模块样式就不再会 与侧边栏样式或者页面通用样式冲突了。
-
位置依赖
去掉布局文件中的样式代码之后,我们就不用再担心因为把日历组件移出侧边栏而造成 样式改变了。
-
多重继承
每个标题都有了自己唯一的 CSS 类名之后,我们就可以任意修改其中的某个样式而不 会影响其他标题了。如果你想改变多个选择器对应的样式,可以使用预处理器变量、混 入(mixin)或继承来帮你做。
-
深层嵌套
即使在日历的层级上,我们也仍然没有给 H2 标签应用任何样式。因此再给日历中的新 H2 添加样式时,就不需要重写通用样式、侧边栏样式或者日历的头部样式了。
其他编写CSS原则
单一职责原则
<div class="calendar">
<h2 class="primary-header">This Is a Calendar Header</h2>
</div>
<div class="blog">
<h2 class="primary-header">This Is a Blog Header</h2>
</div>
<style>
.primary-header {
color: red;
font-size: 2em;
}
</style>
这里的 primary-hader 同时作用在两个block里,当blog的primary-header字体大小更大时,如果直接改primary-header,将会导致其余应用该样式的标签受到影响,只能通过提高优先级来覆盖它
.blog .primary-header{
font-size: 2.4em;
}
这种方法虽然在短期内有效,但是也使我们再次面临本章开头所提到的那几个问题。这个 新的标题样式现在取决于元素位置,具有多重继承性,并且引发了“最高优先级”争夺赛。
所以针对这个问题,应该给每个标签都有单一的,有重点的任务:
<div class="calendar">
<h2 class="calendar-header">This Is a Calendar Header</h2>
</div>
<div class="blog">
<h2 class="blog-header">This Is a Blog Header</h2>
</div>
<style>
.calendar-header {
color: red;
font-size: 2em;
}
.blog-header {
color: red;
font-size: 2.4em;
}
</style>
虽然这种方法确实会导致一些代码重复(红色字体定义了两次),但是它的可持续性带来 的好处大大超过代码重复的任何坏处。这里多出来的代码对网页大小的增加而言微不足道 (gzip 喜欢重复的内容),而且由于博客标题不一定一直保持红色,如果整个项目强制执行 单一责任原则,就能够确保在进一步改变时,我们可以毫不费力地完成,并且也不需要回 顾之前的代码
单一样式来源
单一样式来源的方法将单一责任理论应用到更深层次,不仅每个 CSS 类名被创建为单一用 途,而且每个标签的样式也只有单一的来源。在一个模块化设计中,任何组件的设计必须 由组件本身决定,而不应该被它的父类名限制。让我们看看实际应用中的情况
<div class="blog">
<h2 class="blog-header">This Is a Blog Header</h2>
...
<div class="calendar">
<h2 class="calendar-header">This Is a Calendar Header</h2>
</div>
</div>
<style>
/* calendar.css */
.calendar-header {
color: red;
font-size: 2em;
}
/* blog.css */
.blog-header {
color: red;
font-size: 2.4em;
}
.blog .calendar-header {
font-size: 1.6em;
}
</style>
写这样的样式是为了当日历在博客文章里出现时,缩小日历头部文字的字体。从设计的角 度看,这无可厚非,最终日历组件会根据它在哪里而改变外观。我们称这种带条件的样式 为“上下文”,已被广泛应用到我的设计系统里。
这种方法的主要问题是,定义缩小字体的样式代码在博客组件的文件中,而不是单一地存在于 日历组件文件。在这种情况下,样式散落在多个组件文件里,导致很难预料某个组件放到 页面中会长什么模样。为了缓解这个问题,我建议把带上下文的样式移到日历模块代码中:
/* calendar.css */
.calendar-header {
color: red;
font-size: 2em;
}
.blog .calendar-header {
font-size: 1.6em;
}
/* blog.css */
.blog-header {
color: red;
font-size: 2.4em;
}
将 blog 下的 calendar-header 放到 calendar.css 文件里,目的是不让css散落在各处
通过这种方法,当日历头部出现在博客文章里时,我们仍然能够缩小它的字体,而且如果 把所有带 calendar-header 上下文的样式放到日历文件中,我们可以看到日历头部所有可 能的变动都放在一起。这使得更新日历模块更容易(因为我们知道所有变动情况),也使 得我们能为每一个变动创建适当的测试覆盖
JavaScript
除了使用代码检查工具,在编写函数时,应尽量创造可复用的函数
内容太多了,就这样吧
参考
(图灵程序设计丛书) 前端架构设计 | PDF (scribd.com)
CSS 的模組化方法:OOCSS、SMACSS、BEM、CSS Modules、CSS in JS | Summer。桑莫。夏天 (cythilya.github.io)