月度归档:2014年07月

ubuntu 14.04 系统里安装、配置、运行、连接mongodb

参考http://docs.mongodb.org/manual/tutorial/install-mongodb-on-ubuntu/官方文档
1、导入MongoDB public key
运行

sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10

1
2、创建MongoDB的文件列表
运行

echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list

2
3、更新本地包的数据
运行

sudo apt-get update

4、安装MongoDB

  1. 安装最新稳定版,运行
    sudo apt-get install mongodb-org
  2. 安装特定版本,运行
    apt-get install mongodb-org=2.6.1 mongodb-org-server=2.6.1 mongodb-org-shell=2.6.1 mongodb-org-mongos=2.6.1 mongodb-org-tools=2.6.1

3
5、运行MongoDB

sudo service mongod start

node.js – Difference between app.use and app.get in express.js

http://stackoverflow.com/questions/15601703/difference-between-app-use-and-app-get-in-express-js

app.use() is intended for binding middleware to your application. The path is a “mount” or “prefix” path and limits the middleware to only apply to any paths requested that begin with it. It can even be used to embed another application:

// subapp.js
var express = require('express');
var app = modules.exports = express();
// ...
// server.js
var express = require('express');
var app = express();
app.use('/subapp', require('./subapp'));
// ...

By specifying / as a “mount” path, app.use() will respond to any path that starts with /, which are all of them and regardless of HTTP verb used:
GET /
PUT /foo
POST /foo/bar
etc.
app.get(), on the other hand, is part of Express’ application routing and is intended for matching and handling a specific route when requested with the GET HTTP verb:
GET /
And, the equivalent routing for your example of app.use() would actually be:

app.all(/^/.*/, function (req, res) {
    res.send('Hello');
});

Node.js 连接 mongodb 教程

新建一个文件夹存放我们的js文件
1.package.json
使用mongodb

{
  "name": "test",
  "version": "0.0.1",
  "private": true,
  "scripts": {
  "start": "node app.js"
  },
  "dependencies": {
  "mongodb":"1.4.7"
  }
}

2.创建文件mongo.js

var MongoClient = require('mongodb').MongoClient;
var db;
var connected = false;
module.exports = {
  connect: function(url, callback){
  MongoClient.connect(url, function(err, _db){
    if (err) { throw new Error('Could not connect: '+err); }
    db = _db;
    connected = true;
    callback(db);
    });
  },
  collection: function(name){
    if (!connected) {
    throw new Error('Must connect to Mongo before calling "collection"');
    }
    return db.collection(name);
  }
};

3.创建app.js

var mongo = require('./mongo');
var mongoUrl = "mongodb://localhost:27017/test";
mongo.connect(mongoUrl, function(){
console.log('Connected to mongo at: ' + mongoUrl);
var coll = mongo.collection('users');
var userObject = {
  username: "admin",
  password: "admin"
};
// create the new user
coll.insert(userObject, function(err,user){
  console.log("created user");
});
coll.find().toArray(function(err, results) {
  console.dir(results);
  });
});

 
源码下载:https://github.com/shengoo/mongotest
效果如下:
mongodb test
 

从输入 URL 到页面加载完成的过程中都发生了什么事情

原文链接.

背景

本文来自于之前我发的一篇微博:

不过写这篇文章并不是为了帮大家准备面试,而是想借这道题来介绍计算机和互联网的基础知识,让读者了解它们之间是如何关联起来的。

为了便于理解,我将整个过程分为了六个问题来展开。

第一个问题:从输入 URL 到浏览器接收的过程中发生了什么事情?

从触屏到 CPU

首先是「输入 URL」,大部分人的第一反应会是键盘,不过为了与时俱进,这里将介绍触摸屏设备的交互。

触摸屏一种传感器,目前大多是基于电容(Capacitive)来实现的,以前都是直接覆盖在显示屏上的,不过最近出现了 3 种嵌入到显示屏中的技术,第一种是 iPhone 5 的 In-cell,它能减小了 0.5 毫米的厚度,第二种是三星使用的 On-cell 技术,第三种是国内厂商喜欢用的 OGS 全贴合技术,具体细节可以阅读这篇文章

当手指在这个传感器上触摸时,有些电子会传递到手上,从而导致该区域的电压变化,触摸屏控制器芯片根据这个变化就能计算出所触摸的位置,然后通过总线接口将信号传到 CPU 的引脚上。

以 Nexus 5 为例,它所使用的触屏控制器是?Synaptics S3350B,总线接口为?I?C,以下是 Synaptics 触摸屏和处理器连接的示例:

左边是处理器,右边是触摸屏控制器,中间的 SDA 和 SCL 连线就是 I?C 总线接口。

CPU 内部的处理

移动设备中的 CPU 并不是一个单独的芯片,而是和 GPU 等芯片集成在一起,被称为 SoC(片上系统)。

前面提到了触屏和 CPU 的连接,这个连接和大部分计算机内部的连接一样,都是通过电气信号来进行通信的,也就是电压高低的变化,如下面的时序图:

在时钟的控制下,这些电流会经过?MOSFET?晶体管,晶体管中包含 N 型半导体和 P 型半导体,通过电压就能控制线路开闭,然后这些 MOSFET 构成了?CMOS,接着再由 CMOS 实现「与」「或」「非」等逻辑电路门,最后由逻辑电路门上就能实现加法、位移等计算,整体如下图所示(来自《计算机体系结构》):

cpu

除了计算,在 CPU 中还需要存储单元来加载和存储数据,这个存储单元一般通过触发器(Flip-flop)来实现,称为寄存器。

以上这些概念都比较抽象,推荐阅读「How to Build an 8-Bit Computer」这篇文章,作者基于晶体管、二极管、电容等原件制作了一个 8 位的计算机,支持简单汇编指令和结果输出,虽然现代 CPU 的实现要比这个复杂得多,但基本原理还是一样的。

另外其实我也是刚开始学习 CPU 芯片的实现,所以就不在这误人子弟了,感兴趣的读者请阅读本节后面推荐的书籍。

从 CPU 到操作系统内核

前面说到触屏控制器将电气信号发送到 CPU 对应的引脚上,接着就会触发 CPU 的中断机制,以 Linux 为例,每个外部设备都有一标识符,称为中断请求(IRQ)号,可以通过?/proc/interrupts?文件来查看系统中所有设备的中断请求号,以下是 Nexus 7 (2013) 的部分结果:

shell@flo:/ $ cat /proc/interrupts
            CPU0
  17:          0       GIC  dg_timer
 294:    1973609   msmgpio  elan-ktf3k
 314:        679   msmgpio  KEY_POWER

因为 Nexus 7 使用了 ELAN 的触屏控制器,所以结果中的 elan-ktf3k 就是触屏的中断请求信息,其中 294 是中断号,1973609 是触发的次数(手指单击时会产生两次中断,但滑动时会产生上百次中断)。

为了简化这里不考虑优先级问题,以 ARMv7 架构的处理器为例,当中断发生时,CPU 会停下当前运行的程序,保存当前执行状态(如 PC 值),进入 IRQ 状态),然后跳转到对应的中断处理程序执行,这个程序一般由第三方内核驱动来实现,比如前面提到的 Nexus 7 的驱动源码在这里?touchscreen/ektf3k.c

这个驱动程序将读取 I?C 总线中传来的位置数据,然后通过内核的?input_report_abs?等方法记录触屏按下坐标等信息,最后由内核中的input 子模块将这些信息都写进?/dev/input/event0?这个设备文件中,比如下面展示了一次触摸事件所产生的信息:

130|shell@flo:/ $ getevent -lt /dev/input/event0
[  414624.658986] EV_ABS       ABS_MT_TRACKING_ID   0000835c
[  414624.659017] EV_ABS       ABS_MT_TOUCH_MAJOR   0000000b
[  414624.659047] EV_ABS       ABS_MT_PRESSURE      0000001d
[  414624.659047] EV_ABS       ABS_MT_POSITION_X    000003f0
[  414624.659078] EV_ABS       ABS_MT_POSITION_Y    00000588
[  414624.659078] EV_SYN       SYN_REPORT           00000000
[  414624.699239] EV_ABS       ABS_MT_TRACKING_ID   ffffffff
[  414624.699270] EV_SYN       SYN_REPORT           00000000

从操作系统 GUI 到浏览器

前面提到 Linux 内核已经完成了对硬件的抽象,其它程序只需要通过监听?/dev/input/event0?文件的变化就能知道用户进行了哪些触摸操作,不过如果每个程序都这么做实在太麻烦了,所以在图像操作系统中都会包含 GUI 框架来方便应用程序开发,比如 Linux 下著名的?X

但 Android 并没有使用 X,而是自己实现了一套 GUI 框架,其中有个?EventHub?的服务会通过?epoll?方式监听?/dev/input/?目录下的文件,然后将这些信息传递到 Android 的窗口管理服务(WindowManagerService)中,它会根据位置信息来查找相应的 app,然后调用其中的监听函数(如 onTouch 等)。

就这样,我们解答了第一个问题,不过由于时间有限,这里省略了很多细节,想进一步学习的读者推荐阅读以下书籍。

扩展学习

第二个问题:浏览器如何向网卡发送数据?

从浏览器到浏览器内核

前面提到操作系统 GUI 将输入事件传递到了浏览器中,在这过程中,浏览器可能会做一些预处理,比如 Chrome 会根据历史统计来预估所输入字符对应的网站,比如输入了「ba」,根据之前的历史发现 90% 的概率会访问「www.baidu.com 」,因此就会在输入回车前就马上开始建立 TCP 链接甚至渲染了,这里面还有很多其它策略,感兴趣的读者推荐阅读?High Performance Networking in Chrome

接着是输入 URL 后的「回车」,这时浏览器会对 URL 进行检查,首先判断协议,如果是 http 就按照 Web 来处理,另外还会对这个 URL 进行安全检查,然后直接调用浏览器内核中的对应方法,比如?WebView?中的 loadUrl 方法。

在浏览器内核中会先查看缓存,然后设置 UA 等 HTTP 信息,接着调用不同平台下网络请求的方法。

