# 状态改变

现在你的面前有一份PS文件,你需要把它转换为HTML和CSS(可能会有一些JavaScript在里面)。

从构图到实现代码似乎很简单。但是,页面中的各种组件需要呈现各种状态,默认状态是什么样,状态改变时又是什么样。

# 什么是状态改变?

改变状态有三种方式:

  1. 类名
  2. 伪类
  3. 媒体查询

类名导致的状态变化往往和JavaScript相关。通过一些交互,例如移动鼠标,敲击键盘或者是其他什么事件,一个元素会被添加一个新的类,然后出现不同的视觉效果。

数量众多的伪类也可以对状态进行改变。使用伪类我们就不需要使用JavaScript来描述状态的变化。不过,伪类的应用是有限的,它只能改变伪类所在元素的后代或者兄弟元素。不然,我们就只能去使用JavaScript。

媒体查询则定义了在固定标准(例如不同的视口大小)下的样式规则。

对于基于模块的系统,每个模块进行基于状态的设计是十分重要的。当你不同问自己,“默认状态是怎么的?”,你会发现自己会去主动思考如何逐渐改进现有的设计,这也会让你处理问题的方式略有不同。

# 通过类名改变

大多数情况下,类名的改变是比较简单的。它适用于所有需要展示不同状态的元素。例如,用户点击一个图标来隐藏和显示元素。

/* 通过类名JavaScript改变了状态 */
// with jQuery
$('.btn-close').click(function(){ 
    $(this).parents('.dialog').addClass('is-hidden');        
})

上述的jQuery例子为每个有btn-close类名的元素添加了一个点击的事件。当用户点击按钮的时候,JavaScript会传递事件并在DOM树上找到带有dialog类的祖先元素,然后将is-hidden的状态类添加到该元素上。

状态改变的影响远不止这样。

一个比较通用的用户设计模式是按钮被点击然后显示一个菜单,按钮变为按下状态,菜单变为显示状态。我们如何处理这种状态变化呢?这得看你的HTML结构。例如,在雅虎,菜单会在按钮点击之后被加载,然后将菜单的HTML结构插入DOM当中,并通过命名约定将两者绑定在一起。

<!-- 按钮和菜单在同一文档的不同部分 -->
<div id="content">
   <div class="toolbar">
      <button id="btn-new" class="btn" data-action="menu">New</button>
   </div>
</div>
<div id="menu-new" class="menu">
   <ul> ... </ul>
</div>

data-action这个属性会在JavaScript的点击处理过程中告诉程序:“你好,我要加载一个菜单”。接着程序会获取按钮ID,并找到相应的菜单。下面是模拟这个效果的jQuery代码:

/* 使用jQuery加载菜单 */
// bind a click handler to the button
$('#btn-new').click(function(){
    // wrap the clicked button in jQuery
    var el = $(this); 

    // change the state of the button
    el.addClass('is-pressed');

    // find the menu by stripping btn- and
    // adding it to menu selector
    $('#menu-' + el.id.substr(4)).removeClass('is-hidden');
});

如上述代码所示,通过JavaScript,单个元素的状态更改可以使得在两个不同地方的元素改变。

但是如果菜单在按钮的旁边呢?

<!-- 按钮和菜单在文档的相同部分 -->

<div id="content">
   <div class="toolbar">
      <button id="btn-new" class="btn" data-action="menu">New</button>
      <div id="menu-new" class="menu">
          <ul> ... </ul>
      </div>
   </div>
</div>

之前的代码还是可以起作用,完全可以保留不变。但是,我们还有其他选择。你的第一直觉可能是在父元素上添加一个类,并为按钮和菜单编写样式。

<!-- 在父元素上添加类,并为其子元素编写样式 -->
<div id="content">
   <div class="toolbar is-active">
      <button id="btn-new" class="btn" data-action="menu">New</button>
      <div id="menu-new" class="menu">
          <ul> ... </ul>
      </div>
   </div>
</div>

/* CSS for styling */

