`` 你是小公主`` » 从url到页面加载完会发生什么呢?
wulang8353 opened this issue · comments
从输入url到页面加载完成到底TMD发生了什么?
我希望是个女儿,这样我就见到你小时候的样子,弥补我迟到的20多年
单独把这一点拿出来是因为这是一道经典的面试题,浏览器渲染过程包含在了题目之中。这道题没有一个标准的答案,从前端的角度,我认为起码需要回答以下几点:
1、浏览器的地址栏输入URL并按下回车
2、浏览器查找当前URL是否存在缓存,并比较缓存是否过期
3、DNS解析URL对应的IP
4、根据IP建立TCP连接(三次握手)
5、HTTP发起请求,浏览器接收HTTP响应
6、渲染页面,构建DOM树
7、关闭TCP连接(四次挥手)
1 浏览器的地址栏输入URL并按下回车
Web浏览器通过URL从Web服务器请求页面, 一个网页地址实例:
http://www.runoob.com/html/html-tutorial.html
语法规则如下:
scheme://host.domain:port/path/filename
说明:
- scheme 协议, 比如 http、https、ftp
- domain 域名, 比如 runoob.com
- port 端口号, http默认80, https默认443
- host 主机名, http 的默认主机是 www
- path 定义在服务器上文件的路径
- filename 资源
这个属于基本知识了,其中还涉及到了同源策略、跨域、XSS与CRXF等,这里先不介绍了。
2 浏览器查找当前URL是否存在缓存,并比较缓存是否过期
根据是否需要重新向服务器发起请求来分类,可以将缓存类型分为强制缓存,对比缓存。
- 1、强制缓存: cache-control,Expires
Expires
是一个绝对时间,即服务器时间。浏览器会检查当前时间,如果没有到服务器时间,说明还没有过期,直接使用缓存文件。但是该方法存在一个问题:服务器时间与客户端本地时间可能不一致,就无法判断文件的有效期,因此该字段已经很少使用。
cache-control
中的 max-age 保存一个相对时间。例如Cache-Control: max-age = 484200
,表示浏览器收到文件后,在484200s内缓存有效,在这段时间内,发出的请求都会直接使用缓存。 如果同时存在 cache-control 和 Expires,浏览器总是优先使用 cache-control。
- 2、对比缓存:last-modified,Etag
Last-Modified/If-Modified-Since
上面说的当有效期过后,检查服务端文件是否更新的第一种方式,要配合Cache-Control使用。
按下 ctrl+r 刷新,ctrl+r会默认跳过 max-age 和 Expires 的检验直接去向服务器发送请求
last-modified 第一次请求资源时,服务器返回该字段,表示最后一次更新的时间。下一次浏览器请求资源时,请求中会发送if-modified-since 字段。服务器用自己本地 Last-modified 时间与if-modified-since 时间比较,如果不一致则认为缓存已过期并返回新资源给浏览器;如果时间一致则发送 304
状态码,让浏览器继续使用缓存。
Etag/If-None-Match
ETag并不是文件的版本号,而是一串可以代表该文件唯一的字符串(文件的索引节,大小和最后修改时间进行Hash后得到的)
当客户端发现和服务器约定的直接读取缓存的时间过了,就在请求中发送If-None-Match选项,值即为上次请求后响应头的ETag值,该值在服务端和服务端代表该文件唯一的字符串对比(如果服务端该文件改变了,该值就会变),如果相同,则相应HTTP304,客户端直接读取缓存,如果不相同,HTTP200,下载正确的数据,更新ETag值。
如果两者同时存在,If-None-Match/ETag优先,忽略Last-Modified/If-Modified-Since
HTTP1.1中 ETag 的出现主要是为了解决几个 Last-Modified 比较难解决的问题:
-
Last-Modified 标注的最后修改只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,它将不能准确标注文件的修改时间
-
如果某些文件会被定期生成,但有时内容并没有任何变化(仅仅改变了时间),但 Last-Modified 却改变了,导致文件没法使用缓存
-
有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形
无法被浏览器缓存的请求:
- HTTP响应头中不包含:Cache-Control/Expires
- Last-Modified/Etag; 非 HTTP:
- 需要根据 Cookie,认证信息等决定输入内容的动态请求是不能被缓存的
- 经过 HTTPS 安全加密的请求
- POST 请求无法被缓存
通过上表可以看到,在按 F5 进行刷新的时候,会忽略 Expires/Cache-Control 的设置,会再次发送请求去服务器请求,而 Last-Modified/Etag 还是有效的,服务器会根据情况判断返回 304 还是 200;而当使用 Ctrl+F5 进行强制刷新的时候,只是所有的缓存机制都将失效,重新从服务器拉去资源。
3 DNS解析URL对应的IP
当在浏览器中输入一个地址时,例如 www.baidu.com
,其实不是百度网站真正意义上的地址。
互联网上每一台计算机的唯一标识是它的IP地址,但是IP地址并不方便记忆。用户更喜欢用方便记忆的网址去寻找互联网上的其它计算机,也就是上面提到的百度的网址。
可以这样理解, 整个网络相当于一个非常巨大的电话簿。我们在打电话的时候首先要找到名字,然后再去拨打对应的电话号码。
例如 www.baidu.com 这个域名就相当于名字,而如何找到对应的电话号码呢? DNS域名解析的过程实际是将域名还原为IP地址的过程,这样就能找到电话号码进行通信了。
DNS域名解析的过程
首先浏览器先检查本地hosts文件是否有这个网址映射关系,如果有就调用这个IP地址映射,完成域名解析。
如果没找到则会查找本地DNS解析器缓存,如果查找到则返回。
如果还是没有找到则会查找本地DNS服务器,如果查找到则返回。
最后迭代查询,按根域服务器 ->顶级域,.cn->第二层域,hb.cn ->子域,www.hb.cn的顺序找到IP地址。
4 根据IP建立TCP连接(三次握手)
通过DNS域名解析后拿到IP地址,在获取到IP地址后,便会开始建立一次连接,这是由TCP协议完成的。
主要通过三次握手进行连接。
》 第一次握手: 建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;
》 第二次握手: 服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
》 第三次握手: 客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。
从图中就能看出为什么是三次握手而不是二次。因为要确保客户端与服务器双方的发送和接收的功能均正常。
-
第一次握手,客户端传到服务器,建立连接
-
第二次握手,服务器传到客户端
服务器接收到客户端的消息,并发送消息到客户端,落脚点在客户端
说明服务器能够正常接收消息,所以客户端的发送正常,服务器的接收也正常 -> 服务器
接收
√ | 客户端发送
√ -
第三次握手,客户端传到服务器
客户端接收到服务器的消息,并发送消息到服务器,落脚点在服务器
说明客户端能够正常接收消息,所以服务器的发送正常,客户端的接收也正常 -> 客户端
发送
√接收
√ | 服务器接收
√发送
√
5 HTTP发起请求,浏览器接收HTTP响应
HTTP请求报文是由三部分组成: 请求行, 请求报头和请求正文。
这里就不用说太多,平常开发调试中应该是经常遇的到,就讲讲状态码吧:
-
1xx:指示信息–表示请求已接收,继续处理
-
2xx:成功–表示请求已被成功接收、理解、接受
- 204:无内容。服务器成功处理,但未返回内容
-
3xx:重定向–要完成请求必须进行更进一步的操作
-
301:永久移动。请求的资源已被永久的移动到新URI,会返回新的URI,并且自动定向到该地址
-
302:临时移动。与301类似。但资源只是临时被移动。客户端应继续使用原有URI
-
304: 未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。 缓存经常遇到。
-
-
4xx:客户端错误–请求有语法错误或请求无法实现
-
401:请求身份认证,需要验证
-
405:请求中的方法被禁止,接口中限定了请求方法
-
415:服务器无法处理请求附带的媒体格式,一般是MIME
-
-
5xx:服务器端错误–服务器未能实现合法的请求
-
503:服务器暂时的无法处理客户端的请求,服务器未开启
-
504:网管超时,一般是服务器方法有问题
-
6 渲染页面,构建DOM树
浏览器拿到内容后,渲染大概可以划分成以下几个步骤:
- 1、DOM树:从上到下解析HTML文档建立DOM树
- 2、CSSOM树:加载解析样式生成CSSOM树
- 3、执行JavaScript:加载并执行JavaScript代码
- 4、构建渲染树(render tree):根据DOM树和CSSOM树,生成渲染树(render tree)
- 5、布局(layout 也叫Reflow回流):根据渲染树将节点树的每一个节点布局在屏幕上的正确位置
- 6、绘制(painting):遍历渲染树绘制所有节点
- 7、浏览器会将各层的信息发送给GPU,GPU会将各层合成,显示在屏幕上
如果 DOM 或 CSSOM 被修改,以上过程需要重复执行,这样才能计算出哪些像素需要在屏幕上进行重新渲染。实际页面中,CSS 与 JavaScript 往往会多次修改 DOM 和 CSSOM。
HTML页面加载和解析流程具体一点来说:
1、解析HTML结构。发现 ``<head>`` 标签中有一个 ``<link> `` 标签引用外部CSS文件
2、浏览器又发出CSS文件的请求,服务器返回这个CSS文件
3、浏览器继续载入html中 ``<body>`` 部分的代码,并且CSS文件就位,开始渲染页面了
4、浏览器在代码中发现一个 ``<img>`` 标签引用了一张图片,向服务器发出请求。此时浏览器不会等到图片下载完,而是继续渲染后面的代码
5、服务器返回图片文件,由于图片占用了一定面积,影响了后面段落的排布,因此浏览器需要回过头来重新渲染这部分代码
6、浏览器发现了一个包含一行Javascript代码的 ``<script>`` 标签,解析并运行
7、Javascript命令浏览器隐藏掉掉某个DOM元素,浏览器重新渲染这部分代码
8、解析至</html>的到来,整个流程结束
defer与async
由上可知,script标签的存在会阻塞渲染,但浏览器总是并行加载资源,当浏览器遇到script时候:
1、<script src="script.js"></script>
没有defer和async,浏览器会立即加载并执行脚本,此时会将渲染挂起,页面会有延迟的感觉,所以一般会将其放在</body
>之前
2、<script src="script.js" async></script>
有async,加载和渲染后续文档元素的过程和script.js的加载与执行并行进行,不一定要按照JS的顺序执行
3、<script src="script.js" defer></script>
有defer。加载后续文档元素的过程和script.js的加载并行,但是和script.js的执行要在所有元素解析完成之后, DOMContentLoaded事件(DOM树构建完成)之前完成,并且是按照JS的顺序执行
7 关闭TCP连接(四次挥手)
这一过程可以由客户端或者服务器任一方来触发,流程如下:
第一次挥手:浏览器发完数据后,发送FIN请求断开连接。
第二次挥手:服务器发送ACK表示同意
第三次挥手:但考虑到服务器可能还有数据要发送,那么就等服务器发送数据结束后再发送FIN,表示断开连接
第四次挥手:浏览器需要返回ACK表示同意
为什么是四次挥手?
客户端发送 FIN 仅仅表示不再发送数据,但是还能接受数据。服务器发送ack表示知道你准备关闭,但是我这还没发送完。直到服务器也发送了 FIN 表示不再发送数据,同意现在关闭连接。浏览器接收后发送ack到服务器,整个连接关闭。
抽象理解成:
客户端:"老服同志,我不想讲了,要关闭连接了哈" -> FIN、ack
服务器:“你个老客,你要走了啊? 等回,我还没说完” -> ack
(服务器啰嗦了一会,看到没回应觉得没意思)
服务器: “算了,不说了,没意思,回见了您” -> FIN
客户端:“回见” -> ack
💔 兜兜转转两年, 到现在还是孑然一身。虽然自己也感觉到有所成长,但越来越孤独了呢~ |