需要注意浏览器和浏览器内核是不同的概念,浏览器指的是 Chrome、Firefox,而浏览器内核则是 Blink、Gecko,浏览器内核只负责渲染,GUI 及网络连接等跨平台工作则是浏览器实现的

HTTP 请求的发送

因为网络的底层实现是和内核相关的,所以这一部分需要针对不同平台进行处理,从应用层角度看主要做两件事情:通过 DNS 查询 IP、通过 Socket 发送数据,接下来就分别介绍这两方面的内容。

DNS 查询

应用程序可以直接调用 Libc 提供的?getaddrinfo()?方法来实现 DNS 查询。

DNS 查询其实是基于 UDP 来实现的,这里我们通过一个具体例子来了解它的查找过程,以下是使用?dig +trace fex.baidu.com?命令得到的结果(省略了一些):

; <<>> DiG 9.8.3-P1 <<>> +trace fex.baidu.com
;; global options: +cmd
.           11157   IN  NS  g.root-servers.net.
.           11157   IN  NS  i.root-servers.net.
.           11157   IN  NS  j.root-servers.net.
.           11157   IN  NS  a.root-servers.net.
.           11157   IN  NS  l.root-servers.net.
;; Received 228 bytes from 8.8.8.8#53(8.8.8.8) in 220 ms
com.            172800  IN  NS  a.gtld-servers.net.
com.            172800  IN  NS  c.gtld-servers.net.
com.            172800  IN  NS  m.gtld-servers.net.
com.            172800  IN  NS  h.gtld-servers.net.
com.            172800  IN  NS  e.gtld-servers.net.
;; Received 503 bytes from 192.36.148.17#53(192.36.148.17) in 185 ms
baidu.com.      172800  IN  NS  dns.baidu.com.
baidu.com.      172800  IN  NS  ns2.baidu.com.
baidu.com.      172800  IN  NS  ns3.baidu.com.
baidu.com.      172800  IN  NS  ns4.baidu.com.
baidu.com.      172800  IN  NS  ns7.baidu.com.
;; Received 201 bytes from 192.48.79.30#53(192.48.79.30) in 1237 ms
fex.baidu.com.      7200    IN  CNAME   fexteam.duapp.com.
fexteam.duapp.com.  300 IN  CNAME   duapp.n.shifen.com.
n.shifen.com.       86400   IN  NS  ns1.n.shifen.com.
n.shifen.com.       86400   IN  NS  ns4.n.shifen.com.
n.shifen.com.       86400   IN  NS  ns2.n.shifen.com.
n.shifen.com.       86400   IN  NS  ns5.n.shifen.com.
n.shifen.com.       86400   IN  NS  ns3.n.shifen.com.
;; Received 258 bytes from 61.135.165.235#53(61.135.165.235) in 2 ms

可以看到这是一个逐步缩小范围的查找过程,首先由本机所设置的 DNS 服务器(8.8.8.8)向 DNS 根节点查询负责 .com 区域的域务器,然后通过其中一个负责 .com 的服务器查询负责 baidu.com 的服务器,最后由其中一个 baidu.com 的域名服务器查询 fex.baidu.com 域名的地址。

可能你在查询某些域名的时会发现和上面不一样,最底将看到有个奇怪的服务器抢先返回结果。。。

这里为了方便描述,忽略了很多不同的情况,比如 127.0.0.1 其实走的是?loopback,和网卡设备没关系;比如 Chrome 会在浏览器启动的时预先查询 10 个你有可能访问的域名;还有 Hosts 文件、缓存时间 TTL(Time to live)的影响等。

通过 Socket 发送数据

有了 IP 地址,就可以通过 Socket API 来发送数据了,这时可以选择 TCP 或 UDP 协议,具体使用方法这里就不介绍了,推荐阅读?Beej’s Guide to Network Programming

HTTP 常用的是 TCP 协议,由于 TCP 协议的具体细节到处都能看到,所以本文就不介绍了,这里谈一下 TCP 的 Head-of-line blocking 问题:假设客户端的发送了 3 个 TCP 片段(segments),编号分别是 1、2、3,如果编号为 1 的包传输时丢了,即便编号 2 和 3 已经到达也只能等待,因为 TCP 协议需要保证顺序,这个问题在 HTTP pipelining 下更严重,因为 HTTP pipelining 可以让多个 HTTP 请求通过一个 TCP 发送,比如发送两张图片,可能第二张图片的数据已经全收到了,但还得等第一张图片的数据传到。

为了解决 TCP 协议的性能问题,Chrome 团队去年提出了?QUIC?协议,它是基于 UDP 实现的可靠传输,比起 TCP,它能减少很多来回(round trip)时间,还有前向纠错码(Forward Error Correction)等功能。目前 Google Plus、 Gmail、Google Search、blogspot、Youtube 等几乎大部分 Google 产品都在使用 QUIC,可以通过?chrome://net-internals/#spdy?页面来发现。

虽然目前除了 Google 还没人用 QUIC,但我觉得挺有前景的,因为优化 TCP 需要升级系统内核(比如?Fast Open)。

浏览器对同一个域名有连接数限制,大部分是 6,我以前认为将这个连接数改大后会提升性能,但实际上并不是这样的,Chrome 团队有做过实验,发现从 6 改成 10 后性能反而下降了,造成这个现象的因素有很多,如建立连接的开销、拥塞控制等问题,而像 SPDY、HTTP 2.0 协议尽管只使用一个 TCP 连接来传输数据,但性能反而更好,而且还能实现请求优先级。

另外,因为 HTTP 请求是纯文本格式的,所以在 TCP 的数据段中可以直接分析 HTTP 的文本,如果发现。。。

Socket 在内核中的实现

前面说到浏览器的跨平台库通过调用 Socket API 来发送数据,那么 Socket API 是如何实现的呢?

以 Linux 为例,它的实现在这里?socket.c,目前我还不太了解,推荐读者看看?Linux kernel map,它标注出了关键路径的函数,方便学习从协议栈到网卡驱动的实现。

底层网络协议的具体例子

接下来如果继续介绍 IP 协议和 MAC 协议可能很多读者会晕,所以本节将使用?Wireshark?来通过具体例子讲解,以下是我请求百度首页时抓取到的网络数据:

wireshark

最底下是实际的二进制数据,中间是解析出来的各个字段值,可以看到其中最底部为 HTTP 协议(Hypertext Transfer Protocol),在 HTTP 之前有 54 字节(0x36),这就是底层网络协议所带来的开销,我们接下来对这些协议进行分析。

在 HTTP 之上是 TCP 协议(Transmission Control Protocol),它的具体内容如下图所示:

wireshark

通过底部的二进制数据,可以看到 TCP 协议是加在 HTTP 文本前面的,它有 20 个字节,其中定义了本地端口(Source port)和目标端口(Destination port)、顺序序号(Sequence Number)、窗口长度等信息,以下是 TCP 协议各个部分数据的完整介绍:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          Source Port          |       Destination Port        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        Sequence Number                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Acknowledgment Number                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Data |           |U|A|E|R|S|F|                               |
| Offset| Reserved  |R|C|O|S|Y|I|            Window             |
|       |           |G|K|L|T|N|N|                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           Checksum            |         Urgent Pointer        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Options                    |    Padding    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                             data                              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

具体各个字段的作用这里就不介绍了,感兴趣的读者可以阅读?RFC 793,并结合抓包分析来理解。

需要注意的是,在 TCP 协议中并没有 IP 地址信息,因为这是在上一层的 IP 协议中定义的,如下图所示:

wireshark

IP 协议同样是在 TCP 前面的,它也有 20 字节,在这里指明了版本号(Version)为 4,源(Source) IP 为?192.168.1.106,目标(Destination) IP 为?119.75.217.56,因此 IP 协议最重要的作用就是确定 IP 地址。

因为 IP 协议中可以查看到目标 IP 地址,所以如果发现某些特定的 IP 地址,某些路由器就会。。。

但是,光靠 IP 地址是无法进行通信的,因为 IP 地址并不和某台设备绑定,比如你的笔记本的 IP 在家中是?192.168.1.1,但到公司就变成172.22.22.22?了,所以在底层通信时需要使用一个固定的地址,这就是 MAC(media access control) 地址,每个网卡出厂时的 MAC 地址都是固定且唯一的。

因此再往上就是 MAC 协议,它有 14 字节,如下所示:

wireshark

当一台电脑加入网络时,需要通过?ARP?协议告诉其它网络设备它的 IP 及对应的 MAC 地址是什么,这样其它设备就能通过 IP 地址来查找对应的设备了。

最顶上的 Frame 是代表 Wireshark 的抓包序号,并不是网络协议

就这样,我们解答了第二个问题,不过其实这里面还有很多很多细节没介绍,建议大家通过下面的书籍进一步学习。

扩展学习

第三个问题:数据如何从本机网卡发送到服务器?

从内核到网络适配器(Network Interface Card)

前面说到调用 Socket API 后内核会对数据进行底层协议栈的封装,接下来启动?DMA?控制器,它将从内存中读取数据写入网卡。

以 Nexus 5 为例,它使用的是博通?BCM4339?芯片通信,接口采用了 SD 卡一样的?SDIO,但这个芯片的细节并没有公开资料,所以这里就不讨论了。

连接 Wi-Fi 路由

Wi-Fi 网卡需要通过 Wi-Fi 路由来与外部通信,原理是基于无线电,通过电流变化来产生无线电,这个过程也叫「调制」,而反过来无线电可以引起电磁场变化,从而产生电流变化,利用这个原理就能将无线电中的信息解读出来就叫「解调」,其中单位时间内变化的次数就称为频率,目前在 Wi-Fi 中所采用的频率分为 2.4 GHz 和 5 GHz 两种。

在同一个 Wi-Fi 路由下,因为采用的频率相同,同时使用时会发生冲突,为了解决这个问题,Wi-Fi 采用了被称为?CSMA/CA?的方法,简单来说就是在传输前先确认信道是否已被使用,没有才发送数据。

而同样基于无线电原理的 2G/3G/LTE 也会遇到类似的问题,但它并没有采用 Wi-Fi 那样的独占方案,而是通过频分(FDMA)、时分(TDMA)和码分(CDMA)来进行复用,具体细节这里就不展开了。

