浏览器导航流程
从输入 URL 到页面展示,中间发生了什么?
这个是一道很经典的面试题,它能比较全面的考察一个人的网络、操作系统、Web 等相关知识。同时还可以把网络、操作系统、HTML、CSS、JS 这些知识串联起来。那么今天我们就来探索一下这个流程。
上图就是从输入 URL 到页面展示完整流程示意图。我们先从大体上过一遍这个流程,然后在去看具体的细节。
- 首先,浏览器进程接收到用户输入的 URL 请求,浏览器进程便将该 URL 转发给网络进程。
- 然后,在网络进程中发起真正的 URL 请求。
- 接着网络进程接收到了响应头数据,便解析响应头数据,并将数据转发给浏览器进程。
- 浏览器进程接收到网络进程的响应头数据之后,发送“提交导航 (CommitNavigation)”消息到渲染进程。
- 渲染进程接收到“提交导航”的消息之后,便开始准备接收 HTML 数据,接收数据的方式是直接和网络进程建立数据管道。
- 最后渲染进程会向浏览器进程“确认提交”,这是告诉浏览器进程:“已经准备好接受和解析页面数据了”。
- 浏览器进程接收到渲染进程“提交文档”的消息之后,便开始移除之前旧的文档,然后更新浏览器进程中的页面状态。
在浏览器中,从用户发出 URL 请求到页面开始解析的这个过程,就叫做导航。下面我们就来详细分析一下各个阶段的细节。
从输入 URL 到页面展示
用户输入
当用户在地址栏中输入一个查询关键字时,地址栏会判断输入的关键字是搜索内容,还是请求的 URL。
如果是搜索内容,地址栏会使用浏览器默认的搜索引擎,来合成新的带搜索关键字的 URL。
如果判断输入内容符合 URL 规则,那么地址栏会根据规则,把这段内容加上协议,合成为完整的 URL。
当用户输入关键字并键入回车之后,这意味着当前页面即将要被替换成新的页面,不过在这个流程继续之前,浏览器还给了当前页面一次执行 beforeunload 事件的机会,beforeunload 事件允许页面在退出之前执行一些数据清理操作,还可以询问用户是否要离开当前页面,比如当前页面可能有未提交完成的表单等情况,因此用户可以通过 beforeunload 事件来取消导航,让浏览器不再执行任何后续工作。
当我们按下回车后,标签页上的图标就变成了一个 loading 状态,但是这个时候的页面还是之前的页面。当渲染进程发出“提交文档”的信息后,页面的内容才会被替换。
URL 请求过程
接下来,便进入了页面资源请求过程。这时,浏览器进程会通过进程间通信(IPC)把 URL 请求发送至网络进程,网络进程接收到 URL 请求后,会在这里发起真正的 URL 请求流程。那具体流程是怎样的呢?
首先,网络进程会查找本地缓存是否缓存了该资源。如果有缓存资源,那么直接返回资源给浏览器进程;如果在缓存中没有查找到资源,那么直接进入网络请求流程。
请求的第一步是要进行 DNS 解析。这个时候 Chrome 会先去查它自己的 DNS 缓存,再查系统缓存,再查 host 文件,然后再逐级往上查,直到根域名服务器。为了提高查询效率,这里的每一层都会有很多缓存。
解析完 DNS 后,便可以获取请求域名的服务器 IP 地址。如果请求协议是 HTTPS,那么还需要建立 TLS 连接。
接下来就是利用 IP 地址和服务器建立 TCP 连接。连接建立之后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的 Cookie 等数据附加到请求头中,然后向服务器发送构建的请求信息。
服务器收到请求信息后,会根据请求信息生成响应数据,然后发送给浏览器的网络进程。等网络进程收到响应头后,就开始解析响应头的内容。
如果响应头中的状态码是301或302,那么就说明服务器需要浏览器重定向其他 URL,这个时候网络进程会从响应头的 Location 字段里面取出重定向的地址,然后发起新的请求,这个流程就会重头开始。
如果响应头中的状态码是200,那么网络进程就可以继续处理这个请求。在响应头中有一个非常重要的字段,叫 Content-Type ,它的作用是告诉浏览器服务器返回的响应体数据是什么类型的。然后浏览器会根据 Content-Type 字段的值来决定如何处理响应体的内容。
比如,content-type 的值是 text/html,浏览器就知道服务器返回的数据是 HTML 格式。或者 content-type 的值是 application/octet-stream,这个格式就是一个字节流类型,通常情况,浏览器会判断它为下载类型,那么这个内容会被提交给浏览器的下载管理器,然后去下载它,同时这个 URL 请求的导航流程就到此结束了。但如果是 html 类型,那么浏览器会继续下面的导航流程。
关于浏览器是如何解析服务端发来的字节流(Bytes),下面有个 Toy 版的实现。实现原理就是使用状态机逐个解析响应头中的 statusLine、headers、body,然后封装成对象。
准备渲染进程
默认情况下,Chrome会为每个页面分配一个渲染进程,也就是说,每打开一个新页面就会配套创建一个新的渲染进程。但是,也有一些列外,在某些情况下,浏览器会让多个页面直接运行在同一个渲染进程中。
渲染进程
比如我从百度里打开了另外一个页面—百度搜索,我们看下面的任务管理器截图:
从图中可以看出,打开的这三个页面都是运行在同一个渲染进程中的,进程ID是76086.
那什么情况下会多个页面同时运行在一个渲染进程中呢?
首先我们要先了解一下什么是同一站点(same-site)。具体来讲,同一站点定义如下:
- 跟域名相同
- 协议相同
- 包含该域名下所有的子域名和不同的端口
这里需要注意,同一站点和同源策略是有区别的,同源是指:域名、协议、端口相同。
Chrome 的默认策略是,每个标签对应一个渲染进程。但如果从一个页面打开了另一个新页面,而新页面和当前页面属于同一站点的话,那么新页面会复用父页面的渲染进程。官方把这个默认策略叫 process-per-site-instance。
总结来说,打开一个新页面采用的渲染进程策略就是:
- 通常情况下,打开新的页面都会使用单独的渲染进程。
- 如果从 A 页面打开 B 页面,且 A 和 B 都属于同一站点的话,那么 B 页面复用 A 页面的渲染进程;如果是其他情况,浏览器进程则会为 B 创建一个新的渲染进程。
- 如果连接里面使用了 rel=”noopener noreferrer”这个属性,就会新打开一个渲染进程。使用 noopener noreferrer 就是告诉浏览器,新打开的子窗口不需要访问父窗口的任何内容,这是为了防止一些钓鱼网站窃取父窗口的信息。
渲染进程准备好之后,还不能立即进入文档解析状态,因为此时的文档数据还在网络进程中,并没有提交给渲染进程,所以下一步就进入了提交文档阶段。
4. 提交文档
所谓提交文档,就是渲染进程要告诉浏览器进程,我已经收到网络进程的数据了,已经开始解析了,你可以更新页面状态了。
此时响应报文在网络进程那里,需要浏览器进程做一个统一调度,跟渲染进程说,你可以去接收响应报文了。然后渲染进程就通过 IPC 与网络进程通信,让网络进程把响应报文全部发过来。
渲染进程拿到所有响应报文,就会回复浏览器进程全部响应报文都拿到了,你浏览器进程可以更新界面,我渲染进程可以进行渲染。浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态。包括安全状态、地址栏的 URL、前进后退的历史状态,同时会停止标签页上的 loading 动画,然后更新 Web 页面。但这个时候的页面是白屏的。
总结
到这里,一个完整的导航流程就“走”完了,这之后就要进入渲染阶段了,关于渲染流程我会在下篇文章仔细介绍。 一旦文档被提交,渲染进程便开始页面解析和子资源加载了。渲染完成后,一个完整的页面就生成了。