</lylijincheng>

Node.js 应用程序的 5 条性能建议

原文:5 Performance Tips for Node.js Applications

“如果 #nginx 没有架设在 Node 服务前面,你可能就做错了”,Bryan Hughes 在 Twitter 上说。

Node.js 是用 JavaScript —— 世界上最流行的编程语言 —— 创建服务端应用的领先工具。Node.js 同时提供了 Web 服务器和应用服务器的功能,现在被认为是基于微服务开发和交付的重要工具。(下载关于 Nodejs 和 NGINX 的免费的 Forrester 报告)

Node.js 可以替代或增强 Java 或 .NET 的后端应用程序开发。

Node.js 是单线程(single-threaded)的,使用无阻塞(non-blocking) I/O,使其可以扩展,支持成千上万的并发操作。它和 NGINX 共享这些架构特性,解决了 C10K 问题(支持超过 10,000 并发连接),这也是发明 NGINX 要解决的问题。

那么,这会有什么问题吗?

Node.js 有一些缺陷和弱点会使基于 Node 的服务表现不佳,甚至崩溃。随着基于 Node 的 Web 应用程序快速增长,这些问题也更加频繁地显现出来。

Node.js 同样是构建和运行应用逻辑来生成动态页面内容的很不错的工具。但是它在处理静态内容上面不是非常有优势,比如图片,JavaScript 文件。或者在多服务器之间实现负载均衡同样如此。

除了 Node.js,你还需要缓存静态内容,在多个应用服务中间做代理和负载均衡,在客户端,Nodejs 和辅助工具(比如 Socket.IO 服务) 之间管理端口占用。NGINX 可以来做这些事情,使得 Node.js 的性能发生重大转变。

我们使用以下建议来提高 Node.js 应用程序的性能

  • 实现一个反向代理服务器
  • 缓存静态文件
  • 多服务器之间负载均衡
  • 代理 WebSocket 连接
  • 实现 SSL/TLS 和 HTTP/2

注意:要快速改善 Node.js 应用程序性能的一个方法是修改 Node.js 配置,利用现代的,多核服务的优势。阅读这篇文章来了解如何使 Node.js 产生多个独立的子进程,它等于 Web 服务器上 CPU 的内核数。每个进程会神奇地找到自己的位置,使用其中的一个 CPU,这会这性能上有非常大提升。

1. 实现一个反向代理服务器

在 NGINX 公司,当我们看到应用服务器直接暴露在外部网络流量中,作为高性能站点的核心,有总会有点吃惊。这包括很多基于 WordPress 的站点,Node.js 网站也是如此。

Node.js 很大程度上比大多数应用服务器更加容易扩展,其 web 服务器端可以很好地处理很多网络流量,但是 web 服务不是 Node.js 存在的理由,这不是它要做的事情。

如果你有一个高流量的站点,提高应用服务性能的第一步就是在 Node.js 服务前面架设一个反向理服务器。这样做能避免 Node.js 服务直接暴露在网络流量中,并且可以让你更灵活地处理多个应用服务器,包括跨服务器之间的负载均衡和内容缓存 //内部链接//。

https://www.nginx.com/wp-content/uploads/2015/11/control2.png

将 NGINX 架设在已有的服务前面作为一个反向代理服务器,跟随其他用途,是 NGINX 重要的用途之一,已经被世界上成千上万的站点所运用。

使用 NGINX 作为 Node.js 反向代理服务器有很多具体的好处,其中包括:

  • 简化权限控制和端口分配
  • 更有效地提供静态文件(见下节)
  • 管理 Node.js 崩溃问题
  • 减少 DoS 攻击

注意:一些教程解释了在 Ubuntu 14.04CentOS 环境下如何使用 NGINX 作为一个反向代理服务器,对于所有要将 NGINX 架设在 Node.js 前面的人都是非常有用的概述。

2. 缓存静态文件

随着使用基于 Node.js 服务的站点的增长,这些服务逐渐承受了很多压力。这时候你需要做两件事情:

  1. 把大部分东西从 Node.js 服务上分离开
  2. 使增加应用服务器和实现负载均衡变得简单

事实上很容易做到,通过像上一节中讲的将 NGINX 作为一个反向代理服务器,很容易实现缓存,负载均衡(当有多个 Node.js 服务器时)等。

Modulus 网站,是一个应用程序容器平台,上面有一篇文章,是关于用 NGINX 对 Node.js 应用性能提升的文章。使用 Node.js 本身完成所有的工作,这位作者的网站可以提供平均每秒大约 900 次请求。用 NGINX 作为反向代理服务器,提供静态内容,同样的站点可以提供每秒超过 1600 次请求,性能提升接近 2 倍。

性能翻倍你就会有时间继续寻找新的增长点,例如评审(或者改善)网站的设计,优化程序代码,部署额外的应用服务器。

下面是一段运行在 Moduluss 上的一个网站的配置代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server {
  listen 80;
  server_name static-test-47242.onmodulus.net;

  root /mnt/app;
  index index.html index.htm;

  location /static/ {
     try_files $uri $uri/ =404;
  }

  location /api/ {
     proxy_pass http://node-test-45750.onmodulus.net;
  }
}

这篇来自 NGINX 公司 Patrick Nommensen 的文章,解释了他的个人博客是如何缓存静态内容的,他的博客运行在开源的 Ghost 博客平台上,这是一个 Node.js 应用,尽管一些特定的细节是针对 Ghost 的,你仍然可以在其他 Node.js 应用上复用大部分的代码。

例如,在 NGINX 配置的 location 块中,你可能不想让一些内容被缓存。比如你不想缓存博客平台的管理界面。下面是一段禁用 Ghost 管理界面缓存的代码:

1
2
3
4
5
6
7
location ~ ^/(?:ghost|signout) { 
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header Host $http_host;
  proxy_pass http://ghost_upstream;
  add_header Cache-Control "no-cache, private, no-store,
  must-revalidate, max-stale=0, post-check=0, pre-check=0";
}

想了解关于提供静态文件的一般信息,可以参考 NGINX Plus Admin Guide。这篇管理指南包括配置说明,多种选择响应成功或失败尝试查找一个文件和达到更优性能的优化方法。

使用 NGINX 服务器提供静态文件大大减轻了 Node.js 应用服务器的负担,使它可以实现更高好的性能。

3. 实现 Node.js 的负载均衡

真正的 Node.js 高性能(就是说几乎没有上限)的关键是运行多个应用服务器,在它们中间实现负载均衡。

Node.js 的负载均衡可能会特别棘手,因为 Node.js 启用了 Web 浏览器中运行的 JavaScript 代码 和 Node.js 应用服务器上运行的代码 之间的高级交互,使用 JSON 对象作为数据交互的媒介。这意味着,给定客户端的会话持续运行在一个特定的应用服务器上,多个应用服务器上 Session 持久性问题在根本上很难解决。

Internet 和 web 的一个优势是高度的无状态性,包括客户端请求可以被任何一个可以访问被请求的文件的服务器处理的能力。Node.js 推翻了它的无状态性,可以在有状态的环境下很好地工作,这种环境下,相同的服务会一致地响应特定客户端的请求。

这个需求可以用 NGINX Plus 得到最好的实现,而不是开源的 NGINX 软件。这两个版本的 NGINX 非常像,但是其中一个主要的区别是它们支持不同的负载均衡算法。

NGINX 支持无状态的负载均衡方法

  • 轮询调度 一个新的请求转到列表中的下一个服务器
  • 最少连接 一个新的请求转到有最少活动连接的服务器
  • IP 哈希 一个新的请求转到分配了客户端 IP 地址哈希值的服务器

IP 哈希,这些方法的其中之一,能可靠地发送一个客户端的请求到相同的服务器,这对 Node.js 应用程序是有好处的。然而,IP 哈希很容易导致一个服务器接收到大量的请求,损失了其他服务器,正如这篇文章关于负载均衡技术的描述。以潜在的跨服务器间的资源分配为代价,此方法可支持有状态性。

和 NGINX 不同,NGINX Plus 支持 session 持久化。使用 session 持久化,同一个服务器可以可靠地接受来自给定客户端的所有请求。Node.js 的优势是支持客户端和服务器间有状态的通信,NGINX Plus 有高级的负载均衡功能,都可以达到最大化。

所以你可以用 NGINX 或者 NGINX Plus 的负载均衡支持 Node.js 跨服务器间的负载均衡。如果只用 NGINX Plus,你就可以实现最优的负载均衡以及 Node.js 友好的有状态性。NGINX Plus 内置的应用状况检查监控功能在这里同样有用。

NGINX Plus 同样支持 session 释放,可以让应用服务器结束一个请求之后优雅地完成当前会话。

4. 代理 WebSocket 连接

所有的 HTTP 版本,被设计为“拉取”的通信方式,是客户端请求服务器的方式。WebSocket 开启了“推送”和“推送/拉取”的通信方式,服务器可以主动推送客户端没有请求的文件。

WebSocket 协议使得客户端和服务器之间支持更强的交互变得更容易,同时减少了数据传输量和延迟。在需要的时候,一个全双工的连接,客户端和服务器都会启动和接受请求,就可以实现了。

WebSocket 协议有一个稳健的 JavaScript 接口,很适合 Node.js 作为应用服务器,同样可作为适度业务量的 web 应用程序, 作为 web 服务器。当业务量增加时,将 NGINX 设在客户端和 Node.js 服务器之间,使用 NGINX 或 NGINX Plus 缓存静态文件,并且在多个应用服务器之间配置负载均衡,是很有意义的。

Node.js 经常会和 Socket.IO 一起使用,Socket.IO 是一个 Node.js 应用程序中很流行的一个 WebSocket API。这可能造成 80端口(HTTP)和443端口(HTTPS)非常拥挤,解决办法是代理到 Socket.IO 服务器上。如上所述,你可以使用 NGINX 作为代理服务器,也可以获得额外的功能,如静态文件缓存,负载均衡等。

https://www.nginx.com/wp-content/uploads/2015/11/Screen-Shot-2015-11-16-at-11.03.37-PM.png

下面的代码是一个 node 应用程序的 server.js 文件,其监听 5000 端口,它会作为一个代理服务器(非 web 服务器)将请求发送适当的端口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var io = require('socket.io').listen(5000);

io.sockets.on('connection', function (socket) {
  socket.on('set nickname', function (name) {
    socket.set('nickname', name, function () {
      socket.emit('ready');
    });
  });

  socket.on('msg', function () {
    socket.get('nickname', function (err, name) {
      console.log('Chat message by ', name);
    });
  });
});

在 index.html 文件中,添加一些代码来连接到服务器,在应用程序和用户浏览器之间初始化一个 WebSocket 连接。

1
2
3
4
<script src="/socket.io/socket.io.js"></script>
<script>
    var socket = io(); // your initialization code here.
</script>

要了解完整的说明,包括 NGINX 配置,参见我们的这篇使用 NGINX 和 NGINX Plus 结合 Node.js 和 Socket.IO 的文章。要了解更多关于类似 web 应用程序基础架构搭建的话题,可以参考我的文章:实时的 web 应用程序和 WebSocket。

5. 实现 SSL/TLS 和 HTTP/2

越来越多的站点使用 SSL/TLS 保证所有用户交互的安全性。当然是你来决定是否已经何时去这样做,但是如果你要做的话,NGINX 可支持两种交互方式。

  1. 只要使用 NGINX 当作反向代理,就可以在 NGINX 里终止一个 SSL/TLS 连接到客户端。Node.js 服务器和 NGINX 反向代理服务器来回地发送和接收未加密的请求和内容。
  2. 有迹象表明使用 HTTP/2,新版本的 HTTP 协议,可能大部分或者完全抵消当前使用 SSL/TLS 的性能损失。NGINX 对 HTTP/2 做了支持,你可以结合 SSL 来终止 HTTP/2,再消除 Node.js 应用程序服务器上任何必要的变化。

在实现的步骤中你需要做的是更新 Node.js 配置文件中的 URL,在 NGINX 配置中建立和优化安全连接,如果想要,可以使用 SPDY 或者 HTTP/2。添加 HTTP/2 支持意味着支持和服务器使用 HTTP/2 通信的浏览器使用新的协议,旧版本的浏览器继续使用 HTTP/1.x。