以小米路由为例,它使用的芯片是?BCM 4709,这个芯片由 ARM Cortex-A9 处理器及流量(Flow)硬件加速组成,使用硬件芯片可以避免经过操作系统中断、上下文切换等操作,从而提升了性能。

路由器中的操作系统可以基于?OpenWrt?或?DD-WRT?来开发的,具体细节我不太了解,所以就不展开了。

因为内网设备的 IP 都是类似?192.168.1.x?这样的内网地址,外网无法直接向这个地址发送数据,所以网络数据在经过路由时,路由会修改相关地址和端口,这个操作称为?NAT?映射。

最后家庭路由一般会通过双绞线连接到运营商网络的。

运营商网络内的路由

数据过双绞线发送到运营商网络后,还会经过很多个中间路由转发,读者可以通过 traceroute 命令或者在线可视化工具来查看这些路由的 ip 和位置。

当数据传递到这些路由器后,路由器会取出包中目的地址的前缀,通过内部的转发表查找对应的输出链路,而这个转发表是如何得到的呢?这就是路由器中最重要的选路算法了,可选的有很多,我对这方面并不太了解,看起来维基百科上的词条列得很全。

主干网间的传输

对于长线的数据传输,通常使用光纤作为介质,光纤是基于光的全反射来实现的,使用光纤需要专门的发射器通过电致发光(比如 LED)将电信号转成光,比起前面介绍的无线电和双绞线,光纤信号的抗干扰性要强得多,而且能耗也小很多。

既然是基于光来传输数据,数据传输速度也就取决于光的速度,在真空中的光速接近于 30 万千米/秒,由于光纤包层(cladding)中的折射率(refractive index)为 1.52,所以实际光速是 20 万千米/秒左右,从首都机场飞往广州白云机场的距离是 1967 千米,按照这个距离来算需要花费 10 毫秒才能抵达。这意味着如果你在北京,服务器在广州,等你发出数据到服务器返回数据至少得等 20 毫秒,实际情况预计是 2- 3 倍,因为这其中还有各个节点路由处理的耗时,比如我测试了一个广州的 IP 发现平均延迟为 60 毫秒。

这个延迟是现有科技无法解决的(除非找到超过光速的方法),只能通过 CDN 来让传输距离变短,或尽量减少串行的来回请求(比如 TCP 建立连接所需的 3 次握手)。

IDC 内网

数据通过光纤最终会来到服务器所在的 IDC 机房,进入 IDC 内网,这时可以先通过分光器将流量镜像一份出来方便进行安全检查等分析,还能用来进行。。。

这里的带宽成本很高,是按照峰值来结算的,以每月每 Gbps(注意这里指的是 bit,而不是 Byte)为单位,北京这边价格在十万人民币以上,一般网站使用 1G 到 10G 不等。

接下来光纤中的数据将进入集群(Cluster)交换机,然后再转发到机架(Rack)顶部的交换机,最后通过这个交换机的端口将数据发往机架中的服务器,可以参考下图(来自 Open Compute):

rack

上图左边是正面,右边是侧面,可以看到顶部为交换机所留的位置。

以前这些交换机的内部实现是封闭的,相关厂商(如思科、Juniper 等)会使用特定的处理器和操作系统,外界难以进行灵活控制,甚至有时候需要手工配置,但这几年随着?OpenFlow?技术的流行,也出现了开放交换机硬件(Open Switch Hardware),比如 Intel 的网络平台,推荐感兴趣的读者建议看看它的视频,比文字描述清晰多了。

需要注意的是,一般网络书中提到的交换机都只具备二层(MAC 协议)的功能,但在 IDC 中的交换器基本上都具备三层(IP 协议)的功能,所以不需要有专门的路由了。

最后,因为 CPU 处理的是电气信号,所以光纤中的光线需要先使用相关设备通过光电效应将光信号转成电信号,然后进入服务器网卡。

服务器 CPU

前面说到数据已经到达服务器网卡了,接着网卡会将数据拷贝到内存中(DMA),然后通过中断来通知 CPU,目前服务器端的 CPU 基本上都是?Intel Xeon,不过这几年出现了一些新的架构,比如在存储领域,百度使用?ARM?架构来提升存储密度,因为 ARM 的功耗比 Xeon 低得多。而在高性能领域,Google 最近在尝试基于?POWER?架构的 CPU 来开发的服务器,最新的 POWER8 处理器可以并行执行 96 个线程,所以对高并发的应用应该很有帮助。

扩展学习

第四个问题:服务器接收到数据后会进行哪些处理?

为了避免重复,这里将不再介绍操作系统,而是直接进入后端服务进程,由于这方面有太多技术选型,所以我只挑几个常见的公共部分来介绍。

负载均衡

请求在进入到真正的应用服务器前,可能还会先经过负责负载均衡的机器,它的作用是将请求合理地分配到多个服务器上,同时具备具备防攻击等功能。

负载均衡具体实现有很多种,有直接基于硬件的 F5,有操作系统传输层(TCP)上的?LVS,也有在应用层(HTTP)实现的反向代理(也叫七层代理),接下来将介绍 LVS 及反向代理。

负载均衡的策略也有很多,如果后面的多个服务器性能均衡,最简单的方法就是挨个循环一遍(Round-Robin),其它策略就不一一介绍了,可以参考 LVS 中的算法

LVS

LVS 的作用是从对外看来只有一个 IP,而实际上这个 IP 后面对应是多台机器,因此也被成为 Virtual IP。

前面提到的 NAT 也是一种 LVS 中的工作模式,除此之外还有 DR 和 TUNNEL,具体细节这里就不展开了,它们的缺点是无法跨网段,所以百度自己开发了 BVS 系统。

反向代理

方向代理是工作在 HTTP 上的,具体实现可以基于 HAProxy 或 Nginx,因为反向代理能理解 HTTP 协议,所以能做非常多的事情,比如:

  • 进行很多统一处理,比如防攻击策略、放抓取、SSL、gzip、自动性能优化等
  • 应用层的分流策略都能在这里做,比如对 /xx 路径的请求分到 a 服务器,对 /yy 路径的请求分到 b 服务器,或者按照 cookie 进行小流量测试等
  • 缓存,并在后端服务挂掉的时候显示友好的 404 页面
  • 监控后端服务是否异常
  • ??

Nginx 的代码写得非常优秀,从中能学到很多,对高性能服务端开发感兴趣的读者一定要看看。

Web Server 中的处理

请求经过前面的负载均衡后,将进入到对应服务器上的 Web Server,比如 Apache、Tomcat、Node.JS 等。

以 Apache 为例,在接收到请求后会交给一个独立的进程来处理,我们可以通过编写 Apache 扩展来处理,但这样开发起来太麻烦了,所以一般会调用 PHP 等脚本语言来进行处理,比如在 CGI 下就是将 HTTP 中的参数放到环境变量中,然后启动 PHP 进程来执行,或者使用 FastCGI 来预先启动进程。

(等后续有空再单独介绍 Node.JS 中的处理)

进入后端语言

前面说到 Web Server 会调用后端语言进程来处理 HTTP 请求(这个说法不完全正确,有很多其它可能),那么接下来就是后端语言的处理了,目前大部分后端语言都是基于虚拟机的,如 PHP、Java、JavaScript、Python 等,但这个领域的话题非常大,难以讲清楚,对 PHP 感兴趣的读者可以阅读我之前写的?HHVM 介绍文章,其中提到了很多虚拟机的基础知识。

Web 框架(Framework)

如果你的 PHP 只是用来做简单的个人主页「Personal Home Page」,倒没必要使用 Web 框架,但如果随着代码的增加会变得越来越难以管理,所以一般网站都会会基于某个 Web 框架来开发,因此在后端语言执行时首先进入 Web 框架的代码,然后由框架再去调用应用的实现代码。

可选的?Web 框架非常多,这里就不一一介绍了。

读取数据

这部分不展开了,从简单的读写文件到数据中间层,这里面可选的方案实在太多。

扩展学习

第五个问题:服务器返回数据后浏览器如何处理?

前面说到服务端处理完请求后,结果将通过网络发回客户端的浏览器,从本节开始将介绍浏览器接收到数据后的处理,值得一提的是这方面之前有一篇不错的文章?How Browsers Work,所以很多内容我不想再重复介绍,因此将重点放在那篇文章所忽略的部分。

从 01 到字符

HTTP 请求返回的 HTML 传递到浏览器后,如果有 gzip 会先解压,然后接下来最重要的问题是要知道它的编码是什么,比如同样一个「中」字,在 UTF-8 编码下它的内容其实是「11100100 10111000 10101101」也就是「E4 B8 AD」,而在 GBK 下则是「11010110 11010000」,也就是「D6 D0」,如何才能知道文件的编码?可以有很多判断方法:

  • 用户设置,在浏览器中可以指定页面编码
  • HTTP 协议中
  • <meta>?中的 charset 属性值
  • 对于 JS 和 CSS
  • 对于 iframe

如果在这些地方都没指明,浏览器就很难处理,在它看来就是一堆「0」和「1」,比如「中文」,它在 UTF-8 下有 6 个字节,如果按照 GBK 可以当成「涓?枃」这 3 个汉字来解释,浏览器怎么知道到底是「中文」还是「涓?枃」呢?

不过正常人一眼就能认出「涓?枃」是错的,因为这 3 个字太不常见了,所以有人就想到通过判断常见字的方法来检测编码,典型的比如 Mozilla 的?UniversalCharsetDetection,不过这东东误判率也很高,所以还是指明编码的好。

这样后续对文本的操作就是基于「字符」(Character)的了,一个汉字就是一个字符,不用再关心它究竟是 2 个字节还是 3 个字节。

外链资源的加载

(待补充,这里有调度策略)

JavaScript 的执行

(后续再单独介绍,推荐大家看 R 大去年整理的这个帖子,里面有非常多相关资料,另外我两年前曾讲过?JavaScript 引擎中的性能优化,虽然有些内容不太正确了,但也可以看看)

从字符到图片

二维渲染中最复杂的要数文字显示了,虽然想想似乎很简单,不就是将某个文字对应的字形(glyph)找出来么?在中文和英文中这样做是没问题的,因为一个字符就对应一个字形(glyph),在字体文件中找到字形,然后画上去就可以了,但在阿拉伯语中是不行的,因为它有有连体形式。