.is-active .btn { color: #000; }
.is-active .menu { display: block; }

这个方法的问题是HTML结构被绑定在一起,且必须要有一个容器元素。菜单和按钮必须存在于容器元素中,这使得我们只能寄希望于在工具栏上不会再添加新的按钮了。

另一种方法就是像我们之前做的一样,在按钮上添加active类,然后使用兄弟选择器去激活菜单。

<!-- 用兄弟选择器去激活菜单 -->
<div id="content">
   <div class="toolbar">
      <button id="btn-new" class="btn is-active" data-action="menu">New</button>
      <div id="menu-new" class="menu">
          <ul> ... </ul>
      </div>
   </div>
</div>

/* CSS for styling */

.btn.is-active { color: #000; }
.btn.is-active + .menu { display: block; }

我更喜欢使用这个方法而不是把状态类加在父元素上,这样能够让状态和模块更紧密地联系起来。但是菜单和按钮的HTML仍然存在依赖性:一个元素必须紧接着另一个元素。不过,如果在你的项目你的组织方式就是如此的话,那这就是一个可以为你所用的好方法。

# 为什么在父元素和兄弟元素上标注状态都是有问题的?

这些方法之所以会在应用于模块时出现问题,就是因为我们已经不知道应该在哪里找到相应的规则集。菜单不在只是菜单,它变成了一个按钮菜单。如果你需要改变这个模块的active状态,你是应该在按钮样式中找还是在菜单样式中找呢?

所有这一切告诉我们,为每个按钮分配不同的状态才是更好的选择。这可以使得模块之间的耦合度降低,使你的网站更容易测试,开发,扩展。

# 使用属性选择器改变状态

如果你的浏览器支持的话,你也可以充分发挥属性选择器的优势来进行状态改变。它可以有以下好处:

  • 将状态从布局和模块类中分离出来
  • 状态之间的渐变切换更加简单

让我们来看一个按钮的例子,它有各种状态:默认,按下或者禁用。

你可能会选择子模块的命名约定。

/* 子模块命名约定 */
.btn { color: #333; } 
.btn-pressed { color: #000; } 
.btn-disabled { opacity: .5; pointer-events: none; }

如果一个按钮需要在多个状态之间切换的话,那使用状态的命名规范可能会更有意义一点。

/* 状态命名约定 */
.btn { color: #333; } 
.is-pressed { color: #000; } 
.is-disabled { opacity: .5; pointer-events: none; }

我喜欢将上面两个例子做比较,这显示出SMACSS的命名清晰且灵活。两种方式出现在项目里,我都乐于见得。下面让我们看另一个方法:属性选择器。

/* 属性选择器约定 */
.btn[data-state=default] { color: #333; } 
.btn[data-state=pressed] { color: #000; } 
.btn[data-state=disabled] { opacity: .5; pointer-events: none; }
<!-- HTML -->
<button class="btn" data-state="disabled">Disabled</button>

data-前缀是HTML5规范的一部分,我们可以把自定义的属性名放在data命名空间下,这样就不会与未来的HTML属性相冲突了。通过属性选择器,改变一个按钮的状态并不需要添加或者删除类。我们只需要改变属性值即可。

/* 用jQuery改变状态 */
// bind a click handler to each button
$(".btn").bind("click", function(){
    // change the state to pressed
    $(this).attr('data-state', 'pressed');
});

无可否认,用像jQuery这样的库来改变状态一点都不复杂。jQuery的hasClassaddClasstoggleClass方法使得类名操作变得十分简单。

不过表示状态可不止上述几种方法。

# 基于类用CSS动画做状态改变

CSS动画是有趣而强大,但很多人认为它不应该用在CSS上。毕竟,CSS是用来描述样式的,而JavaScript才是描述行为的。

不过这其实是有区别的,CSS动画定义的是一种可视化的状态,而我们利用JavaScript来改变页面中元素的状态。但JavaScript不应该用来描述状态,也就是说不能用JavaScript添加行内样式。

从历史上来看,我们之所以使用JavaScript创建动画仅仅是因为这是唯一可行的方法(HTML+TIME (opens new window)是非标准的)

当我们用上述的结论来思考问题时,你会发现它使我们能应对各种不同情况。例如,让一个消息短时间内出现在页面上,然后淡出,这是十分正常的特效。

/* JavaScript处理状态变化 */
function showMessage (s) {
    var el = document.getElementById('message');
    el.innerHTML = s;

    /* set state */
    el.className = 'is-visible'; 
    setTimeout(function(){
        /* set state back */
        el.className = 'is-hidden';
    }, 3000);
}

消息状态从隐藏变为可见,然后又变为隐藏。JavaScript负责处理状态之间的转化,而CSS就可以处理状态切换过程中的动画(利用CSS渐变或者是CSS动画)。

/* CSS处理渐变 */
@keyframes fade {
    0% { opacity:0; display:block; }
  100% { opacity:1; display:block; }
}

.is-visible {
    display: block;
    animation: fade 2s;
}

.is-hidden {
    display: none;
    animation: fade 2s reverse;
}

不过,我必须承认的是,上述例子并不会起作用,因为当前的动画规范及浏览器实现并不允许我们在动画当中定义一个非动画的属性(display: none)。幸运的是,规范仍然在不断修订中,相信随着浏览器的实现,这个特性很快就会被补充进去。为了让我们例子起作用,我们需要像下面这样做:

/* 现代浏览器上的动画 */
@-webkit-keyframes fade {
    0% { opacity:0;  }
  100% { opacity:1; display:block; }
}

.is-visible {
    opacity: 1;
    animation: fade 2s;
}

.is-hidden {
    opacity: 0;
    animation: fade 2s reverse;
}

.is-removed {
    display: none;
}

/* and then our javascript */
function showMessage (s) {
    var el = document.getElementById('message');
    el.innerHTML = s;

    /* set state */
    el.className = 'is-visible'; 
    setTimeout(function(){
        /* set state back */
        el.className = 'is-hidden';
        setTimeout(function(){
            el.className = 'is-removed';
        }, 2000);
    }, 3000);
}

这个例子当中,我们仍然使用CSS动画,但是使用JavaScript在动画结束时将元素从流程中移除。

利用这样的方式,我们使得样式(也称为状态)和行为分离了。

# 通过伪类改变状态

如上述所说,我们可以使用类和属性处理模块上的状态变化。但是CSS同样提供了各种伪类来帮助我们管理状态。

从CSS2.1开始,最有用的三个伪类就是:hover:focus:active,这三个伪类能够响应用户操作。CSS3添加了很多新的伪类,大多数是基于HTML结构的(例如:nth-child:last-child)。
还有很多UI伪类可以响应表单操作,也是相当不错的。

一个模块的默认状态通常不会带伪类。伪类通常被定义为模块的辅助状态。

/* 使用伪类定义状态 */
.btn {
    background-color: #333; /* gray */
}

.btn:hover {
    background-color: #336; /* blueish */
}

.btn:focus {
    /* blueish focus ring */
    box-shadow: 0 0 3px rgba(48,48,96,.3); 
}

由于模块可能有很多子类,所以可能会变得十分复杂,因为你同样需要为子模块设计默认状态。

/* 定义子模块的伪类状态 */

.btn {
    background-color: #333; /* gray */
}

.btn:hover {
    background-color: #336; /* blueish */
}

.btn:focus {
    /* blueish focus ring */
    box-shadow: 0 0 3px rgba(48,48,96,.3); 
}

/* a default button state is the default choice from a
 * selection of buttons 
 */
.btn-default {
    background-color: #DEDB12; /* yellowish */
}

.btn-default:hover {
    background-color: #B8B50B; /* darker yellow */
}

/* no need to define a different focus state */

最后的代码例子,我们编写了5个模块变体:一个基础模块,一个子模块,以及它们出现的3个伪类状态。我们基于类引入的状态越多,那么代码就会变得越复杂

/* 模块,子模块,类状态和伪类状态。我的天哪! */
.btn { ... }
.btn:hover { ... }
.btn:focus { ... }

.btn-default { ... }
.btn-default:hover { ... }

.btn.is-pressed { ... }
.btn.is-pressed:hover { ... }

.btn-default.is-pressed { ... }
.btn-default.is-pressed:hover { ... }

幸好,在你的设计里很少有模块有如此复杂的状态管理。但是,需要明确的是,良好的状态管理会让你的项目更易维护。

# 通过媒体查询改变状态

虽然使用类和伪类进行状态改变已经相当平常了,但是媒体查询正快速成为管理状态的另一条途径——往常只能通过JavaScript来实现的。适配设计和响应式Web设计使用媒体查询来响应各种标准。打印样式(Print Style Sheets)是其中一条媒体查询,允许我们定义页面打印时的样式。

媒体查询可以通过在link元素上定义media属性来定义单独的样式表,也可以通过在@media块中定义特殊的样式。

<!-- 链接样式表 -->
<link href="main.css" rel="stylesheet">
<link href="print.css" rel="stylesheet" media="print">

/* inside main.css */
@media screen and (max-width: 400px) {
    #content { float: none; }
}

大多数媒体查询的例子都是定义一个断点,然后把所有和这个断点相关的样式全部放进媒体查询指令中。

SMACSS的做法就是把属于某一模块的媒体查询的样式放在模块的剩余部分。这就是说,不单单有一个断点就可以了,而是不管在主要CSS文件还是在分开的媒体查询样式表,都应该讲媒体查询置于模块状态的位置(一般是默认样式的后面)。

/* 模块化媒体查询 */

/* default state for nav items */
.nav > li {
   float: left;
}

/* alternate state for nav items on small screens */
@media screen and (max-width: 400px) {
    .nav > li { float: none; }
}

... elsewhere for layout ...

/* default layout */ 
.content { 
    float: left;
    width: 75%;
}

.sidebar {
    float: right;
    width: 25%;
}

/* alternate state for layout on small screens */
@media screen and (max-width: 400px) {
    .content, .sidebar { 
        float: none;
        width: auto; 
    }
}

是的,这其实意味着媒体查询的定义可能被定义很多次,但是这也使得所有和模块相关的信息全部放在了一起。记住,把所有模块信息放在一起(尤其是在自己的CSS文件中)有利于单个模块的隔离测试,并且允许模块化的资源(例如模块化的模板和CSS)能够在初始化页面被加载之后加载。

# 这就是关于状态的全部了

这个章节回顾了状态变化的三个方式:类,伪类及媒体查询。如果你能模块化地思考你的设计并且思考这些模块在各个状态下的表现,你就能更轻松地对样式进行合理的拆分,并且建设易于维护的网站。