10月17, 2016

【译】CSS伪类与伪元素完全指南

原文:http://www.zcfy.cc/article/239

刚开始从事Web设计时,我犯了很多错误,也因此获得了进步。那时候没有Smashing MagazineCan I Use CodePen,也没有其他我们现在常见的工具。只要有人能告诉一个设计思路,特别是CSS前沿方向的,那就谢天谢地了。

今天我的经验已经很丰富了,所以想本着友好、随意、探讨的原则,跟大分享一下CSS中的伪类和伪元素。

如果你已经是有经验的Web设计者和开发者了,那么一定对本文要讨论的伪类和伪元素有所了解。不过,还是建议你先看看本文后面完整的列表,看有没有一两个你还不知道的?

在真正开始之前,因为我们想讲伪类和伪元素嘛,所以先问个基本的问题:你知道这里的“伪”是什么意思吗?不确定的话,可以参考Dictionary.com的定义

形容词

1. 不是真实的但有其外观;伪装的;假的或欺骗的;骗人的。

2. 差不多,很接近,或尽可能一样。

不用管W3C是怎么定义的,反正伪类就是某个元素的一种虚拟状态,或者说一种特有的性质,这种状态或性可以通过CSS捕捉到。常见的伪类有::link:visited:hover:active:first-child:nth-child。当然这只是一少部分,一会儿我们都会介绍。

伪类是一个冒号(:)后跟伪类的名字构成的,有时候名字后面还会有一个放在括号里的值。:nth-child是第几个?

好了,再说伪元素。伪元素是一种虚拟的元素,CSS把它当成普通HTML元素看待。之所以叫伪元素,就因为它们在文档树或DOM中并不实际存在。换句话说,我们不会在HTML中包含伪元素,只会通过CSS来创建伪元素。

以下是几个常见的伪元素::after:before:first-letter。伪元素会在本文后面介绍。

伪元素是一个冒号还是两个冒号?

简单回答:多数情况下,都行。

两个冒号(::)是CSS3为了区分::before::after这样的伪元素和:hover:active等伪类才引入的。除了IE8及以下版本,所有浏览器都支持两个冒号的伪元素表示法。

不过,有些伪元素只能使用两个冒号,像::backdrop

我个人使用一个冒号,为了跟以前的浏览器兼容。当然,不用两个冒号不行的时候,还是要用两个冒号。

这里没有对错,完全看你个人喜好。

不过,我在写这篇文章时查了一下,规范建议使用单冒号表示法,原因也是向后兼容:

请注意CSS3中表示伪元素使用双冒号,比如a::after { … },这是为了与伪类区分开。伪类应该是在CSS中经常出现的。不过,CSS3也允许单冒号的伪元素,目的是向后兼容。我们也建议暂时使用单冒号。

如果伪元素同时支持单、双冒号的形式,本文标题会给出两种形式。如果只支持双冒号,那就只有一种形式。

什么时候使用(不使用)生成的内容

通过CSS生成内容需要用到CSS属性content和伪元素:before:after

其中的“内容”(content)可是纯文本,也可以是一个容器,通过CSS操作来显示某种图形或者装饰性元素。本文只介绍第一种内容,即文本。

重要的内容可不要使用生成的内容,原因如下:

  • 屏幕阅读器读不到它
  • 无法选中
  • 如果为了装饰而在生成内容中使用了多余的内容,那么支持CSS生成内容的屏幕阅读器会大声地把它读出来,导致用户体验更差

CSS生成的内容只适用于装饰性、不重要的内容,但也要确保屏幕阅读器能够适当处理它,让使用这些辅助技术的用户不至于分心。这里适用“渐进增强”原则。

Smashing Magazine上,Gabriele Romanato为此写过一篇非常棒的文章

实验性伪类和伪元素

实验性的伪类和伪元素,指的是那些不稳定或没最终定案的伪类和伪元素。它们的语法和行为还可能有变。

不过,加上厂商前缀就可以使用这些实验性的伪类和伪元素。可以参考Can I Use,以及一些自动加前缀的工具,比如-prefix-freeAutoprefixer就是必备的。

本文会在实验性的伪类和伪元素的名字旁边加上“experimental”标签。

全部伪类和伪元素(按字母顺序)

  • :active
  • ::after/:after
  • ::backdrop (experimental)
  • ::before/:before
  • :checked
  • :default
  • :dir (experimental)
  • :disabled
  • :empty
  • :enabled
  • :first-child
  • ::first-letter/:first-letter
  • ::first-line/:first-line
  • :first-of-type
  • :focus
  • :fullscreen (experimental)
  • :hover
  • :in-range
  • :indeterminate
  • :invalid
  • :lang
  • :last-child
  • :last-of-type
  • :link
  • :not
  • :nth-child
  • :nth-last-child
  • :nth-last-of-type
  • :nth-of-type
  • :only-child
  • :only-of-type
  • :optional
  • :out-of-range
  • ::placeholder (experimental)
  • :read-only
  • :read-write
  • :required
  • :root
  • ::selection
  • :scope (experimental)
  • :target
  • :valid
  • :visited
  • Bonus content: A Sass mixin for links

好啦,诸位,好戏开场了!

伪类

首先,我们讨论伪类,从状态伪类开始。

状态伪类

状态伪类通常出现在用户执行某个操作的情况下。在CSS里,“操作”也可以是“无操作”,比如尚未点过的链接。

下面就有请它们一个一个地上场。

:link伪类表示链接的正常状态,选择那些尚未被点过的链接。建议在其他链接相关的伪类之前声明:link,它们的顺序为::link:visited:hover:active

a:link {
    color: orange;
}

当然,这个伪类也可以省略:

a {
    color: orange;
}

:VISITED

:visited伪类选择点过的链接,应该声明在第二位(在:link之后)。

a:visited {
    color: blue;
}

:HOVER

:hover伪类在用户指针悬停时生效。而且它不只可以用于链接。

它应该在第三位(在:visited之后)。

a:hover {
    color: orange;
}

看示例:http://codepen.io/ricardozea/pen/vGEzJK

:ACTIVE

:active伪类选择被鼠标指针或触摸操作“激活的” 元素,也可以通过键盘来激活,就像:focus伪类一样。

:focus类似,但区别在于:active只发生在鼠标被按下到被释放的这段时间里。

它应该在第四位(在hover后面)。

a:active {
    color: rebeccapurple;
}

:FOCUS

:focus用于选择已经通过指针设备、触摸或键盘获得焦点的元素,在表单里使用得非常多。

a:focus {
    color: green;
}

或者:

input:focus {
    background: #eee;
}

扩展内容:Sass中针对链接的混入

如果你用过CSS预处理器,那应该对这一部分感兴趣。

(如果你不熟悉CSS预处理器,没问题,跳过这一节,直接看下一节吧。)

为了简化CSS编码工作,这里介绍一下创建一组基本的链接样式的Sass混入(mixin)。

这里的混入没有默认参数,因此我们必须以一种友好的方式,声明链接的全部4种状态。

:focus:active伪类的声明通常在一块,当然也可以给它们分开。

注意这个混入不仅仅适用于链接,而是适用于任何 HTML元素。

这就是我们定义的混入:

@mixin links ($link, $visited, $hover, $active) {
    & {
        color: $link;
        &:visited {
            color: $visited;
        }
        &:hover {
            color: $hover;
        }
        &:active, &:focus {
            color: $active;
        }
    }
}

使用方法:

a {
    @include links(orange, blue, yellow, teal);
}

编译结果:

a {
  color: orange;
}
a:visited {
  color: blue;
}
a:hover {
  color: yellow;
}
a:active, a:focus {
  color: teal;
}

看示例:http://codepen.io/ricardozea/pen/wMyZQe

结构化伪类

结构化伪类选择通过其他选择符无法选择的文档树或DOM中的其他信息。

:FIRST-CHILD

:first-child伪类选择父元素的第一个子元素。

在下面的例子中,只有第一个li元素的文本是橙色的。

HTML:

<ul>
    <li>This text will be orange.</li>
    <li>Lorem ipsum dolor sit amet.</li>
    <li>Lorem ipsum dolor sit amet.</li>
</ul>

CSS:

li:first-child {
    color: orange;
}

:FIRST-OF-TYPE

:first-of-type伪类选择父元素容器内任意类型子元素的第一个元素。

在下面的例子中,第一个li元素和第一个span元素的文本才是橙色的。

HTML:

<ul>
    <li>This text will be orange.</li>
    <li>Lorem ipsum dolor sit amet. <span>This text will be orange.</span></li>
    <li>Lorem ipsum dolor sit amet.</li>
</ul>

CSS:

ul :first-of-type {
    color: orange;
}

:LAST-CHILD

:last-child伪类选择父元素的最后一个子元素。

在下面的例子中,只有最后一个li元素的文本是橙色的。

HTML:

<ul>
    <li>Lorem ipsum dolor sit amet.</li>
    <li>Lorem ipsum dolor sit amet.</li>
    <li>This text will be orange.</li>
</ul>

CSS:

li:last-child {
    color: orange;
}

:LAST-OF-TYPE

:last-of-type伪类选择父元素容器内任意类型子元素的最后一个元素。

在下面的例子中,最后一个li元素和最后一个span元素的文本才是橙色的。

HTML:

<ul>
    <li>Lorem ipsum dolor sit amet. <span>Lorem ipsum dolor sit amet.</span> <span>This text will be orange.</span></li>
    <li>Lorem ipsum dolor sit amet.</li>
    <li>This text will be orange.</li>
</ul>

CSS:

ul :last-of-type {
    color: orange;
}

:NOT

:not伪类也叫取反伪类,它通过括号接受一个参数,一个“选择符”。实际上,这个参数也可以是另一个伪类。

这个伪类可以连缀使用,但不能包含别的:not选择符。

在下面的例子中,:not伪类选择与参数不匹配的元素。

HTML:

<ul>
    <li class="first-item">Lorem ipsum dolor sit amet.</li>
    <li>Lorem ipsum dolor sit amet.</li>
    <li>Lorem ipsum dolor sit amet.</li>
    <li>Lorem ipsum dolor sit amet.</li>
</ul>

CSS:

应用下面的CSS,除了类为.first-itemli之外的li元素的文本都是橙色的:

li:not(.first-item) {
    color: orange;
}

下面看一看“连缀”两个:not伪类。应用下面的CSS规则,除了类为.first-itemli和最后一个li,其他li都会有黄色背景和黑色文本:

li:not(.first-item):not(:last-of-type) {
    background: yellow;
    color: black;
}

看示例:http://codepen.io/ricardozea/pen/dGmqbg

:NTH-CHILD

:nth-child伪类根据元素在标记中的次序选择相应的元素。

这个伪类在CSS中是用途最广、支持也最广的。

所有:nth伪类都接受一个参数,这个参数是一个公式。公式可以是一个整数,或者关键字oddeven,或者形如an+b的结构。

对于an+b:

  • a是一个数值(整数)
  • n就是n
  • +是运算符,可以是加号+或减号-
  • b也是一个整数,但只有使用了运算符的时候才会用到

以希腊字母的英文列表为例,以下是HTML标记结构:

<ol>
    <li>Alpha</li>
    <li>Beta</li>
    <li>Gamma</li>
    <li>Delta</li>
    <li>Epsilon</li>
    <li>Zeta</li>
    <li>Eta</li>
    <li>Theta</li>
    <li>Iota</li>
    <li>Kappa</li>
</ol>

CSS:

选择第2个子元素,结果Beta会变成橙色:

ol :nth-child(2) {
    color: orange;
}

从第2个子元素起,隔一个选一个,结果Beta、Delta、Zeta、Theta和Kappa会变成橙色:

ol :nth-child(2n) {
    color: orange;
}

选择所有偶数个子元素:

ol :nth-child(even) {
    color: orange;
}

从第6个子元素起,隔一个选一个,结果Zeta、Theta和Kappa会变成橙色:

ol :nth-child(2n+6) {
    color: orange;
}

看示例:http://codepen.io/ricardozea/pen/adYaER

:NTH-LAST-CHILD

除了是从后往前选择元素,:nth-last-child:nth-child完全一样。

CSS:

选择倒数第2个子元素,只有Iota是橙色:

ol :nth-last-child(2) {
    color: orange;
}

从倒数第2个子元素开始,隔一个选一个,结果Iota、Eta、Epsilon、Gamma和Alpha会变成橙色:

ol :nth-last-child(2n) {
    color: orange;
}

从后往前,选择所有偶数个子元素:

ol :nth-last-child(even) {
    color: orange;
}

从倒数第6个元素开始,隔一个选一个,因此Epsilon、Gamma和Alpha会变成橙色:

ol :nth-last-child(2n+6) {
    color: orange;
}

:NTH-OF-TYPE

:nth-of-type伪类与:nth-child类似,主要区别是它更具体了,只针对特定类型的元素。

在下面的例子中,所有容器内的第2个p元素将为橙色。

HTML:

<article>
    <h1>Heading Goes Here</h1>
    <p>Lorem ipsum dolor sit amet.</p>
    <a href=""><img src="images/rwd.png" alt="Mastering RWD"></a>
    <p>This text will be orange.</p>
</article>

CSS:

p:nth-of-type(2) {
    color: orange;
}

:NTH-LAST-OF-TYPE

:nth-last-of-type伪类是从后往前数,其余跟:nth-of-type一样。

对于下面的例子,因为是从末尾开始,所以第1个段落会变成橙色。

HTML:

<article>
    <h1>Heading Goes Here</h1>
    <p>Lorem ipsum dolor sit amet.</p>
    <a href=""><img src="images/rwd.png" alt="Mastering RWD"></a>
    <p>This text will be orange.</p>
</article>

CSS:

p:nth-last-of-type(2) {
    color: orange;
}

相关资源

建议大家在使用:nth伪类前,一定要参考下面这两篇不错的文章:

:ONLY-CHILD

:only-child选择父元素中唯一的子元素。

在下面的例子中,第一个ul只有一个子元素,因此该子元素将变成橙色。第二个ul有多个子元素,因此其子元素不会受:only-child伪类影响。

HTML:

<ul>
    <li>This text will be orange.</li>
</ul>

<ul>
    <li>Lorem ipsum dolor sit amet.</li>
    <li>Lorem ipsum dolor sit amet.</li>
</ul>

CSS:

ul :only-child {
    color: orange;
}

:ONLY-OF-TYPE

:only-of-type伪类选择同级中类型唯一的元素,与:only-child类似,但针对特定类型的元素,让选择符有了更强的意义。

在下面的例子中,第一个ul只有一个li元素,因此其文本将为橙色。

HTML:

<ul>
    <li>This text will be orange.</li>
</ul>

<ul>
    <li>Lorem ipsum dolor sit amet.</li>
    <li>Lorem ipsum dolor sit amet.</li>
</ul>

CSS:

li:only-of-type {
    color: orange;
}

:TARGET

:target伪类通过元素的ID及URL中的锚名称选择元素。

在下面的例子中,当浏览器中的URL以#target结尾时,ID为target的文章将被选中。

URL:

http://awesomebook.com/#target

HTML:

<article id="target">
    <h1><code>:target</code> pseudo-class</h1>
    <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit!</p>
</article>

CSS:

:target {
    background: yellow;
}

提示: background:background-color:的简写形式,用于指定颜色时效果一样。

验证伪类

表单验证一直是Web设计与开始中最不好搞的。有了验证伪类,可以让用户填写表单的过程更平顺。

有一点要注意,虽然本节介绍的伪类都用于表单元素,但其中有的伪类也可以用于其他HTML元素。

下面就来看看这些伪类吧!

:CHECKED

:checked伪类选择被勾选或选中的单选按钮、多选按钮及列表选项。

在下面的例子中,复选框被勾选后,标签会突出显示,增加了用户体验。

看示例:http://codepen.io/ricardozea/pen/wMYExY

:DEFAULT

:default伪类从表单中一组类似元素里选择默认的元素(即“提交”按钮。——译者注)。

如果要选择表单中没有类的默认按钮,可以使用:default

注意,在表单中使用Reset或Clear按钮会招致严重的可用性问题,所以除非绝对必要再用。参考下面两篇文章:

看示例:http://codepen.io/ricardozea/pen/WrzJKO

:DISABLED

:disabled伪类选择禁用状态的表单元素。处于禁用状态的元素,不能被选中、勾选,不能获得焦点。

在下面的例子中,name输入框处于禁用状态,因此会半透明。

HTML:

<input type="text" id="name" disabled>

CSS:

:disabled {
    opacity: .5;
}

提示: 标记中是非要使用disabled="disabled",只写一个disabled属性就行了。在XHTML中,disabled="disabled"这种写法才是必须的。

看示例:http://codepen.io/ricardozea/pen/NxOLZm

:EMPTY

:empty伪类选择其中不包含任何内容的空元素。只要包含一个字母、其他HTML元素,甚至一个空格,都不算空。

关于空或非空,以下是定义:


  • 元素中没有内容或字符。元素中包含HTML注释不算有内容。
  • 非空
    出现在元素中的字符。空格也算。

在下面的例子中,

  • 第一个元素中包含文本,因此背景不会变成橙色
  • 第二个元素包含一个空格,空格也是内容,因此也不会有橙色背景
  • 第三个元素中什么也没有(空的),因此背景为橙色
  • 最后一个元素中只有一个HTML注释(也是空的),因此也有橙色背景。

HTML:

<div>This box is orange</div>
<div> </div>
<div></div>
<div><!-- This comment is not considered content --></div>

CSS:

div {
  background: orange;
  height: 30px;
  width: 200px;
}

div:empty {
  background: yellow;
}

看示例:http://codepen.io/ricardozea/pen/rxqqaM

:ENABLED

:enabled伪类选择启用的元素。所有表单元素默认都是启用的,除非在标记中添加了disabled属性。

通过:enabled:disabled可以提供视觉上的反馈,改善用户体验。

在下面的例子中,禁用后又被启用的name输入框的不透明度将变为1,同时会有一个1像素的边框:

:enabled {
    opacity: 1;
    border: 1px solid green;
}

提示: 标记中是非要使用enabled="enabled",只写一个enabled属性就行了。在XHTML中,enabled="enabled"这种写法才是必须的。

看示例:http://codepen.io/ricardozea/pen/zqYQxq

:IN-RANGE

:in-range伪类选择有范围且值在指定范围内的元素。

在下面的例子中,输入元素支持输入5~10。输入值在这个范围内,会触发绿色边框。

HTML:

<input type="number" min="5" max="10">

CSS:

input[type=number] {
    border: 5px solid orange;
}

input[type=number]:in-range {
    border: 5px solid green;
}

看示例:http://codepen.io/ricardozea/pen/XXOKwq

:OUT-OF-RANGE

:out-of-range伪类选择有范围且值超出指定范围的元素。

在下面的例子中,输入元素支持输入1~12。输入值超出这个范围内,会触发橙色边框。

HTML:

<input id="months" name="months" type="number" min="1" max="12">`

CSS:

input[type=number]:out-of-range {
    border: 1px solid orange;
}

看示例:http://codepen.io/ricardozea/pen/XXOKwq

:INDETERMINATE

:indeterminate伪类选择单选按钮或复选框在页面加载时没有被勾选的。

比如,页面加载后,一组单选按钮中没有默认或预先勾选的,或者一个复选框已经通过JavaScript设置为indeterminate状态。

HTML:

<ul>
    <li>
        <input type="radio" name="list" id="option1">
        <label for="option1">Option 1</label>
    </li>
    <li>
        <input type="radio" name="list" id="option2">
        <label for="option2">Option 2</label>
    </li>
    <li>
        <input type="radio" name="list" id="option3">
        <label for="option3">Option 3</label>
    </li>
</ul>

CSS:

:indeterminate + label {
    background: orange;
}

看示例:http://codepen.io/ricardozea/pen/adXpQK

:VALID

:valid伪类选择输入格式符合要求的表单元素。

在下面的例子中,email输入框中的电子邮箱格式是正确的,因此这个输入框会被认为有效,将出现1像素的绿色边框:

input[type=email]:valid {
    border: 1px solid green;
}

看示例:http://codepen.io/ricardozea/pen/bEzqVg

:INVALID

:invalid伪类选择输入格式不符合要求的表单元素。

在下面的例子中,email输入框中的电子邮箱格式不正确,因此这个输入框会被认为无效,将出现橙色边框:

input[type=email]:invalid {
    background: orange;
}

看示例:http://codepen.io/ricardozea/pen/bEzqVg

:OPTIONAL

:optional伪类选择表单中非必填的输入字段。换句话说,只要输入字段中没有required属性,就会被:optional伪类选中。

在下面的例子中,这个数值字段是可以选填的,因此其中的文本将为灰色。

HTML:

<input type="number">

CSS:

:optional {
    color: gray;
}

:READ-ONLY

:read-only伪类选择用户不能编辑的元素,与:disabled伪类相似,标记中使用的属性决定了使用哪个伪类。

不能编辑的元素可以用来显示预先填好、不允许修改,但又需要连同表单一起提交的信息。

在下面的例子中,文本框有一个readonly属性,因此会被:read-only伪类选中,文本将为灰色。

HTML:

<input type="text" value="I am read only" readonly>

CSS:

input:read-only {
    color: gray;
}

看示例:http://codepen.io/ricardozea/pen/Nxopbj

:READ-WRITE

:read-write伪类选择用户可以编辑的元素,适用于有contenteditable属性的HTML元素。

有时候,可以与:focus伪类一块使用以增强用户体验。

在下面的例子中,点击div元素就可以编辑其中的内容,为此可以应用特殊的样式,让用户知道自己可以编辑其中的内容。

HTML:

<div class="editable" contenteditable>
    <h1>Click on this text to edit it</h1>
    <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit!</p>
</div>

CSS:

:read-write:focus {
    padding: 5px;
    border: 1px dotted black;
}

看示例:http://codepen.io/ricardozea/pen/LGqWxK

:REQUIRED

:required伪类选择有required属性的表单元素。

除了通过标签中的星号(*)提示必填,也可以通过这个伪类为输入字段应用样式。这样就万无一失了。

在下面的例子中,输入框有required属性,通过这个伪类为它应用特殊样式,可以提醒用户它是必填项。

HTML:

<input type="email" required>

CSS:

:required {
    color: black;
    font-weight: bold;
}

看示例:http://codepen.io/ricardozea/pen/KVJWmZ

:SCOPE (EXPERIMENTAL)

:scope伪类适用于style标签中有scoped属性的情形。

如果页面中某一部分的style标签里没有scoped属性,那么:scope伪类会一直向上查找,直到html元素,即当前样式表的默认作用范围。

在下面的例子中,第二个section中有一个scoped样式表,因此这个section中的文本会变成斜体。

HTML and CSS:

<article>
    <section>
        <h1>Lorem ipsum dolor sit amet</h1>
        <p>Lorem ipsum dolor sit amet.</p>
    </section>
    <section>
        **<style scoped>
                        :scope {
                            font-style: italic;
                        }
                  </style>**
        <h1>This text will be italicized</h1>
        <p>This text will be italicized.</p>
    </section>
</article>

看示例:http://codepen.io/ricardozea/pen/ZQobzz

语言伪类

语言伪类与页面中包含的文本相关,与图片、视频等媒体无关。

:DIR (EXPERIMENTAL)

:dir伪类选择文档中指定了语言方向的元素。换句话说,为了使用:dir伪类,需要在标记中为相关元素指定dir属性。

语言方向目前有两种:ltr(从左到右)和rtl(从右往左)。

写这篇文章时,支持:dir伪类的只有Firefox(-moz-dir()),下面的例子同时使用带前缀和不带前缀的:dir选择符。

注意: 要用带前缀和不带前缀的选择符分别创建规则,两种选择符共享一条规则是不行的。

在下面的例子中,段落中的文字是阿拉伯文(是从右往左书写的),因此其颜色是橙色。

HTML:

<article dir="rtl">
    <p>التدليك واحد من أقدم العلوم الصحية التي عرفها الانسان والذي يتم استخدامه لأغراض الشفاء منذ ولاده الطفل.</p>
</article>

CSS:

/* 带前缀 */
article :-moz-dir(rtl) {
    color: orange;
}

/* 不带前缀 */
article :dir(rtl) {
    color: orange;
}

下面段落中的文字是英文(从左到右),颜色为蓝色。

HTML:

<article dir="ltr">
    <p>If you already know some HTML and CSS and understand the principles of responsive web design, then this book is for you.</p>
</article>

CSS:

/* 带前缀 */
article :-moz-dir(ltr) {
    color: blue;
}

/* 不带前缀 */
article :dir(ltr) {
    color: blue;
}

看示例:http://codepen.io/ricardozea/pen/adrxJy

:LANG

:lang伪类选择的元素通过lang=""属性、相应的meta元素以及HTTP首部的协议信息来确定。

lang=""属性常用于html标签,其实也可以用于其他标签。

插一句,这里通常的做法是使用CSS的quotes 属性来标记特定的语言。不过,多数浏览器(包括IE9及更高版本)会在CSS中没有声明的情况下自动添加适当的引用标记。

不过,自动添加的引用标记也可能不合适。因为浏览器自动添加的与CSS添加的还不太一样。

比如浏览器为德语(de)添加的引用标记如下:

„Lorem ipsum dolor sit amet.“

但通过CSS为德语添加的引用标签则通常如下:

»Lorem ipsum dolor sit amet.«

这两种都对。因此,使用浏览器自动添加的引用标记,还是自己通过CSS的:lang伪类及quotes属性添加,都看你的需要。

下面看看怎么通过CSS来添加引用标记。

HTML:

<article lang="en">
    <q>Lorem ipsum dolor sit amet.</q>
</article>
<article lang="fr">
    <q>Lorem ipsum dolor sit amet.</q>
</article>
<article lang="de">
    <q>Lorem ipsum dolor sit amet.</q>
</article>

CSS:

:lang(en) q { quotes: "“" "”"; }
:lang(fr) q { quotes: "«" "»"; }
:lang(de) q { quotes: "»" "«"; }

看示例:http://codepen.io/ricardozea/pen/gPJyvJ

其他伪类

下面再看看拥有其他功能的伪类。

:ROOT

:root伪类选择文档中最高层次的父元素。

在HTML中,:root伪类选择的就是html元素。但在SVG或XML等标记语言中,它可能选择不同的元素。

以下规则为HTML文档中最高层次的父元素html添加背景颜色:

:root {
    background: orange;
}

注意: 使用html也可以设置相同的样式,但:root是一个类,拥有比元素选择符(即html)更高的特指度。

:FULLSCREEN (EXPERIMENTAL)

:fullscreen伪类选择在全屏模式下显示的元素。

不过,这不适用于用户按F11进入的全屏模式,只适用于通过JavaScript Fullscreen API切换进入的全屏模式,通常由父容器中的图片、视频或游戏来调用。

怎么知道已经进入全屏模式呢?一般浏览器会在窗口顶部提示你,并告诉你按Escape键可以退出全屏模式。

使用:fullscreen伪类前必须知道,浏览器应用样式的方式差别很大。而且,不仅要在CSS中使用前缀,JavaScript中也一样。推荐使用Hernan Rajchert的screenfull.js,它帮我们填了不少浏览器的“坑”。

本文不会讨论全屏API,只给出一个在WebKit和Blink浏览器中可用的例子。

HTML:

<h1 id="element">This heading will have a solid background color in full-screen mode.</h1>
<button onclick="var el = document.getElementById("element"); el.webkitRequestFullscreen();">Trigger full screen!</button>

CSS:

h1:fullscreen {
    background: orange;
}

看示例:http://codepen.io/ricardozea/pen/ZQNZqy

伪元素

如前所述,伪元素类似一种虚拟元素,可以将其视为普通的HTML元素。但伪元素并不存在于文档树或DOM中,因此不能在HTML中输入,只能通过CSS创建。

同样,双冒号(::)与单冒号(:)也只是CSS3与CSS2.1的不同。

::BEFORE/:BEFORE

:before伪元素与:after类似,都可以为其他HTML元素添加内容(文本或图形)。同样,这里的内容并不实际存在于DOM中,但可以像存在一样操作它们。需要在CSS中声明content属性。

记住,通过这个伪元素生成的内容不能通过其他选择符选中。

HTML:

<h1>Ricardo</h1>

CSS:

h1:before {
    content: "Hello "; /* 注意Hello后面有一个空格 */
}

结果网页中会变成这样:

Hello Ricardo!

注意: 看到“Hello ”后面的空格了吗?没错,空格也算数。

::AFTER/:AFTER

:after伪元素也用于为其他HTML元素添加内容(文本或图形)。这里的内容并不实际存在于DOM中,但可以像存在一样操作它们。为了使用这个伪元素,必须在CSS中声明content属性。

同样,通过这个伪元素添加的任何内容都无法通过其他选择符选中。

HTML:

<h1>Ricardo</h1>

CSS:

h1:after {
    content: ", Web Designer!";
}

结果如下:

Ricardo, Web Designer!

::BACKDROP (EXPERIMENTAL)

::backdrop伪元素是在全屏元素后面生成的一个盒子,与:fullscreen伪类连用,修改全屏后元素的背景颜色。

注意: ::backdrop伪元素必须用双冒号。

还看前面:fullscreen伪类的例子。

HTML:

<h1 id="element">This heading will have a solid background color in full-screen mode.</h1>
<button onclick="var el = document.getElementById("element"); el.webkitRequestFullscreen();">Trigger full screen!</button>

CSS:

h1:fullscreen::backdrop {
    background: orange;
}

看示例:http://codepen.io/ricardozea/pen/bEPEPE

::FIRST-LETTER/:FIRST-LETTER

:first-letter伪元素选择一行文本第一个字符。

如果相应行前面包含图片、视频或表格元素,那么不会影响选择第一个字符。

这个伪元素非常适合对段落进行排版,有了它就不必用图片或其他技巧了。

提示: 这个伪元素也可以选中:before伪元素生成的第一个字符。

CSS:

h1:first-letter  {
    font-size: 5em;
}

::FIRST-LINE/:FIRST-LINE

:first-line选择元素的第一行,只适用于块级元素,行内元素不适用。

即使一段文本有多行,也会选中第一行。

CSS:

p:first-line {
    background: orange;
}

::SELECTION

::selection选择文档中被高亮选中的部分。

注意,基于Gecko的浏览器要求使用前面:::-moz-selection

注意: 在一条规则中同时使用带前缀和不还前缀的::selection是不行的,要分别写。

CSS:

::-moz-selection {
    color: orange;
    background: #333;
}

::selection  {
    color: orange;
    background: #333;
}

::PLACEHOLDER (EXPERIMENTAL)

::placeholder伪元素选择表单元素中通过placeholder属性设置的占位文本。

也可以写成::input-placeholder

注意: 这个伪元素不是标准的,因此将来有可能会变化。

在某些浏览器(IE10及Firefox 18之前)中,::placeholder伪元素的实现类似一个伪类。其他浏览器都将其视为伪元素。因此,除非要兼容IE10或旧版本的Firefox浏览器,因此应该这样写:

HTML:

<input type="email" placeholder="name@domain.com">

CSS:

input::-moz-placeholder {
    color:#666;
}

input::-webkit-input-placeholder {
    color:#666;
}

/* IE 10 only */
input:-ms-input-placeholder {
    color:#666;
}

/* Firefox 18 and below */
input:-moz-input-placeholder {
    color:#666;
}

小结

CSS伪类和伪元素相当有用,对不?这些伪类和伪元素提供了丰富的选择便利。

不要光看,自己动手试一试吧。广受支持的伪类和伪元素是很靠谱的。

希望大家看了这篇长文能有所收获。别忘了收藏它!

英文原文:https://www.smashingmagazine.com/2016/05/an-ultimate-guide-to-css-pseudo-classes-and-pseudo-elements/#?

阅读全文 »

08月18, 2016

【译】JavaScript中该如何安排后台任务

原文:http://www.zcfy.cc/article/258

关于JavaScript,大家要牢记一点:它阻塞

想象一下,浏览器里有一个进程小精灵,负责处理一切。渲染HTML、响应菜单命令、绘制屏幕、处理鼠标点击、运行JavaScript函数……。跟我们人一样,这个小精灵每次只能做一件事。如果一次交给它很多任务,那么就会有一个待办事项列表,小精灵按顺序一项一项去处理。

小精灵在碰到script标签或者要运行JavaScript函数时,会停下其他任务。下载代码(必要时)然后立即运行,之后才会触发其他事件 ,以及渲染页面。这是必要的,因为脚本几乎什么都可能做:加载更多代码、删除DOM元素、重定向URL,等等。就算有两个甚至更多小精灵,那其他小精灵也需要在首次处理代码时停下来。这就是阻塞。这也是为什么运行时间过长的脚本会导致浏览器无响应的原因。

我们通常想让JavaScript尽快运行,因为代码要初始化部件和事件处理程序。可是,有些没那么重要的后台任务并不会直接影响用户体验,比如:

  • 记录分析数据
  • 向社交网络改送数据(或发送57个“分享”按钮)
  • 预先取得内容
  • 预先处理或预先渲染HTML

这些任务并不要求立即完成,而为了保证页面持续响应,不应该在用户滚动页面或浏览内容期间执行这些任务。

为此可以使用Web Workers,在另一个线程里并发地运行代码。这个技术非常适合预先取得和处理数据,但无权直接访问或修改DOM。你可保证自己的脚本不那么干,但却无法保证Google Analytics等第三方脚本不那么干。

还有一个选择是setTimeout,比如setTimeout(doSomething, 1);。浏览器会在立即执行的任务完成后紧接着执行doSomething()函数。实际上是把它放在了待办事项的最后一项。问题在于无论有没有处理需求,这个函数都会被调用。

requestIdleCallback

requestIdleCallback是新API,用于在浏览器空闲的时候安排一些没那么重要的后台任务。这个API会让人联想到requestAnimationFrame,后者会在下一次绘制前调用函数更新动画。具体内容可以参考这篇文章:Simple Animations Using requestAnimationFrame

可以这样检测浏览器是否支持requestIdleCallback

if ("requestIdleCallback" in window) {
  // 支持requestIdleCallback
  requestIdleCallback(backgroundTask);
}
else {
  // 不支持,换一种方式
  setTimeout(backgroundTask1, 1);
  setTimeout(backgroundTask2, 1);
  setTimeout(backgroundTask3, 1);
}

还可以通过一个选项对象参数,指定暂停时间(以毫秒计),比如:

requestIdleCallback(backgroundTask, { timeout: 3000; });

这样可以保证你的函数在三秒钟内执行,无论浏览器是否空闲。

requestIdleCallback只会调用一次你的函数,并传入一个包含以下属性的期限(deadline)对象:

  • didTimeout — 如果可选的暂停时间已到则为true
  • timeRemaining() — 函数,返回留给任务执行的毫秒数

timeRemaining()会给你的任务分配不超过50ms的执行时间。它不会让任务停止超过这个时间限制,不过你可以再次调用requestIdleCallback以安排进一步的处理。

下面看一个例子,按顺序执行几个任务。任务以函数引用形式保存在一个数组中:

// 要运行的函数数组
var task = [
  background1,
  background2,
  background3
];

if ("requestIdleCallback" in window) {
  // 支持requestIdleCallback
  requestIdleCallback(backgroundTask);
}
else {
  // 不支持,待会一次性运行所有任务
  while (task.length) {
    setTimeout(task.shift(), 1);
  }
}

// requestIdleCallback回调函数
function backgroundTask(deadline) {

  // 有可能的话运行下一个任务
  while (deadline.timeRemaining() > 0 && task.length > 0) {
    task.shift()();
  }

  // 还有未执行的任务,再次申请处理
  if (task.length > 0) {
    requestIdleCallback(backgroundTask);
  }
}

有没有什么不应该通过requestIdleCallback做的

正如Paul Lewis在他关于这个主题的博客文章所说的,requestIdleCallback应该执行小任务,不适合执行时间不确定的任务(像操作DOM,最好还是用requestAnimationFrame回调来做)。在resolve(或reject)Promise的时候也要注意,因为空闲回调完成后会立即调用Promise的回调,而不管剩下的时间还够不够执行该回调。

requestIdleCallback的浏览器支持情况

requestIdleCallback是一个实验性的API,规范仍在制定中,今后很可能有改动。Chrome 47支持它,Opera应该也会跟进。Microsoft和Mozilla都在关注它,前景不错。Apple照例没有表态。如果你现在就想试一试,最好用Chrome Canary

Paul Lewis写了一个简单的requestIdleCallback“垫片脚本”,实现了上述API的行为,但它不是一个可以模拟浏览器空闲检测行为的“腻子脚本”。他使用的是类似前面例子中的setTimeout。不过,如果你不想依赖对象检测也不想写分支代码,用这个脚本还是不错的。

虽然今天的浏览器对requestIdleCallback的支持有限,但这个API的确非常有助于提升网页性能。你对此有什么想法吗?欢迎留言。

英文原文:http://www.sitepoint.com/how-to-schedule-background-tasks-in-javascript/

阅读全文 »

08月18, 2016

【译】10倍提升应用性能的10个建议

原文:http://www.zcfy.cc/article/22

提升Web应用的性能从未像今天这样刻不容缓。在线经济活动的比例日益提高,就连发展中国家和地区的经济活动都已经有5%以上在线进行了(相关数据请参考本文后面的资源)。在这个超级链接、随时在线的现代世界,用户的期望也远非昔日可比。如果你的网站不能马上响应,你的应用不能立即运行,用户转身就会投奔你的竞争对手。

亚马逊大约10年前的一项研究表明,页面加载时间减少1/10秒,能够使其营收增长1%。另一项近期的调查也显示,一多半受访站点所有者提到因为自己应用的性能不佳导致了收入减少或者用户流失。

一个网站到底多快才行?页面加载每花1秒钟,就有大约4%的用户走掉。排名最靠前的电商站点的首次交互时间为1至3秒,这个区间的转换率最高。显而易见,Web应用性能的重要性与日俱增。

提升性能其实不难,难的是怎么看到结果。本文给出能够提升大约10倍网站性能的10个建议供大家参考。如此全面地涵盖各种性能优化技术,这还是头一回,但这些建议可能需要NGINX的一点支持。除了性能,这些建议也会涉及提升安全性。

建议一:使用反向代理服务器让应用更快更安全

如果你的Web应用只跑在一台机器上,那要提升其性能非常简单:换一台更快的,多配几个处理器,多加几条内存,磁盘阵列也要高速的。换了以后,这台机器上跑的WordPress服务器、Node.js或Java应用速度都会加快。(要是应用还会访问另一台数据库服务器,那也简单:找两台更快的机器,用更快的网络连起来就行了。)

麻烦在于,机器速度并不是问题。很多时候Web应用慢,是因为要在各种任务之间切换,一会儿要处理数千个连接上的用户请求,一会儿要向磁盘读写文件,一会儿又要运行应用的代码,一会儿又要去干别的。应用服务器因此可能出现各种状况,耗尽内存、交换文件,或者让很多请求等待一个硬盘I/O之类的任务。

除了升级硬件,其实你还可以选择另外一种完全不同的方法:加一台反向代理服务器,分担上述一些任务。反向代理服务器位于运行应用的机器之前,负责处理来自外网的请求。反向代理服务器直接连到互联网,它与应用服务器通信使用的是快速的内部网络。

反向代理服务器可以让应用服务器专注于构建页面,然后交给反向代理向外网发送,而不必理会用户与应用的交互。由于不必等待客户端的响应,应用服务器的运行速度能达到接近最优的水平。

增加反向代理服务器同时也可以为Web服务器增添灵活性。比如,假设执行某种任务的服务器过载了,那随时可以再增加一台同类服务器;而如果这台服务器挂了,替换它也很容易。

鉴于这种灵活性,反向代理服务器往往也是其他性能优化手段的先决条件,比如:

  • 负载均衡(参见“建议二”),反向代理服务器上运行负载均衡服务,把流量平均分配给几台应用服务器。有了负载均衡,添加应用服务器根本不需要修改应用。
  • 缓存静态文件(参见“建议三”),图片或代码之类的可以直接请求的文件,都可以保存在反向代理服务器中,以便直接发给客户端。这样不仅可以更快地响应请求,还能减轻应用服务器的负担,加快其运行速度。
  • 保证站点安全,可以配置反向代理服务器提升其安全级别,通过它监控来快速识别和响应攻击,从而保存应用服务器安全。

NGINX专门为使用反向代理服务器做了设计,使其天然支持上述优化。由于使用事件驱动的处理机制,NGINX比传统服务器效率更高。NGINX Plus则增加了更高端的反向代理功能,如应用体检、特有的请求路由、高级缓存和售后支持。

NGINX Worker Process helps increase application performance

传统服务器与NGINX Worker的比较

建议二:增加负载均衡服务器

增加负载均衡服务器相对简单,但却能显著提升站点性能和安全性。通过它把流量分配给多个服务器,就可以不必升级Web服务器了。就算应用本身写得不太好,或者难以扩展,负载均衡都可以在不做其他改变的情况下提升用户体验。

负载均衡服务器首先是一个反向代理服务器(参见“建议一”),负责把来自互联网的请求转发给其他服务器。这里关键在于负载均衡服务器可以支持两台以上的应用服务器,使用一种选择算法在不同的服务器间分配请求。最简单的负载均衡算法是循环调度,即把新请求依次转发给可用服务器中的下一台服务器。其他算法还有把请求发给活动连接最少的服务器。NGINX Plus支持一种功能,就是把用户会话保持在同一台服务器上,叫做会话保持。

负载均衡服务器可以避免一台服务器过载而其他服务器过闲,从而极大提升性能。同时,有了它还可以让Web服务器扩容更简单,因为可以选用比较便宜的服务器,同时保证物尽其用。

可以通过负载均衡调度的协议包括HTTP、HTTPS、SPDY、HTTP/2、WebSocket、FastCGI、SCGI、uwsgi、memcached,以及其他一些应用形式,包括基于TCP的应用和其他第四层的协议。为此,首先要分析Web应用,看性能短板在哪里,然后再确定使用哪一个。

同一台服务器或用于负载均衡的服务器也可以承担其他任务,比如SSL终止、视客户端不同支持HTTP/1/x或HTTP/2、缓存静态文件。

NGINX经常被用来做负载均衡,更多信息请参考我们以前发的介绍性文章有关配置的文章电子书和相关的在线视频,当然还有文档。我们的商业版本NGINX Plus支持更多的负载均衡功能,如基于服务器响应时间路由负载和支持微软NTLM协议的负载均衡。

建议三:缓存静态及动态内容

缓存能提升Web应用性能,因为可以更快地把内容交付给客户端。缓存的策略包括预处理内容、在较快的设备上存储内容、把内容保存在靠近客户端的地方,以及同时运用这些策略。

缓存有两种。

  • 静态内容缓存,不常变化的文件,如图片(JPEG、PNG)和代码(CSS、JavaScript),可以保存在边缘服务器中,以便快速从内容或磁盘中获取。
  • 动态内容缓存,很多Web应用会为每个页面请求生成全新的HTML,把生成的每个HTML都缓存一小段时间,可能显著减少需要生成的页面总数,同时又可以保证交付的内容足够新鲜。

假设一个页面每秒被查看10次,而你缓存它1秒,那么90%针对这个页面的请求都将来自在缓存。如果你单独缓存静态内容,那么即使全新生成的页面,很可能大部分都来自缓存的内容。

缓存Web应用生成内容的技术主要分三种。

  • 把内容放到离用户近的地方。离用户近,传输时间少。
  • 把内容放到较快的机器上。机器快,检索速度快。
  • 把内容从过度使用的机器中拿走。有时候机器会比在专注执行特定任务时慢很多,那是因为太多任务让它们分心。这时候把内容拿到其他机器上,不仅对缓存的内容有好处,对非缓存的内容同样有利,因为托管它们的主机的负担减轻了。

Web应用的缓存可以在Web应用服务器内部或外部实现。首先,考虑缓存动态内容,以减轻应用服务器的负载。其次,缓存用于静态内容(包括那些动态生成内容的临时副本),进一步减轻应用服务器的负担。然后,考虑把缓存转移到其他更快或更靠近用户的机器,给应用服务器减负,缩短传输时间。

用好缓存能显著加快应用的响应速度。对很多网页来说,大图片之类的静态数据,往往占据一半以上的内容。不用缓存,查询和传输这类数据可能会花好几秒钟,而用缓存,则可能只要花几分之一秒。

可以举一个例子来说明怎么使用缓存,NGINX和NGINX Plus通过两个指令来设置缓存proxy_cache_pathproxy_cache指定缓存的位置和大小、最长缓存时间以及其他参数。使用第三个(也是很受欢迎的)指令proxy_cache_use_stale,甚至可以告诉缓存在本来应该提供新鲜内容的服务器太忙或宕机时,提供原来的旧文件,对客户端来说,拿到内容总比拿不到强。从用户角度看,这样也可以树立你的站点或应用非常稳定的形象。

NGINX Plus支持高级缓存功能,包括缓存净化(caching purging)和通过控制板以可视化的形式展示缓存状态,实现实时监控。

要了解NGINX中关于缓存的更多信息,可以看看参考文档和NGINX Plus Admin Guide中的NGINX Content Caching

注意: 缓存涉及开发、决策和运维,完善的缓存策略,比如本文提到的这些,能够体现从DevOps角度考虑的价值。也说是说,开发人员、架构师、运维人员此时携手,共同保障一个网站的功能、响应时间、安全和业务目标。

建议四:压缩数据

压缩同样能极大提升性能。图片、视频、音乐等文件都有非常成熟和高效的压缩标准(JPEG和PNG、MPEG-4、MP3),任何一个标准都可以把文件大小缩小一个数量级甚至更多。

文本文件,包括HTML(纯文本和HTML标签)、CSS和JavaScript代码,经常在不压缩的情况下传输。压缩这些数据对提升Web应用的感知性能有时候特别明显,尤其是移动用户的网络很慢又不稳定的情况下。

因为文本数据通过对于页面交互能够起到必要的支援作用,而多媒体数据则更多是锦上添花的作用。聪明的内容压缩可以把HTML、JavaScript、CSS等文本内容的缩小30%以上,因此能够相应地减少加载时间。

如果你使用SSL,压缩又可以减少必须经过SSL编码的数据量,从而补偿了压缩这些数据的CPU时间。

压缩数据的方法非常多。比如,建议六中关于HTTP/2的部分就描述了一个新颖的压缩思路,特别适合首部数据压缩。还有一个关于文本压缩的例子,就是可以在NGINX中开启GZIP压缩。预压缩文本数据之后,可以使用gzip_static指令直接发送.gz文件。

建议五:优化SSL/TLS

越来越多的网站在使用Secure Sockets Layer(SSL)及后来的Transport Layer Security(TLS)协议。SSL/TLS通过加密从源服务器发送给用户的数据来提升网站安全性。Google会提升使用SSL/TLS的网站的搜索引擎排名,将有力地推动这一进程。

尽管采用率越来越高,但SSL/TLS造成的性能损失也困扰着很多网站。SSL/TLS拖慢网站的原因有两个。

  1. 每次打开新连接的初次握手都必须创建加密密钥,而浏览器使用HTTP/1.x对每个服务器建立多个连接的方式进一步加剧了这个问题。
  2. 服务器端加密数据和客户端解密数据的操作同样也是开销。

为了鼓励人们使用SSL/TLS,HTTP/2和SPDY(参见建议六)的作者将这两个协议设计为只让浏览器针对一次会话建立一个连接。这样就把SSL导致性能降低的两个主要原因之一消灭掉了。然而,说到优化SSL/TLS性能,还是有很多事情可做。

优化SSL/TLS的方法因Web服务器而异。以NGINX为例,NGINX使用OpenSSL,运行于普通机器上,能够提供接近定制机器的性能。NGINX SSL performance详细介绍了如何将SSL/TLS加密和解密的开销降至最低。

此外,这里还有一篇文章,介绍了很多种提升SSL/TLS性能的方法。简单总结一下,涉及的技术主要有如下几种。

  • 会话缓存。使用ssl_session_cache指令开启缓存,缓存每次SSL/STL连接时用到的参数。
  • 会话票或ID。把特定SSL/TLS会话的信息保存为一个会话票或ID,以便连接重用,而不必重新握手。
  • OCSP封套。通过缓存SSL/TLS证书信息减少握手时间。

NGINX和NGINX Plus都可以来终止SSL/TLS,即处理客户端信息的加密和解密,同时与其他服务器保持明文通信。在NGINX或NGINX Plus中设置处理SSL/TLS终止可以采取这几个步骤。而对于在接受TCP连接的服务器上使用NGINX Plus而言,可以参考这里的设置步骤

建议六:实现HTTP/2或SPDY

已经使用SSL/TLS的站点,如果再使用HTTP/2或SPDY则很可能提升性能,因为一个连接只要一次握手。尚未使用SSL/TLS、HTTP/2和SPDY的站点切换到SSL/TLS(通常会降低性能),从响应速度方面看,可能是一次倒退。

谷歌2012年开始SPDY项目,致力于在HTTP/1.x之上实现更快的速度。HTTP/2则是IETF最近批准的基于SPDY的标准。SPDY得到了广泛支持,但很快就将被HTTP/2取代。

SPDY和HTTP/2的关键在于只用一个连接,而非多个连接。这一个连接是多路复用的,因此可以同时承载多个请求和响应。

只维持一个连接,可以省掉多个连接所需的设置和管理消耗。而且一个连接对SSL特别重要,因为可以将SSL/TLS建立安全连接所需的握手时间降至最少。

SPDY协议要求使用SSL/TLS,HTTP/2并没有正式要求,但目前所有支持HTTP/2的浏览器都只会在启用SSL/TLS的情况下才会使用它。换句话说,支持HTTP/2的浏览器只有在网站使用SSL且服务器接受HTTP/2流量的情况下才会使用HTTP/2。否则,浏览器会基于HTTP/1.x通信。

实现了SPDY或HTTP/2之后,域名分片、资源合并、图片精灵等之前针对HTTP的性能优化措施就用不着了。因此也可以简化代码和部署。关于HTTP/2会带来哪些变化,可以参考我们的这个白皮书

NGINX Supports SPDY and HTTP/2 for increased web application performance

NGINX很早就开始支持SPDY,而且今天使用SPDY的大多数站点都在运行NGINX。NGINX同样率先支持了HTTP/2,2015年9月,NGINX开源和NGINX Plus开始支持 HTTP/2。

随着时间推移,NGINX希望大多数站点启用SSL并迁移到HTTP/2。这样不仅可以让网站更安全,而且随着新的优化技术不断涌现,也可以通过简单的代码实现更高的性能。

建议七:升级软件

提升应用性能的一个简单的方法,就是根据可靠性及性能选择软件。此外,高质量组件的开发者更可能不断提升性能和修复问题,因此使用最新的稳定版本是划算。新发布的版本会得到开发者和用户更多的关注,同时也会利用新的编译器优化技术,包括针对新硬件的调优。

相对旧版本,新发布的稳定版本明显性能更高。坚持升级,也可以保证在调优、问题修复和安全警报方面与时俱进。

不升级软件也会妨碍利用新能力。比如,HTTP/2目前要求OpenSSL 1.0.1。从2016年下半年开始,HTTP/2会要求OpenSSL 1.0.2,该版本发布于2015年1月。

NGINX用户可以从NGINX开源软件的最新版本NGINX Plus开始,它们支持套接字共享、线程池(参见下文),而且都会持续优化性能。因此,检查一下自己的软件,尽量把它们升级到最新的版本。

建议八:调优Linux

Linux是今天大多数Web服务器的底层操作系统,作为一切基础设施的基础,Linux对提升性能至关重要。默认情况下,很多Linux系统都比较保守,仅以桌面办公为需求,以占用少量资源为调优目标。对于Web应用而言,为达到性能最佳,肯定需要重新调优。

Linux优化因Web服务器而异。以NGINX为例,可以从以下几方面考虑。

  • 存量队列。如果发现有一些连接得不到处理,可以增大net.core.somaxconn,即等待NGINX处理的最大连接数。如果这个连接数限制过小,应该可以看到错误消息,可以逐步提高这个值,直到错误消息不再出现。
  • 文件描述符。NGINX对每个连接最多使用两个文件描述符。如果系统服务于很多连接,可能需要增大sys.fs.file_max这个对描述符的系统级限制,以及nofile这个用户文件描述符限制,以支持增大后的负载。
  • 临时端口。在作为代理使用时,NGINX会为每个上游服务器创建临时端口。可以设置net.ipv4.ip_local_port_range,增大端口值的范围,以增加可用的端口量。此外,还可以减小net.ipv4.tcp_fin_timeout的值,它控制非活动端口释放重用的等待时间,加快周转。

对NGINX而言,请参考NGINX性能调优指南,了解如何不费吹灰之力将你的Linux系统优化为能够支持更大的吞吐量。

建议九:调优Web服务器

无论使用什么Web服务器,都需要针对应用对其调优。以下建议适用于任何Web服务器,但会给出只有NGINX的设置说明。

  • 访问日志。不要每个请求的日志都马上写到磁盘,可以在内存里做个缓存,然后批量定入。对NGINX而言,将buffer=_size_参数添加到access_log指令,等内存缓冲区写满后再把日志写到磁盘。如果你添加了**flush=_time_**参数,那么缓冲区的内容也会按照指定时间写入磁盘。
  • 缓冲。缓冲用于在内存里保存部分响应,直到缓冲区被填满,可以实现对客户端更有效的响应。无法写入内存的响应会被写到磁盘,从而降低性能。在NGINX的缓冲启用时,可以使用proxy_buffer_sizeproxy_buffers指令来管理它。
  • 客户端活动连接。活动连接可以减少时间消耗,特别是在使用SSL/TLS的情况下。对NGINX而言,可以针对客户端提高keepalive_requests的数值,默认值为100;也可以增大keepalive_timeout的值,让活动连接持续时间更长,从而让后续请求得到更快响应。
  • 上游活动连接。上游连接,即连接到应用服务器、数据库服务器的连接,同样可以从活动连接的设置中获得好处。对上游连接来说,可以增加活动连接,也就是每个工作进程可用的空闲活动连接的数量。这样可以增进连接重用,减少重开连接。关于活动连接的更多信息,请参考这篇博客
  • 限制。限制客户端使用的资源可以提升性能和安全性。对NGINX而言,limit_connlimit_conn_zone指令限制指定源的连接数,而limit_rate限制带宽。这些设置可以防止合法用户“侵吞”资源,同时也有助于防止攻击。limit_reqlimit_req_zone指令限制客户端请求。对于到上游服务器的连接,可以在上游配置区的服务器指令中使用max_conns参数,它限制对上游服务器的连接,防止过载。相关的队列指令会创建一个队列,在max_conns限制到达后将指定的请求数保存指定的时间。
  • 工作进程。工作进程负责处理请求。NGINX采用基于事件的模型和OS相关的机制有效地在工作进程间分配请求。建议将worker_processes的值设置为每个CPU一个工作进程。如果需要,大多数系统都支持提高worker_connections的值(默认为512)。可以通过试验找到最适合你系统的这个值。
  • 套接字分片。通常,一个套接字监听器向所有工作进程分发新连接。套按字分片则为每个工作进程都创建一个套接字监听器,由内核在套接字监听器可用时为其指定连接。这样可以减少锁争用,提升多核系统上的性能。要启用套接字分片,在listen指令中包含reuseport参数。
  • 线程池。一个费时的操作会阻塞任何计算机进程。对Web服务器软件来说,磁盘访问可能阻碍很多较快的操作,比如内存中的计算和复制。在使用线程池的情况下,慢操作会被指定给一组独立的任务,而主处理循环会继续运行较快的操作。磁盘操作完成后,结果会返回到主处理循环。在NGINX中,read()系统调用和sendfile()被转载到了线程池

Thread pools help increase application performance by assigning a slow operation to a separate set of tasks

提示 修改任何操作系统及周边设备的设置时,每次只修改一项,然后测试性能。如果该项修改导致了问题,或者并未提升性能,再改回去。

关于调优NGINX的更多内容,请参考这篇博客

建议十:监控实时动态以发现问题和瓶颈

保存应用高性能的关键是实时监控应用性能。必须实时监控特定设备及相应Web基础设施中应用的动态。

监控站点活动多数情况下是被动的,它只告诉你发生了什么,至于如何发现和解决问题,则是你自己的事情。

监控可以捕获以下几种问题:

  1. 服务器停机
  2. 服务器不稳,漏处理连接
  3. 服务器出现大面积缓存失效
  4. 服务器发送的内容不对

New Relic或Dynatrace等全局性的性能监控工具,可以帮我们监控远程加载页面的时间,而NGINX则可以帮你监控应用交付这一端。应用的性能数据可以告诉你优化手段什么时候真正给用户带去了不同的体验,以及什么时候需要扩容以满足越来越多的流量。

为了帮助用户尽快发现问题,NGINX Plus增加了应用程序体检功能,会报告经常重复出现的问题。NGINX Plus还具备session draining特性,会在已有任务完成前阻止新连接,以及慢启动容量,从而让恢复的服务器在负载均衡集群中达到应有的速度。使用得当的情况下,健康体检会在问题显著影响用户体验之前帮你定位问题,而session draining和慢启动则让你替换服务器时不影响感知的性能和在线时间。这张图展示了NGINX Plus内置的实时活动监控的控制板,涵盖了服务器、TCP连接和缓存。

Use real-time application performance monitoring tools to identify and resolve issues quickly

结论:10倍性能提升

性能提升因Web应用不同会有巨大差异。实际的提升取决于预算、时间,以及现有实现的与理想性能的差距。那么怎么让你的应用获得10倍的性能提升呢?

为了帮大家理解每项优化建议的潜能,下面再针对之前的建议给出一些实施方针,希望大家各取所需。

  • 反向代理服务器及负载均衡。没有负载均衡或池负载均衡,可能导致极低的性能。添加一个反向代理服务器,比如NGINX,可以减少Web应用在内存和磁盘之间的往返。负载均衡可以把任务从过载的服务器转移到空闲的服务器,也便于扩展。这些改变能极大地提升性能,与原有的部署方式最差的时候相比,10倍性能提升是很轻松的事,即使不到10倍那也在总体上有了质的飞跃。
  • 缓存动态和静态内容。如果你的Web服务器同时又充当了应用服务器,那么通过缓存动态内容就可以达到高峰期10倍的性能提升。缓存静态内容也可以有几倍的性能提升。
  • 压缩数据。使用JPEG、PNG、MPEG-4以及MP3等压缩格式能显著提升性能。如果这些手段都用上了,那么压缩的文本数据(代码及HTML)可以将初始页面加载时间提升两倍。
  • 优化SSL/TLS。安全握手对性能有很大影响,因此对其进行优化可以让初次响应加快两倍,对于文本内容较多的网站尤其如此。优化SSL/TLS下的媒体文件带来的性能提升很小。
  • 实施HTTP/2和SPDY。在使用SSL/TLS的情况下,这两个协议有可能提升网站的整体性能。
  • 调优Linux和Web服务器。使用优化的缓冲策略、使用活动连接,将耗时的任务转载至独立的线程池,可以显著提升性能。比如线程池可以将磁盘操作密集性任务的性能提升至少一个数量级

希望大家自己多尝试以上技术,也希望大家分享自己在性能改进方面的心得。如果有好的想法和实现方案,欢迎留言。

参考资源与网络数据

英文原文:https://www.nginx.com/blog/10-tips-for-10x-application-performance/

阅读全文 »

06月24, 2016

【译】RethinkDB实例大全JavaScript版

原文:http://www.zcfy.cc/article/484

目录

基本命令

创建库

创建数据库,可以像下面这样使用dbCreate命令:

r.dbCreate("blog").run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

还可以通过Web界面创建数据库。打开http://HOST:8080,点Tables,然后再点Add Database按钮。

重命名数据库

最简单的方式是使用config 命令访问db_config 系统表,然后使用update命令。

r.db("old_db_name").config().update({name: "new_db_name"}).run(conn,
    function(err, result) {
        if (err) throw err;
        console.log(result);
    }
);

创建表

先用db命令选择要在哪个数据库中创建表,然后再像下面这样用tableCreate创建表:

r.db("blog").tableCreate("posts").run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

如果是在连接的默认数据库(除非在connect指定,否则默认数据库为test)中创建表,可以省略db命令。

还可以通过Web界面创建表。打开http://HOST:8080,点Tables,然后再点Add Table按钮。

插入文档

在相应的表上调用insert命令可以插入文档:

r.table("user").insert({
    name: "Michel",
    age: 26
}).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

insert传入文档数组可以一次插入多个文档:

r.table("user").insert([
    {
        name: "Michel",
        age: 26
    },
    {
        name: "Slava",
        age: 30
    }
]).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

删除文档

先选择要删除的文档,然后使用delete命令。下面的例子会删除所有作者为Michel的文章:

r.table("posts").filter(r.row("author").eq("Michel")).delete().run(conn,
    function(err, result) {
        if (err) throw err;
        console.log(result);
    }
);

或者,可以只删除一个用户:

r.table("posts").get("7644aaf2-9928-4231-aa68-4e65e31bf219").delete().run(conn,
    function(err, result) {
        if (err) throw err;
        console.log(result);
    }
);

这样可以删除一个表中的所有文档:

r.table("posts").delete().run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

取得文档

要取得表中的所有文档,使用table命令:

r.table("posts").run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

table命令返回一个游标,可以使用nextk或each命令迭代结果,或者使用toArray将结果转换为数组。

要通过ID取得特定文档,使用get

r.table("posts").get(1).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

要通过特定字段值取得文档,使用filter

r.table("posts").filter({author: "Michel"}).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

要通过特定索引(index)取得文档,使用getAll

r.table("posts").getAll("review", {index: "category"}).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

(要了解更复杂的筛选操作,请继续往下看。)

筛选

根据多个字段筛选

假设你想查找作者名为Michel,类别为Geek的所有文章,可以这样做:

r.table("posts").filter({
    author: "Michel",
    category: "Geek",
}).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

或者,可以使用and命令先写一个断言,然后把它传给filter

r.table("posts").filter(
    r.row("author").eq("Michel").and(r.row("category").eq("Geek"))
).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

还可以使用前缀写法(把所有参数传给r.and):

r.table("posts").filter(
    r.and(r.row("author").eq("Michel"), r.row("category").eq("Geek"))
).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

类似地,使用r.or可以根据多个条件中的一个来筛选。

根据数组中是否有某个值筛选

假设有一个users表,其中保存下列形式的文档:

{
    name: "William Adama"
    emails: ["bill@bsg.com", "william@bsg.com"],
    ship: "Galactica"
}

要是我想找到所有电子邮件是user@email.com的用户,可以这样写:

r.table("user").filter(r.row("emails").contains("user@email.com"))
 .run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

想找到所有“银河号”(Galactica)和“天马号”(Pegasus)上的用户,可以这样写:

r.table("user").filter(function (user) {
    r(["Galactica", "Pegasus"]).contains(user("ship"))
}).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

根据嵌套字段筛选

在JavaScript中可以使用()取得字段的值,可以连环使用它取得嵌套字段的值。

假设users表中的文档是这样的:

{
    name: "William Adama"
    contact: {
        phone: "555-5555"
        email: "bill@bsg.com"
    }
}

这样就可以根据嵌套的email字段实现筛选:

r.table("user").filter(
    r.row("contact")("email").eq("user@email.com")
).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

对于很多ReQL命令,也可以使用JSON风格的嵌套语法,更灵活。请参考“访问嵌套的字段”。

通过主键迅速取得多个文档

要取得主键为123的所有文章,可以使用getAll命令:

r.table("posts").getAll(1, 2, 3).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

通过二级索引快速取得多个文档

假设表posts通过author_id字段将文章与作者联系起来。如果已经在author_id上创建了二级索引,而且想取得所有author_id123的文章,可以像这样使用getAll命令:

r.table("posts").getAll(1, 2, 3, {index: "author_id"})
 .run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

参考在RethindDB中创建二级索引

将流(游标)中的所有对象作为数组返回

如果命令返回的是流,而你想一次取得所有结果,不想通过游标迭代,可以使用toArray命令将结果保存到数组中。

r.table("posts").run(conn, function(err, cursor) {
    if (err) throw err;
    cursor.toArray(function(result) {
        console.log(result);
    });
});

参见数据类型文档了解关于流的更多信息。

取得文档的特定字段

取得文档中的个别字段可以使用pluck命令。比如,从users表中取得每个文档的nameage

r.table("users").pluck("name", "age").run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

这相当于SQL中的SELECT name, age FROM users

pluck命令也支持选择嵌套的字段。比如,要取得以下文档中的phoneemail字段:

{
    name: "William Adama"
    contact: {
        phone: "555-5555"
        email: "bill@bsg.com"
    }
}

可以这样写:

r.table("users").pluck(
    {contact: {phone: true, email: true}}
).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

根据日期范围筛选

要检索2012年1月1日到2013年1月1日之间的文章,这样写:

r.table("posts").filter(function(post) {
    return post("date").during(r.time(2012, 1, 1, "Z"), r.time(2013, 1, 1, "Z"));
}).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

也可以手工比较日期:

r.table("posts").filter(function(post) {
    return post("date").ge(r.time(2012, 1, 1, "Z")).and(
        post("date").lt(r.time(2013, 1, 1, "Z")));
}).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

使用正则筛选

要检索姓以Ma开头的所有用户,可以使用r.match

// 会返回Martin、Martinez、Marshall等
r.table("users").filter(function(user) {
    return user("lastName").match("^Ma");
}).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

要检索姓以s结尾的所有用户,可以这样:

// 返回Williams、Jones、Davis等
r.table("users").filter(function(user) {
    return user("lastName").match("s$");
}).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

检索姓中包含ll的所有用户:

// 返回Williams、Miller、Allen等
r.table("users").filter(function(user) {
    return user("lastName").match("ll");
}).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

不区分大小写的筛选

检索所有名为Wiliam(不区分大小写)的用户。

// 返回william、William、WILLIAM、wiLLiam等
r.table("users").filter(function(user) {
    return user("name").match("(?i)^william$");
}).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

同时执行多次聚合

执行返回多个不同字段聚合的查询,属于典型的map-reduce

假设有一个数据集是热门电影,按用户票数排名。你希望取得总票数和前25部电影的平均年份,即votes列的sum()year列的avg(),按rank列排序取1~25位。

为此,通过map把前25部电影映射到一个新的结果集,并添加count列,然后将映射结果集的每一行reduce为每个字段(votesyearcount)的总和。再用do返回包含总票数和平均年份(用年数和除以数量)的结果。

r.table("movies").orderBy("rank").limit(25).map(function (doc) {
    return { totalVotes: doc("votes"), totalYear: doc("year"), count: 1 };
}).reduce(function (left, right) {
    return {
        totalVotes: left("totalVotes").add(right("totalVotes")),
        totalYear: left("totalYear").add(right("totalYear")),
        count: left("count").add(right("count"))
    };
}).do(function (res) {
    return {
        totalVotes: res("totalVotes"),
        averageYear: res("totalYear").div(res("count"))
    };
}).run(conn, callback);

我们还在研发聚合多个字段的更简单的语法,会使用group命令。要了解进度,请关注issue 1725

操作文档

添加/重写文档中的某个字段

要添加/重写某个字段,使用update命令。例如,要给表posts中的所有文档添加值为Michel的author字段,可以这样:

r.table("posts").update({ author: "Michel" }).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

删除文档中的某个字段

update可以重写字段,但不能删除字段。删除字段,要用replace命令以作为参数传入的新文档替换旧文档。以下代码会删除ID为1的文章中的author字段:

r.table("posts").get("1").replace(r.row.without("author"))
 .run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

根据条件原子性更新文档

updatereplace命令对单个文档都是原子性的。假设要在countable字段为true的情况下原子性地更新页面浏览量,并在一个查询中取得老的和新的结果,就可以这样:

r.table("pages").update(function(page) {
    return r.branch(page("countable").eq(true),  // 如果页面可以计数
        { views: page("views").add(1) },         // 浏览量加1
        {}                                       // 否则什么也不做
    );
}, {returnChanges: true}).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

有条件的插入或替换

使用与之前的例子类似的技术,可以用branchreplace维护文档的updated_atcreated_at字段,根据指定ID的文档是否存在决定是插入新文档,还是更新已有文档。

r.table("users").get(id).replace(function (doc) {
    return r.branch(
        doc.eq(null),
        r.expr(userObject).merge({id: id, created_at: r.now()}),
        doc.merge(userObject).merge({updated_at: r.now()})
    )
}).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

将时间戳和JSON日期字符串保存为Time类型

可以用epochTimeISO8601命令将Unix时间戳(以秒为单位)和JSON日期时间字符串(ISO 8601格式)转换为ReQL的时间类型。ReQL驱动程序也能将JavaScript Date对象转换为ReQL时间。.

var theDate = new Date();
var timestamp = theDate.getTime();
var JSONDate = theDate.toJSON();
r.table("dates").insert({
    from_object: theDate,
    from_epoch: r.epochTime(timestamp/1000.0),
    from_iso: r.ISO8601(JSONDate)
}).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

toEpochTimetoISO8601用于反向转换。

递增字段的值

可以在服务器端每次递增文档中一个字段(如计数器)的值。

r.table("aggregated").get(id).update(
    { count: r.row("count").default(0).add(1) }
).run(conn, callback);

这里用default确保在文档中没有count字段时能正确相加,而不会抛出错误。

分页

限制返回的文档数

使用limit命令查询可以限制返回的文档数:

r.table("posts").orderBy("date").limit(10).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

实现分页

RethinkDB提供了多种分页方式。最简单的是使用skiplimit(类似于SQL的OFFSETLIMIT),但效率也最差。较好的方式是使用slice,而更好的方式是使用between和一个二级索引。

slice命令返回给定范围内(不包含结束值)的结果,替换skip/limit也很方便:起始值就是要取得的第一项,结束值是第一项加限制的数量。以下示例用slice取得第11~20篇文章:

r.table("posts").orderBy("date").slice(11,21).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

如果有二级索引,可以使用between命令与orderBylimit。这是最有效的分页方式,但要求查询的值必须在二级索引的字段中。

假设每页有25个用户,那么前25个用户使用limit效率更高:

r.table("users").orderBy({index: "name"}).limit(25).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

后续每一页,从前一页的姓开始。

r.table("users").between(lastName, r.maxval, {leftBound: "open", index: "name"})
 .orderBy({index: "name"}).limit(25).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

将前一页保存的lastName传入between作为起始索引。对于结束索引,传入null表示从起始索引到表末尾。leftBound参数告诉between不包含第一条记录,因为它已经在前一页中返回了。

变换

计算表格中的文档数

使用count命令:

r.table("posts").count().run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

计算某个字段的平均值

使用avg命令:

r.table("posts").avg("num_comments").run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

通过子查询返回更多字段

假设要取得表post中的所有文章,同时还要返回一个额外字段comments,一个保存在comments表中的相关评论的数组。可以用子查询:

r.table("posts").merge(function(post) {
    return {
        comments: r.table("comments").filter(function(comment) {
            return comment("id_post").eq(post("id"))
        }).coerceTo("ARRAY")
    }
}).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

旋转

假设表marks中保存着学生每一科的分数:

[
    {
        "name": "William Adama",
        "mark": 90,
        "id": 1,
        "course": "English"
    },
    {
        "name": "William Adama",
        "mark": 70,
        "id": 2,
        "course": "Mathematics"
    },
    {
        "name": "Laura Roslin",
        "mark": 80,
        "id": 3,
        "course": "English"
    },
    {
        "name": "Laura Roslin",
        "mark": 80,
        "id": 4,
        "course": "Mathematics"
    }
]

你希望取得的结果为如下格式:

[
    {
        "name": "Laura Roslin",
        "Mathematics": 80,
        "English": 80
    },
    {
        "name": "William Adama",
        "Mathematics": 70,
        "English": 90
    }
]

此时,可以使用groupcoerceTo命令执行旋转操作:

r.db("test").table("marks").group("name").map(function (row) {
    return [row("course"), row("mark")];
}).ungroup().map(function (res) {
    return r.expr({name: res("group")}).merge(res("reduction").coerceTo("object"));
}).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

注意: 我们会增加一种更友好的语法,参见Github issue 838

反旋转

要执行反旋转操作以抵销之前的旋转操作,可以使用concatMapmapkeys命令:

r.table("pivotedMarks").concatMap(function (doc) {
    return doc.without("id", "name").keys().map(function (course) {
        return {
            name: doc("name"),
            course: course,
            mark: doc(course)
        };
    });
}).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

注意: 我们会增加一种更友好的语法,参见Github issue 838

取得文档时重命名字段

假设从表users中取得文档时要把字段id重命名为idUser。在子查询中,可以使用merge添加一个新字段,并给它赋予一个已有字段的值,然后用without删除原来的字段:

r.table("users").map(
    r.row.merge({ idUser: r.row("id") }).without("id")
).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

通过日期/时间分组查询结果

ReQL有从日期和时间里提取某一部分的命令,包括yearmonthdaydayOfWeek,等等。可以使用这些命令与group按照不同的时间长度来分组。假设有一个收入表,想按照年和月来分组:

r.table("invoices")
    .group([r.row("date").year(), r.row("date").month()])
    .ungroup()
    .merge({invoices: r.row("reduction"), month: r.row("group")})
    .without("reduction", "group")
    .orderBy("month")
.run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

(这里也使用了前面提到的重命名字段的技术,把reductiongroup换成了更有用的invoicesmonth。)可以在分组时使用任意ReQL日期/时间的区间组合,或者将日期/时间作为原生对象使用。

目前,ReQL默认限制数组最多包含10万个元素,group的实现要求分组后的文档总数不超过这个数,因此最多只能处理10笔收入。不过,这个限制可以改,要把选项arrayLimit传给run。(另外,ungroup始终返回数组,未来的版本中可能会有改动。参见#2719。)

可以在要分组的时间区间上使用复合索引

r.table("invoices").indexCreate(
    "byDay", [r.row("date").year(), r.row("date").month(), r.row("date").day()]
).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

然后就可以在group函数中使用该索引。这个查询会返回每天的最高收入。

r.table("invoices")
    .group({index: "byDay"})
    .max("price")
.run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

其他

生成单调增加的主键值

在分布式系统中有效生成单调增加的主键值是个难题。如果插入的文档没有主键,RethinkDB会生成一个随机的UUID。未来,我们还会支持其他自动生成方式。(参见https://github.com/rethinkdb/rethinkdb/issues/117。 但在此期间,建议选择开源的分布式ID生成库(比如twitter snowflake)。

把RethinkDB的响应解析到一个写查询

发送写查询(insertdeleteupdatereplace)的时候,RethinkDB会返回一个这样的摘要对象:

{deleted:0, replaced:0, unchanged:0, errors:0, skipped:0, inserted:1}

其中最重要的字段是errors。一般来说,没有异常抛出且errors为0,写操作就成功了。(RethinkDB会在不能访问表的时候抛出异常,如果能访问表但写的时候出错,它会设置errors字段。由于存在这样的约定,批量写操作才不会因为碰到一个错误就半途而废。)

以下字段始终会存在于这个对象中。

  • inserted – 添加到数据库中的新文档数量。
  • deleted – 从数据库中删除的文档数量。
  • replaced – 修改的文档数量。
  • unchanged – 要不是新值与旧值相同,同样会修改的文档的数量。
  • skipped – 一次写操作中未修改的文档数量,原因是文档不能删除或更新。可能是由于同时有另一个操作已经将文档删除了,或者get操作的键并不存在。
  • errors – 由于某个错误导致未能修改的文档数量。

此外,以下两个字段视情况出现。

  • generated_keys – 如果是插入操作,而所有或部分行没有主键,服务器会为你生成主键,并在这个字段返回一个包含那些键的数组(数组中主键的顺序与插入行的顺序一致)。
  • first_error – 如果errors大于0,那这个字段保存第一条错误消息。这个消息对排错非常重要。(之所以不给出所有错误消息,是因为单纯一处拼写错误就可能在操作大型数据库时导致上百万个错误)。

在ReQL命令中使用动态键

有时候在写入文档时,可能会使用保存在变量中的字段名。为此,可以使用object命令,它接受一系列键值((key, value, key, value ...))并返回一个对象。

r.table("users").get(1).update(r.object(propertyName, value)).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

字段名可以服务器端确定。比如,要更新一个字段,而该字段的名字需要从另一个字段的值中读取:

r.table("users").forEach(function (doc) {
  return r.table("users").get(doc("id")).update(r.object(doc("field"), newValue));
}).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

实际当中,可能会有像前面旋转示例中一样的数据,其中每个文档表示一名学生一科的成绩。

[
    {
        "name": "John",
        "mark": 70,
        "id": 1,
        "course": "Mathematics"
    },
    {
        "name": "John",
        "mark": 90,
        "id": 2,
        "course": "English"
    }
]

但我们想从文档中得到的是“成绩单”:

{
    "Mathematics": 70,
    "English": 90
}

使用object和一次旋转就可以:

r.table("marks").filter({student: "John"}).map(function(mark) {
    return r.object(mark("course"), mark("mark"));
}).reduce(function(left, right) {
    return left.merge(right);
}).run(conn, function(err, result) {
    if (err) throw err;
    console.log(result);
});

将ReQL查询作为字符串返回

为了测试或记日志,可能需要把生成的ReQL查询保存为字符串。(比如ReQL的错误消息中就有。)虽然没有相应的ReQL命令,但只要在查询语句末尾用toString()代替run()就行了:

`r.table("users").filter(r.row("groups").contains("operators")).toString()`

分行写ReQL查询

有时候需要以编程方式组装查询语句,比如先实例化一个查询对象,然后根据情况再调用并添加相应的查询命令,最后再调用执行前面添加的命令。就是运行时根据条件来动态修改查询。理想的写法可能是这样的:

var query = r.table("posts");
if (request.filter !== undefined) {
    query.filter(request.filter);
}
query.orderBy("date");
query.run(conn, callback);

但是不行。因为查询对象并不保存状态。第一条命令之后的命令同样会基于query最初的值(即这里的posts表)来运行。为此,可以把每次命令的运行结果都保存到query变量中:

var query = r.table("posts");
if (request.filter !== undefined) {
    query = query.filter(request.filter);
}
query = query.orderBy("date");
query.run(conn, callback);

将多条变动源消息合成一条

要产生“联结的”变动源消息(changefeed),以便通过一个源监控多个表或查询,可以这样:

`r.table("table1").union(r.table("table2")).changes().run(conn, callback);`

为明确哪个变动属于哪个表,还可以加“标签”:

r.table("table1").merge({table: "table1"})
 .union(r.table("table2").merge({table: "table2"})
 .changes().run(conn, callback);

此外,还可以针对每个查询调用changes,而不必等到最后再调用:

r.table("table1").filter({flag: "blue"}).changes()
 .union(r.table("table2").filter({flag: "red"}).changes())
 .run(conn, callback);

英文原文:https://www.rethinkdb.com/docs/cookbook/javascript/

阅读全文 »

06月17, 2016

【译】SVG图标好用后备难做

原文:http://www.zcfy.cc/article/123

最近,图标字体用得越来越少了,好像有很多不错的理由都建议不再用图标字体,而改用SVG图形。对于《金融时报》(Financial Times)而言,我们大致同意这个观点, 是该考虑一下过渡方案了。

浏览器对SVG的支持非常好,94%以上都完全支持SVG 1.1,如果你不用遮罩,这个比例会提高到96%。

视情况不同,有人可能不准备为剩余的4%准备后备方案。而实际上,很多用户都是依赖图标的。不管你觉得用户体验好不好,很多网站确实在使用图标实现导航、切换菜单、关闭提示或者让用户执行登录/退出等重要操作。

下面就是Facebook、Twitter和Google Drive使用图标的情况。

Examples of Facebook, Twitter and Google Drive icon usage

一般来说,在界面空间有限的时候,用图标可以省掉文字。这时候,用户就更依赖图标了。

在FT(金融时报),我们的图标是由金融时报统一平台和Web开发工具包Origami提供的,这个平台必须适应全公司的各种应用场景,要好用、可靠、稳定。考虑到FT的用户都是一些大型企业和老板1,这就意味着必须给那4%看不到SVG的用户提供后备。

于是,难题来了。

标记

实现SVG图标的方法很多,比如行内元素、图标或背景。说到提供后备,其实结果显而易见。

  1. 使用行内<svg>元素加后备太过复杂。<img>元素依赖的srcset属性没有被IE、Opera Mini以及Android系支持。后备图片会导致每个图片都发送一次请求。✘
  2. 背景图片早在2000年初就用来实现雪碧图,而从图标字体升级不会改动太多代码。✓

过去,基于图标的Web字体实现都是添加一个基类,然后再为必要的符号添加特殊类名。字体可以继承颜色和大小,而基于雪碧图的背景不能继承颜色和大小,必须使用额外的类来指定颜色和大小:

<!-- 当前基于字体的实现 -->
<span class="icon icon--{symbol}"></span>

<!-- 建议未来的实现 -->
<span class="icon icon--{size} icon--{colour} icon--{symbol}"></span>

雪碧图

既然后备图片这么重要,使用SVG片段标识符也不行,那我必须按照2004年的做法,将图标按常规、不相互重叠的形式布局。

我试过的所有SVG雪碧图工具都可以输出一张一种颜色的雪碧图,要么照搬来源,要么重写。我真不想为我们20多种颜色搭配分别生成不同的文件,因此必须改进我们自己的系统。

FT有很多符号以及前面提到的大调色板,如果每个网站的用户都要下载一个巨大的雪碧图,结果却只用到其中几个图标,那对这些用户肯定不公平。我曾经做过一个小型应用的原型,用于只生成用到的符号及颜色的雪碧图。

这个应用从SVG源文件中找到路径,经过优化按定义把结果包含在输出中。每个定义都会计算viewbox,因此它们占的地方一样大。定义没有可见的界面,因此程序会循环一遍请求的颜色,引用相应的路径,再为它们应用相应的填充。

Screenshot of SVG sprite generator output

这个SVG雪碧图生成器抓到每一个SVG源文件,优化并计算一个viewbox,以便它们占的地方一样大。程序循环所有请求的颜色并引用相应的路径,然后进行填充。

目前我们只使用系统来生成静态文件,包括SVG和PNG。不过,今后如果有需求,我们还可以再实现一个类似的工具,为我们实时生成雪碧图。

样式

以下是最初的Sass代码。别急着嗤之以鼻,容我稍后解释。首先定义了图标大小、颜色和符号。

使用Sass写会更清晰地展示计算过程,为了清晰起见,稍有改动。

$icon-sizes: (16, 24, 36, 48);
$icon-colors: ("black", "white", "blue", "red");
$icon-symbols: ( "chronometer", "cog", "hearts", "rocket", "sign", "speech", "user");

.icon {
  position: relative;
  display: inline-block;
  overflow: hidden;

  &:after {
    content: "";
    position: absolute;
    width: percentage(length($icon-colors));
    height: percentage(length($icon-symbols));
    background-image: url("sprite-sheet.svg");
    -webkit-background-size: 100%;
    background-size: 100%;
  }
}

这里没有给.icon元素应用雪碧图作为背景,反倒引入了一个伪元素,觉得奇怪吗?原因是雪碧图的横轴从左到右依次是各种颜色,纵轴从上到下是相应的符号,而background-position属性无法一个符号一个符号地指定坐标。

只使用背景定位,需要为每个一个图标和颜色的可能组合定义样式,对FT的50个符号、20种颜色来说,那就是1000个声明

而使用绝对定位的伪元素不仅所有浏览器都支持,而且可以使用单独的类分别指定leftright属性:.icon--{colour}用于横轴,.icon--{symbol}用于纵轴。

相对来说,background-position-xbackground-position-y属性是有某些浏览器支持,但它们并不属于CSS规范,而且没有得到所有浏览器支持。已经有人建议将这两个属性已放到背景与边框模块4级了。

如果应用背景图片的话,还是需要进行绽放才能让每个图片按照包含元素的大小填充。因为雪碧图由普通的大小一样的正方形组成,颜色和符号数量已知,因此可以计算。只要用100%(元素宽度)乘以颜色/符号的数量即可。不过,不是使用这些值去设置background-size属性,而是用它们去设置伪元素以及雪碧图的大小。

Diagram of icon element and pseudo-element layout

雪碧图由普通的大小一样的正方形组成,颜色和符号数量已知,因此可以计算。

初始的设置就是这样,但光有这些还不行。需要不同的类分别指定大小、颜色和符号。在通过列表定义这些参数后,可以迭代它们分别生成不同的声明:

@each $size in $icon-sizes {
  .icon--#{$size} {
    width: $size + px;
    height: $size + px;
  }
}

@each $color in $icon-colors {
  .icon--#{$color} {
    &:after {
      $color-index: index($icon-colors, $color);
      left: percentage($color-index - 1) * -1;
    }
  }
}

@each $symbol in $icon-symbols {
  .icon--#{$symbol} {
    &:after {
      $symbol-index: index($icon-symbols, $symbol);
      top: percentage($symbol-index - 1) * -1;
    }
  }
}

在支持SVG的浏览器中,这样就行了!看这个示例

现在解决难题……

后备

不支持SVG的浏览器大致可分为两类:老版本IE(< IE9)和Android Froyo、Gingerbread等版本中的浏览器。这些浏览器都需要栅格图作为后备,而且不是指定两张背景图那么简单,还必须阻止它们下载和应用SVG文件。

可以使用不可见的渐变技巧作为SVG支持的代理。不完美(IE9及Android 3中确实支持SVG的浏览器会被排除在外),但还是可以接受的:

.icon:after {
  background-image: url("sprite-sheet.png");
  background-image: -webkit-linear-gradient(transparent, transparent),  
    url("sprite-sheet.svg");
  background-image: linear-gradient(transparent, transparent),
    url("sprite-sheet.svg");
}

解决了老版本Android中的浏览器问题之后,接下来是老版本IE及其他不支持CSS3背景属性的浏览器。

一个方案,就是像颜色一样,为每一种大小都提供一个单独的雪碧图。的确可以通过图片服务预生成并提供雪碧图,但这就意味着会有额外的请求,而且只针对需要它们的浏览器也会导致问题。除非你真的能容忍这些问题,否则这个方案还是不行。

老版本IE中有ActiveX滤镜,其中AlphaImageLoader有一个sizingMethod选项,可以模拟background-size: 100% 100%。遗憾的是滤镜不能应用给伪元素,于是这个奇妙的方案也不能用。

最终解决问题,用的是另一个非标准属性:zoom。因为必须给图标元素一个尺寸,而且雪碧图已经按已知大小生成了,这里的比例是可以计算的。虽然不能给图标元素指定相对大小,但相信这个问题很好解决。

让人生气的是,元素的绝对位置也必须根据雪碧图的原始大小重新计算,因为这些属性也是放大的。

zoom虽然也不属于CSS规范,但它已经得到了相当广泛的支持。我们的后备方案在最新版Chrome中的效果和在IE8中一样。

我们把每个要重写的后备属性都包装在了相应的媒体查询中。媒体查询与CSS3背景选项放在一块完全没有问题。

$icon-intrinsic-size: 50;

@each $size in $icon-sizes {
  .icon--#{$size} {
    width: $size + px;
    height: $size + px;

    &:after {
      zoom: 1 / $icon-intrinsic-size * $size;

      @media all and (min-width: 0) {
        zoom: 1;
      }
    }
  }
}

@each $color in $icon-colors {
  .icon--#{$color} {
    &:after {
      $color-index: index($icon-colors, $color);
      left: $icon-intrinsic-size * ($color-index - 1) * -1 + px;

      @media all and (min-width: 0) {
        left: percentage($color-index - 1) * -1;
      }
    }
  }
}

@each $symbol in $icon-symbols {
  .icon--#{$symbol} {
    &:after {
      $symbol-index: index($icon-symbols, $symbol);
      top: $icon-intrinsic-size * ($symbol-index - 1) * -1 + px;

      @media all and (min-width: 0) {
        top: percentage($symbol-index - 1) * -1;
      }
    }
  }
}

最后的话

想必大多数看到这篇文章的朋友都不会碰上与FT Origami团队面临的同样的问题。如果你不需要非SVG的后备,或者你的图标只需要显示为一种大小,那么实现SVG雪碧图跟实现图标字体一样很简单。但我们由于面临如此严苛的要求……目前已经发布了一个beta版本的实现,希望得到大家尽量多的反馈,我会及时更新。我愿意回答如下一些问题:

  • 提供一张高分辨率的PNG是不是更好?
  • 在一个页面中使用多个SVG图标会不会影响性能?
  • 克服图标字体的不足比实现SVG图标(以及适当后备)还要难吗?

在CodePen上查看源代码


  1. 大公司IT设备的升级是一件很复杂的事,因为要花不少钱,所以流程很慢。这不是用户的问题!

英文原文:http://maketea.co.uk/2015/12/14/svg-icons-are-easy-but-the-fallbacks-arent.html

阅读全文 »

06月13, 2016

【译】不用jQuery实现简单的JavaScript幻灯片

原文:http://www.zcfy.cc/article/411

“我就想不用jQuery实现简单的JavaScript幻灯片”

这里所说的幻灯片,或者叫图片传送带、图片滑块、旋转图等等,是JavaScript学习者必修课之一。

本教程内容如下:

  • 不用jQuery等外部库实现简单的幻灯片
  • 理解UX和无障碍问题,包括是否该用幻灯片
  • 给幻灯片添加控件

不依赖库实现幻灯片的主要好处是页面速度会变快,因为代码少了。而且,随便用到哪里都不用担心加载外部文件的问题。

读者最好懂JavaScript,包括函数、事件、样式过渡。另外推荐大家看看我的这个尽可能只用JavaScript做一些实用的东西都要学什么的路线图

做个简单的幻灯片

HTML代码

HTML需要有个容器,容器里是幻灯片:

<ul id="slides">
    <li class="slide showing">Slide 1</li>
    <li class="slide">Slide 2</li>
    <li class="slide">Slide 3</li>
    <li class="slide">Slide 4</li>
    <li class="slide">Slide 5</li>
</ul>

CSS

CSS要做下面几件事:

  • 确定幻灯片的容器
  • 在容器里将幻灯片堆叠起来
  • 确定幻灯片长啥样以及何时显示或隐藏
  • 实现淡入淡出效果

看CSS前,请注意不要使用与你的网页可能冲突的类或ID。本文使用了简短的名字。

CSS在这儿:

/*
基本样式:
支撑幻灯片
*/

#slides {
    position: relative;
    height: 300px;
    padding: 0px;
    margin: 0px;
    list-style-type: none;
}

.slide {
    position: absolute;
    left: 0px;
    top: 0px;
    width: 100%;
    height: 100%;
    opacity: 0;
    z-index: 1;

    -webkit-transition: opacity 1s;
    -moz-transition: opacity 1s;
    -o-transition: opacity 1s;
    transition: opacity 1s;
}

.showing {
    opacity: 1;
    z-index: 2;
}

再添加一些装饰性的幻灯片样式,以下是示例:

/*
非基本样式:
想要什么样,你自己定
*/

.slide {
    font-size: 40px;
    padding: 40px;
    box-sizing: border-box;
    background: #333;
    color: #fff;
}

.slide:nth-of-type(1) {
    background: red;
}
.slide:nth-of-type(2) {
    background: orange;
}
.slide:nth-of-type(3) {
    background: green;
}
.slide:nth-of-type(4) {
    background: blue;
}
.slide:nth-of-type(5) {
    background: purple;
}

JavaScript

JavaScript做一件事:隐藏当前幻灯片,显示下一张。为此,只需改变相关幻灯片的类名。

JavaScript代码:

var slides = document.querySelectorAll("#slides .slide");
var currentSlide = 0;
var slideInterval = setInterval(nextSlide,2000);

function nextSlide() {
    slides[currentSlide].className = "slide";
    currentSlide = (currentSlide+1)%slides.length;
    slides[currentSlide].className = "slide showing";
}

下面分析一下。

  1. 先用querySelectorAll 拿到容器中的幻灯片
  2. 然后声明一个变量保存当前幻灯片
  3. 最后通过定时器每2秒(2000毫秒)切换一次幻灯片

重点看看nextSlide函数。

  • 首先,把当前幻灯片的类置为不可见(去掉showing),CSS过渡会自动实现淡出。
  • 然后给当前幻灯片加1,这里使用%运算实现到最后一张后自动归零,即回到第一张;%运算符执行两个数相除,然后取得余数。 这个运算特别适合计算时间、日历等需要循环归零的情景。我们这个例子有5张幻灯片,那么依次执行取余运算的过程就是:1%5=1, 2%5=2, 3%5=3, 4%5=4, and 5%5=0。
  • 得到当前幻灯片编号之后,再给它的类加上showing。同样,CSS不透明度的渐变会自动出现。

祝贺你,一个简单的幻灯片诞生了。

注意兼容性

CSS过渡在IE9及以下版本是不能用的,在这些浏览器里,幻灯片会简单地切换到下一张。

这个简单的幻灯片是什么效果呢,看这里:Basic JavaScript Slideshow without jQuery

用户体验与无障碍

使用幻灯片之前,先要好好想想为什么在网页里用它。如果不小心的话,幻灯片很可能给你的网页带来用户体验和无障碍方面的问题。

幻灯片可能隐藏重要内容

如果是非常重要的内容,不应该放在幻灯片里。不能指望所有人都能使用合适的浏览器,这还不算无障碍问题。

根据 美国圣母大学的一项研究,只有1.07%的人会点击幻灯片中最突出的一张图,3%甚至更少的人会点击第一张之后的其他幻灯片。这说明通过幻灯片传达重要内容有多危险。

用户可能分不清你的网站是干什么的

如果幻灯片做得非常大,那么这真是个问题。如果你都不知道向用户展示什么,还怎么指望用户做出你期望的决定? (原推文)

网站应该明确告诉用户自己需要他们干什么,如果幻灯片妨碍了你达成这个目标,那该好好反思一下。

转换率优化公司WiderFunnel曾对幻灯片的有效性做过测试,并得出如下结论:

我们多次测试了轮播图,发现通过它展示主页内容效果很差;

有空的话,真的建议读一读他们这篇文章

移动用户可能不会高兴

幻灯片会导致加载时间过长,造成界面卡顿。

什么情况下可以使用幻灯片

既然幻灯片有这么多问题,那么什么情况下可以用它呢?下面是我的一些建议。

给人一个大体的印象

如果你不在乎用户是否会注意到某张幻灯片,只是想传达一种大概的印象,可以用。这种情况下,只要用户看到一张幻灯片,目的就达到了。

幻灯片外的主要内容很容易找到

比如,可能你想通过幻灯片展示某个景点、博物馆、会议或某系列产品的图片,但网站的其他地方也准备了相应图库或产品图片的完整展示区。这时候用幻灯片还是不错的。

在渐进增强的情况下

这种情况更普遍,前提是不要因为这个非网站主要功能的装饰带来太多问题。只要它只是装饰不是障碍就好。

无障碍指南

如果幻灯片的内容比较重要,应该做无障碍处理。当然,应该首先考虑到底该不该用幻灯片。

如果你(或你的客户)坚持要用,这是一篇关于幻灯片无障碍化的文章

根据这篇文章

幻灯片要做到无障碍,必须有五个条件:

  1. 允许用户停止全部切换

  2. 为键盘、鼠标和触摸设备提供无障碍访问机制

  3. 为幻灯片提供有效、可理念的焦点顺序指示

  4. 有效的代码和样式

  5. 为幻灯片提供有意义的替换

此外,这篇文章有一位评论者给出了是否决定使用幻灯片的有用资源

为了让幻灯片无障碍,下面我们就来添加一些控件。

为幻灯片添加控件

我们要添加“暂停/播放”、“下一张”和“上一张”按钮。

暂停/播放

首先为按钮添加HTML:

<button class="controls" id="pause">Pause</button>

接下来写JavaScript:

var playing = true;
var pauseButton = document.getElementById("pause");

function pauseSlideshow() {
    pauseButton.innerHTML = "Play";
    playing = false;
    clearInterval(slideInterval);
}

function playSlideshow() {
    pauseButton.innerHTML = "Pause";
    playing = true;
    slideInterval = setInterval(nextSlide,2000);
}

pauseButton.onclick = function() {
    if(playing) {
    pauseSlideshow();
  } else {
    playSlideshow();
  }
};

以上脚本都做什么了?

  • playing变量保存着幻灯片是否在播放的状态
  • 接着把暂停按钮也保存到一个变量中
  • pauseSlideshow函数负责暂停幻灯片,并把“暂停”改成“播放”
  • playSlideshow相反
  • 最后,为暂停/播放按钮添加一个单击处理程序,用于切换暂停和播放

加了这个按钮后的效果怎么样?看这里:JavaScript Slideshow With Pause Button

下一张和上一张

还是先添加HTML:

<button class="controls" id="previous">Previous</button>
<button class="controls" id="next">Next</button>

JavaScript,把

function nextSlide() {
    slides[currentSlide].className = "slide";
    currentSlide = (currentSlide+1)%slides.length;
    slides[currentSlide].className = "slide showing";
}

改成:

function nextSlide() {
    goToSlide(currentSlide+1);
}

function previousSlide() {
    goToSlide(currentSlide-1);
}

function goToSlide(n) {
    slides[currentSlide].className = "slide";
    currentSlide = (n+slides.length)%slides.length;
    slides[currentSlide].className = "slide showing";
}

为灵活起见,增加了goToSlide函数。同时,currentSlide变量的计算也做了一点改动,目的是防止出现负值。如果你想测试,可以给slides.length一个值,看看随着n变化currentSlide会怎么样。

下面添加JavaScript,让这个按钮在被点击时干它该干的事:

var next = document.getElementById("next");
var previous = document.getElementById("previous");

next.onclick = function() {
    pauseSlideshow();
    nextSlide();
};
previous.onclick = function() {
    pauseSlideshow();
    previousSlide();
};

控制添加好了。效果如何呢?看这里:JavaScript Slideshow With Controls

为用户提供了“下一张”和“上一张”按钮,用户就不会被自动播放“欺负”了。

因为这些控件就是HTML按钮,所以可以通过键盘来操控。

至于它们的样式和位置,随便你,只要明显,能用就行。

如果你还想加上用键盘上的左、右键控制,那么要保证用户在其他地方使用这两个键的时候关闭此处的控件。

防止没有JavaScript

偶尔会碰到JavaScript加载失败、被关闭,甚至设备不支持的情况。理想的情况当然是让用户仍然可以正常使用。如何做后备取决于你的目的。可以只展示第一张图片,也可以用一个列表展示所有图片。

如果幻灯片的主要目的只是给人一个大概的印象,那么显示保持网页布局不变比展示所有图片更重要。

如果确实需要展示所有图片,那你就把它们都罗列出来呗。

这两种情况,下面都会介绍。

无JavaScript时隐藏控件

默认使用CSS隐藏控制:

.controls {
    display: none;
}

然后使用JavaScript显示控制。这样,用户会在有JavaScript的时候看到控件。

var controls = document.querySelectorAll(".controls");
for(var i=0; i<controls.length; i++){
    controls[i].style.display = "inline-block";
}

以上代码循环所有控制,把它们都显示出来。

无JavaScript时列出所有图片

首先把.slide类的CSS由position: absolute;改为position: static;。这样,默认就是列出所有幻灯片。

然后添加JavaScript循环每一张幻灯片并将它们的定位改为绝对,像下面这样(一定要把这些代码放在slides变量后面):

for(var i=0; i<slides.length; i++) {
    slides[i].style.position = "absolute";
}

这样,幻灯片在JavaScript可用时就不会列出来了。

小结一下

我们讲了怎么做一个简单的幻灯片,如果处理用户体验和无障碍问题,以及如何添加控件。

本文最重要的,是想让大家思考:到底什么时候该用幻灯片,如果你想在网页中放个什么东西,请先想一想会对用户造成什么影响,以及它对你实现目标有没有帮助。如果答案明确,再去做或不做。

你想到什么了?关于幻灯片你有没有自己的经验或教训?或者有好玩的故事?欢迎留言。

英文原文:https://www.sitepoint.com/make-a-simple-javascript-slideshow-without-jquery/

阅读全文 »

06月11, 2016

【译】5行JavaScript代码实现页面平滑滚动

原文:http://www.zcfy.cc/article/406

HTML本身具有跳到页面任意位置的能力,只需给目标元素一个id 属性即可。这个技术叫做“锚链接”。

不过,这种跳转是瞬间完成的。为了让人看起来舒服,很多网站都会给页内跳转加上平滑或慢慢滚动到页面某处的设计。

过去人们大都用jQuery来实现此设计,但为这就加载一个框架有点小题大做。现代JavaScript为此提供了原生的window.scrollTo方法,效率更高。

首先,还是要有一个标准的锚链接,以防新技术不可用,还可以照老样子跳到目标。

<a href="#destination">Click me: I’m <em>smoooooth</em>.</a><p id="destination">This is the target, further down the page.

显然,只有页面长度超过窗口高度才可能真的跳转。为此,请自行在前面代码中省略号位置补充足够多的内容(或者试试我们推荐的填充文本生成器)。

两种滚动

页面滚动的API有两种实现方式:CSS和JavaScript。两种方式本身就容易让人困惑,再加上有些浏览器支持有些浏览器不支持,或者只以实验方式支持,问题就显得复杂了。(参见下文。)

CSS实现方式下,需要给“平滑滚动”的元素(通常是body)应用scroll-behavior: smoothCSS参考):

body {
    scroll-behavior: smooth;
}

注意,别用英式写法,是scroll-behavior,没有“u”。

JavaScript方式

JavaScript代码要添加到页面最后,以免阻塞。没错,我们说了只用5行代码。这里为方便解释,我稍微增加几行:

var anchorLink = document.querySelector("a[href="#destination"]"),
target = document.getElementById("destination");
anchorLink.addEventListener("click", function(e) {
    if (window.scrollTo) {
        e.preventDefault();
        window.scrollTo({"behavior": "smooth", "top": target.offsetTop});
    }
})

先用querySelector通过CSS属性选择符找到href目标为#destination的链接。在链接的点击事件中,测试浏览器是否支持scrollTo方法。如果支持,先用e.preventDefault阻止浏览器立即跳转,然后调用scrollTo方法,由前面的CSS声明实现平滑效果。有3个选项:behaviortop必需,left可选。后两个指的是跳转目标在页面中的位置坐标值。

可以用window.scroll代替window.scrollTo,它们功能一样。

相对于加载框架,这几行代码简单得多。唯一的缺点是不允许修改计时函数或缓动函数,据说是防止被滥用。

脚本抽象

前面的脚本可以用,但要求你必须提前知道跳转目标的名字,而且只能应用到一个链接。要是页面中有多个链接,需要平滑跳转到多个目标呢?这时就需要对脚本做一点抽象。

因为我们要引用nodeList,所以先写一个帮助函数,在forEach循环里关联链接:

var forEach = function (array, callback, scope) {
  for (var i = 0; i < array.length; i++) {
    callback.call(scope, i, array[i]);
  }
};

然后,找到所有锚链接和它们的地址,给它们的点击事件注册scrollTo

 var anchorLinks = document.querySelectorAll("a[href^="#"]");
if (window.scrollTo) {
    forEach(anchorLinks, function(index, element) {
        var target = document.getElementById(element.getAttribute("href").substring(1));
        element.addEventListener("click", function(el) {
            el.preventDefault();
            window.scrollTo(0, target.offsetTop); 
        })
    });
}

注意,同样需要在body标签的CSS样式中添加scroll-behavior声明。

浏览器支持

以上技术当前面临的问题是浏览器兼容性,现在只有Firefox和Chrome支持window.scrollTo,而且后者还把实现隐藏在chrome://flags中一个“实验性网络平台功能”的选项里(需要手工启用)。

Chrome浏览器“实验性网络平台功能”选项

正像一开始咱们说的,前面的脚本是基于“渐进增强”原则写的,所以不支持的浏览器照样能正确跳转,只是不能平滑地滚过去而已。为此,我推荐Dustan Kasten写的“腻子脚本”smoothscroll

结论

这个技术经常与“粘贴便签”(sticky)联用,后者会随着页面滚动一直出现在窗口中。更多内容,欢迎参考我的另一篇文章

英文原文:http://thenewcode.com/507/Smooth-Page-Scroll-in-5-Lines-of-JavaScript

阅读全文 »

06月02, 2016

【译】Web会不会重生

原文:http://www.zcfy.cc/article/244

2015年7月,受Mat MarquisTXJS 2015上发言的启发,我建议网页的平均大小应该等于经典DOC游戏Doom安装的图片大小。

大约7个月平均下来,网页大小与Doom安装的图片大小相同。我们做到了,越来越大。 pic.twitter.com/xtSAtZjPGl

— ronan cremin (@xbs) July 30, 2015

我们做到了,虽然比预期晚了一点。这是我们当前的情况:

web page size revisited revised

阅读全文 »

05月24, 2016

如何在众成翻译获得打赏

众成翻译(http://www.zcfy.cc)是360最大前端团队奇舞团推出的一个在线翻译平台,今年5月10号刚刚发布了1.0版。众成翻译的目标是做国内最好的技术翻译和内容产出社区,同时力争做最懂译者的翻译平台。本周一(5月23日),众成翻译上线译文打赏功能,本文将为大家详细介绍如何在众成翻译平台让自己的译文获得读者的打赏。

概述

要获得打赏,关键是在自己译文下方显示一个(或两个)收款二维码,这样读者才能把银子给你。比如这样:

alt

因此,要获得打赏,最重要的是学会生成自己的收款二维码。众成翻译目前支持上传两个二维码,默认为微信和支付宝的。下面就来看一看怎么在这两个手机应用中生成你自己的收款二维码。

阅读全文 »

05月18, 2016

【译】FIDO联盟规范简介

原文:http://www.zcfy.cc/article/189

FIDO(Fast IDentity Online)联盟成立于2012年7月,致力于解决当前网络环境下强认证设备间缺乏互操作性,以及用户需要创建并记住多套用户名和密码的问题。FIDO联盟将开发出开放、可扩展、具有互操作性的规范,取代依赖密码认证的传统方式。这个针对安全设备和Web浏览器的规范,将允许任何网站和云应用与支持FIDO的设备通信,实现更便捷、更安全的在线用户认证。

FIDO联盟有两套规范:U2F和UAF。联盟为部署该技术者提供维护新的fido-dev@fidoalliance.org公共邮件讨论列表的支持。

最新修订版可以访问规范下载页面

阅读全文 »