(以后续再单独介绍,这里非常复杂)

跨平台 2D 绘制库

在不同操作系统中都提供了自己的图形绘制 API,比如 Mac OS X 下的 Quartz,Windows 下的 GDI 以及 Linux 下的 Xlib,但它们相互不兼容,所以为了方便支持跨平台绘图,在 Chrome 中使用了?Skia?库。

(以后再单独介绍,Skia 内部实现调用层级太多,直接讲代码可能不适合初学者)

GPU 合成

(以后续再单独介绍,虽然简单来说就是靠贴图,但还得介绍 OpenGL 以及 GPU 芯片,内容太长)

扩展学习

这节内容是我最熟悉,结果反而因为这样才想花更多时间写好,所以等到以后再发出来好了,大家先可以先看看以下几个站点:

第六个问题:浏览器如何将页面展现出来?

前面提到浏览器已经将页面渲染成一张图片了,接下来的问题就是如何将这张图片展示在屏幕上。

Framebuffer

以 Linux 为例,在应用中控制屏幕最直接的方法是将图像的 bitmap 写入?/dev/fb0?文件中,这个文件实际上一个内存区域的映射,这段内存区域称为 Framebuffer。

需要注意的是在硬件加速下,如 OpenGL 是不经过 Framebuffer 的。

从内存到 LCD

在手机的 SoC 中通常都会有一个 LCD 控制器,当 Framebuffer 准备好后,CPU 会通过?AMBA?内部总线通知 LCD 控制器,然后这个控制器读取 Framebuffer 中的数据,进行格式转换、伽马校正等操作,最终通过?DSI、HDMI 等接口发往 LCD 显示器。

以?OMAP5432?为例,下图是它所支持的一种并行数据传输:

DISPC-color24

LCD 显示

最后简单介绍一下 LCD 的显示原理。

首先,要想让人眼能看见,就必须有光线进入,要么通过反射、要么有光源,比如 Kindle 所使用的 E-ink 屏幕本身是不发光的,所以必须在有光线的地方才能阅读,它的优点是省电,但限制太大,所以几乎所有 LCD 都会自带光源。

目前 LCD 中通常使用?LED?作为光源,LED 接上电源后,在电压的作用下,内部的正负电子结合会释放光子,从而产生光,这种物理现象叫电致发光(Electroluminescence),这在前面介绍光纤时也介绍过。

以下是 iPod Touch 2 拆开后的样子:(来自?Wikipedia):

IPod_Touch_2G_Backlight.jpg

在上图中可以看到 6 盏 LED,这就是整个屏幕的光源,这些光源将通过反射的反射输出到屏幕中。

有了光源还得有色彩,在 LED 中通常做法是使用彩色滤光片(Color filter)来将 LED 光源转成不同颜色。

另外直接使用三种颜色的 LED 也是可行的,它能避免了滤光导致的光子浪费,降低耗电,很适用于智能手表这样的小屏幕,Apple 收购的 LuxVue 公司就采用的是这种方式,感兴趣的话可以去研究它的专利

LCD 屏幕上的每个物理像素点实际上是由红、绿、蓝 3 种色彩的点组成,每个颜色点能单独控制,下面是用显微镜放大后的情况(来自Wikipedia):

Wikipedia 上的图片解释

从上图可以看到每 3 种颜色的滤光片都全亮的时候就是白色,都灭就是黑色,如果你仔细看还能看到有些点并不是完全黑,这是字体上的反锯齿效果。

通过这 3 种颜色亮度的不同组合就能产生出各种色彩,如果每个颜色点能产生 256 种亮度,就能生成 256 * 256 * 256 = 16777216 种色彩。

并不是所有显示器的亮度都能达到 256,在选择显示器时有个参数是 8-Bit 或 6-Bit 面板,其中 8-Bit 的面板能在物理上达到 256 种亮度,而 6-Bit 的则只有 64 种,它需要靠刷新率控制(Frame rate control)技术来达到 256 的效果。

如何控制这些颜色点的亮度?这就要靠液晶体了,液晶体的特性是当有电流通过时会发生旋转,从而将部分光线挡住,所以只要通过电压控制液晶体的转动就能控制这个颜色点的亮度,目前手机屏幕中通常使用 TFT 控制器来对其进行控制,在 TFT 中最著名的要数 IPS 面板。

这些过滤后的光线大部分会直接进入眼睛,有些光还会在其它表面上经过漫(diffuse)反射或镜面(specular)反射后再进入眼睛,加上环境光的影响,要真正算出有多少光到眼睛是一个积分问题,感兴趣的读者可以研究基于物理的渲染

当光线进入眼睛后,接下来就是生物学的领域了,所以我们到此结束。

扩展学习

本文所忽略的内容

为了编写方便,前面的介绍中将很多底层细节实现忽略了,比如:

  • 内存相关
    • 堆,这里的分配策略有很多,比如 malloc?的实现
    • 栈,函数调用,已经有很多优秀的文章或书籍介绍了
    • 内存映射,动态库加载等
    • 队列几乎无处不在,但这些细节和原理没太大关系
  • 各种缓存
    • CPU 的缓存、操作系统的缓存、HTTP 缓存、后端缓存等等
  • 各种监控
    • 很多日志会保存下来以便后续分析

FAQ

从微博反馈来看,有些问题被经常问到,我就在这里统一回答吧,如果有其它问题请在评论中问。

Q:学那么多有什么用?根本用不着

A:计算机是人类最强大的工具,你不想了解它是如何运作的么?

Q:什么都了解一点,还不如精通一项吧?

A:非常认同,初期肯定需要先在某个领域精通,然后再去了解周边领域的知识,这样还能让你对之前那个领域有更深刻的理解。

Q:晒出来培养一堆面霸跟自己过不去?

A:本文其实写得很浅,每个部分都能再深入展开。

Q:这题要把人累死啊,说几天都说不完的

A:哈哈哈,大神你暴露了,题目只是手段,目的是将你这样的大牛挖掘出来。

大家的讨论

非常感谢各位大牛的参与讨论,这里搜集了其中的一些回答。

@WOODHEAD笨笨:请求被送往本地路由,接入商路由,旁路分析是否违法地址,连接被中断,浏览器无辜得显示网页不存在。严重的有人来查水表

caoz: 这不是我的面试题么! 还有一道题,用户反应我们网站卡,请问都有哪些可能性,以及排方法。

@caoz:写的还是不错的,但是还是有一些缺漏,比如arp欺骗? 著名的GFW的阻断策略,以及,一个URL可不是只有一个请求,多个请求的排队和寻址?此外,cdn, 智能dns解析机制等。//@ZRJ-:?http://t.cn/8smHpMF?从点击到呈现 — 详解一次HTTP请求 我大三的时候写的。。 啊

@唐福林:与时俱进,现在应该问从打开app到刷新出内容,整个过程中都发生了什么,如果感觉慢,怎么定位问题,怎么解决

@寒冬winter: 回复@Ivony:这题胜在区分度高,知识点覆盖均匀,再不懂的人,也能答出几句,而高手可以根据自己擅长的领域自由发挥,从URL规范、HTTP协议、DNS、CDN、到浏览器流式解析、CSS规则构建、layout、paint、onload/domready、JS执行、JS API绑定??

@JS小组:[哈哈] 小编想起来了,貌似刚从业那会儿,前端界最美丽的姐@sherrie_wong 面试问过小编这道题.然后我当时把知道的全说了,从浏览器解析,发请求,7层网络模型实际用的模型,TCP三次握手.经路由,交换机,DNS,到服务器.在是否需要与文件系统还是数据库打交道,再者分布式运算hadoop啥的…聊了太多.

@莴怖熵崴箔:这种就是流氓问题,我还想问从你按了键盘到屏幕上出现字符,中间都发生了什么事,提示一下:设想你是一个电子。哦,不对,电子又是什么

@寒冬winter:http://t.cn/zH20bR1?http://t.cn/zH20bR1?之前写了开头两篇,后面荒废中??

@ils传言:不提电厂发电机转了几圈的也干掉!//@Philonis高:不提交换机和路由器工作原理的全干掉!//@南非蜘蛛:从7层协议的角度说会比较全面。这种问题只有全栈工程师才能回答。

@耸肩的阿特拉斯阁下:DNS解析URL出IP/Port,浏览器连接并向此地址发出GET请求,web服务端(nginx、apache)接收到请求后,通过CGI等接口协议调用动态语言(php等),动态语言再连接数据库查询相应数据并处理,然后反馈给浏览器,浏览器解析反馈页面,通过html、javascript、css处理后呈现到屏幕??每个细节的话估计要800页的书

@一棹凌烟:这种面试题在系统领域的招聘里其实简单好使。还有一个类似的:从在键盘上敲下一个字符键开始,到在虚拟机里的terminal里显示出来,中间的过程是什么?

@ICT_朱亚东:记得6年前上胡伟武的芯片设计课,老胡第一节课就说,上完这门课,我希望你们能搞清楚,我翻了一页PPT,计算机内部都做了那些流水操作,当然啦,我是一点都不记得了。

@julyclyde:我们运维一般问一个TCP segment in a IP packet in an ethernet frame经过一个路由器之后发生什么变化

@西西福厮:从浏览器说起,操作系统相应键盘中断,事件队列处理,到互联网路由,到服务器网卡中断,到最后输出缓冲。。。细说能说两小时。

@Xscape:从键盘中断说起?回车前的预解析都很靠后了..//@纯白色燃烧: 从键盘到弹簧入万有引力而后直达量子力学。

@Bosn:然后从硬件再到电子??量子…薛定谔之猫…平行宇宙??乃至万能的哲学!!

@imPony:可深入到PN结中的电子流动层面

@巩小东-TX: 猜一下,浏览器组http报文sock发出,proxy过滤,收到处理头,未过期cache返回,http svr处理校验包,转为cgi协议给后端,后端map url,load code,与逻辑交互后生成html给svr,svr过滤cache给proxy,proxy给浏览器,拉去js完成html,浏览器渲染。

@yuange1975:我算对整个过程比较清楚,包含服务器的处理,web服务器和浏览器的处理以及安全问题,估计少有对两者的安全都研究过的。但面试时要清晰的比较完整的把大块流程列出来说明白,也有难度。估计也很难有机会时间去整理文章了。