https://www.nginx.com/wp-content/uploads/2015/11/Screen-Shot-2015-11-16-at-11.06.09-PM.png

下面是一个 Ghost 博客使用 SPDY 的配置代码,可以在这里看一下介绍。包括了高级特性比如 OCSP 整合。要考虑使用 NGINX 作为 SSL 终端,包括 OCSP 选项,可以参考这里。要了解同样主题一般的概述,参考这里

目前,或者在 2016 年初 SPDY 不支持的时候,从 SPDY 到 HTTP/2,你只需要做很小修改配置你的 Node.js 应用程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
server {
   server_name domain.com;
   listen 443 ssl spdy;
   spdy_headers_comp 6;
   spdy_keepalive_timeout 300;
   keepalive_timeout 300;
   ssl_certificate_key /etc/nginx/ssl/domain.key;
   ssl_certificate /etc/nginx/ssl/domain.crt;
   ssl_session_cache shared:SSL:10m;  
   ssl_session_timeout 24h;           
   ssl_buffer_size 1400;              
   ssl_stapling on;
   ssl_stapling_verify on;
   ssl_trusted_certificate /etc/nginx/ssl/trust.crt;
   resolver 8.8.8.8 8.8.4.4 valid=300s;
   add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains';
   add_header X-Cache $upstream_cache_status;
   location / {
        proxy_cache STATIC;
        proxy_cache_valid 200 30m;
        proxy_cache_valid 404 1m;
        proxy_pass http://ghost_upstream;
        proxy_ignore_headers X-Accel-Expires Expires Cache-Control;
        proxy_ignore_headers Set-Cookie;
        proxy_hide_header Set-Cookie;
        proxy_hide_header X-powered-by;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header Host $http_host;
        expires 10m;
    }
    location /content/images {
        alias /path/to/ghost/content/images;
        access_log off;
        expires max;
    }
    location /assets {
        alias /path/to/ghost/themes/uno-master/assets;
        access_log off;
        expires max;
    }
    location /public {
        alias /path/to/ghost/built/public;
        access_log off;
        expires max;
    }
    location /ghost/scripts {
        alias /path/to/ghost/core/built/scripts;
        access_log off;
        expires max;
    }
    location ~ ^/(?:ghost|signout) { 
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_pass http://ghost_upstream;
        add_header Cache-Control "no-cache, private, no-store,
        must-revalidate, max-stale=0, post-check=0, pre-check=0";
        proxy_set_header X-Forwarded-Proto https;
    }
}

总结

这篇文章讲述了在 Node.js 应用程序里你可以做的最重要的性能提升建议。关注使用 NGINX 作为反向代理服务器,缓存静态文件,增加负载均衡,代理 WebSocket 连接,实现 SSL/TLS 和 HTTP/2 协议,将 Nginx 加入到你的 Node.js 应用程序里。

NGINX 和 Node.js 的结合是一中被广泛采取的方式来创建微服务友好的应用程序。或者为现有的基于 SOA 的 使用 Java 或 .NET 的应用程序增加灵活性和功能。这篇文章会帮你优化你的 Node.js 应用程序,如果你选择这些建议,会为 Node.js 和 NGINX 之间的合作更有活力。

Zhihu-top

工业设计

风险投资(VC)

电影

艺术

音乐

互联网

移动互联网

创业

阅读

学习

电子商务

法律

前端开发

健康

游戏

字体

字体设计

美食

生活

美剧

创业想法

产品经理

互联网产品

摄影

投资

心理学

旅行

设计

维基百科

iOS 应用

健身

文化

JavaScript

运动

教育

商业模式

创业团队

创业故事

房价

商业

手机游戏

生活方式

理财

交互设计

科技

HTML5

互联网创业

赚钱

职场

用户界面设计

摄影技巧

律师

经济学

职业规划

股票市场

流行音乐

基金

爱情

抑郁症

谷歌 (Google)

心理健康

生活常识

交互设计师

前端工程师

社会心理学

医学

金融

法律常识

逃离北上广

离职原因

异步 JavaScript 进化史

原文:The Evolution of Asynchronous JavaScript

async 函数很快就要来了,但是通往这里的历程却非常长。不久前我们都在写回调函数,后来出现了 Promise/A+ 规范,紧接着是 generator 函数,到现在是异步函数(async)声明。

让我们回顾一下,这些年异步 JavaScript 是如何进化演变的。

Callbacks(回调函数)

一切从回调函数说起。

异步 JavaScript

异步编程,如众所周知的 JavaScript,只能在函数作为第一公民的语言里实现,它们可以像其他变量一样传递给其他函数。回调函数就是这样产生的,如果你将一个函数作为参数传递给另外一个函数(又名,高阶函数),在完成自己的工作后,在另一个函数中可以调用它。没有返回值,只会传递值来调用另一个函数。

1
2
3
4
5
6
7
Something.save(function(err) {
  if (err)  {
    //error handling
    return;
  }
  console.log('success');
});

这些所谓的错误优先(error-first)回调函数在 Node.js 里占据重要地位,核心模块以及大多数在 NPM 上的模块都在使用它。

回调函数面临的挑战:

  • 如果使用不当,很容易写出大量回调(callback hells)和混乱的代码(spaghetti code)
  • 容易忽略错误处理
  • 使用 return 语句不能返回值,也不能使用 throw 关键字

大多由于这些原因,JavaScript 开始寻找一种解决方案使得异步 JavaScript 开发变得容易一些。

其中一个答案是使用 async 模块,如果你有很多回调函数,你就会明白并行,按顺序运行,甚至使用异步函数映射数组会有多复杂。所以感谢Caolan McMahon,编写了异步模块。

使用异步模块,你可以很轻松地做下面的事情

1
2
3
4
async.map([1, 2, 3], AsyncSquaringLibrary.square,
  function(err, result){
  // result will be [1, 4, 9]
});

但仍然不易读,也不容易写,因此出现了 Promises。

Promises

目前的 JavaScript Promise 规范始于 2012 年,从 ES6 以后提供了支持。然而 Promises 不是 JavaScript 社区发明的,这个术语是 1976 年 Daniel P. Friedman 提出的。

一个 Promise 代表一个异步操作最终的结果

使用 Promises 后,上面的例子可能像这样

1
2
3
4
5
6
7
Something.save()
  .then(function() {
    console.log('success');
  })
  .catch(function() {
    //error handling
  })

你会注意到 Promises 同样使用了回调函数,thencatch 注册的回调函数会在异步操作完成或者因为某些原因未能完成时被调用。另一个好处是 Promises 可以链式调用。

1
2
3
4
saveSomething()
  .then(updateOtherthing)
  .then(deleteStuff)
  .then(logResults);

当使用 Promises 时,在运行时没有提供时,你可能需要做一下兼容。这种情况通俗的方法是使用 bluebird,这些库可能提供了比原生对象更多的功能,即使是这样,也应该限制使用 Promises/A+提供的特性

为什么不应该使用那些额外方法呢,请阅读以下 Promises: 扩展的问题。想了解更多关于 Promises 的信息,可以参考 Promises/A+ 规范

你可能会问,当大多数库只暴露一个回调接口的时候该如何使用 Promises 呢?

这也很容易,你要做的唯一的事情就是使用 Promise 包装一个回调函数,在里面调用原来的函数,像这样

