网页访问过程中发生了什么
这是揭秘浏览器原理系列的第二篇,在上一篇,我们讲解了浏览器如何利用不同的进程和线程去运作对应功能模块。本文会更深入地探讨不同的进程和线程是如何协作来展示一个网页的。
当你在浏览器输入一个网址,浏览器会从互联网获取倒数据,并将其展示出来。这篇文章将会重点讲用户输入地址到浏览器准备渲染网页的这个过程。
从浏览器主进程开始
在第一篇CPU、GPU、内存和多进程架构中,我们提到tab页以外的一切都在浏览器的主进程中运转。浏览器的主进程里包括有负责绘制导航栏上按钮和输入框等UI的线程,有负责网络数据获取的线程,有控制文件操作权限的存储线程等。当你输入一个URL到地址栏时,这个输入过程是由浏览器主进程中的UI线程来处理。
网页访问
第一步:处理输入
当用户在地址栏输入时,UI线程需要先知道输入的是搜索关键词还是URL?在谷歌浏览器里,地址栏同时也是搜索框,所以UI线程会先解析输入内容,再决定是跳转搜索引擎还是输入的地址。
第二步:开始跳转
当用户按下回车键,UI线程会将网址传给网络处理线程,让其初始化网络调用准备去拿网页内容。这时当前tab标签的一角会有加载中的菊花动画,网络线程则要通过一系列协议,如DNS查询和建立TLS连接来发起请求。
有时候,网络处理线程也许会从服务器那收到一个重定向的响应头,如HTTP 301(永久重定向)。在这种情况下,网络处理进程会告知UI线程服务器请求重定向,接下来,另一个URL请求将会被初始化。
第三步:读取响应数据
拿到响应数据后,网络线程将在必要时查看数据流前面的若干个字节。响应头的Content-Type(媒体类型)字段会说明这是什么类型的数据,如果这个字段不存在,则会进行媒体类型嗅探,源码里的注释说嗅探这个操作很棘手,因为需要考虑很多东西。如果你想了解不同的浏览器是如何处理媒体类型和数据有效载荷的,你可以去看一下源码的注释。
如果请求返回的是HTML文件,那下一步就是将其传给渲染进程。但如果是压缩文件或其他类型的文件,则意味着这是个下载文件的请求,那就要将文件数据交给下载管理器了。
这个环节还会做浏览安全检测,如果域名或返回数据跟系统黑名单匹配上了,那网络处理线程会展示一个警告页面。并且,跨域限制检查也会触发,以保证跨域敏感数据不会进入渲染线程。
第四步:准备渲染进程
一旦所有的检查都通过了,网络线程确定目标网站是安全的,就会告诉UI线程所有数据都准备就绪了。接下来,UI线程就会让已经初始化好的渲染进程开始处理页面。
因为网路线程去请求数据通常需要几百毫秒,为了充分利用这个时间空档,当在第二步UI线程将URL传给网络线程后,UI线程就马上异步地去为这个URL查找或创建一个渲染进程。如果一切进行顺利,则这个准备好的渲染进程就能在网络请求完成后立马开始工作。但如果进行了跨站重定向,则之前预先准备的渲染进程将不会被使用,而是针对新的网址重新创建。
第五步:完成跳转
当数据和渲染进程都准备好了,浏览器主进程会通过IPC(进程间通信)通信,把HTML数据以数据流的方式持续传输给渲染进程。一旦浏览器主进程收到渲染进程接受完毕的确认后,这次跳转就完成了,进入文档加载阶段。
此时,地址栏会更新,网站安全标示和网站设置UI会根据当前网站的信息来显示。tab的访问历史会更新,也就是前进/后退键会去到之前访问过的地方。为了保证tab的访问记录之后还能恢复,这个历史记录将会保存进硬盘。
额外的步骤:初始化加载完成
跳转完成后,渲染进程还需要继续加载资源和渲染页面。在下一篇文章中,我们将详细介绍此阶段发生的情况。当渲染进程“结束”渲染,它会通过IPC(进程间通信)告知浏览器主进程(这是在所有onload函数,包括iframe内的,都执行完毕后才进行的通信)。收到信息后,浏览器主进程内的UI线程将会停止tab上的loading的菊花动画。
上面说“结束”带了双引号,是因为客户端的JavaScript此时依然可以加载额外的资源和渲染新的试图。
跳转至不同的站点
至此,一个简单的网页访问完成了。但如果用户在地址栏再输入一个不同的URL会怎么样呢?当然,浏览器进程还是会走跟上面同样的步骤。但在此之前,它需要检查当前网页是否有声明 beforeunload 事件。
在你关闭tab页时,beforeunload事件中可能会写有一些提醒之类的代码,如“是否确定离开此页”。tab下的所有东西包括JavaScript代码都是由渲染进程处理的,所以浏览器主进程在跳转其他页面时,需要检查一下这个渲染进程内是否声明了这个事件。
注意:如非必要,不要随便声明 beforeunload 事件,因为只有在执行完这个事件后才能跳转下一个页面,所以在此事件里添加了一些无条件执行的内容可能会造成潜在问题。
如果跳转是在渲染进程里发起的(比如用户点击跳转链接或JavaScript运行了window.location = "https://newsite.com"),渲染进程会先检查 beforeunload 事件,接下来就是走之前同样的步骤。唯一不同的是,这次跳转是由渲染进程去通知浏览器主进程。
当跳转到另一个站点,会加入另一个渲染进程来处理。当前的渲染进程还需要做一些收尾工作,如触发 unload 事件。更多内容,可以查看页面生命周期一览和通过页面生命周期API了解如何使用钩子函数。
如果有Service Worker
跳转过程最近有一个新改动就是引入了service worker。service worker可以让你在应用里搭建一个网络代理,方便控制需要缓存的数据和数据的新鲜度。如果service worker设置了可以读缓存,那就没必要去请求网络数据了。
但问题是,service worker是运行在渲染进程中的JavaScript代码,当访问一个网页时,浏览器主进程怎么知道是否存在service worker呢?
当一个service worker被注册,它的作用域将被保存为一个引用。(更多关于作用域的信息可以参考这篇:service worker生命周期)跳转页面时,网络处理线程通过注册的service worker作用域去检查这个域名下是否注册有service worker,如果有就会引入渲染进程让它去执行service worker的代码。接下来,这个service worker开始运行,可能会从缓存取旧数据,免去请求,或者去请求新数据。
导航预载
你可以想象一下,如果service worker最终需要请求网络数据,那浏览器主进程和这个渲染进程间的频繁通信会有很大延迟。而导航预载就是优化此问题的一个机制,它可以在service worker启动的同时异步地去加载资源。你在头部声明需要的请求,就会允许服务器为这些请求发送不同的内容,如只更新数据而不是整个文档。
总结
这篇文章,我们了解了跳转过程中的细节,和你的网页应用里响应头、客户端JavaScript等,是如何与浏览器交互的。说明了浏览器获取网络数据的步骤,让你更容易了解像导航预载这种API的作用。下一篇我们将会深入了解浏览器是如何执行HTML/CSS/JavaScript来渲染页面的。
如有翻译错误,欢迎指正