@ShopEx王磊:我也问这个问题题好多年, 或者变通一下:从输入URL到展现, 都涉及到哪些缓存环节, 缓存的更新机制是怎样的

@一棹凌烟:这种面试题在系统领域的招聘里其实简单好使。还有一个类似的:从在键盘上敲下一个字符键开始,到在虚拟机里的terminal里显示出来,中间的过程是什么?

@智慧笨蛋: 确实可以维度不同的说,主要还是看颗粒度,光网络这段从wifi 解密,到NAT,到局间交换,ip包在以太网包映射等等就可以写一本书了

/@乔3少:放开了说所有互联网相关的知识都能体现的,比如dns、浏览器缓存,tcp连接、http响应,web服务的工作原理,浏览器的响应和渲染等等,刚刚在本子上列了下想到的安全威胁,很有意思!

前端工程与性能优化

来源:?fexbaidu? ? ?原文链接

每个参与过开发企业级 web 应用的前端工程师或许都曾思考过前端性能优化方面的问题。我们有雅虎 14 条性能优化原则,还有两本很经典的性能优化指导书:《高性能网站建设指南》、《高性能网站建设进阶指南》。经验丰富的工程师对于前端性能优化方法耳濡目染,基本都能一一列举出来。这些性能优化原则大概是在 7 年前提出的,对于 web 性能优化至今都有非常重要的指导意义。

然而,对于构建大型 web 应用的团队来说,要坚持贯彻这些优化原则并不是一件十分容易的事。因为优化原则中很多要求与工程管理相违背,比如“把 css 放在头部”和“把 js 放在尾部”这两条原则,我们不能让整个团队的工程师在写样式和脚本引用的时候都去修改同一份的页面文件。这会严重影响团队成员间并行开发的效率,尤其是在团队有版本管理的情况下,每天要花大量的时间进行代码修改合并,这项成本是难以接受的。因此在前端工程界,总会看到周期性的性能优化工作,辛勤的前端工程师们每到月圆之夜就会倾巢出动根据优化原则做一次最佳实践。

本文从一个全新的视角来思考 web 性能优化与前端工程之间的关系,通过解读百度前端集成解决方案小组(F.I.S)在打造高性能前端架构并统一百度 40 多条前端产品线的过程中所经历的技术尝试,揭示前端性能优化在前端架构及开发工具设计层面的实现思路。

性能优化原则及分类

笔者先假设本文的读者是有前端开发经验的工程师,并对企业级 web 应用开发及性能优化有一定的思考。因此我不会重复介绍雅虎 14 条性能优化原则,如果您没有这些前续知识的,请移步这里来学习。

首先,我们把雅虎 14 条优化原则,《高性能网站建设指南》以及《高性能网站建设进阶指南》中提到的优化点做一次梳理,如果按照优化方向分类可以得到这样一张表格:

优化方向 优化手段
请求数量 合并脚本和样式表,CSS Sprites,拆分初始化负载,划分主域
请求带宽 开启 GZip,精简 JavaScript,移除重复脚本,图像优化
缓存利用 使用 CDN,使用外部 JavaScript 和 CSS,添加 Expires 头,减少 DNS 查找,配置 ETag,使 AjaX 可缓存
页面结构 将样式表放在顶部,将脚本放在底部,尽早刷新文档的输出
代码校验 避免 CSS 表达式,避免重定向

目前大多数前端团队可以利用yui compressor或者google closure compiler等压缩工具很容易做到“精简 javascript ”这条原则,同样的,也可以使用图片压缩工具对图像进行压缩,实现“图像优化”原则,这两条原则是对单个资源的处理,因此不会引起任何工程方面的问题;很多团队也通过引入代码校验流程来确保实现“避免 css 表达式”和“避免重定向”原则;目前绝大多数互联网公司也已经开启了服务端的 Gzip 压缩,并使用 CDN 实现静态资源的缓存和快速访问;一些技术实力雄厚的前端团队甚至研发出了自动 CSS Sprites 工具,解决了 CSS Sprites 在工程维护方面的难题。使用“查找 – 替换”思路,我们似乎也可以很好的实现“划分主域”原则。

我们把以上这些已经成熟应用到实际生产中的优化手段去除掉,留下那些还没有很好实现的优化原则,再来回顾一下之前的性能优化分类:

优化方向 优化手段
请求数量 合并脚本和样式表,拆分初始化负载
请求带宽 移除重复脚本
缓存利用 添加 Expires 头,配置 ETag,使 Ajax 可缓存
页面结构 将样式表放在顶部,将脚本放在底部,尽早刷新文档的输出

诚然,不可否认现在有很多顶尖的前端团队可以将上述还剩下的优化原则也都一一解决,但业界大多数团队都还没能很好的解决这些问题,因此接下来本文将就这些原则的解决方案做进一步的分析与讲解,从而为那些还没有进入前端工业化开发的团队提供一些基础技术建设意见,也借此机会与业界顶尖的前端团队在工业化工程化方向上交流一下彼此的心得。

静态资源版本更新与缓存

如表格 2 所示,在“缓存利用”分类中保留了“添加 Expires 头”和“配置 ETag ”两项,或许有些人会质疑,明明这两项只要配置了服务器的相关选项就可以实现,为什么说它们难以解决呢?确实,开启这两项很容易,但开启了缓存后,我们的项目就开始面临另一个挑战:如何更新这些缓存。

相信大多数团队也找到了类似的答案,它和《高性能网站建设指南》关于“添加 Expires 头”所说的原则一样——修订文件名。即:

思路没错,但要怎么改变链接呢?变成什么样的链接才能有效更新缓存,又能最大限度避免那些没有修改过的文件缓存不失效呢?

先来看看现在一般前端团队的做法:

或者

大家会采用添加 query 的形式修改链接。这样做是比较直观的解决方案,但在访问量较大的网站,这么做可能将面临一些新的问题。

通常一个大型的 web 应用几乎每天都会有迭代和更新,发布新版本也就是发布新的静态资源和页面的过程。以上述代码为例,假设现在线上运行着 index.html 文件,并且使用了线上的 a.js 资源。index.html 的内容为:

这次我们更新了页面中的一些内容,得到一个 index.html 文件,并开发了新的与之匹配的 a.js 资源来完成页面交互,新的 index.html 文件的内容因此而变成了:

好了,现在要开始将两份新的文件发布到线上去。可以看到,a.html 和 a.js 的资源实际上是要覆盖线上的同名文件的。不管怎样,在发布的过程中,index.html 和 a.js 总有一个先后的顺序,从而中间出现一段或大或小的时间间隔。对于一个大型互联网应用来说即使在一个很小的时间间隔内,都有可能出现新用户访问,而在这个时间间隔中访问了网站的用户会发生什么情况呢:

  1. 如果先覆盖 index.html,后覆盖 a.js,用户在这个时间间隙访问,会得到新的 index.html 配合旧的 a.js 的情况,从而出现错误的页面。
  2. 如果先覆盖 a.js,后覆盖 index.html,用户在这个间隙访问,会得到旧的 index.html 配合新的 a.js 的情况,从而也出现了错误的页面。

这就是为什么大型 web 应用在版本上线的过程中经常会较集中的出现前端报错日志的原因,也是一些互联网公司选择加班到半夜等待访问低峰期再上线的原因之一。此外,由于静态资源文件版本更新是“覆盖式”的,而页面需要通过修改 query 来更新,对于使用 CDN 缓存的 web 产品来说,还可能面临 CDN 缓存攻击的问题。我们再来观察一下前面说的版本更新手段:

我们不难预测,a.js 的下一个版本是“ 1.0.1 ”,那么就可以刻意构造一串这样的请求“ a.js?v=1.0.1 ”、“ a.js?v=1.0.2 ”、……让 CDN 将当前的资源缓存为“未来的版本”。这样当这个页面所用的资源有更新时,即使更改了链接地址,也会因为 CDN 的原因返回给用户旧版本的静态资源,从而造成页面错误。即便不是刻意制造的攻击,在上线间隙出现访问也可能导致区域性的 CDN 缓存错误。

此外,当版本有更新时,修改所有引用链接也是一件与工程管理相悖的事,至少我们需要一个可以“查找 – 替换”的工具来自动化的解决版本号修改的问题。

对付这个问题,目前来说最优方案就是 基于文件内容的 hash 版本冗余机制 了。也就是说,我们希望工程师源码是这么写的:

但是线上代码是这样的:

其中”_82244e91 ”这串字符是根据 a.js 的文件内容进行 hash 运算得到的,只有文件内容发生变化了才会有更改。由于版本序列是与文件名写在一起的,而不是同名文件覆盖,因此不会出现上述说的那些问题。那么这么做都有哪些好处呢?

  1. 线上的 a.js 不是同名文件覆盖,而是文件名 +hash 的冗余,所以可以先上线静态资源,再上线 html 页面,不存在间隙问题;
  2. 遇到问题回滚版本的时候,无需回滚 a.js,只须回滚页面即可;
  3. 由于静态资源版本号是文件内容的 hash,因此所有静态资源可以开启永久强缓存,只有更新了内容的文件才会缓存失效,缓存利用率大增;
  4. 修改静态资源后会在线上产生新的文件,一个文件对应一个版本,因此不会受到构造 CDN 缓存形式的攻击

虽然这种方案是相比之下最完美的解决方案,但它无法通过手工的形式来维护,因为要依靠手工的形式来计算和替换 hash 只,并生成相应的文件将是一项非常繁琐且容易出错的工作。因此,我们需要借助工具。有了这样的思路,我们下面就来了解一下 fis 是如何完成这项工作的。

首先,之所以有这种工具需求,完全是因为 web 应用运行的根本机制决定的:web 应用所需的资源是以字面的形式通知浏览器下载而聚合在一起运行的。这种资源加载策略使得 web 应用从本质上区别于传统桌面应用的版本更新方式,也是大型 web 应用需要工具处理的最根本原因。为了实现资源定位的字面量替换操作,前端构建工具理论上需要识别所有资源定位的标记,其中包括:

  • css 中的@import url(path)、background:url(path)、backgournd-image:url(path)、filter 中的 src
  • js 中的自定义资源定位函数,在 fis 中我们将其规定为__uri(path)。
  • html 中的<script src=” path ”><link href=” path ”><img src=” path ”>已经 embed、audio、video、object 等具有资源加载功能的标签。