1
2
3
4
5
6
7
8
9
10
function saveToTheDb(value) {
  return new Promise(function(resolve, reject) {
    db.values.insert(value, function(err, user) { // remember error first ;)
      if (err) {
        return reject(err); // don't forget to return here
      }
      resolve(user);
    })
  }
}

一些库或者框架,已经都做了支持,同时提供回调函数和Promise 接口,如果你在构建一个库,支持两者是一个好办法。你可以很容易地像下面这样做

1
2
3
4
5
6
7
8
function foo(cb) {
  if (cb) {
    return cb();
  }
  return new Promise(function (resolve, reject) {

  });
}

或者可以更简单,你可以选择仅提供 Promises 接口,然后用向后兼容工具,比如 callbackify。Callbackify 基本上和上面的代码做了同样的事情,但用更一般的方法。

Generators / yield

JavaScript Generators 是一个相对新的概念,他们在 ES6(又称为 ES2015) 里面有介绍。

不是很好吗,当函数执行时,你可以在任何地方暂停,计算点别的,做其他的事情,然后再返回出去,甚至带有一些值还能继续?

这就是 generators 函数做的事情,当我们调用一个 generator 函数时,它还没有开始运行,我们要手动来遍历它。

1
2
3
4
5
6
7
8
9
10
11
function* foo () {
  var index = 0;
  while (index < 2) {
    yield index++;
  }
}
var bar =  foo();

console.log(bar.next());    // { value: 0, done: false }  
console.log(bar.next());    // { value: 1, done: false }  
console.log(bar.next());    // { value: undefined, done: true }  

如果你想使用 generators 轻松地编写异步的 JavaScript,你还需要 co 库。

Co 是一个为 Node.js 和浏览器提供的基于 generator 的控制流,使用 promises 漂亮地写出无阻塞的代码。

使用 co,我们之前的可能像下面这样

1
2
3
4
5
6
7
8
co(function* (){
  yield Something.save();
}).then(function() {
  // success
})
.catch(function(err) {
  //error handling
});

你或许会问,如果是并行操作会怎么样?答案会比你想象的要简单(内部仅仅是一个 Promise.all):

1
yield [Something.save(), Otherthing.save()];

Async / await

ES7 中引入了异步函数,当前只能使用通过转译(如 babel)工具来使用。(声明:现在讨论的是 async 关键字,而不是 async 模块包)

简单来讲,使用 async 关键字,我们可以做 co 和 generators 相结合的工作,而不是 hack。

async-hack

在其内部, async 关键字使用了 Promises,这也是异步函数会返回一个 Promise 对象的原因。

那么,如果我们想要做上面例子中的事情,我们应该将其重写为下面这样

1
2
3
4
5
6
7
8
async function save(Something) {
  try {
    await Something.save()
  } catch (ex) {
    //error handling
  }
  console.log('success');
}

正如你看到的,使用异步函数,必须将 async 关键字放在函数声明前面。在新创建的异步函数中,你可以使用 await 关键字。

使用 async 函数并行运行和使用 yield 的方法很像,除了 Promise.all 没有被隐藏,你需要调用它

1
2
3
async function save(Something) {
  await Promise.all[Something.save(), Otherthing.save()]
}

Koa 已经支持了 async 函数,所以你现在可以借助 babel 来尝试。

1
2
3
4
5
6
7
8
9
10
import koa from koa;
let app = koa();

app.experimental = true;

app.use(async function (){
  this.body = await Promise.resolve('Hello Reader!')
})

app.listen(3000);

扩展阅读

目前我们的大部分项目在生产环境使用 Hapi with generators,同时还有 koa

你更喜欢哪一种?为什么?欢迎评论!

Async/Await: JavaScript 中当之无愧的英雄

asyncawait

原文:Async/Await: The Hero JavaScript Deserved Eddie Zaneski,写于2015年10月2日

异步代码往往比较难写,一提到 JavaScript,我们会严重依赖回调函数来实现不太直观的异步任务。这种认知超载对新手编程产生了一个屏障,甚至会对使用了一段时间这门语言的人造成频繁的心痛。

这篇文章我们将尝试如何使用 ECMAScript 2016(ES7) 中的一个提案来改善 JavaScript 中异步编程的体验,使得代码更容易理解和书写。

现实的世界

我们先来看看现实的异步编程,下面的例子使用 request 库向罗恩斯旺森名言接口发起一个 HTTP 请求,然后再控制台打印出相应的内容。将下面的代码粘贴到一个名为 app.js 的文件中,然后运行 npm install request 安装依赖,如果你还没有安装 Node.js,可以在这里安装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var request = require('request');

function getQuote() {
  var quote;

  request('http://ron-swanson-quotes.herokuapp.com/quotes', function(error, response, body) {
    quote = body;
  });

  return quote;
}

function main() {
  var quote = getQuote();
  console.log(quote);
}

main();

如果你已经接触过 JavaScript 的异步编程,你可能已经明白为什么没有输出一条名言。如果是这样我会和你击掌。

high-five

运行 node app.js 你很快会发现输出了 undefined

为什么会这样

原因是 quote 变量的值是 undefined,因为在 request 函数执行完成时,给它赋值的回调函数还没有被调用。由于 request 函数操作是异步执行的,JavaScript 不会等他的执行结果,而是继续执行下面一条语句,返回了未赋值的变量。 如果想深入了解 JavaScript 异步执行是如何工作,可以看看 Philip Roberts 在 JSConf EU 上的一个很棒讨论

同步代码通常更容易理解和编写,因为所有的代码都是按编写的顺序执行的。return 语句在其他语言中被广泛使用并且相当直观,但是在 JavaScript 中我们不能同我们想要的那样使用它,因为这不太符合 JavaScript 的异步的本质。

为什么不与异步代码对抗呢?性能。网络请求和磁盘读取操作我们称之为 I/O(input/output) 操作,同步 I/O 执行会阻塞程序,停下来等待数据传输完成才返回。如果需要60秒去数据库查询,程序会停留在那里60秒,不做其他事情。然而在异步 I/O 操作中,程序会继续正常执行后面的代码,I/O 操作完成之后就会处理它的结果。这就是为什么回调函数会存在,但是在阅读程序源码的时候会感到难用和不好理解。

理想的世界

我们可以都有好的一面——可以很好地处理块级操作的异步代码,并且更容易阅读和书写?答案是肯定的。感谢 ES7 对异步函数(Async/Await)的 提案

当一个函数被声明为异步 async 时,它可以在 promise resolved 之前退出(yield)当前函数的执行,如果你不了解 promise,可以参考这些很棒的资料中的

app.js 中的代码替换为下面的代码,此外我们还需安装 Babel 转译工具来运行它。通过 npm install babel 安装 babel 模块,他会将 ES7 代码可以在当前环境可运行的代码,你可以在这里了解更多关于 Babel 的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var request = require('request');

function getQuote() {
  var quote;

  return new Promise(function(resolve, reject) {
    request('http://ron-swanson-quotes.herokuapp.com/quotes', function(error, response, body) {
      quote = body;

      resolve(quote);
    });
  });
}

async function main() {
  var quote = await getQuote();
  console.log(quote);
}

main();
console.log('Ron once said,');

可以看到我们将网络请求操包装为一个新函数 getQuote,返回了 promise 对象。

request 的回调函数中,我们调用 promise 的 resolve 函数来处理返回的结果。

执行下面的代码来运行新的例子

1
2
3
4
./node_modules/.bin/babel-node app.js

// Ron once said,
// {"quote":"Breakfast food can serve many purposes."}

哇,看起来非常酷,快要接近原来的期望了。尽管是异步的,它看起来已经非常像同步代码了。

如果你没注意到,Ron once said, 先被打印出来,尽管它是在 main 之后调用的。这表明但我们等待网络请求完成是并没有阻塞代码的执行。

实施改进

实际上我们可以用 try/catch 块增加错误处理来进一步改善它。如果在请求过程中出现错误,可以调用 promise 的 reject 函数,这将会在 main 里面捕获一个错误。和 return 语句一样,try/catch 块在过去很容易被理解,因为他们很难正确地同异步代码使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var request = require('request');

function getQuote() {
  return new Promise(function(resolve, reject) {
    request('http://ron-swanson-quotes.herokuapp.com/quotes', function(error, response, body) {
      if (error) return reject(error);
      resolve(body);
    });
  });
}

async function main() {
  try {
    var quote = await getQuote();
    console.log(quote);
  } catch(error) {
    console.error(error);
  }
}

main();
console.log('Ron once said,');

将请求 URL 修改为 http://foo 重新运行代码,你会发现捕获到一个异常。

好处

这些非常酷的优势能真正改边我们编写异步 JavaScript 的方式。可写出异步执行的代码,但是看起来是同步的,使其变得更容易使用常用的编程结构,比如 return 和 try/catch,会让语言变得更易懂。

最大的好处是可以通过返回 promise 对象,使用我们最喜欢的特性。我们看一下 Twilio Node.js library 的例子,如果你没有使用过 Twilio Node.js 库,可以在这里了解一下,你还需要一个 Twilio 账号,可在这里注册。

运行 npm install twilio,然后把下面的代码复制到一个名为 twilio.js 的文件中,将标记 // TODO 替换为你自己的凭证和号码。

1
./node_modules/.bin/babel-node twilio.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var twilio = require('twilio');

var client = twilio('YOUR_TWILIO_ACCOUNT_SID', 'YOUR_TWILIO_AUTH_TOKEN'); // TODO

async function sendTextMessage(to) {
  try {
    await client.sendMessage({
      to: to,
      from: 'YOUR_TWILIO_PHONE_NUMBER', // TODO
      body: 'Hello, Async/Await!'
    });
    console.log('Request sent');
  } catch(error) {
    console.error(error);
  }
}

sendTextMessage('YOUR_PHONE_NUMBER'); // TODO
console.log('I print out first to show I am async!');

和上面 getQuote 函数一样,我们将 sendTextMessage 标记为 async,这样可以使它 awaitclient.sendMessage 返回的 promise 对象成功的结果。

结束工作

我们已经看到如何利用 ES7 提案的特性来改善编写异步 JavaScript 的体验。

我非常高兴 Async/Await 提案能够继续向前推进,但是在等待期间,现在我们可以利用 Babel 结合任意返回 promise 对象来使用它。这则提案最近进入了候选阶段(第3步),需要被推广使用及反馈。如果你对它有不错的用法,记得在评论里面告诉我,或者 Twitter @eddiezane

编写明智的,可维护,可扩展的 CSS 高级建议和准则

Translation: http://cssguidelin.es/

目录

简介

CSS 不是一种漂亮的语言。尽管它学习和入门简单,但很快会在扩展时遇到问题。虽然我们不能在改变 CSS 如何工作上做出更多,但我们可以在创作和构建方式上面做出改变。

在大型的,长时间运行的并且有几十名专长和能力不同的开发者合作的项目中,我们在一个统一约定的方式中工作,特别是为了能做到以下几点:

  • 保持样式文件是可维护的;
  • 保持代码透明,意义明确拥有可读性;
  • 保持样式文件的可扩展性。

目前有各种方法来达到这些目的,CSS Guidlines 就是完成这一目标所记录的建议和方法。

准则的重要性

拥有一种代码风格(注意不是视觉上的风格),对如具有下特点的团队来说是很有价值的

  • 构建和维护产品到一个合理的时间周期
  • 拥有不同能力和专长的开发者
  • 在给定的时间内,有很多开发着开发同一个项目
  • 有定期招募新同事
  • 开发者要经常参考大量其他的代码库

同时,这些准则规范一般更适合产品团队,他们拥有庞大的代码库,长期运行和持续进化的项目,需要多个开发者维持很长一段时间,所有开发者应该争取在代码中形成某种程度上的标准化。

一份好的风格准则,在被遵循后,会有以下效果

  • 有一个代码质量的标准
  • 促进代码之间的一致性
  • 在代码库中给开发者一种熟悉感
  • 增加生产效率

在一个被管理的项目中,风格准则应该始终被运用,理解和实施,任何规则都要有合理的理由。

声明

CSS Guidelines 是一份风格准则,但不是唯一必须要遵循的准则。它包含方法论,各种技能和技巧。我会严格建议我的客户和团队遵守它。你自己的爱好和情况可能有所不同,结果也不一样。

这份风格准则是主观的,但是它被反复地在各种大小的项目中经过试验,测试,加强,精炼,破碎,再造和修订了多年。

语法和格式

一份风格准则最简单的方式之一就是制定一些关于语法和格式的规则。一个标准的 CSS 书写方式(字面上书写)意味着代码对团队中所有成员来都会感觉到很熟悉。

更进一步,代码在视觉和感觉上会比较整洁。这会提供一个更好的工作环境,促使其他团队成员去维护他们发现的代码整洁性的标准,对丑陋的代码有一个反面的例子。

从大的方面说,我们要做到

  • 4个空格缩进,无 tab 符号
  • 列宽为80个字符
  • 多行书写 CSS
  • 使用有意义的空白

但是,正如所有的事情一样,这些细节可能无关紧要,但保持一致性是关键。

多文件

伴随着大量的后来的 CSS 预处理器,更常见的情况是开发者们将 CSS 分割为多个文件。

即使不使用预处理器,将不相关的代码块拆分成多个文件是一个好的做法。这些文件在构建阶段会合并起来。

无论何种原因,如果你不需要要多个文件,下面的部分可能需要一些妥协来适应你的设置。

内容列表

内容列表是一个相当大的维护开销,但是它带来的好处远比任何成本重要。这需要一个勤奋的开发者更新这个列表,并且值得坚持做下去。一个持续更新的内容列表可以为团队提供一个 CSS 项目单独规范的目录,记录做了什么以及什么顺序来做。

一个简单的内容列表是按顺序,自然地提供了每个部分的名称以及这部分用来做什么的简要概括,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
 * 目录
 *
 * 配置
 * 全局.................全局可见的变量和配置.
 *
 * 工具
 * 混合(mixin)..........有用的混合类
 *
 * 一般样式
 * Normalize.css........统一一般的样式.
 * Box-sizing...........更好的默认 `box-sizing` 声明.
 *
 * 基本样式
 * 标题..................H1–H6 样式.
 *
 * 对象
 * 包装器................包装和约束内部元素.
 *
 * 组件
 * 页面头部..............页面主头部.
 * 页面尾部..............页面主尾部.
 * 通用按钮..............按钮元素.
 *
 * 秘籍
 * 文本.................文本辅助.
 */

每一条会映射到一个部分或者引入的文件。

当然,这部分在大多数项目中大得多,但是我们希望可以看到主样式文件如何给开发者提供一个项目角度的认识,各个部分用在什么地方,以及为什么用在此处。

80字符宽

尽可能限制 CSS 文件中列宽到80个字符,原因如下:

  • 可以使多文件并排打开
  • 可以像在Github上一样阅读网站的 CSS,或者在终端窗口中阅读
  • 提供一个合适的行的长度,方面添加评论
1
2
3
4
5
/**
 * 这是一条长注释. 将详细描述下面的 CSS.
 * 这条注释很长,很容易打破了80列字符的的限制,
 * 所以将他分成了几行.
 */

这一规则不可避免地会有例外情况,如URL,渐变语法等,这些可以不必担心。

标题

CSS项目中,每个主要部分的开头应该有一个标题

1
2
3
4
5
/*------------------------------------*\
    #SECTION-TITLE
\*------------------------------------*/

.selector {}

部分的标题以一个 “#” 符号作为前缀,方便执行多目标的搜索(比如grep等)。如果只是搜索 “SECTION-TITLE” 可能会出现很多结果,更精确的搜索 “#SECTION-TITLE” 应该只会返回这个部分的结果。

在标题的上一行和下一行应保留两行注释。

如果项目中的每个部分是一个文件,标题应该出现在文件顶部。如果项目中的每个文件有多个部分,在标题之前应该有5个空行。当浏览大文件时,这些额外的空行加上后面的标题会使得这部分更加容易被定位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*------------------------------------*\
    #A-SECTION
\*------------------------------------*/

.selector {}





/*------------------------------------*\
    #ANOTHER-SECTION
\*------------------------------------*/

/**
 * 注释
 */

.another-selector {}

规则解析

在讨论如何书写规则集之前,我们要先了解一下相关术语。

1
2
3
4
[selector] {
    [property]: [value];
    [<--declaration--->]
}

例如

1
2
3
4
5
6
.foo, .foo--bar,
.baz {
    display: block;
    background-color: green;
    color: red;
}

从上面可以看出:

  • 关联的选择器在同一行,不关联的选择器不在同一行
  • 左大括号({)之前有一个空格
  • 属性名和属性值处在同一行
  • 属性名和属性值以冒号(:)分割,后面有一个空格
  • 每个声明独占一行
  • 左大括号({)和最后一个选择器在同一行
  • 左大括号({)后面的一行为第一个声明
  • 右大括号(})独占一行
  • 每个声明缩进4个空格
  • 最后一个声明有分号(;)结尾

这似乎是通用的格式标准(除了空格数目,很多开发者使用2个空格缩进)。按照这些规则,下面是个不妥的例子

1
2
3
4
5
.foo, .foo--bar, .baz
{
    display:block;
    background-color:green;
    color:red }

存在的问题有

  • 使用tab而不是空格缩进
  • 不相关的选择器出现在同一行
  • 左大括号({)单独占位一行
  • 右大括号(})没有单独占一行
  • 最后一个声明结束时没有分号(;)
  • 冒号(:)后面缺少一个空格

多行 CSS

CSS 应该跨行来写,除了很特殊的情况。这样做有几个好处:

  • 减少合并时的冲突,因为每个声明占单独一行
  • 更加真实可靠的文件 diff,因为一行只会记录一个变化

此规则的例外情况也很明显,如只有一行声明的相似的规则,例如

1
2
3
4
5
6
7
8
9
10
11
.icon {
    display: inline-block;
    width:  16px;
    height: 16px;
    background-image: url(/img/sprite.svg);
}

.icon--home     { background-position:   0     0  ; }
.icon--person   { background-position: -16px   0  ; }
.icon--files    { background-position:   0   -16px; }
.icon--settings { background-position: -16px -16px; }

这些规则处在一行有几个好处:

  • 依旧遵从每一行的变化只对应一个原因的规则
  • 它们之间有很多相似之处,不需要完全相对其他规则一起阅读。同时可以更好地对比他们的选择器,在这种情况下我们更希望有这些好处。

缩进

同缩进单个声明一样,缩进整个相关的规则集表示他们与另一个之前的关系,例如:

1
2
3
4
5
.foo {}

    .foo__bar {}

        .foo__baz {}

这样做开发者可以直观地看出 .foo__baz {}.foo__bar {} 内部,.foo__bar {}.foo {} 内部。

类似这种 DOM 复制使开发者知道很多应该使用类,而不是引用 HTML 片段的情况。

Sass缩进

Sass 提供嵌套功能,也就是说,下面的写法

1
2
3
4
5
6
7
8
.foo {
    color: red;

    .bar {
        color: blue;
    }

}

会被编译成如下CSS

1
2
.foo { color: red; }
.foo .bar { color: blue; }

在 Sass 中处理缩进的时候,我们坚持使用4个空格,同时在内嵌的规则前后各保留一个空行。

注意:应该尽量避免在 Sass 中使用嵌套,详见 特殊性部分。

对齐

在声明中应该使公共和相关的相同字符串对齐,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.foo {
    -webkit-border-radius: 3px;
       -moz-border-radius: 3px;
            border-radius: 3px;
}

.bar {
    position: absolute;
    top:    0;
    right:  0;
    bottom: 0;
    left:   0;
    margin-right: -10px;
    margin-left:  -10px;
    padding-right: 10px;
    padding-left:  10px;
}

这使得有列编辑功能编辑器的开发者更容易修改,可以一次改变相同和对齐的行中多处。

有意义的空白

和缩进一样,我们提供很多在规则集中自由明智地使用空白的信息

  • 紧耦合的规则及之间使用 1 个空行
  • 松耦合的规则集之间使用 2 个空行
  • 完全新的章节之间使用 5 个空行

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*------------------------------------*\
    #FOO
\*------------------------------------*/

.foo {}

    .foo__bar {}


.foo--baz {}





/*------------------------------------*\
    #BAR
\*------------------------------------*/

.bar {}

    .bar__baz {}

    .bar__foo {}

两个规则及之间至少有1个空行,下面的写法是不正确的

1
2
3
.foo {}
    .foo__bar {}
.foo--baz {}

HTML

HTML 和 CSS 之间有内在的关联性,如果不涵盖一些标记的语法和格式准则,将会是我的疏忽。

标记属性要有双引号,即使没有他们也可以正常工作。这样做可以减少意外,并且对大多数开发者是一种熟悉的格式。尽管下面这种可以运行(同样是有效的)

1
<div class=box>

但是,建议用下面这种

1
<div class="box">

属性值的引号不是必须的,但加引号会更安全。

当 class 属性中包含多个值的时候,用使用两个空格分隔

1
<div class="foo  bar">

当多个类名是相互关联的,可以考虑将他们放在一对方括号([])内,比如

1
<div class="[ box  box--highlight ]  [ bio  bio--long ]">

这个不会强行推荐,仍然在尝试中,但它确实能带来很多好处。如果有兴趣,可以阅读 在标记中分组相关的类名

正如我们的规则及,在标记中使用有意义的空白成为可能,你可以用5个空行表示内容块,比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<header class="page-head">
    ...
</header>





<main class="page-content">
    ...
</main>





<footer class="page-foot">
    ...
</footer>

将独立松耦合的标记块用一个空行分隔,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<ul class="primary-nav">

    <li class="primary-nav__item">
        <a href="/" class="primary-nav__link">Home</a>
    </li>

    <li class="primary-nav__item  primary-nav__trigger">
        <a href="/about" class="primary-nav__link">About</a>

        <ul class="primary-nav__sub-nav">
            <li><a href="/about/products">Products</a></li>
            <li><a href="/about/company">Company</a></li>
        </ul>

    </li>

    <li class="primary-nav__item">
        <a href="/contact" class="primary-nav__link">Contact</a>
    </li>

</ul>

这使得开发者可以迅速定位 DOM 块,同时使得某些编辑器(如Vim)操作空行分隔的标记块。

注释

使用 CSS 的认知开销是巨大的。要注意太多东西,还要留意很多项目相关的细节,最坏的情况是开发者都不知道自己写过类似的代码。回想自己的类,规则,对象和辅助工具在一定程度上是可控的,但继承 CSS 的时候很少能做到。

CSS需要更多注释

由于 CSS 更像是一种声明式的语言,因此没有留下太多书面记录。如果单独观察 CSS ,总是很难辨识。

  • 这些 CSS 是否依赖其他地方的代码
  • 修改这些代码会对其他地方有什么影响
  • 这些 CSS 可能会用在其他什么地方
  • 哪些样式可能会被继承(有意无意地)
  • 哪些样式可能会传递下去(有意无意地)
  • 作者的意图是想把这些代码用在什么地方

这还不考虑一些CSS的怪异特性,例如不同状态的 overflow 触发块级上下文,或者某个变换属性触发硬件加速等。这使得开发者继承项目更加困惑。

由于CSS不能很好地描述自身,因此是一个受益于大量注释的语言。

通常,你应该为所有不能立刻看出明显意图的代码添加注释。也就是说,不需要告诉别人 color: red; 会使某些东西为红色。但是如果你使用 overflow: hidden 来清除浮动,而不是剪切元素的溢出,这可能需要去注释点什么。

高级注释

对于大的注释记录整个章节或组件,我们使用文档块式的多行注释,并且限制要有80字符宽。

下面是现实中CSS里的一个例子,是CSS Wizardry页面的头部样式。

1
2
3
4
5
6
7
8
/**
 * 网站的主头部有两个不同的状态:
 *
 * 1) 普通的头部没有背景和其他处理; 只包含商标(logo)和导航.
 * 2) 刊头有自动高度 (在某个点之后变为固定),并且有一个大背景图和一些文本.
 *
 * 普通头部非常简单, 当栏目头部稍微有点复杂,依赖它的包装容器。
 */

这个级别的详情应该是正式代码的标准规范,描述其状态,内容组织,使用环境和如何处理等。

对象扩展

当处于多个部分,或者以面向对象的CSS方式的时候,经常会发现可以互相结合的规则集不在同一处或文件内。例如,有一个通用的按钮对象,它只提供了结构性样式,并且在组件中会被扩展。我们简单地将这种跨文件的关系记为“对象扩展”。在对象文件中

1
2
3
4
5
/**
 * 从 _components.buttons.scss 中扩展 `.btn {}`.
 */

.btn {}

主题文件中

1
2
3
4
5
6
7
/**
 * 这些规则扩展了 _objects.buttons.scss 中的 `.btn {}`.
 */

.btn--positive {}

.btn--negative {}

这个简单的注释对不清楚项目相互关系的开发者很重要,对于想要知道怎样,为什么,什么地方样式会被继承的开发者同样有意义。

低级注释

在规则中我们常想注释一个特定的声明(一行),我们用一种反向脚注来完成。下面是个更复杂的注释,详细记录上面提到的网站头部

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
 * 大型站点头部更像刊头. 它们会模拟流动高度,是由里面的包装
 * (wrapper)元素控制的.
 *
 * 1. 刊头一般会有深色背景, 所以我们要把保证对比起来没有问题. 
 *    它的值跟随背图变化而变化.
 * 2. 我们要将刊头的布局委托给它的包装元素,而非它自己,里面
 *    的很多元素是根据它来定位的.
 * 3. 包装器需要定位上下文来放置刊头文字.
 * 4. 模拟流动高度技术: 通过创建百分比内边距空间来创建流动高
 *    度的视觉效果, 然后在他上面定位所有的内容. 百分比给出16
 *    :9的比例.
 * 5. 当可视范围为 758px 宽时, 16:9意味着刊头当前渲染高度是
 *    480px. ...
 * 6. …在此高度上无缝剪下流动效果…
 * 7. …高度固定在 480px. 这意味着当刊头从流动到固定过程中我们
 *    应该看不到高度跳动. 当前的值考虑了头部的内边距和上边框.
 */

.page-head--masthead {
    margin-bottom: 0;
    background: url(/img/css/masthead.jpg) center center #2e2620;
    @include vendor(background-size, cover);
    color: $color-masthead; /* [1] */
    border-top-color: $color-masthead;
    border-bottom-width: 0;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1) inset;

    @include media-query(lap-and-up) {
        background-image: url(/img/css/masthead-medium.jpg);
    }

    @include media-query(desk) {
        background-image: url(/img/css/masthead-large.jpg);
    }

    > .wrapper { /* [2] */
        position: relative; /* [3] */
        padding-top: 56.25%; /* [4] */

        @media screen and (min-width: 758px) { /* [5] */
            padding-top: 0; /* [6] */
            height: $header-max-height - double($spacing-unit) - $header-border-width; /* [7] */
        }

    }

}

这种类型的注释使我们所有的记录归结在一起,同时指出他们所属规则集的部分。

预处理注释

对于大部分 CSS 预处理器,我们选择书写编译后不会在 CSS 文件中的注释,通常使用这些注释描述同样不会在 CSS 文件中写出的代码。如果你写的代码需要被编译,就使用可被编译的注释,例如,这是正确的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Dimensions of the @2x image sprite:
$sprite-width:  920px;
$sprite-height: 212px;

/**
 * 1. Default icon size is 16px.
 * 2. Squash down the retina sprite to display at the correct size.
 */
.sprite {
    width:  16px; /* [1] */
    height: 16px; /* [1] */
    background-image: url(/img/sprites/main.png);
    background-size: ($sprite-width / 2 ) ($sprite-height / 2); /* [2] */
}

我们记录的变量,使用预处理器注释,这些代码不会被编译到CSS文件中,而我们的 CSS文件 会使用 CSS 注释,这些注释会被编译到 CSS 文件中。这意味着我们在调试编译后的样式文件时,有正确和相关的可用信息。

移除注释

毫无疑问,生产环境中不需要注释,在部署之前,所有 CSS 应该被压缩,从而去掉评论。

命名约定

CSS 中的命名约定非常有用,可以使你的代码更严禁,更透明,传达更多的信息。

好的命名约定会使你和你团队明白

  • 一个类是做什么的
  • 一个类可以被用在什么地方
  • 一个类可能和其他什么东西有关联

我们所遵循的命名约定很简单,用连字符(–)分隔字符串,对复杂的代码使用 BEM 命名规则。

需要注意的是,命名约定在写作CSS的时候通常不是很有用,他们真正有用的地方是在查看 HTML 的时候。

命名约定

连字符分隔

类中所有的字符串使用连字符(–)分隔,像这样

1
2
3
.page-head {}

.sub-content {}

普通的类中不使用驼峰式和下划线来命名,下面是错误的

1
2
3
.pageHead {}

.sub_content {}

BEM命名

对于大型的,更多内相关的视图片段会用到大量的类,我们使用 BEM 命名约定。

BEM,意思是块(Block),元素(Element),修饰符(Modifier)。是 Yandex 公司的开发者提出的一种前端方法论。虽然 BEM 是一个完全的方法论,但我们只需要关注其命名约定。并且,这里是类BEM命名,原则是完全相同的,只是实际语法稍有不同。

BEM 把组件类名分成三个部分

  • 块,组件唯一的根
  • 元素,块的组成部分
  • 修饰符,块的变体或扩展

举个例子(注意不是实例)

1
2
3
.person {}
.person__head {}
.person--tall {}

元素使用2个下划线(_)分隔,修饰符使用2个连字符(–)分隔。

我们可以看到,.person {} 代表块,它是分离实体的根。.person__head {} 是一个元素,它是 .person {} 块的一小部分。最后的 .person--tall {} 是一个修饰符,它是 .person {} 块的变体。

起始上下文

块应该起始于一个合乎逻辑的,独立的离散的位置。继续我们上面的例子,我们不应该有像 .room__preson {} 的类,因为 room 是另一个,更高级的上下文环境。我看可能会有分离的块, 像这样

1
2
3
4
5
6
7
8
9
10
.room {}

    .room__door {}

.room--kitchen {}


.person {}

    .person__head {}

如果确实要在 .room {} 中嵌套一个 .person {},应该使用一个像 .room .person {} 的选择器,将两个块连接起来,而不是增加已经存在的快和元素的范围。

一个可能包含块的理想的例子可能像下面这样,每段代码都代表自己的块

1
2
3
4
5
6
7
8
9
10
11
12
.page {}


.content {}


.sub-content {}


.footer {}

    .footer__copyright {}

错误的写法

1
2
3
4
5
6
7
8
9
.page {}

    .page__content {}

    .page__sub-content {}

    .page__footer {}

        .page__copyright {}

知道 BEM 作用范围的开始和结束很重要,通常BEM适用于视图中独立,离散的部分。

多层级

如果我们想增加另一个元素给 .person {},我们叫 .person__eye {},我们不需要遍历每一层 DOM,这就是说,正确的写法应该是 .person__eye {},而不是 .person__head__eye {}。类里面不需要反映所有的 DOM 层级。

修饰元素

你可能会有很多元素变体,它们可以由很多方式来表示,依赖于他们是如何以及为何被修改的。继续上面的例子,表示蓝色的眼睛应该这样

1
.person__eye--blue {}

从这里可以看出,我们直接修改了眼睛元素。

然而,实际情况可能更复杂。请联系上面的最早的例子,假设我们有一个脸的元素,并且它很英俊。如果一个人原本不是很英俊,我们可以直接修饰脸元素,使其变得英俊。

1
.person__face--handsome {}

但是如果这个人是英俊的,但是我们想要修饰一下他的脸,及一个英俊的人有一张普通的脸

1
.person--handsome .person__face {}

我们使用一个后代选择器修改一个块的基于修饰符的元素,这是少有的情形之一。

如果使用Sass,我们可能会这样写

1
2
3
4
5
6
7
8
9
.person {}

    .person__face {

        .person--handsome & {}

    }

.person--handsome {}

注意,我们没有在 .person--handsome {} 中内嵌一个新的 .person__face {} 实例,而是在利用 Sass 的父选择器在已有的 .person__face {} 上预置了 .person--handsome {}。这意味着所有和 .person__face {} 相关的规则都集中在两一个地方,不会分散在文件中的其他地方。这在处理内嵌代码时是一个好的做法,即使所有的上下文(如所有的 .person__face {} 代码)封装在同一个位置。

HTML命名约定

正如我前面所提到的,命名约定在CSS中不一定有用,真正起作用的地方在于 HTML 标记。下面是一段没有命名约定的HTML。

1
2
3
4
5
6
7
<div class="box  profile  pro-user">

    <img class="avatar  image" />

    <p class="bio">...</p>

</div>

boxprofile 两个类有什么相互关系,profileavatar 又是什么关系,它们有关系吗?你是不是应该将 pro-userbio 一起使用,imgageprofile 会在CSS中的同一部分么,avatar 可以用在其他地方吗?

从上面的标记中看,很难回答这些问题,如果使用了命名约定,就会发生变化

1
2
3
4
5
6
7
<div class="box  profile  profile--is-pro-user">

    <img class="avatar  profile__image" />

    <p class="profile__bio">...</p>

</div>

现在我们可以清楚地知道这些类之间有什么关系,也知道哪些类不能在这个部件范围之外使用,哪些类可以被用在任何地方。

JavaScript钩子

通常情况下,在 HTML 中将 CSS 和 JS 绑定到同一个类不是个好的做法,因为两者之间有了依赖的关系,如果把特定的类绑定到 JS 上就会很清楚,更透明,和更易维护。

在我重构一段 CSS 的时候遇到了这种情况,改动之后无意中把JS功能去掉了,因为 CSS 和 JS 是绑定在一起的,去掉其中一个就不完整了。

通常这些类有一个 js- 的前缀,如

1
input type="submit" class="btn  js-btn" value="Follow" />

这就是说,我们可以有一个带有 .btn {} 样式的元素,但是没有 .js-btn {} 的行为。

data-*属性

一般的做法是用 data-* 属性作为 JS 钩子,但是这样做是不对的。按照规范,data-* 属性是用来“存储对页面或应用私有的自定义数据”。所以 data-* 属性是为存储数据设计的,而不是去绑定对象。

进一步考虑

正如前面提到的,这些是很简单的命名约定。是一些比代表三个不同类的分组不能做更多的约定。

我会鼓励你继续阅读,观察你的命名约定来提供更多功能,这也是我想进一步投入和研究的地方。

扩展阅读

CSS选择器

或许有点奇怪,编写可维护和可扩展的 CSS 最基本,最关键的方面之一是选择器。它们的特殊性,可移植性,可重用性在我们摆脱 CSS 的历程,以及它们可能给我们带来的头痛的问题历程中,都会有直接影响。

选择器意图

在我们编写 CSS 的时候,正确地确定选择器的范围,找到合理的理由选择正确的内容很重要。选择器意图是一个决定和定义你将给什么内容应用样式以及如何选择他们的过程。例如,如果你想为你网站的主导航添加样式,下面的选择器很不明智

1
header ul {}

我们的目的是给网站主导航应用样式,但这个选择器的意图是给任何一个 header 元素里的任何 ul 匹配样式。这是差的选择器意图,因为在页面中你可以任意多个 header 元素,他们中也可以有任意多 ul,所以类似这样的选择器会有一个风险,可能将特定的样式应用到很多的元素上面。这将导致会有更多的 CSS 去撤销选择器贪婪的人特性。

更好的选择器会是这样

1
.site-nav {}

这是个清晰,明确的选择器并且有良好的选择器意图。我们明确地选择了正确的内容出于正确的理由。

CSS 项目中令人头痛的最大的一个原因就是差的选择器意图。规则编写的过于贪婪,即对于很具体的情形采用一个很宽泛的选择器,这会导致不可预知的副作用及很混乱的样式表。这种样式中的选择器越过了他们的意图,并且影响和干扰了其他不相关的规则集。

CSS 不能被封装,这算是先天缺失,但我们可以通过不去写类似的全局操作选择器来弥补,你的选择器应该尽可能地明确和合理,作为你想要选择匹配的原因

可重用性

伴随着基于组件的方式来构建UI的趋势,可重用性是至关重要的。我们希望可移动,重复利用,复制和集成组件贯穿于项目中。

为此,我们使用了大量的类。使用ID会非常地具体,在页面中只能使用一次,而类是可以重用无限多次的。从选择器类型和名称,无论你选择什么,都应该使其可以复用。

位置独立性

由于大多数 UI 项目会不断变化,并且会朝着基于组件的结构发展,编写样式基于它们是什么,而不是它们在什么位置,对我们来讲是有益的。这就说说,我们的组件样式不应该依赖他们所在的位置,它们和位置应该是无关的。

我们拿一个按钮的例子,用下面的选择器

1
.promo a {}

这种写法没有良好的选择器意图,由于贪婪性,将会使得所有在 .promo 中的链接看起来像一个按钮,并且很浪费造成对位置的依赖,我们不能在 .promo 之外使用此样式,因为它明确绑定到了所在的位置上。一个好的多的选择器应该像这样

1
.btn {}

这个单独的类可以在 .promo 之外任何地方使用,并应用正确的样式。作为一个更好的选择器,此UI片段会容易移植,重复利用,没有任何依赖,并且有更好的选择器意图。一个组件不应该在一个特定的地方使它看起来特殊

可移植性

减少或者理论上移除位置依赖意味着我们可以在标记中灵活地移动组件,但对于提高组件周围移动类会怎么样呢?至少我们可以做到,是选择器更加便携(与组件的创建相反),看下面的例子

1
input.btn {}

这是一个限定的选择器,前置的 input 使得此规则是能作用于 input 元素上。如果忽略检查,我们可以将 .btn 类使用在任何元素上,比如 a 元素或者 button

当然,有时候你可能想通过合理的手段限定一个选择器,你可能想应用一些很特殊的样式到一个特定的元素,且这个元素具有某个类,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * 为具有 `.error` 类的元素添加颜色和粗体样式.
 */
.error {
    color: red;
    font-weight: bold;
}

/**
 * 如果此元素是 `div`, 再给它一个边框.
 */
div.error {
    padding: 10px;
    border: 1px solid;
}

这个例子是一个被合理利用的限定选择器,但我仍然建议另一种方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * 文本级别错误.
 */
.error-text {
    color: red;
    font-weight: bold;
}

/**
 * 包含错误的元素.
 */
.error-box {
    padding: 10px;
    border: 1px solid;
}

意味着我们可以给任何元素应用 .error-box 类,而不仅仅是 div 元素,这比限定选择器有更好的可重用性。

类限定选择器

限定选择器一个有用的地方是可以预示一个选择期望或打算在什么地方使用。例如

1
ul.nav {}

我们可以看到,.nav 类计划是要用在一个 ul 元素上的,而不是一个 nav 元素。使用类限定选择器我们仍然可以表达上述意义,但是不用限定这个选择器。

1
/*ul*/.nav {}

通过注释前置元素,它仍然有可读性,但避免了限定和增加选择器的特殊性。

命名

Phil Karlton 曾说,在计算机科学里只有两个难题:缓存失效和命名。在这里我不想评论前者,但是多年来后者一直困扰着我。在 CSS 中对于命名,我的建议是选取一个合理的,但有点模糊的名称,力求高可重用性。例如,.primary-nav 会比 .site-na 更好,.sub-links.footer-links 要好。

这两个例子中,几个命名之间的区别是,每个例子中,后者被绑在一个非常具体的实例:它们各自只能被用在站点的导航或底部链接。使用含义模糊的名字,可以提高组件在不同情况下的可重用性。

引用 Nicolas Gallagher 的话:

类名语义和内容本质的紧密结合已经降低了架构的扩展能力或被其他开发者投入使用的能力。

这就是说,我们应该使用合理的名称,像 .border 或者 .red 之类的类名绝不建议使用,但我们应该避免使用描述内容精确含义和其用例的命名。用类名描述类名是不必要的,因为内容会描述其本身。

围绕语义的争论已经持续了数年,但对我们来说,重要的是采取一种更实用,合理的方式去命名,使其更有效地运行。相比“语义性”,更需要关注的是灵活性和长久性,基于易维护性来命名,而不是其感知的意义。

命名是为所有人,只有他们才阅读你的类名(只是为了匹配)。在强调一次,类名力求可重用,可循环,而不是为了特例:我们看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
 * 有过时的风险, 不太好维护.
 */
.blue {
    color: blue;
}

/**
 * 依赖位置, 以便正确地渲染.
 */
.header span {
    color: blue;
}

/**
 * 太过具体, 制约了可重用性.
 */
.header-color {
    color: blue;
}

/**
 * 不错的抽象, 很方便, 没有过时的风险.
 */
.highlight-color {
    color: blue;
}

在名称之间取得一个平衡很重要,这些名字没有字面上描述类带来的样式,同时没有明确地描述特定的实例。选择 .masthead 而非 .home-page-panel.primary-nav 而非 .site-nav.btn-primary 而不是 .btn-login

视图组件命名

记住不可知论和可重用确实能更快帮助开发者构建和修改视图,并尽量减少浪费。不过,有时提供更具体或有意义的命名和模糊的类名一样是有益的。特别是当几个未知的类和一个更负载和具体的组件一起的时候,这个组件可能受益于一个有意义的名称。在这种场景下,我们给类扩展一个 data-ui-component 属性,它包含一个更具体的名字,例如

1
<ul class="tabbed-nav" data-ui-component="Main Nav">

现在我们可以受益于类名的高度可重用性,并没有描述内容,绑定到一个特定的实例,使用我们的 data-ui-component 属性增加意义。data-ui-component 属性的内容可以随意设置,比如标题类的:

1
<ul class="tabbed-nav" data-ui-component="Main Nav">

或者像类名的

1
<ul class="tabbed-nav" data-ui-component="main-nav">

或命名空间型的

1
<ul class="tabbed-nav" data-ui-component="nav-main">

其实现很大程度上依据个人偏好,但理念是一样的:通过一种方法增加何有用或特定的意义,这种方法不会约束你和你的团队重复和复用 CSS 的能力

选择器性能

对于当今浏览器的质量,比其重要性更有趣的一个话题是浏览器性能。这就是说,一个浏览器匹配 CSS 中选择器的速度取决于字在 DOM 中找到这些节点的速度。

通常,一个选择器越长(即有更多组成部分)匹配会越慢,例如

1
body.home div.header ul {}

上面的选择器会比下面的选择器效率低很多

1
.primary-nav {}

这是因为,浏览器读取一个选择器是从右向左的,浏览器是这样读取上面第一个选择器的

  • 找到 DOM 中所有的 ul 元素
  • 然后检查他们是否在一个有 .header 类的元素内部
  • 之后检查 .header 类是否应用在一个 div 元素上
  • 然后检查上一步的结果是否在任何有 .home 类的元素中
  • 最后检查 .home 是否在 body 元素上

与第二选择器相比,只需要简单的浏览器读取

  • 找到所有具有 .primary-nav 类的元素。

进一步对比这个问题,我们在使用后代选择器(如 .foo .bar {}),结果是浏览器必须从选择器的最右面(即 .bar)开始,一直向上查找 DOM 直到找到下个部分(即 .foo)。这可能会大量增加 DOM 查找的时间,知道找到匹配的节点。

这仅是为什么使用预处理器嵌套往往是一个虚假经济的一个原因。同让原本没必要的选择器变得更具体,位置依赖一样,这还会个浏览器增加额外的工作。

利用子选择器(例如,.foo > .bar {}),我们可以使这处理得更有效,因为只需要浏览器查找一级 DOM,同时也会忽略有没有找到一个匹配。

关键选择器

由于浏览器是从右向左读取选择器的,最右边的选择器通常是影响一个选择器性能最关键的因素:被称为关键选择器

下面的选择器可能第一眼看上去性能很高。它使用一个 ID,不仅很好也很快,因为一个页面中只能有一个,因此查找肯定会很迅速,只需要找到那个 ID,然后在里面应用样式

1
#foo * {}

但是,这个选择器的问题是其关键选择器(*)会有很大的影响。这个选择器实际要做的是,找出 DOM 中的每一个节点(即使 <title><link><head> 等所有元素),然后确认每个几点是否在 #foo 中的任何位置以及任何层级。这是一个花销非常巨大的选择器,应该尽最大可能避免或重写。

幸运的是,带有好的选择器意图去写选择器,我们很可能已经避免了低效率的选择器。如果我们 的目的是对的,并且有合理的理由,我们很不喜欢贪婪的关键选择器。

尽管如此,但是在你的优化列表里面 CSS 性能应该在相当低的位置,浏览器很快,而且会越来越快,只有在极少数的情况下,低效率的选择器才会引起问题。

同它们具体问题一样,嵌套,限定,不好的选择器意图都会造成选择器效率低下。

一般规则

选择器是编写良好 CSS 的基础,现简要总结上面的部分

  • 明确选择想要什么,不会依赖环境或巧合,好的选择器意图会控制范围和样式的泄漏。
  • 编写可重用的选择器,可以更高效,减少浪费和重复。
  • 不要嵌套多余的选择器,会增加特殊性,对其他地方使用造成影响。
  • 不要限定多余的选择器,会影响样式应用到不同元素的数目。
  • 尽量使得选择器简短,为了减低特殊性,提升性能。

延伸阅读

特殊性

正如我们所看到的,CSS 不是最友好的语言:全局操作,易泄漏,位置依赖,很难封装,基于继承……,但是它们都没有特殊性那样可怕。

无论你如何考虑命名,无论源代码如何完美的顺序和层级,以及如何很好地控制规则集的范围,只需要一个很特殊的选择器就可以重写所有东西。这是不可思议的,它破坏了 CSS 中最本质的层级,继承和源码顺序。

特殊性的问题是,它可以设置优先级,并且不能被简单的撤销。我举一个几年前的例子

1
#content table {}

这不仅表现出糟糕的选择器意图,实际上我并不需要 #content 里面所有的 table,我我只是需要一个碰巧在这里的特定类型的 table,这是一个很具体的选择器。而我需要另一个 table 时,过了几周就出问题了

1
2
3
4
5
6
#content table {}

/**
 * 喔,不! 我的样式被 `#content table {}` 重写了.
 */
.my-new-table {}

第一个选择器的特殊性比后面定义的那个更高,破坏了基于CSS源码顺序的样式应用。为改正这个问题,我有两种思路

  1. 重构 CSS 和 HTML 移除选择器 ID
  2. 书写更特殊的选择器覆盖此规则

不好的是,重构会花费很长时间。这是个成熟的产品,删除此ID的连锁效应比第二种选择,即只需写一个更具体的选择器,会产生更大的业务成本。

1
2
3
#content table {}

#content .my-new-table {}

现在我有一个更具体的选择器,如果我想要覆盖它,我需要在它后面定义一个至少有相同特殊性的选择器。I’ve started on a downward spiral.

除此之外,特殊性还会

  • 限制扩展和操控代码的能力
  • 扰乱 CSS 层级,继承特性
  • 在项目中造成本可以避免的冗余
  • 当代码移动到另一个环境中,会使其不能得到预期效果
  • 导致严重的开发者失望情绪

当很多开发者共同开发一个大型项目的时候,所有这些问题都会被极大地放大。

时刻保持低调

特殊性的问题不在于其高或者低,事实是它变化多样并且不能分离出来,处理它的唯一办法是让它逐步变得更具体,就是上面看到的臭名昭著的特殊性之争;

编写 CSS(特别是需要扩展的)的最简单单一的技巧之一就是始终要试图让它的特殊性尽可能低。确保在代码库中选择器之间没有太多的差异性,所有的选择器尽量保持低特殊性。

这样做可以迅速帮助你控制和管理项目,即没有过于具体的选择器会影响到其他地方较低特殊性的选择器。同样会让你不太可能继续争论特殊性的问题,也可能使你写出更小的样式表。

我们会做如下变化,包括但不限于

  • 在 CSS 中不使用 ID
  • 不嵌套选择器
  • 不限定类名
  • 不用链式选择器

特殊性可以被争论,被理解,但如果完全避免它是最安全的。

CSS中的ID

如果我们想保持低特殊性,我们要做的是用一个简单高效,易于遵循的规则来帮我们:避免在 CSS 中使用 ID。

ID 不仅天生不能被复用,而且比其他选择器更特殊,因此成为特殊性异常。相对地,基于 ID 的选择器与其他选择器相比有更高的特殊性。

事实上,要突出这种差异的严重性,可以看看1000个链式类都不能覆盖一个 ID 的特殊性:jsfiddle.net/0yb7rque。(注意:在 Firefox 中可能会看到文字是蓝色的,这是已知的问题,用256个链式类覆盖一个ID的问题也是一样的)

注意,在 HTML 和 JavaScript 里面完全可以用 ID,只是在 CSS 中可能会有问题。

那些在 CSS 中不使用 ID 的开发者经常被认为只是“不理解特殊性是如何工作的”。这是不对的因为具有攻击性:无论你是你一个多么有经验的开发者,都不会避免这种行为,即使再多的知识也不会使 ID 变得不特殊。

选择这种工作方式只会增加沿着这条路线出现问题的机会。特别是大型的项目中,都应该尽量去避免出问题的可能,总结一句话

不值得冒这个险;

嵌套

我们已经了解过嵌套是如何导致位置依赖和导致潜在的低效的代码,现在我们看一下它的另一个缺陷,使选择器更具体。

当我们讨论嵌套时,说的不一定是预处理中的嵌套,例如

1
2
3
4
5
.foo {

    .bar {}

}

我们实际上说的是后代选择器或选择器,那些有相互依赖的选择器。想下面的这些

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * 在 `.foo` 元素内,处于任何地方的具有 `.bar` 类的元素.
 */
.foo .bar {}


/**
 * 在 `.module` 元素的直接子元素中,具有 `.module-title` 类的元素.
 */
.module > .module-title {}


/**
 * 在 `nav` 内部,所有存在于 `ul` 中的 `li` 元素
 */
nav ul li {}

这些 CSS 是不是通过预处理器生产不重要,要注意的是预处理器将其视为一个功能,但事实上应该尽量去避免

一般来说,复合选择器的每一部分都会增加一层特殊性,因此一个符合选择器的组成越少,其全局特殊性也会越低,而我们应当保持低特殊性,引用 Jonathan Snook 的话

无论何时定义样式,使用最少的选择器为元素应用样式

我们看一个例子

1
2
3
4
5
6
7
.widget {
    padding: 10px;
}

    .widget > .widget__title {
        color: red;
    }

要给一个具有 .widget__title 的类的元素应用样式,我需要一个和它相比有两倍特殊的选择器。这意味着如果我们要对 .widget__title 做出修改,我们需要一个至少同样特殊性的选择器。

1
2
3
4
5
6
7
.widget { ... }

    .widget > .widget__title { ... }

    .widget > .widget__title--sub {
        color: blue;
    }

这是可以完全避免的,是我们自己造成的问题,我们的选择器具有他所需要的两倍的特殊性。我们使用了实际所需200%的特殊性。不仅如此,还导致不必要的代码冗长,增加了网络负载。

通常,如果一个选择器不嵌套可以正常生效,就不要嵌套它。

作用域

嵌套可能会有一个好处,就是给我们提供类别命名空间,但是所增加的特殊性弊大于利。类似于 .widget .title 的选择器会限制 .title 的应用范围,只会在具有 .widget 类的元素里生效。

这在一定程度上给 CSS 提供了作用域和封装,但仍然会是选择器具有和它原本所需的两倍的特殊性。 更好的提供作用域的方法是通过命名空间,使用 [类BEM命名]法,不会导致增加不必要的特殊性。

现在我们有更好的作用域,并且使用最小的特殊性,两全其美。

扩展阅读

!important规则

!important 会给几乎所有的前端开发者下一个终极令,!important 是特殊性最直接的体现,是一个欺骗你远离特殊性争论的方法,但是要付出很大的代价。它经常被视为最后的手段去修补代码中一个更大的问题。

一般规则是,使用!important 总是不好的,但是可以引用 Jamie Mason 的话

规则是原则的子类

是说,单一的规则很简单,坚守一个更大原则的方式。当写代码时,绝不使用 !important 是一个好规则。

然而,当你成为一个成熟开发者时就会发现规则后面的原则很简单,要保持低特殊性。也会知道规则何时何地被顺从。

!important确实在 CSS 项目中会用到,但是会在极少情况下主动使用。

当你遇到任何特殊性问题之前,会积极地使用 !important。会被用作是有保障恶如不是修复问题,例如

1
2
3
4
5
6
7
.one-half {
    width: 50% !important;
}

.hidden {
    display: none !important;
}

这两个帮手或工具,类的意图非常具体,只希望在呈现到50%宽或完全呈现。如果想要此行为,就不用用这些类,因此在任何时候使用时,要确保他们不会有问题。

我们主动使用 !important 能确保它不会有问题,使用这些王牌能保证正常工作,不会被其他更特殊的规则意外覆盖,就是对的。

当在某些情况后解决特殊性问题而使用 !important 是被动的,也是不对的,这些情况由于 CSS 设计不当而在声明时使用 !important。假设有下面的 HTML

1
2
3
<div class="content">
    <h2 class="heading-sub">...</h2>
</div>

如下CSS

1
2
3
4
5
6
7
.content h2 {
    font-size: 2em;
}

.heading-sub {
    font-size: 1.5em !important;
}

我们可以看到,在 .heading-sub {} 中使用 !important 强行覆盖了 .content h2 {} 选择器。这其实很容易避免,包括使用更好的选择器意图,或者避免嵌套。

这些情形中,应该更好地去调研重构强行的规则,试图降低特殊性,而不是引入重量级的特殊性。

只在主动的情况下使用 !important,而非被动。

特殊性探究

我们讨论了很多特殊性问题,以及保持低特殊性,但是我们仍然会遇到问题。无论我们多么努力,多么仔细,我们还回去探究和争论特殊性的问题。

当出现这些情况,我们能够安全优雅地处理它显得非常重要。

如果你需要增加一个选择器的特殊性,有很多方法。可以通过嵌套来提升特殊性。例如我们用 .header .site-nav {} 来提升单一的 .site-nav {} 的特殊性。

我们讨论过,这会造成一个问题,就是位置依赖,即这些样式只会在 .header 中的 .site-nav 起作用。

取而代之的是我们可以用更安全的技巧而不影响其移植性,即链接选择器自身

1
.site-nav.site-nav {}

这种链式写法将选择器特殊性提升了一倍,但是不会引起位置依赖。

如果我们的标记中有一个 ID,并且不能将其替换成类,我们通过属性选择器而不是 ID选择器来选择它。例如,假设我们的页面中嵌入了一个第三方的组件,我们可以通过它的标记书写样式,当我们不能改变它的标记。

1
2
3
<div id="third-party-widget">
    ...
</div>

尽管我们知道在 CSS 中不去使用 ID,还有什么其他方法呢?我们要给 HTML 添加样式但是获取不到它,并且只有一个ID。

我们这样做

1
[id="third-party-widget"] {}

我们可以通过属性而不是 ID 去选择它,属性选择器和类拥有同样的特殊性。我们可以通过这种方法给有 ID 的元素添加样式,而不增加特殊性。

注意的是这些只是些技巧,除非我没有更好的方法,否则我们不应该是去使用它们。

扩展阅读

设计原则

你可能会想到 CSS 的架构设计是一个不实的,非必需的概念:为何一个如此简单直白的东西,需要想得复杂去考虑一个架构设计呢?

正如我们看到的,CSS简单,宽松,它的灵活的本质意味着在任何合理的范围内,维护它最好的方式是通过一种严格而具体的架构设计。一个严格的架构能帮我们控制特殊性,强制命名约定,管理源代码结构,创建稳健的开发环境,使我们的 CSS管理更加轻松协调。

没有工具,没有预处理,没有灵丹妙药能让你的 CSS 自己变得更好,对于如此宽松的语法,开发者最好的工具就是自律,责任感和勤奋,一个好的架构设计有助于加强和促进这些特质。

架构设计很大,是重要的,原则导向的一些细小的约定集合,这些约定一起提供代码编写和维护的环境。架构通常是高级别的,比如命名约定,语法细节和格式这些细节,可能会留给团队去实施。

大部分架构设计基于现有的设计模式和规范,这些规范通常来自计算机科学和软件工程师。CSS不是程序,没有很多编程语言的特性,但我们发现可以应用一些原则到我们的工作中。

在这部分,我们会了解这些设计模式和规范,以及如何使用它们优化 CSS 项目中的代码,增加代码重用性。

高级别概述

在最顶层,架构应该能完成以下事情

  • 提供哦你可持续和稳健的环境
  • 能适应变化
  • 增加和扩展代码库
  • 提升重用性和效率
  • 增加效率

一般情况下,会是一个基于类和组件的架构,分割成易于管理的模块,也可能使用预处理器。当然,这还不成为一个架构,所以我们来看一下设计原则。

面向对象

面向对象是一个编程的模型,它将大型的程序分解成对立的对象,这些对象有自己的角色和职责,以下来源于维基百科

面向对象编程(OOP)是一个程序模型,描述了对象的概念,对象通常是类的实例,通过他们之间的交互设计应用和计算机程序。

对于 CSS,我们称它为面向对象的 CSS 或者 OOCSS。OOCSS 被 Nicole Sullivan 提出并且推广,他的媒体对象已成为了方法论的典型代表。

OOCSS将UI分离成结构和外表,把UI组件分离成基本结构的形式,并将修饰样式分层。这意味着我们可以很容易地复用通用和重复设计的样式,同时不需要重复具体实现细节。OOCSS提倡代码重用,可以使得我们效率更快,同时减小了代码量。

结构上可以理解为骨架;提供无需设计的公共的重复使用的模块,即对象和抽象。对象和抽象是无任何修饰的简单设计模式,把一系列组件共享的结构特征抽象为一个通用的对象。

外表是一个层,可以选择性地添加到结构上,给对象和抽象物特别的外观,我们看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
 * 一个简单没有设计过的按钮元素. 使用 `.btn--*` 来扩展其外观
 */
.btn {
    display: inline-block;
    padding: 1em 2em;
    vertical-align: middle;
}


/**
 * 肯定的按钮外观. 继承自 `.btn`.
 */
.btn--positive {
    background-color: green;
    color: white;
}

/**
 * 否定的按钮外观,继承自 `.btn`.
 */
.btn--negative {
    background-color: red;
    color: white;
}

上面可以看到,.btn {} 类是如何为一个元素提供结构,而不关心它的修饰。我们给 .btn {} 对象增加第二个类,比如 .btn--negative {},给这个节点增加特殊的修饰

1
<button class="btn  btn--negative">Delete</button>

通过使用像 @extend: 的方法支持多个类,在标记中使用多个类而不是将类包含着预处理器中,这样可以

  • 更好地在标记中检索,可以迅速明确地找出 HTML 片段中有哪些类起作用
  • 可以更好地组合类,而不是和其他样式绑定到一起

当构建UI组件时,你要尝试看是否可以将它分成两部分,一部分是结构样式(边距,布局等)另一部分是外表(颜色,字体等)。

扩展阅读

单一职能原则

单一职能原则是一种松散的模式,描述代码中(CSS中的类)所有的片段都应该只关注一件事情,更正式地描述为

单一职能原则描述了每一个语境(类,函数,变量等)应该有单一的职责,这个职责应该完全封装在此语境中。

类只关注很特定和有限的功能。因此我们要将 UI 分解成小部件,每个部件提供单一的功能,它们都只负责一件事,但可以很容易地组合成复杂的结构。我们举一个不遵循单一职能原则的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.error-message {
    display: block;
    padding: 10px;
    border-top: 1px solid #f00;
    border-bottom: 1px solid #f00;
    background-color: #fee;
    color: #f00;
    font-weight: bold;
}

.success-message {
    display: block;
    padding: 10px;
    border-top: 1px solid #0f0;
    border-bottom: 1px solid #0f0;
    background-color: #efe;
    color: #0f0;
    font-weight: bold;
}

我看可以看到,尽管用来具体的实例来命名,这些类做了很多事情:布局,结构和修饰。还有很多重复的地方。我们需要重构它,抽象出一些共享的对象(OOCSS)并且使用单一职能原则将它变得内联,我们将这两个类分解成四个小的职能部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.box {
    display: block;
    padding: 10px;
}


.message {
    border-style: solid;
    border-width: 1px 0;
    font-weight: bold;
}

.message--error {
    background-color: #fee;
    color: #f00;
}

.message--success {
    background-color: #efe;
    color: #0f0;
}

现在我们有一般抽象的盒子,可以单独使用,完全从消息组件中分离出来了。另外有一个基本的消息组件,可以被更小职能的类去扩展。重复量大大减少,类的可扩展和组合能力也大大增加。这是一个 OOCSS 很好的例子,并且符合单一职能的原则。

通过单一职能原则,可以使用的代码更灵活,如果兼顾开闭原则(下一节介绍),扩展组件功能也将变得很简单。

扩展阅读

开闭原则

在我看来,开闭原则不是一个好名字。因为从名字看来有50%的重要信息被省掉了。开闭原则是说

软/硬件实体(类,模块,函数等)应该对扩展开放,对修改封闭。

明白了没?最重要的关键词扩展和修改没有了,意义就不大了。

当你想要记起来开关实际的意义时,你会发现开闭原则非常简单:增加到类上面的任何新增,新功能,或特性应该通过扩展来做,不应该直接修改这些类。这好像违背了单一职能原则,应该我们不应该直接去修改对象和抽象,我们首先应该确定使它们尽可能的简单。这意味着我们不需要真正地改变抽象,可以不使用他,但任何轻微的变化可很容易地扩展出来。

举个例子

1
2
3
4
5
6
7
8
.box {
    display: block;
    padding: 10px;
}

.box--large {
    padding: 20px;
}

我们看到 .box {} 对象很简单,我们将它变成很小的单一职能的对象。如果要修改它,我们用另一个类 .box--large {} 去扩展。所以 .box {} 类关闭了修改,但是开放扩展。

一种错误的实现方式可能像下面这样

1
2
3
4
5
6
7
8
.box {
    display: block;
    padding: 10px;
}

.content .box {
    padding: 20px;
}

这样不仅过于具体,位置依赖,潜在地显出不好的选择器意图,直接修改了 .box {} 。我们不应该将一个对象或抽象类作为一个复合选择器中的关键选择器。

类似 .content .box {} 的选择器会有潜在问题

  • 会使 .content 内部所有的 .box 应用指定的样式,就是说修改依赖开发人员,而开发者应该能够明确地选择样式变化。
  • 这种情况下,.box 的样式对开发者是未知的,由于内嵌选择器产生强行约束,所以没有遵循单一职能原则。

所有的修改,新增,变化都应该是选择性而非强制性的。如果你认为可能有需要调整的东西要在规范中去掉,可以提供另外一个类添加此功能。

如果在团队中工作,确保写出类 API 的 CSS。总是能保证已有的样式保持先后兼容(如不要修改根元素)并且提供新的钩子增加新功能。对根元素,抽象对象,或组件的修改会对开发者在其他地方使用代码产生巨大的连锁效应,所以不要直接修改已有的代码。

当根对象要重写或重构时,可能会出现异常。但是只有在这次特定的情形下你才应该修改代码。请记住:对扩展开放,对修改封闭

扩展阅读

DRY

DRY 主张不要做重复的事情,是软件开发中的一个微原则,旨在把重要信息的重复性降到最小。正式定义为

系统中的每一个部分,必须有一个独立的,清晰的,正式的表述。

尽管是一个非常简单的原则,但在项目中 DRY 经常被误解为绝不把同样的事情做两次的必要性。这会不太现实,甚至通常适得其反,并且可能导致被迫抽象,过度思考和工程化的代码,以及特殊依赖。

关键的问题不是避免所有的重复,而是常态化和抽象意义的重复。如果两个地方碰巧共享同一个声明,这时候就不需要考虑 DRY。这种重复是偶然的,不能被共享和抽象,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.btn {
    display: inline-block;
    padding: 1em 2em;
    font-weight: bold;
}

[...]

.page-title {
    font-size: 3rem;
    line-height: 1.4;
    font-weight: bold;
}

[...]

    .user-profile__title {
        font-size: 1.2rem;
        line-height: 1.5;
        font-weight: bold;
    }

从上面的代码中,我们合理推断出,font-weight: bold; 的声明完全巧合地出现了三次。如果尝试去抽象,混入(mixin)或者用 @extend 指令去减少这种重复,就有显得有点多余,并且会将这三条偶然的规则绑定到一起。

然而,假设我们用 Web字体,它需要 font-weight: bold; 在每一个 font-family 中声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.btn {
    display: inline-block;
    padding: 1em 2em;
    font-family: "My Web Font", sans-serif;
    font-weight: bold;
}

[...]

.page-title {
    font-size: 3rem;
    line-height: 1.4;
    font-family: "My Web Font", sans-serif;
    font-weight: bold;
}

[...]

    .user-profile__title {
        font-size: 1.2rem;
        line-height: 1.5;
        font-family: "My Web Font", sans-serif;
        font-weight: bold;
    }

这里我们重复写了一段有意义的 CSS片段,这两个声明要始终在一起声明。这种情况下,我们可能需要将重复的部分分离出来

这里建议使用混入(mixin)而不是 @extend,理由是即使这两个声明题材上分为了一组,但规则集仍然是分开的,无关的实体。使用 @extend 会使得这些不相关规则集作为一组,把不相关变成了相关

下面是混入(mixin)后的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@mixin my-web-font() {
    font-family: "My Web Font", sans-serif;
    font-weight: bold;
}

.btn {
    display: inline-block;
    padding: 1em 2em;
    @include my-web-font();
}

[...]

.page-title {
    font-size: 3rem;
    line-height: 1.4;
    @include my-web-font();
}

[...]

    .user-profile__title {
        font-size: 1.2rem;
        line-height: 1.5;
        @include my-web-font();

现在两个声明只在一个存在一份,这样减少了重复工作。如果我们要改变 Web字体,或者将它变成 font-weight: regular;,我们只需要修改一个地方就行了。

简而言之,只有 DRY 代码在事实上,题材上相关的。不要试图减少完全巧合的重复:重复会比错误的抽象更好

扩展阅读

组合而非继承

现在我们习惯于关注抽象和建立单一职责,我们应该在一个大的立场上从一系列更小的组件部分编写更复杂的合成物。Nicole Sullivan 将它比作乐高,微小的,单一功能的片段可以合成很多不同数量和排列来创建大量看起来不一样的结果组合。

通过组合构建的想法一直就存在,并且经常被称为组合而非继承。这条原则建议大型的项目应该由较小的,对立的部分组合而成,而不是从另一个更大的,整体对象中继承行为而来,这能使你的代码解耦,本质上不依赖其他东西。

组合是在架构中使用的非常有价值的一个原则,特别是当考虑到基于组件的用户界面。这将意味着你能更容易地重复利用功能,也可以迅速从已有的可组合的对象上构建更大的UI组件。再回到单一职能原则中错误消息的例子,我们通过很多较小的不相关的对象来创建了一个完整的UI组件。

关注点分离

关注分离点原则,首先听起来像是单一职能原则。关注点分离描述了代码应该被

分成不同的部分,每一个部分强调独立的重点。一个关注点是能够影响计算机程序代码的一组信息。管组点分离(SoC)表现较好的程序被称为是模块化编程。

模块化很可能是我们知道的一个词,理念是将 UI和CSS 分解成更小,能组合的部分。关注点分离是一个正式的定义,它包含了模块化和封装的概念。在 CSS 中意思是构建独立的组件,编写在同一时间它只关注一个任务的代码。

这个词是 Edsger W. Dijkstra 提出的,他有更优雅的描述:

略。

很完美,这里的思想是在同一时间完全专注一件事情,很好地完成一件事情同时尽可能少地关注代码的其他方面。一旦完成,并且这些分离点是独立的,就意味着他们可能是独立的,解耦和封装好的,你就可以开始后将它们组合成一个更大的项目。

布局是一个很好的例子。如果你在用栅格(grid)系统,参与布局的代码都应该独立存在,不要引用其他东西。你缩写的代码只是为了布局,仅此而已。

1
2
3
4
5
6
7
8
9
<div class="layout">

    <div class="layout__item  two-thirds">
    </div>

    <div class="layout__item  one-third">
    </div>

</div>

接下来要写的是新的独立的代码,来处理布局内部的样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div class="layout">

    <div class="layout__item  two-thirds">
        <section class="content">
            ...
        </section>
    </div>

    <div class="layout__item  one-third">
        <section class="sub-content">
            ...
        </section>
    </div>

</div>

关注点分离能让代码保持独立,隔离,最终更多的维护。注重关注点分离的代码可以更自信的被修改,编辑,扩展和维护,因为知道他的职责范围。我们知道,例如修改布局,我们只需要修改布局就行了。

关注点分离增加了代码的可重用性和信心同时减少了依赖。

误区

对关注点分离,当应用到 HTML 和 CSS 时,我能感觉到有很多不幸的误解。它们好像都围绕一些形式

在标记中用 CSS 类打破了关注点分离。

不幸地,并不是这样。关注点分离确实存在于 HTML和 CSS(包括JS) 中,但不是很多人认为的那样。

当关注点分离应用到前端代码中时,不是说 CSS在HTML中完全是样式的钩子,模糊了关注点之间的界限,而是关于我们使用标记和样式使用不同语言的事实。

在CSS已经被广泛地应用之前,我们使用 table 布局内容,使用 font 元素加上 color 属性来修饰元素样式。这里的问题是 HTML 被用来创建内容同时改变样式,他们之间少了任何一个都不行。这完全缺失了关注点分离,这是个问题。CSS 的职责是提供全新的语法来为元素应用样式,允许我们将内容和样式在两种技术中分离开。

另一个常见的争论是 在 HTML中使用类会把样式信息加入标记中

所以,为了规避它,有人采用了类似如下选择器

1
2
body > header:first-of-type > nav > ul > li > a {
}

这段CSS看起来是为网站主导航应用样式,有一个常见的问题是位置依赖,没有选择器意图,并且高特殊性,不过确实做到了开发者试图要避免的东西(仅仅是反面的),即它将DOM信息放在了CSS中。积极地尝试避免任何把样式信息和钩子放在标记找中只会导致因为DOM信息的样式过重。

简而言之,把类放在标记中没有违反关注点分离原则。类很少作为 API 去连接两个独立的重点,分离关注点最简单的方法是编写组织较好的 HTML和CSS,并且将二者用合理的明确的类连接到一起。

ES6 Examples

Source: tagtree.io/courses/expert-es6/do

Arrow functions

1
2
3
4
5
6
7
8
9
let func = (x,y) => x + y;

let func = (x,y) => {
    let result = x + y;

    console.log('calculated result as ', result);

    return result;
}

Arrow scope

1
2
3
4
5
6
7
8
9
10
function sayHello(){
    this.name = 'hendrik';

    setTimeout(()=>{
        console.log('hello ' + this.name);
    }, 1500);
}

sayHello();
//output: 'hello hendrik'

String templates

1
2
3
4
function sayHello(name, surname){
    console.log(`hello there ${name} ${surname}.
    The time is now     ${new Date()}`);
}

Let scope

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var name = 'Fido';
var breed = 'schnauzer';
var owners = ['Hendrik', 'Alice'];

console.log(`${name} (${breed}):`);
for(let i = 0; i < owners.length; i++){
    let name = owners[i];
    console.log('Owner ' + name);
}

console.log(name);

//output:
//Fido (schnauzer):
//Owner Hendrik
//Owner Alice
//Fido

Destructuring arrays

1
2
3
4
5
6
let positions = ['Guy that won the race', 'Runner up', 'Third guy'];

let [winner,runnerUp] = positions;

// winner === 'Guy that won the race'
// runnerUp === 'Runner up'

Destructuring objects

1
2
3
4
5
6
7
8
9
function sayHello(args){
    let {firstName: name, lastName: surname, message: message} = args;

    console.log(`${message} ${name} ${surname}`);
}

sayHello({firstName: 'Hendrik', lastName: 'Swanepoel', message: 'Hi there '});

//output: Hi there Hendrik Swanepoel

Object literals

1
2
3
4
5
6
7
let genre = 'Alternative';
let band = 'Radiohead';

let searchRequest = {
    genre,
    band
};

Default args

1
2
3
function ajax(url, method='GET'){
    //do some xhr magic here
}

Spread operator

1
2
3
4
5
6
7
function sayHello(name, surname){

}

let args = ['Thom', 'Yorke'];

sayHello(...args);

Classes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Album {
    constructor(currentSales) {
        this.sales = currentSales;
    }

    recordSales(nr) {
        this.sales+= nr;
    }
}

let album = new Album(50);

album.recordSales(5);

console.log(album.sales);

//output: 55

Class inheritance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Ball {
    constructor(colour) {
        this.colour = colour
    }

    getShape() {
        return "round";
    }

    kick() {
        return "fly in opposite direction of foot";
    }
}

// The super keyword allows you to call the 
// corresponding function on the class you are extending.
class RugbyBall extends Ball {
    constructor(colour) {
        super(colour);
    }

    getShape() {
        return "oval";
    }

    kick(contact) {
        if(contact == 'clean') {
            return super();
        }else {
            return "Some random direction";
        }
    }
}

let myBall = new RugbyBall('brown')

Generators

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Tell the JS engine that the function is a generator function
function* generate5(){
    let i = 0;

    while(true){
        i += 5;

        yield i;
    }
}


let gen5 = generate5();

for(let i = 0; i < 10; i += 1){
    console.log(gen5.next().value);
}

Git 常用的分支操作

Create a branch

Create a new develop branch based on remote

1
git checkout -b develop origin/develop

Push branch develop to remote

1
git push origin develop

Merge branch

Merge branch develop to branch master

1
2
git checkout master
git merge --no-ff develop

Merge and update

1
2
3
4
5
6
7
8
9
10
git checkout develop

# Update FETCH_HEAD
git fetch origin develop

# Merge feature to develop
git merge feature

# Merge remote branch to local's
git merge origin/develop

Delete branchs

Delete local branch release

1
git branch -d release

Delete remote branch release

1
2
3
4
git push origin :release

# or
git push origin --delete release

Delete remotes/origin/xxx

1
2
3
4
git branch --delete --remotes origin/xxx

# shorthand
git branch -dr origin/xxx

References

Underscore 中的 _.throttle 和 _.debounce 函数

Underscore 是一个常用的JavaScript函数库,提供了大量工具函数,对原生 JavaScript 中集合(Collections)、数组(Arrays)、函数(Functions)、对象(Objects)提供很便利的操作,其函数式编程的风格灵活有且简洁。其中的常用工具中包括两个函数 _.throttle_.debounce,用来控制函数的调用或执行的方式。

场景

当我们用 JavaScript 去做一些用户与 Web App 交互的操作任务时,通常会去监听页面视图上的事件,以响应用户的某一次操作。其中有些事件在简单的操作时会频繁的发生,比如滚动,缩放等。如果此时有对应事件的回调处理函数,这些函数会在短时间被连续调用多次。如果函数中处理的事情很繁重,浏览器很可能会响应缓慢。

_.throttle

文档给的解释为:

创建并返回一个新的函数,此函数是原函数的一个包装。当此函数被反复调用时,在给定的时间段(毫秒)内只会调用原来的函数一次。当此函数首次被调用时会立即执行原函数,即默认在时间段的开始执行,如果不想一开始就调用可以传递参数 {leading: false},如果不想再在时间段末尾执行,可以传递选项 {trailing:false}

_.throttle 函数调用方式为

_.throttle = function(func, wait, options)

debounce 函数可以使某个任务执行不是很频繁,以此提升浏览器性能。在频繁执行的事件回调,连续重复的Ajax请求时可以用来控制调用的频率和次数。

_.debounce

文档解释是:

创建并返回一个新封装后的函数,但其连续被调用时,函数会延迟其执行直到其上一次被调用完成等待(wait/millseconds)的时间之后。第三个参数 immediate 表示是否立即执行原函数。

_.debounce 函数调用方式为

_.debounce(function, wait, [immediate]) 

在限制一个函数执行频率时很有用,比如 resize 和 scroll 事件的回调函数。

参考文档