http://blog.aijc.net/server/2015/11/03/%E4%BB%8E%E8%BE%93%E5%85%A5URL%E5%88%B0%E9%A1%B5%E9%9D%A2%E5%8A%A0%E8%BD%BD%E5%AE%8C%E7%9A%84%E8%BF%87%E7%A8%8B%E4%B8%AD%E9%83%BD%E5%8F%91%E7%94%9F%E4%BA%86%E4%BB%80%E4%B9%88%E4%BA%8B%E6%83%85/
这其实是一个经典的面试题了,都可以自由发挥各个方面,说出自己的理解,而且涉及的面也是巨多,就看怎么看待了。本篇就可以说是我对于这个问题的一些综合、总结;其中不会涉及深层次(硬件啊等)的一些东西,例如说按下按键发生了什么事情,关于这些更基础更深入的建议可以看下百度的文章从输入 URL 到页面加载完成的过程中都发生了什么事情?。
首先来看第一个点,输入的URL。
URL,英文是Uniform / Universal Resource Locator,中文的翻译就是统一资源定位符,俗称网页地址,简短的说法是网址,用于完整地描述Internet上网页和其他资源的地址的一种标识方法。它从左到右由如下部分构成:
传送协议protocol:最常用的是HTTP
协议(超文本传输协议),它也是目前WWW中应用最广的协议;其他也还有ftp
、file
、https
、、mailto
、git
等,当然也有自定义的协议(私有协议),例如tencent
等
主机host:通常为域名或者IP地址,当然在其前面还可以有连接到服务器所需的用户名和密码
端口号port:以数字形式表示,每种协议都有自己默认的端口号,例如http协议的默认端口号就是80,https的默认端口号就是443等
路径path:以“/”字元区别路径中的每一个目录名称,一般表示的就是主机上的一个目录或文件地址
查询query:以“?”字元为起点,每个参数以“&”隔开,再以“=”分开参数名称与其对应的值
片段fragment:也就是在浏览器环境下location的hash值,用于指定网络资源中的片断,一般用于定位到某个位置
参考:
统一资源定位符
什么是URL
介绍了URL,下边直说最简单的通过浏览器发起HTTP请求资源过程,没有代理,通过域名访问的情况。根据http://blog.csdn.net/iaiti/article/details/28339145中相关介绍,将要发生如下事情:
浏览器查询缓存,如果缓存存在跳到第9步
浏览器询问操作系统服务器的IP地址
操作系统做DNS查询,返回IP地址给浏览器
浏览器打开对服务器的TCP连接
浏览器通过TCP连接发送HTTP请求
浏览器接收HTTP响应并且可能关掉TCP连接,或者是重新使用连接处理新请求(也就是keepalive)
浏览器检查HTTP响应是否为一个重定向(3xx 结果状态码 ),一个验证请求(401),错误(4xx 5xx)等等,这些都是不同响应的正常处理(2xx)
如果响应可缓存,将存入缓存
浏览器解码响应(例如:如果它是gziped压缩)
浏览器决定如何处理这些响应(例如,它是HTML页面,一张图片,一段音乐)
浏览器展现响应,对未知类型还会弹出下载对话框(现在一般不会弹出了,用户对浏览器设置而定)
下边就来说下这个过程中一些关键点。
首先来看下DNS。
DNS,英文是Domain Name System,中文叫域名系统,是Internet的一项服务,他将域名和IP地址相互映射的一个分布式数据库,引入域名是为了解决IP地址不便于记忆这个问题的。所以说这时候就需要有DNS这样的服务来解决域名和IP地址是怎么映射的。
在DNS的定义中也说了他是一个分布式数据库,更通俗来讲的话就是有对应的域名服务器(装有DNS的主机),利用域名服务器来实现对应的名称解析。
要理解DNS,首先得知道域名;域名是为了识别主机名称和组织机构名称的一种具有分层的名称。
根域:也就是.
,在上图中就是最顶部的那个,而对应的根域服务器,之前会有错误的认为根域名服务器只有13台,但实际上不是13个,而是13组,而根服务器目前则有504台,还会更多,目前是被编号为从A到M13个标号,他们是只有13个IP地址,这么多服务器通过任播Anycast技术,标号相同根服务器使用1个IP。而具体分布情况则可以查看http://root-servers.org/
顶级域:也叫一级域,主要分为四类:国家及地区顶级域(.cn
, .jp
等)、通用顶级域(.com
, .edu
, .net
等)、基础设施顶级域(.arpa
,之前属于通用顶级域)和测试顶级域(例如.测试
)。
二级域:可变长度的个体或组织,以便在Internet上使用的注册的名称,这些名称一定会基于相应的顶级域,例如aijc.net
。
其他还有三级域(子域,也就是从已注册的二级域名自定义派生的),还可以有四级域(主机或资源名称),当然还可以更多级。
下边继续来说下域名解析过程。
进行DNS查询的主机或软件叫做DNS解析器,用户使用的工作站或电脑都属于解析器。域名解析就是利用DNS解析器得到对应IP过程,解析器会向域名服务器进行查询处理。
解析过程就是一个查询的过程,来一张来自http://xdays.me/dns%E5%8D%8F%E8%AE%AE%E8%AF%A6%E8%A7%A3.html的图:
假设用户在浏览器中输入的是www.google.com
,大概过程:
下边的这几个步骤和上边图上的过程是一一对应的,但是在这之前还有一些步骤:
从浏览器缓存中查找域名www.google.com
的IP地址
在浏览器缓存中没找到,就在操作系统缓存中查找,这一步中也会查找本机的hosts看看有没有对应的域名映射(当然已经缓存在系统DNS缓存中了)
在系统中也没有的话,就到你的路由器来查找,因为路由器一般也会有自己的DNS缓存
如果依旧找不到,接着对应图上的步骤继续(参考http://xdays.me/dns%E5%8D%8F%E8%AE%AE%E8%AF%A6%E8%A7%A3.html):
用户电脑的解析器向LDNS(也就是Local DNS,互联网服务提供商ISP),发起域名解析请求,查询www.google.com
的IP地址,这是一个递归查找过程
在缓存没有命中的情况下,LDNS向根域名服务器.
查询www.google.com
的IP地址,LDNS的查询过程是一个迭代查询的过程
根告诉LDNS,我不知道www.google.com
对应的IP,但是我知道你可以问com
域的授权服务器,这个域归他管
LDNS向com
的授权服务器问www.google.com
对应的IP地址
com
告诉LDNS,我不知道www.google.com
对应的IP,但是我知道你可以问google.com
域的授权服务器,这个域归他管
LDNS向google.com
的授权服务器问www.google.com
对应的IP地址
google.com
查询自己的ZONE文件(也称区域文件记录),找到了www.google.com
对应的IP地址,返回给LDNS
LDNS本地缓存一份记录,把结果返回给用户电脑的解析器
在这之后,用户电脑的解析器拿到结果后,缓存在自己操作系统DNS缓存中,同时返回给浏览器,浏览器依旧会缓存一段时间。
上边有提到域名服务器会查询自己的ZONE文件,其实也就是常说的DNS记录,主要有几种类型:
A记录,用来指定域名对应的IPv4地址的记录
NS记录,也就是域名服务器的记录,如果需要将域名去指定某个域名服务器去解析的话,就需要NS记录
CNAME记录,域名的对应的别名,其实是另一个域名,实现与指向的域名相同访问效果
MX记录,邮件交换记录,如果需要建立邮箱服务,将指向的是邮件服务器地址的记录
AAAA记录,将主机名(域名)指向一个IPv6地址的记录
TXT记录,任意填写文本内容,通常用作SPF记录(反垃圾邮件)使用
SRV记录,记录哪台计算机提供了哪个服务的记录,主要用于服务器选择
当然还有一些其他记录类型(SOA、WKS、PTR、HINFO、MINFO、SIG、KEY、GPOS、NXT等),这里不再细说。
在DNS解析这里还涉及到了另外一个技术CDN,下边来介绍下:
CDN,英文Content Delivery Network,中文翻译是内容分发网络,目的就是通过现有的Internet中增加一新的网络架构,将网站内容发布到离用户最近的网络“边缘”,提高用户访问网站的速度,所以更像是增加了一层CACHE(缓存)层。从技术上全面优化由于网络宽带小、用户访问量大、网点分布不均匀等导致的用户访问网站响应速度慢的情况。
那他的实现原理是啥呢?其实主要是通过接管DNS来实现,注意上边DNS域名解析过程的那张图,在倒数第二步中(也就是第7步),可能需要更多过程来完成,这里就举一个例子,例如说上边要访问的域名是img.alicdn.com
:
依旧是上边的第7步:
7)img.alicdn.com
查找自己的ZONE文件,发现了一条CNAME
记录,指向的是img.alicdn.com.danuoyi.alicdn.com.
,通过dig img.alicdn.com
可以得到这样的结果:
img.alicdn.com. 51969 IN CNAME img.alicdn.com.danuoyi.alicdn.com.
8)LDNS得到的不是具体的IP地址,即不是A记录,而是一条CNAME记录,别名地址是img.alicdn.com.danuoyi.alicdn.com.
,所以LDNS重复上边的几步,不再细说,最终得到img.alicdn.com.danuoyi.alicdn.com.
对应的其中一个IP地址返回给LDNS
这里需要细说的就是CDN的核心原理部分,也就是怎么从img.alibaba.com.danuoyi.tbcache.com.
得到真正的离用户“近”的CDN节点的IP地址。
这路里拿一张来自http://www.51know.info/system_performance/cdn/cdn.html的图来看:
阿里是自建CDN的,然后就可以说阿里自己的CDN智能调度器返回了一个合适的IP地址给LDNS(CDN选择优质节点的过程,不一定说一定是最近的,更多的是一个综合策略,例如还会考虑网络成本、流量、源站负载等)。这个IP地址对应的是阿里CDN的其中一个CDN节点,可以说每个节点都可以认为是一个服务器。
CDN网络架构主要有两大部分组成:中心和边缘两部分。中心的话其实也就是CDN网管中心和DNS重定向解析中心,主要负责全局负载均衡;而边缘只要是指的分布在全球各地的节点,主要包含缓存服务器以及负载均衡器等组成。
当用户访问加入CDN服务的网站时,域名解析请求将最终交给全局负载均衡DNS进行处理。全局负载均衡DNS通过一组预先定义好的策略,将当时最接近用户的节点地址提供给用户,使用户能够得到快速的服务。同时,它还与分布在世界各地的所有CDN节点保持通信,搜集各节点的通信状态,确保不将用户的请求分配到不可用的CDN节点上,实际上是通过DNS做全局负载均衡。
对于普通的Internet用户来讲,每个CDN节点就相当于一个放置在它周围的WEB。通过全局负载均衡DNS的控制,用户的请求被透明地指向离他最近的节点,节点中CDN服务器会像网站的原始服务器一样,响应用户的请求。由于它离用户更近,因而响应时间必然更快。
每个CDN节点由两部分组成:负载均衡设备和高速缓存服务器
负载均衡设备负责每个节点中各个Cache的负载均衡,保证节点的工作效率;同时,负载均衡设备还负责收集节点与周围环境的信息,保持与全局负载DNS的通信,实现整个系统的负载均衡。
高速缓存服务器(Cache)负责存储客户网站的大量信息,就像一个靠近用户的网站服务器一样响应本地用户的访问请求。
CDN的管理系统是整个系统能够正常运转的保证。它不仅能对系统中的各个子系统和设备进行实时监控,对各种故障产生相应的告警,还可以实时监测到系统中总的流量和各节点的流量,并保存在系统的数据库中,使网管人员能够方便地进行进一步分析。通过完善的网管系统,用户可以对系统配置进行修改。
从上边的分析可以知道,DNS查询得到IP地址还是一个复杂的过程的。主要过程就是通过设备的解析器将要访问的域名发送给各个级别的域名服务器,得到最终的IP地址(这中间可能还会涉及到CDN技术)。
参考:
https://zh.wikipedia.org/wiki/%E5%9F%9F%E5%90%8D%E7%B3%BB%E7%BB%9F
http://blog.csdn.net/crazw/article/details/8986504
http://xdays.me/dns%E5%8D%8F%E8%AE%AE%E8%AF%A6%E8%A7%A3.html
http://www.51know.info/system_performance/cdn/cdn.html
http://cstdlib.com/tech/2015/08/18/what-is-cdn/
http://kb.cnblogs.com/page/121664/
TCP是一种面向有连接的传输层协议。他可以保证两端(发送端和接收端)通信主机之间的通信可达。他能够处理在传输过程中丢包、传输顺序乱掉等异常情况;此外他还能有效利用宽带,缓解网络拥堵。
而建立TCP连接一开始都要经过三次握手,建立连接过程中会涉及TCP的标志位Flag,一共有6种标志:SYN(synchronize同步序号) ACK(acknowledgement 确认) PSH(push传送) FIN(finish结束) RST(reset重置) URG(urgent紧急)
还有额外的两个号码:Sequence number(顺序号码) Acknowledge number(确认号码)
三次握手过程:
第一次握手,请求建立连接,发送端发送连接请求报文,将SYN置为1,产生随机的顺序号seq=x
第二次握手,接收端收到发送端发过来的报文,由SYN为1可知发送端现在要建立联机。然后接收端会向发送端发送一个SYN为1和ACK为x+1的报文,同时设置了自己随机产生的一个随机的顺序号seq=y
第三次握手,发送端收到了发送过来的报文,需要检查一下返回的ACK是否是正确的(x+1);若正确的话,发送端再次发送确认包,ACK为y+1,设置顺序号seq=x+1。
发送端在收到接收端返回的ACK,确认后也就意味着连接成功了,就可以发送数据了;而接收端则必须等到发送端发送的ACK确认后才可以发送数据。
在TCP连接建立完成之后就可以发送HTTP请求了。
参考:
http://www.seanyxie.com/wireshark%E6%8A%93%E5%8C%85%E5%9B%BE%E8%A7%A3-tcp%E4%B8%89%E6%AC%A1%E6%8F%A1%E6%89%8B%E5%9B%9B%E6%AC%A1%E6%8C%A5%E6%89%8B%E8%AF%A6%E8%A7%A3/
http://baike.baidu.com/view/1003841.htm
http://www.jellythink.com/archives/705
HTTP,英文Hyper Text Transfer Protocol,也就是超文本传输协议的缩写,他互联网上最普遍使用的一种应用协议,他主要是为了从Web服务器传输超文本到浏览器而设计的协议,由请求和响应构成。他是一种无连接的协议,也就意味着限制每次连接只处理一个请求,服务端处理完成且收到客户端应答后立即断开连接;同时也是无状态的,也就意味着没有记忆能力,每次连接都需要带上需要的信息。
完整的HTTP请求消息包含了:一个请求行、请求消息报头以及请求正文
其中Method表示请求方法,Request-URI是统一资源标识符,HTTP-Version表示请求的HTTP协议版本,CRLF表示回车和换行
请求方法主要有:GET(获得指定URL的数据) POST(请求服务器接收URI指定的文档作为可执行信息) HEAD(仅获取文档头部) PUT(请求服务器保存客户端传送过来的数据到URI指定文档) DELETE(请求服务器删除URI指定资源) TRACE(请求消息返回客户端,主要用于测试或诊断) OPTIONS(请求查询服务器性能或者查询与资源相关的选项和需求)
请求消息报头,请看下边关于消息报头的讲解
请求正文,注意,请求正文和请求消息报头之间会有一空行(只有CRLF的行);类似name=XXX&pwd=XXXX
的内容
来张HTTP请求图:
再来看响应消息,也是由三部分构成:状态行、响应消息报头以及响应正文
其中Status-Code,状态码,在HTTP1.1中定义了5类状态码,由三位数字组成,第一个数字定义的是响应类别:
1xx: 提供信息,表示请求以及被成功接收,需要继续处理
2xx: 肯定应答,表示请求已成功被服务器接收、理解并接受了
3xx: 重定向,代表了客户端需要进一步的操作才能完成请求,通常后续的请求地址会在本次响应Location域中指明
4xx: 客户端请求内存出现错误,妨碍了服务器处理。除非响应是一个HEAD请求,否则服务器返回一个解释当前错误状况以及是临时的还是永久的的实体正文内容。
5xx: 服务器错误,代表服务器在处理过程中发生了错误或者异常,也有可能是服务器无法完成对请求处理。除非这是一个HEAD请求,否则服务端应该响应一个包含解释当前错误状态以及是临时的还是永久的实体正文内容。
响应消息报头,参见下边关于消息报头的讲解
响应正文,同样,在正文和消息报头之间有一空行;内容就是服务器返回资源内容
来张HTTP响应图:
消息报头由众多报头域组成。每一个报头域都由名字+“:”+空格组成,消息报头域的名字是大小写无关的。主要包括普通报头、请求报头、响应报头和实体报头。
普通报头,常见普通报头有:
请求报头,常见的有:
响应报头,常见的有:
实体报头,请求和响应都是可以传送实体的,一个实体由实体报头域和实体正文组成,但并不是说实体报头域和实体正文要在一起发送,可以只发送实体报头域。实体报头定义了关于实体正文(eg:有无实体正文)和请求所标识的资源的元信息
常见的实体报头:
Content-Encoding,被用作媒体类型的修饰符,它的值指示了已经被应用到实体正文的附加内容的编码,因而要获得Content-Type报头域中所引用的媒体类型,必须采用相应的解码机制。Content-Encoding主要用于记录文档的压缩方法
Content-Language,描述了资源所用的自然语言。没有设置该域则认为实体内容将提供给所有的语言阅读者
Content-Length,用于指明实体正文的长度,以字节方式存储的十进制数字来表示。即一个数字字符占一个字节,用其对应的ASCII码来存储传输
Content-Type,用于指明发送给接收者的实体正文的媒体类型
Expires,给出响应过期的日期和时间。为了让代理服务器或浏览器在一段时间以后更新缓存中(再次访问曾访问过的页面时,直接从缓存中加载,缩短响应时间和降低服务器负载)的页面,我们可以使用Expires实体报头域指定页面过期的时间
Last-Modified,用于指示资源的最后修改日期和时间
其他:Allow Content-Location Content-MD5 Content-Range
参考:
https://zh.wikipedia.org/wiki/%E8%B6%85%E6%96%87%E6%9C%AC%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE
https://zh.wikipedia.org/wiki/HTTP%E7%8A%B6%E6%80%81%E7%A0%81
http://blog.csdn.net/gueter/article/details/1524447
http://hao.jser.com/archive/8331/
http://www.cricode.com/1880.html
http://www.jianshu.com/p/e83d323c6bcc
https://www.zybuluo.com/yangfch3/note/167490
根据服务器响应的消息报头,来决定响应的内容是如何缓存的,这里呢就需要探讨下浏览器的缓存机制了。
首先一个要点,为啥要缓存,也就是说缓存有啥好处呢:
减少了数据传输,也就意味着减少网络带宽消耗,省钱
降低服务器压力,大大提高服务器性能
减少网络延迟,加快页面打开速度
有了好处就要看看浏览器是如何缓存的。
对于浏览器缓存而言,规则主要是在HTTP的消息报头和HTML页面的Meta标签中定义的。有新鲜度和校验值两个维度来定义缓存的具体细则的。
新鲜度,其实也就是过期机制,指定一个有效期。
校验值,主要是当发现不再新鲜的时候,用于再次请求的时候的校验机制,发现校验的结果不匹配的话就说明需要更新了,匹配的话就直接返回304状态码,代表没有修改。
之前在规则中说过,对于控制可以写在HTML页面的Meta标签中,例如:
HTTP-EQUIV="Pragma" CONTENT="no-cache">
上边的代码就是告诉浏览器页面不缓存,但是这个支持情况不佳,而且缓存代理服务器肯定不支持,所以说不推荐。
下边重点来看和缓存相关的HTTP消息报头:
从上图可以看出两中不同的规则都对应的会有哪些消息报头,以及对应的类型和作用。这里主要细说的就是两组:
Cache-Control与Expires
Last-Modified与ETag
来看第一组,Cache-Control与Expires,他们的作用是一致的,都是控制有效期的。Expires的值是一个确定的日期时间,这个时间是格林威治时间(GMT),表明在这个日期时间之前都是可以使用缓存内容的,容易产生日期时间不一致(不同步)问题;而Cache-Control则可以控制更多优先级高于Expires。
这里仔细看下Cache-Control有用的响应头:
max-age=[秒]:表示在这个时间范围内缓存是新鲜的无需更新。类似Expires时间,不过这个时间是相对的,而不是绝对的。也就是某次请求成功后多少秒内缓存是新鲜的。
s-maxage=[秒]:类似max-age, 除了仅应用于共享缓存(如代理)。
public:标记认证的响应才能够被缓存。一般而言,需要认证的HTTP请求内容会自动私有化(不会被缓存)。
private:允许缓存专门为某一个用户存储响应,比方说在浏览器中;共享缓存一般不会,例如在代理中。
no-cache:每次在释放缓存副本之前都强制发送请求给源服务器进行验证,这在确保认证有效性上很管用(和public结合使用)或者保证内容必须是即时的,不得无视缓存的所有优点,如国内的微博、twitter等的刷新显示内容,必须不能有缓存啊。
no-store:强制缓存在任何情况下都不要保留任何副本。
must-revalidate:告诉缓存,我给你准备了一些关于新鲜度的信息,在表现的时候要严格遵循之。HTTP允许缓存在某些特定情况下返回过期数据,指定了这个属性,相对于告诉缓存,你丫必须严格遵循我的规则。
proxy-revalidate:类似must-revalidate,除了只能应用于代理缓存。
来一张公有缓存和私有缓存的区别图:
然后再看另一组Last-Modified与ETag,Last-Modified表明服务器该资源最后的修改时间,用于让浏览器知道本地缓存中副本是否是新鲜的,那为啥还需要ETag呢,主要是为了解决这几个问题:
某些服务器不能精确得到文件的最后修改时间,这样就无法通过最后修改时间来判断文件是否更新了。
某些文件的修改非常频繁,在秒以下的时间内进行修改,Last-Modified只能精确到秒。
一些文件的最后修改时间改变了,但是内容并未改变,此时该文件的缓存就无法被使用。
所以在HTTP1.1中加入了ETag,实体标识,他是服务器自动生成或者由开发者生成的对应的资源在服务器端的唯一标识。只有内容发生了改变这个值才会改变,这个值是类似于对文件进行MD5或者SHA1之后的结果。
那他们两组又会有啥子区别呢?下边请看:
配置Last-Modified/ETag的情况下,浏览器再次访问统一URI的资源,还是会发送请求到服务器询问文件是否已经修改,如果没有,服务器会只发送一个304回给浏览器,告诉浏览器直接从自己本地的缓存取数据;如果修改过那就整个数据重新发给浏览器;
Cache-Control/Expires则不同,如果检测到本地的缓存还是有效的时间范围内,浏览器直接使用本地副本,不会发送任何请求。
两组一起使用时,Cache-Control/Expires的优先级要高于Last-Modified/ETag。即当本地副本根据Cache-Control/Expires发现还在有效期内时,则不会再次发送请求去服务器询问修改时间(Last-Modified)或实体标识(Etag)了。一般情况下,使用Cache-Control/Expires会配合Last-Modified/ETag一起使用,因为即使服务器设置缓存时间,当用户点击“刷新”按钮时,浏览器会忽略缓存继续向服务器发送请求,这时Last-Modified/ETag将能够很好利用304,从而减少响应开销。
而对于浏览器缓存如何才能命中呢?这个根据不同的行为还有不同的结果,请看下图:
如果之前对两组的对比中说的那样,当按F5或者点击刷新的时候,会忽略Cache-Control/Expires的设置,也就是说会再次去向服务端请求,而Last-Modified/Etag还是有效的,服务器会根据情况判断返回304还是200,但是如果只有Cache-Control/Expires的话,服务端就不知道如何check,所以会返回完整资源了;而当用户使用Ctrl+F5进行强制刷新的时候,只是所有的缓存机制都将失效,重新从服务器拉去资源。
需要注意的是上边说的控制缓存的那些方法规则对于POST请求则无效的,因为POST请求是无法被缓存的;如果说HTTP响应头中不包含Last-Modified/Etag,也不包含Cache-Control/Expires的话,请求也无法被缓存。
参考:
http://www.alloyteam.com/2012/03/web-cache-2-browser-cache/
http://www.cnblogs.com/TankXiao/archive/2012/11/28/2793365.html
http://www.path8.net/tn/archives/2745
http://www.zhangxinxu.com/wordpress/2013/05/caching-tutorial-for-web-authors-and-webmasters/
如果说响应的内容是HTML文档的话,就需要浏览器进行解析渲染呈现给用户。整个过程涉及两个方面:解析和渲染;这里可能会有一个疑问,一定是先解析后渲染的吗?对于现代浏览器,答案是否定的,因为为了达到更好的用户体验,浏览器的呈现引擎会力求尽快将内容显示到屏幕上;而不必等到整个HTML文档解析完毕之后再去构建渲染树然后布局渲染;也就是说这是一个渐进的过程。
对于整个呈现引擎而言,他的基本流程是这样的:
而对于主流的Webkit和Gecko而言,他们的流程还是不太一样的,术语也不大一样,但是大概意思是一样的。可以先看看他们的主流程图。
WebKit主流程:
Mozilla 的 Gecko 呈现引擎主流程:
下边就来看下整个过程,先不考虑JS脚本。
在渲染页面之前,需要构建DOM树和CSSOM树。构建的基础就需要解析,而这个解析构建的过程都可以这样描述:
Bytes → characters → tokens → nodes → object model.
所以这个过程就是对HTML进行解析构建出DOM,对CSS进行解析构建出CSSOM。
首先来看DOM,假设有这样的HTML页面:
name="viewport" content="width=device-width,initial-scale=1">
href="style.css" rel="stylesheet">
Critical Path
Hello web performance students!
src="awesome-photo.jpg">
那浏览器会怎么处理呢?一张图来表示就是:
基本过程也就是:
Conversion转换:浏览器将获得的HTML内容(Bytes)基于他的编码转换为单个字符
Tokenizing分词:浏览器按照HTML规范标准将这些字符转换为不同的标记token。每个token都有自己独特的含义以及规则集
Lexing词法分析:分词的结果是得到一堆的token,此时把他们转换为对象,这些对象分别定义他们的属性和规则
DOM构建:因为HTML标记定义的就是不同标签之间的关系,这个关系就像是一个树形结构一样,例如:body对象的父节点就是HTML对象,然后段略p对象的父节点就是body对象。
最终的DOM树就是这样子的:
通过Chrome浏览器的开发者工具,我们可以看到这样的一些记录:
上边活动的记录就是上边解析HTML构建DOM所花费的时间。
再来看CSSOM,上边的HTML代码中,假设style.css内容如下:
body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }
和构建DOM的过程类似,我们需要将受到的CSS规则们转换为浏览器能够理解的东西CSSOM:
最终的CSSOM树就是:
要想知道解析CSS花了多长时间,看一下timeline:
注意上边的是在https://developers.google.com/web/fundamentals/performance/critical-rendering-path/?hl=en文章中介绍的内容,而内容是比较老的,最新的Chrome的开发者工具中,Timeline的记录中会有详细的过程:
有了DOM和CSSOM,那么就可以通过他们来构建渲染树了:
注意结果就是渲染树是和DOM树是相对应的,但是不是一一对应的,因为非可视化的DOM元素不会插入到渲染树中,例如head元素;而如果元素的display属性的值是none的话,也不会出现在渲染树中。
有了渲染树,就可以进行渲染了,渲染的基本流程可以说是这样的:
也就是黄色的四个步骤:
计算CSS样式
构建渲染树
布局,主要定位坐标和大小,是否换行,各种position overflow z-index属性
调用操作系统Native GUI的API绘制内容
那上边的图中那么多线是啥意思呢?其实就是表示通过JS动态修改了DOM或者CSSOM,且导致了重新布局或者渲染。
这里就涉及了两个重要概念:Reflow和Repaint
Reflow,也称作Layout,中文叫回流,一般意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树,这个过程称为Reflow
Repaint,中文重绘,意味着元素发生的改变只是影响了元素的一些外观之类的时候(例如,背景色,边框颜色,文字颜色等),此时只需要应用新样式绘制这个元素就OK了,这个过程称为Repaint
所以说Reflow的成本比Repaint的成本高得多的多。DOM树里的每个结点都会有reflow方法,一个结点的reflow很有可能导致子结点,甚至父点以及同级结点的reflow。
下面这些动作有很大可能会是成本比较高的:
增加、删除、修改DOM结点时,会导致Reflow或Repaint
移动DOM的位置,或是搞个动画的时候
内容发生变化
修改CSS样式的时候
Resize窗口的时候(移动端没有这个问题),或是滚动的时候
修改网页的默认字体时
注:display:none会触发reflow,而visibility:hidden只会触发repaint,因为没有发生位置变化。
基本上来说,reflow有如下的几个原因:
Initial,网页初始化的时候
Incremental,一些Javascript在操作DOM树时
Resize,其些元件的尺寸变了
StyleChange,如果CSS的属性发生变化了
Dirty,几个Incremental的reflow发生在同一个frame的子树上
这里需要注意的一件事情就是在HTML解析过程中回去加载外链的CSS,但是不会影响继续解析HTML的;在外链CSS得到之后要解析CSS。从前面的介绍可知渲染的话是需要DOM和CSSOM一起构建出来渲染树,然后渲染出来的,也就是说默认情况下CSS是会阻塞渲染的,为啥说默认情况呢,难道还有不阻塞渲染的时候?答案是有的,通过media query就可以使得CSS资源是非阻塞渲染的。
那说完了DOM和CSSOM了,就该说说这个JS脚本了,首先来看一张加入了脚本的整个渲染过程的流程图:
可以看出,通过JS脚本可以通过DOM API和CSSOM API来才做DOM树和CSSOM树(或者说CSS规则树);但是呢JS是会阻塞DOM的构建(除非显示的声明为异步async的)也会阻塞CSSOM的构建,也就意味着会推迟这个页面的渲染完成。
在页面中的脚本有两种情况,一种就是内嵌的,还有一种外链的。
对于脚本内嵌的情况,在解析HTML的过程中,直接执行脚本,这个时候会阻塞HTML解析来构建DOM,因为CSS不会修改DOM;还有一种情况那就是如果说正在脚本前面还有CSS的话,而此时CSSOM还未构建完成,那么浏览器就会推迟脚本的执行直至下载并构建好了CSSOM,而且在这个等待的过程中DOM的构建也会停止。所以说,在内嵌脚本之前不要有外链CSS,否则的话就会出现所谓的“CSS阻塞”,其实就是必须等到CSS加载完成解析构建CSSOM之后才会执行脚本,执行完脚本才会继续解析HTML构建DOM(这里Webkit则更智能一点,在执行脚本过程中发现引用了样式的话才暂停脚本的执行,等待CSS下载解析,然后再恢复)。
然后第二种情况,对于外链脚本而言,在解析HTML的过程中发现了外链的脚本,会发一个请求去得到脚本内容,但是这个过程是同步的,需要等待脚本下载完成且执行之后才会继续解析HTML构建DOM;但是对于现代浏览器在这个时候会生成第二个线程解析HTML文档,会继续下载资源,所以有多个外链脚本的话,会并行请求下载脚本内容,但是浏览器对于一个域的资源是有最大并行限制的,一般是6个,超过的就只能等待了。脚本虽然可以并行加载,但是执行的顺序是按照在页面中先后顺序执行的,执行的过程会阻塞后续解析构建渲染,同样也会阻止其他资源的下载。关于这方面JS加载对性能的影响可以看 http://www.alloyteam.com/2015/05/wang-ye-xing-neng-zhi-html-css-javascript/
到这里解析HTML并渲染的整个过程算是完了,有一些具体的细节没有说,想要了解的话可以看下边参考中的链接。
参考:
http://www.html5rocks.com/zh/tutorials/internals/howbrowserswork/
http://coolshell.cn/articles/9666.html
http://www.jianshu.com/p/e305ace24ddf
http://www.jianshu.com/p/e141d1543143
https://developers.google.com/web/fundamentals/performance/critical-rendering-path/?hl=en
http://stackoverflow.com/questions/1795438/load-and-execution-sequence-of-a-web-page
http://coolshell.cn/articles/9749.html
http://www.51testing.com/html/38/225738-220986.html
http://javascript.ruanyifeng.com/bom/engine.html
关于整个过程呢,只是说了一些我认为比较重要部分;而其他的例如浏览器根据不同的响应类型采取不同的策略(是下载,还是预览等)没有细说,当然并不是说不重要,因为涉及的实在是太广了,所以只是捡了部分来细说。
这里列一下《从输入URL到页面加载完的过程中都发生了什么事情》的一些参考链接:
http://network.51cto.com/art/201103/252335_all.htm
http://segmentfault.com/q/1010000000489803
http://www.guokr.com/question/554991/
http://fex.baidu.com/blog/2014/05/what-happen/
https://friendlybit.com/css/rendering-a-web-page-step-by-step/
http://www.zhihu.com/question/19645229