为了工程上的维护方便,我们希望工程师在源码中写的是相对路径,而工具可以将其替换为线上的绝对路径,从而避免相对路径定位错误的问题(比如 js 中需要定位图片路径时不能使用相对路径的情况)。

image2

fis 有一个非常棒的资源定位系统,它是根据用户自己的配置来指定资源发布后的地址,然后由 fis 的资源定位系统识别文件中的定位标记,计算内容 hash,并根据配置替换为上线后的绝对 url 路径。

要想实现具备 hash 版本生成功能的构建工具不是“查找 – 替换”这么简单的,我们考虑这样一种情况:

image3

由于我们的资源版本号是通过对文件内容进行 hash 运算得到,如上图所示,index.html 中引用的 a.css 文件的内容其实也包含了 a.png 的 hash 运算结果,因此我们在修改 index.html 中 a.css 的引用时,不能直接计算 a.css 的内容 hash,而是要先计算出 a.png 的内容 hash,替换 a.css 中的引用,得到了 a.css 的最终内容,再做 hash 运算,最后替换 index.html 中的引用。

这意味着构建工具需要具备“递归编译”的能力,这也是为什么 fis 团队不得不放弃 gruntjs 等 task-based 系统的根本原因。针对前端项目的构建工具必须是具备递归处理能力的。此外,由于文件之间的交叉引用等原因,fis 构建工具还实现了构建缓存等机制,以提升构建速度。

在解决了基于内容 hash 的版本更新问题之后,我们可以将所有前端静态资源开启永久强缓存,每次版本发布都可以首先让静态资源全量上线,再进一步上线模板或者页面文件,再也不用担心各种缓存和时间间隙的问题了!

静态资源管理与模板框架

让我们再来看看前面的优化原则表还剩些什么:

优化方向 优化手段
请求数量 合并脚本和样式表,拆分初始化负载
请求带宽 移除重复脚本
缓存利用 使 Ajax 可缓存
页面结构 将样式表放在顶部,将脚本放在底部,尽早刷新文档的输出

很不幸,剩下的优化原则都不是使用工具就能很好实现的。或许有人会辩驳:“我用某某工具可以实现脚本和样式表合并”。嗯,必须承认,使用工具进行资源合并并替换引用或许是一个不错的办法,但在大型 web 应用,这种方式有一些非常严重的缺陷,来看一个很熟悉的例子:

image4

某个 web 产品页面有 A、B、C 三个资源

image5

工程师根据“减少 HTTP 请求”的优化原则合并了资源

image6

产品经理要求 C 模块按需出现,此时 C 资源已出现多余的可能

image7

C 模块不再需要了,注释掉吧!但 C 资源通常不敢轻易剔除

image8

不知不觉中,性能优化变成了性能恶化……

事实上,使用工具在线下进行静态资源合并是无法解决资源按需加载的问题的。如果解决不了按需加载,则势必会导致资源的冗余;此外,线下通过工具实现的资源合并通常会使得资源加载和使用的分离,比如在页面头部或配置文件中写资源引用及合并信息,而用到这些资源的 html 组件写在了页面其他地方,这种书写方式在工程上非常容易引起维护不同步的问题,导致使用资源的代码删除了,引用资源的代码却还在的情况。因此,在工业上要实现资源合并至少要满足如下需求:

  1. 确实能减少 HTTP 请求,这是基本要求(合并)
  2. 在使用资源的地方引用资源(就近依赖),不使用不加载(按需)
  3. 虽然资源引用不是集中书写的,但资源引用的代码最终还能出现在页面头部(css)或尾部(js)
  4. 能够避免重复加载资源(去重)

将以上要求综合考虑,不难发现,单纯依靠前端技术或者工具处理的是很难达到这些理想要求的。现代大型 web 应用所展示的页面绝大多数都是使用服务端动态语言拼接生成的。有的产品使用模板引擎,比如 smarty、velocity,有的则干脆直接使用动态语言,比如 php、python。无论使用哪种方式实现,前端工程师开发的 html 绝大多数最终都不是以静态的 html 在线上运行的,接下来我会讲述一种新的模板架构设计,用以实现前面说到那些性能优化原则,同时满足工程开发和维护的需要,这种架构设计的核心思想就是:

考虑一段这样的页面代码:


    
        hello world
        
        
        
    
    
        
html of A
html of B
html of C

根据资源合并需求中的第二项,我们希望资源引用与使用能尽量靠近,这样将来维护起来会更容易一些,因此,理想的源码是:


    
        hello world
    
    
        
html of A
html of B
html of C

当然,把这样的页面直接送达给浏览器用户是会有严重的页面闪烁问题的,所以我们实际上仍然希望最终页面输出的结果还是如最开始的截图一样,将 css 放在头部输出。这就意味着,页面结构需要有一些调整,并且有能力收集资源加载需求,那么我们考虑一下这样的源码:


    
        hello world
        
    
    
        {require name="A.css"}
html of A
{require name="B.css"}
html of B
{require name="C.css"}
html of C

在页面的头部插入一个 html 注释“<!--[CSS LINKS PLACEHOLDER]-->”作为占位,而将原来字面书写的资源引用改成模板接口(require)调用,该接口负责收集页面所需资源。require 接口实现非常简单,就是准备一个数组,收集资源引用,并且可以去重。最后在页面输出的前一刻,我们将 require 在运行时收集到的“ A.css ”、“ B.css ”、“ C.css ”三个资源拼接成 html 标签,替换掉注释占位“<!--[CSS LINKS PLACEHOLDER]-->”,从而得到我们需要的页面结构。

经过 fis 团队的总结,我们发现模板层面只要实现三个开发接口,既可以比较完美的实现目前遗留的大部分性能优化原则,这三个接口分别是:

  1. require(String id):收集资源加载需求的接口,参数是资源 id。
  2. widget(String template_id):加载拆分成小组件模板的接口。你可以叫它为 load、component 或者 pagelet 之类的。总之,我们需要一个接口把一个大的页面模板拆分成一个个的小部分来维护,最后在原来的大页面以组件为单位来加载这些小部件。
  3. script(String code):收集写在模板中的 js 脚本,使之出现的页面底部,从而实现性能优化原则中的“将 js 放在页面底部”原则。

实现了这些接口之后,一个重构后的模板页面的源代码可能看起来就是这样的了:


    
        hello world
        
        {require name="jquery.js"}
        {require name="bootstrap.css"}
    
    
        {require name="A/A.css"}{widget name="A/A.tpl"}
        {script}console.log('A loaded'){/script}
        {require name="B/B.css"}{widget name="B/B.tpl"}
        {require name="C/C.css"}{widget name="C/C.tpl"}
        
    

而最终在模板解析的过程中,资源收集与去重、页面 script 收集、占位符替换操作,最终从服务端发送出来的 html 代码为:


    
        hello world
        
        
        
        
    
    
        
html of A
html of B
html of C

不难看出,我们目前已经实现了“按需加载”,“将脚本放在底部”,“将样式表放在头部”三项优化原则。

前面讲到静态资源在上线后需要添加 hash 戳作为版本标识,那么这种使用模板语言来收集的静态资源该如何实现这项功能呢?答案是:静态资源依赖关系表。 假设前面讲到的模板源代码所对应的目录结构为下图所示:

image9

那么我们可以使用工具扫描整个 project 目录,然后创建一张资源表,同时记录每个资源的部署路径,可以得到这样的一张表:

{
  "res": {
    "A/A.css": {
      "uri": "/A/A_1688c82.css",
      "type": "css"
    },
    "B/B.css": {
      "uri": "/B/B_52923ed.css",
      "type": "css"
    },
    "C/C.css": {
      "uri": "/C/C_6dda653.css",
      "type": "css"
    },
    "bootstrap.css": {
      "uri": "bootstrap_08f2256.css",
      "type": "css"
    },
    "jquery.js": {
      "uri": "jquery_9155343.css",
      "type": "js"
    },
  },
  "pkg": {}
}

基于这张表,我们就很容易实现 {require name=” id ”} 这个模板接口了。只须查表即可。比如执行{require name=” jquery.js ”},查表得到它的 url 是“/jquery_9151577.js ”,声明一个数组收集起来就好了。这样,整个页面执行完毕之后,收集资源加载需求,并替换页面的占位符,即可实现资源的 hash 定位,得到:


    
        hello world
        
        
        
        
    
    
        
html of A
html of B
html of C

接下来,我们讨论如何在基于表的设计思想上是如何实现静态资源合并的。或许有些团队使用过 combo 服务,也就是我们在最终拼接生成页面资源引用的时候,并不是生成多个独立的 link 标签,而是将资源地址拼接成一个 url 路径,请求一种线上的动态资源合并服务,从而实现减少 HTTP 请求的需求,比如:


    
        hello world
        
    
    
        
html of A
html of B
html of C

这个“/combo?files=file1,file2,file3,…”的 url 请求响应就是动态 combo 服务提供的,它的原理很简单,就是根据 get 请求的 files 参数找到对应的多个文件,合并成一个文件来响应请求,并将其缓存,以加快访问速度。

这种方法很巧妙,有些服务器甚至直接集成了这类模块来方便的开启此项服务,这种做法也是大多数大型 web 应用的资源合并做法。但它也存在一些缺陷:

  1. 浏览器有 url 长度限制,因此不能无限制的合并资源。
  2. 如果用户在网站内有公共资源的两个页面间跳转访问,由于两个页面的 combo 的 url 不一样导致用户不能利用浏览器缓存来加快对公共资源的访问速度。

对于上述第二条缺陷,可以举个例子来看说明:

  • 假设网站有两个页面 A 和 B
  • A 页面使用了 a,b,c,d 四个资源
  • B 页面使用了 a,b,e,f 四个资源
  • 如果使用 combo 服务,我们会得:
    • A 页面的资源引用为:/combo?files=a,b,c,d
    • B 页面的资源引用为:/combo?files=a,b,e,f
  • 两个页面引用的资源是不同的 url,因此浏览器会请求两个合并后的资源文件,跨页面访问没能很好的利用 a、b 这两个资源的缓存。

很明显,如果 combo 服务能聪明的知道 A 页面使用的资源引用为“/combo?files=a,b ”和“/combo?files=c,d ”,而 B 页面使用的资源引用为“/combo?files=a,b ”,“/combo?files=e,f ”就好了。这样当用户在访问 A 页面之后再访问 B 页面时,只需要下载 B 页面的第二个 combo 文件即可,第一个文件已经在访问 A 页面时缓存好了的。

基于这样的思考,fis 在资源表上新增了一个字段,取名为“ pkg ”,就是资源合并生成的新资源,表的结构会变成:

{
  "res": {
    "A/A.css": {
      "uri": "/A/A_1688c82.css",
      "type": "css"
    },
    "B/B.css": {
      "uri": "/B/B_52923ed.css",
      "type": "css"
    },
    "C/C.css": {
      "uri": "/C/C_6dda653.css",
      "type": "css"
    },
    "bootstrap.css": {
      "uri": "bootstrap_08f2256.css",
      "type": "css"
    },
    "jquery.js": {
      "uri": "jquery_9155343.css",
      "type": "js"
    },
  },
  "pkg": {
    "p0": {
      "uri": "/pkg/utils_b967346.css",
      "type": "css",
      "has": ["bootstrap.css", "A/A.css"]
    },
    "p1": {
      "uri": "/pkg/others_0d4552a.css",
      "type": "css",
      "has": ["B/B.css", "C/C.css"]
    }
  }
}

相比之前的表,可以看到新表中多了一个 pkg 字段,并且记录了打包后的文件所包含的独立资源。这样,我们重新设计一下{require name=” id ”}这个模板接口:在查表的时候,如果一个静态资源有 pkg 字段,那么就去加载 pkg 字段所指向的打包文件,否则加载资源本身。比如执行{require name=” bootstrap.css ”},查表得知 bootstrap.css 被打包在了“ p0 ”中,因此取出 p0 包的 url “/pkg/utils_b967346.css”,并且记录页面已加载了“ bootstrap.css ”和“ A/A.css ”两个资源。这样一来,之前的模板代码执行之后得到的 html 就变成了:


    
        hello world
        
        
    
    
        
html of A
html of B
html of C

css 资源请求数由原来的 4 个减少为 2 个。 这样的打包结果是怎么来的呢?答案是配置得到的。 我们来看一下带有打包结果的资源表的 fis 配置:

fis.config.set('pack', {
  'pkg/util.css': [ 'bootstrap.css', 'A/A.css'],
  'pkg/other.css': [ '**.css' ]
});

我们将“ bootstrap.css ”、“ A/A.css ”打包在一起,其他 css 另外打包,从而生成两个打包文件,当页面需要打包文件中的资源时,模块框架就会收集并计算出最优的资源加载结果,从而解决静态资源合并的问题。

这样做的原因是为了弥补 combo 在前面讲到的两点技术上的不足而设计的。但也不难发现这种打包策略是需要配置的,这就意味着维护成本的增加。但好在它有两个优势可以一定程度上弥补这个问题:

  1. 打包的资源只是原来独立资源的备份。打包与否不会导致资源的丢失,最多是没有合并的很好而已。
  2. 配置可以由工程师根据经验人工维护,也可以由统计日志生成,这为性能优化自适应网站设计提供了非常好的基础。

关于第二点,fis 有这样辅助系统来支持自适应打包算法:

image10

至此,我们通过基于表的静态资源管理系统和三个模板接口实现了几个重要的性能优化原则,现在我们再来回顾一下前面的性能优化原则分类表,剔除掉已经做到了的,看看还剩下哪些没做到的:

优化方向 优化手段
请求数量 拆分初始化负载
请求带宽 拆分初始化负载
缓存利用 使 Ajax 可缓存
页面结构 尽早刷新文档的输出

“拆分初始化负载”的目标是将页面一开始加载时不需要执行的资源从所有资源中分离出来,等到需要的时候再加载。工程师通常没有耐心去区分资源的分类情况,但我们可以利用组件化框架接口来帮助工程师管理资源的使用。还是从例子开始思考:



    hello world
    {require name="jquery.js"}


    
    {script}
        $('#myBtn').click(function(){
            var dialog = require('dialog/dialog.js');
            dialog.alert('you catch me!');
        });
    {/script}
    

在 fis 给百度内部团队开发的架构中,如果这样书写代码,页面最终的执行结果会变成:



    hello world


    
    
    
    
    

fis 系统会分析页面中 require(id)函数的调用,并将依赖关系记录到资源表对应资源的 deps 字段中,从而在页面渲染查表时可以加载依赖的资源。但此时 dialog.js 是以 script 标签的形式同步加载的,这样会在页面初始化时出现资源的浪费。因此,fis 团队提供了 require.async 的接口,用于异步加载一些资源,源码修改为:



    hello world
    {require name="jquery.js"}


    
    {script}
        $('#myBtn').click(function() {
            require.async('dialog/dialog.js', function( dialog ) {
                dialog.alert('you catch me!');
            });
        });
    {/script}
    

这样书写之后,fis 系统会在表里以 async 字段来标准资源依赖关系是异步的。fis 提供的静态资源管理系统会将页面输出的结果修改为:



    hello world


    
    
    
    
    

dialog.js 不会在页面以 script src 的形式输出,而是变成了资源注册,这样,当页面点击按钮触发 require.async 执行的时候,async 函数才会查表找到资源的 url 并加载它,加载完毕后触发回调函数。

到目前为止,我们又以架构的形式实现了一项优化原则(拆分初始化负载),回顾我们的优化分类表,现在仅有两项没能做到了:

优化方向 优化手段
缓存利用 使 Ajax 可缓存
页面结构 尽早刷新文档的输出

剩下的两项优化原则要做到并不容易,真正可缓存的 Ajax 在现实开发中比较少见,而尽早刷新文档的输出的情况 facebook 在 2010 年的 velocity 上提到过,就是 BigPipe 技术。当时 facebook 团队还讲到了 Quickling 和 PageCache 两项技术,其中的 PageCache 算是比较彻底的实现 Ajax 可缓存的优化原则了。fis 团队也曾与某产品线合作基于静态资源表、模板组件化等技术实现了页面的 PipeLine 输出、以及 Quickling 和 PageCache 功能,但最终效果没有达到理想的性能优化预期,因此这两个方向尚在探索中,相信在不久的将来会有新的突破。

总结

其实在前端开发工程管理领域还有很多细节值得探索和挖掘,提升前端团队生产力水平并不是一句空话,它需要我们能对前端开发及代码运行有更深刻的认识,对性能优化原则有更细致的分析与研究。fis 团队一直致力于从架构而非经验的角度实现性能优化原则;解决前端工程师开发、调试、部署中遇到的工程问题;提供组件化框架,提高代码复用率;提供开发工具集,提升工程师的开发效率。在前端工业化开发的所有环节均有可节省的人力成本,这些成本非常可观,相信现在很多大型互联网公司也都有了这样的共识。 本文只是将这个领域中很小的一部分知识的展开讨论,抛砖引玉,希望能为业界相关领域的工作者提供一些不一样的思路。欢迎关注fis项目,对本文有任何意见或建议都可以在 fis 开源项目中进行反馈和讨论。

一个密码改变了我的人生

英文原文:How a password changed my life.

接下来的故事,发生于沮丧和幸福之间

  “她怎么能对我做出这种事?”这个问题不停在我脑海里盘旋。一刻也不停,每天每刻。

  在 2011 年,那时候渐变还很流行,iOS 的图标还有点理智,大家都还用体香剂,而我正深陷在沮丧的情绪中。我离婚了。

  幸运的是,我还是足够理智(还有一群了不起的朋友)。所以我找了些方法来维持下去。

  有天我到办公室,开启电脑准备一天的工作。事情都很顺利,直到我看到这条消息:

你的密码已经到期,点击“更改密码”设置新的密码

  卧槽。我当时以为“更改密码”是别的什么设置。

  我看着这个愚蠢的消息,好像有个愤怒的老奶奶在我耳旁絮叨:这该死的密码到期了。

  在我的公司,IT 管理设置微软 Exchange 服务要求全球上千的同事更改密码。每 30 天一次。

  狗屎的是:服务器强制要求至少一个大写字母,至少一个小写字母,至少一个特殊字符和至少一个数字。而且这堆麻烦的东西还必须超过 8 位。还有,过去三个月内的密码不能重复!

  那天早上我真是气炸了。上午 9 点 40 分,礼拜二。相当热的一天,我到公司的时候已经满身是汗,而我还得去工作。我迟到了,我还带着机车头盔。我好像还没吃早饭。嘴里一股类似香烟的味道。我必须得在早上 10 点的会议前把这个混蛋问题给解决了,而我面临的是浪费的大把时间。

  来看看吧…输入框带着跳动的光标,等着我输入一个要连续输入 30 天的密码,一天要输入无数遍的密码。

  好吧。摆脱这些挫折的事,我想起从前领导?Rasmus?学到的一个方法。他成功的将待办清单和密码结合起来。而我要试试增强版。

  我要用一个密码来改变自己的生活

  显而易见,如果维持那时候的生活习惯和状态,我几乎没有办法完成任何事。当然,我清楚知道自己需要做什么,或者要达成什么来重新掌控自己的生活,但是我们通常会对它们视而不见。

  我的密码可以作为一个暗示。我的密码能提醒我不要再让自己作为分手的受害者,而我足够坚强来做些什么。

  我的密码改成了:“Forgive@h3r”

  开会的时候我一直在回想自己刚刚做的事。好像有些什么东西给我的脸颊画上了一个微笑。

  剩下的一周里,我每天都要重复输入这个密码。每次我的电脑锁屏,每次我的屏保(她的照片)出现,每次我孤零零吃过午饭回来。

  我觉得给自己释放了一个咒语,我不是在输入密码,而是在不停提醒自己“Forgive her(原谅她)”。

  这个简单的行动改变了我对前妻的态度。不停地重复提醒我去原谅她,让我能够接受婚姻结束的事实,并能够拥抱新的生活,坦然面对沮丧,不再沉浸其中。

  接下来的日子,我的状态大大提升。第二周末,我发现密码的效率下降了,开始失去了效果。我重新释放了这个“咒语”。在我每次输入密码的时候,想着我原谅她。那种治愈的效果马上就再次生效。

  一个月后,亲爱的 Exchange 服务器又问我改密码了。我想了想下一件应该做的事。

  我的新密码是Quit@smoking4ever

  猜猜看发生了什么?我了个去,我第二天就戒烟了。几乎没人相信我是这么做到的。我试过各种书、电子香烟、膏药等等,都没用,但是这个小戏法就起到了效果。

  这个密码在那个月相当令人难受,但每次我输入这个声明,都是让我在脑海里对自己呐喊。这不断激励我完成每个月的目标。

  有一个月过去,我的密码改成Save4trip@thailand

  想想我三个月后去了哪儿?泰国。

  还有些结余。

  看到这些积极的肯定和提醒是如何帮助我物质化每个月的目标,帮助我保持能动性和积极性了吗?我们得承认:找出下一个目标不是容易的事。有时候发现我们需要改变什么,或者我们往哪个方向继续不是件容易的事。

  密码用最简单的形式让你在数字世界达到一个目的地。比如拷贝一个文件、解锁电脑、发个邮件。这种微成就,这种“神秘咒语帮助我搞定事情”的想法能够帮助建立起不断激励你保持专注于每月目标的动力。这种不起眼的习惯改变了我。

  就这样,我发现如果我用正确的方式,真的能够改变自己的人生。我正是这样月复一月,取得了了不起的结果。

  下面摘取一些我在过去两年用过的密码,让大家了解下我的人生都发生了哪些改变,多亏了这个方法:

  • Forgive@her 致我的前妻,让我开始了这段旅程。
  • Quit@smoking4ever 奏效。
  • Save4trip@thailand 奏效。
  • Eat2times@day 呃,没用,还是很胖。
  • Sleep@before12 奏效。
  • Ask@her4date?奏效。我又恋爱了。
  • No@drinking2months 奏效。感觉棒极了!
  • MovE@togeth3r 奏效。
  • Get@c4t! 奏效。我们养了只超级可爱的喵星人。
  • Facetime2mom@sunday 奏效。我每周都和妈妈通电话。

  上个月我用得这个:

  • Save4@ring?嗯哼。人生又要进入新的篇章了。很快。

  现在我每个月都有些小期待,每个月都能更改一次密码,进入下一个专注阶段,来激励我搞定下一件事。

  在过去的两年间,这个方法对我一直奏效。我和几位亲密的朋友和亲戚分享了它。我想这种小习惯可能不是什么重大突破,但它确实对我的人生产生积极的影响。因此,我在这里向大家分享这个方法。

  不妨试一下。带着正确的心态和态度,写下写这些声明,你会改变你的生活。

  当然,处于安全的考虑,试着一些复杂的描述。使用一些符号或者数字,让密码长一些,在密码的最开始或结尾用一些特殊的手段处理。S4f3ty_f1rst!(安全第一!)

  记得把这个方法告诉其他你觉得可能需要的朋友们。

  2014 年 6 月 21 日更新:她说“我愿意”。

来自:?jianshu.io

JavaScript类型转换 type conversions

var a = new Boolean(false);
console.log(a);//{}
console.log(a == false)//true
console.log(a === false)//false
if(a){
    console.log("new Boolean(false) is true")//new Boolean(false) is true
}else{
    console.log("new Boolean(false) is false")
}

这里a转换为true,因为a是一个Boolean的object,而object如果不是null或者undefined,就会被转换为true
这里主要需要分清楚值类型(primitive)和对象类型(object)
下面是JavaScript类型转换

JavaScript type conversions
转换为:
String Number Boolean Object
undefined "undefined" NaN false throws TypeError
null "null" 0 false throws TypeError
true "true" 1

new Boolean(true)

false

"false"

0

new Boolean(false)

""?(empty string)

0

false

new String("")

"1.2"?(nonempty, numeric)

1.2

true

new String("1.2")

"one"?(nonempty, non-numeric)

NaN

true

new String("one")

0

"0"

false

new Number(0)

-0

"0"

false

new Number(-0)

NaN

"NaN"

false

new Number(NaN)

Infinity

"Infinity"

true

new Number(Infinity)

-Infinity

"-Infinity"

true

new Number(-Infinity)

1?(finite, non-zero)

"1"

true

new Number(1)

{}?(any object)

true

[]?(empty array)

""

0

true

[9]?(1 numeric elt)

"9"

9

true

['a']?(any other array)

use join() method

NaN

true

function(){}?(any function)

NaN true

在程序员的眼里,用户是这样使用他们开发的软件的

我曾经说过,程序员不是一般的人,是具有某种超能里的人。但问题是,程序员往往意识不到自己的这种特异功能,在他们的眼里,会认为自己很普通,跟常人一样,所以,程序员能做到的事情,其他人——比如他们的客户/软件用户——也应该很容易做到。但事实上,由于大部分人——绝大部分人(包括软件开发公司的客户/购买软件的用户)——都是电脑小白(对电脑知识/计算机知识/软件知识知之甚少的人)。一个对于程序员来说很显而易见的软件操作,换成让用户来操作,就会出现各种各样奇怪的事情。这让程序员非常痛苦。
记得有一次,一个客户打电话给我,说他电脑桌面上的大e找不到了,我没听懂,什么大e找不到了?客户解释说:就是那个长的像大个儿的英文字母e的图标找不到了。我倒。终于明白了他指的是桌面上的 IE 浏览器的图标不见了。
还有一次,有个客户提出一个需求,要求在页面上增加一个搜索功能,我问它,系统里有搜索功能,为什么还要在这个地方新增一个搜索功能,他说他要的不是那个搜索,他要的是在这个页面上搜在某个关键词。经过进一步的沟通,我明白了,他要的是浏览器上的快捷键 CTRL+F 的功能。
因为用户的这些特征,导致了程序员认为完美的程序,到了客户的手里,却变成极其难用的软件,投诉电话如乡下骂街的泼妇似的响个不停。而事后分析发现,根本原因都是应为程序员高估了用户对软件的掌控能力,低估了自己对软件的创造能力,于是导致了他们看这些客户使用他们开发的软件时,都是那样一种可笑的行为,如下图:
在程序员的眼里,用户是这样使用他们开发的软件的
在程序员的眼里,用户是这样使用他们开发的软件的
如果是脾气暴躁的程序员,遇到这种情况,难免会对着客户发一顿牢骚,而且,程序员的脾气一般都不是很好,所以,通常跟客户沟通时,项目经理一般都是跟着一起,以免事态激化。
用户虽然给程序员带来很多麻烦,但其实程序员的所有荣耀感都来自客户,因为只有客户用得满意,程序员才会有成就感。比如像下面这几个客户在使用一个新款软件时显露出来的表情,足够让一个处在北京重度雾霾的下午的程序员也能露出笑容:
用户在使用一款新软件时的表情
用户在使用一款新软件时的样子
程序员虽然脾气不好,但他们都是为工作着想,不带任何个人恩怨。当开发软件有紧急任务时,他们都是任劳任怨的加班加点,当在已经发布的软件中出现了重大 bug 时,他们都会深深在自责,会连夜赶制出紧急修复 bug,如果不能在第一时间让用户满意,他们会茶不思、饭不想、觉不睡。即使在实在没有短期内完整的补救措施的情况下,他们也会想出一些歪招,但也是行之有效的方案,让用户暂时度过难关。比如,下面就是一个紧急修复补丁:
紧急修复补丁
紧急修复补丁
用户应该体谅程序员。程序员的生活实际处在一种十分矛盾的状态中。编程不像其它行业,比如泥瓦匠砌砖,砌一层砖,墙就会高一次。但编程不一样,有时候一个程序员写了一天的代码,急得满头大汗,但开发进度未必就有所进展,有时候甚至还会倒退。软件编程是一个亦虚亦实的世界,有时候你搞不清一段代码为什么好用,有时候也会诧异由那样的代码构成的软件也能跑起来,正如下面这张图片中所示:
软件中有鬼
软件中有鬼
最后,说一下跟程序员打交道的一些注意事项。程序员因为整天和编程逻辑打交道,所以对因果关系特别敏感。如果你的话语的因果关系不是很明确,这会让他们感到疑惑,如果你的话语的因果关系不完整,这会让他们办错事。如果你的话中有if,最好后面用then做结束,或者用else给出选择,主语要明晰。如果不明晰,就会出现下图中出现的事故:
程序员是这样理解这个指示牌上的话的
程序员是这样理解这个指示牌上的话的
如果你是一个程序员,你会理解我说的话。

javascript:null and undefined

声明而没有赋值的变量是undefined
没有返回值的函数返回的是undefined

var a;//undefined
console.log(a);//undefined
console.log(typeof a);//undefined
console.log(typeof a == undefined)//false
console.log(typeof a == "undefined")//true
console.log(typeof a === undefined)//false
console.log(typeof a === "undefined")//true
console.log(a==null);//true
console.log(a==undefined);//true
console.log(a===null);//false
console.log(a===undefined);//true
console.log("*******************");
var b = null;//null
console.log(b);//null
console.log(typeof b);//object
console.log(typeof b == undefined)//false
console.log(typeof b == "undefined")//false
console.log(typeof b === undefined)//false
console.log(typeof b === "undefined")//false
console.log(b==null);//true
console.log(b==undefined);//true
console.log(b===null);//true
console.log(b===undefined);//false

iis 7 – IIS7 URL Rewrite – Add "www" prefix

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
    <rewrite>
      <rules>
        <!--<rule name="Add www" patternSyntax="Wildcard" stopProcessing="true">
          <match url="*" />
        <conditions>
        <add input="{HTTP_HOST}" pattern="test.com" />
          </conditions>
        <action type="Redirect" url="http://www.test.com/{R:0}" />
        </rule>-->
        <rule name="Add www" patternSyntax="ECMAScript" stopProcessing="true">
      <match url=".*" />
        <conditions>
          <add input="{HTTP_HOST}" pattern="^test.com$" />
        </conditions>
        <action type="Redirect" url="http://www.test.com/{R:0}" />
    </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>