本文是根据Mariko Kosaka在谷歌开发者网站上的系列文章https://developer.chrome.com/blog/inside-browser-part2/ 翻译而来,共有四篇,该篇是第二篇。对于其中一些直译出来不太好理解的句子,笔者做了加工处理和提炼。

在导航中发生了什么

在上一篇文章中,我们了解到了不同的进程和线程如何处理浏览器中的各个部分。在这篇文章中,我们将更深入探讨各个进程和线程如何通信以展示出一个网页来。

让我们来看一个简单的浏览网页的场景:当你输入URL后,浏览器从因特网中获取数据并把页面展示出来。在本文中,我们将重点关注用户请求站点和浏览器准备呈现页面的部分——也就是导航。

从浏览器进程开始

如我们在第一篇文章中https://www.cnblogs.com/geek1116/p/16419720.html所述,tab页之外的所有内容都是由浏览器进程处理的。浏览器进程具有诸如渲染按钮和输入框的UI线程、处理网络栈以从因特网中接收数据的网络线程,以及控制文件等内容访问的存储线程;当你向地址栏中输入URL时,就是由浏览器进程的UI线程来处理你的输入。

顶部的浏览器UI,底部是包含UI、网络和存储线程的浏览器进程

一个简单的导航

步骤1:处理输入

当用户在地址栏中输入的时候,UI线程首先需要考虑的是:“输入内容是一个搜索请求还是URL呢?”。在Chrome中,地址栏同时也是搜索输入域,因此UI线程需要解析并决定将输入内容发送到搜索引擎还是你想要请求的站点。

UI线程在判断输入内容是一个搜索请求还是URL

步骤2:开始导航

当用户敲击回车键后,UI线程发起一个网络调用来获取站点内容。在tab栏的一角会显示加载效果,网络线程会查找合适的协议做接下来的事 例如根据DNS查找域名、为请求建立TSL连接。

UI线程通知网络线程导航至mysite.com

此时,网络线程可能会收到HTTP 301之类的服务重定向标头(header)。这种情况下,网络线程会告知UI线程该服务需要请求重定向。接着便会发起另一个URL请求。

步骤3:读取响应

一旦响应主体(有效负载)开始进入,网络线程会在有必要时检查流(stream)的前面几个字节。这是因为:响应头部的Content-Type字段会告知数据的类型,但它可能会丢失或者出错,所以在这里进行MIME 嗅探。如chromium源码注释中所说,这是一项“棘手的业务”;你可以阅读该处的注释并看到不同浏览器是如何处理content-type/payload的。

如果响应是一个HTML类型文件,则会将该数据传递给渲染进程;而如果是一个zip或其他类型文件的话,意味着这是一个下载请求所以需要将该数据传递给下载管理器。

网络线程检查响应数据是否是来自安全站点的HTML文件

↑这里也是进行安全浏览检查的地方。如果响应数据和对应的域(domain)与已知的恶意站点相匹配的话,网络线程会发出警告页面。此外,还会做的一项检查CROB——跨域源读阻止(注意是CROB不是CROS),确保跨域的敏感数据不会到达渲染进程。

步骤4:查找渲染进程

当所有的检查完成并且网络线程确信浏览器应该导航至请求站点后,就会告知UI线程数据已经准备就绪。然后UI线程找出一个渲染进程来进行网页的渲染:

由于网络请求可能需要花费数百毫秒之久来等待响应返回,所以一项加速此过程的优化手段应运而生:当UI线程在上述步骤2中向网络线程发送URL请求时,是已经知道它所要导航的站点的,此时它会在网络请求期间并行地查找或启动一个渲染进程。这样一来,如果一切按预期进行,当网络线程的数据到达时,渲染进程就已经处于就绪状态了。如果导航发生重定向是且跨站点的话,则可能不会使用该就绪进程,这种情况下需要使用另外的进程了。

步骤5:提交导航

现在数据和渲染进程都已经准备好了,浏览器进程会向渲染进程发送一个IPC(第一篇文章中提到的进程间通讯手段)来提交导航。该IPC也会被用来传递数据流(stream)以让渲染进程可以持续接收HTML数据。当浏览器进程收到渲染进程中对导航提交的确认,本次导航就结束了,接着开始HTML文档的加载阶段。

这时候地址栏就被更新了,安全指示器和站点设置UI(地址栏最左侧的????图标)上会反映出新页面的站点信息。该tab页的会话历史将会被更新——因此可以让顶部的后退/前进按钮在曾经导航过的站点间逐次跳转。为了能够让你在关闭tab页/窗口后恢复tab页/会话,会话历史会被储存到磁盘上。

浏览器进程和渲染进程之间的IPC,请求渲染页面

额外步骤:初始化加载完成

导航提交后,渲染进程会继续加载资源和渲染页面。我们会在下一篇文章中详细此阶段完成的内容。当渲染进程“完成”渲染之后,会返回IPC给浏览器进程(这发生在页面以及所有内嵌iframe的onload事件触发并执行完之后)。此时,UI线程会停止tab标签旁边的加载效果。

上面一段中的“完成”字样我特意用双引号括起来,这并不代表着真正的完成,因为在此之后Javascript仍然可以加载其他资源并渲染新的视图。

从渲染进程到浏览器进程的IPC来通知页面“已加载”

导航到其他站点

这个简单的导航过程就完成了!但是如果用户又在地址栏中输入一个不同URL的话会发生什么呢?好吧,浏览器进程将通过相同的步骤来达到新的站点。但在此之前,它需要检查下当前渲染的页面是否需要关注beforeunload事件。

beforeunload可以在你试图导航至别处或关闭tab页时创建“离开此站点?”的警告。tab中的所有东西报错你的Javascript代码都是渲染进程处理的,所以浏览器进程必须在新的导航请求到来时检查当前的渲染进程。

从浏览器进程到渲染进程的IPC,告知即将导航到新站点

如果导航是从渲染进程启动的(例如用户点击了一个链接或者客户端的Javascript运行了window.location="https://newsite.com"),渲染进程会首先检查beforeunload处理器。之后所经历的过程就和浏览器进程启动导航的一样了。唯一的区别在于导航请求是从渲染进程到浏览器进程中的。

当新导航指向的站点不同于当前渲染的站点时,会调用另一个单独的渲染进程来处理新导航,而当前的渲染进程会被保留以处理诸如unload等事件。更多详情请参见页面生命周期状态概述以及如何使用页面生命周期API

从浏览器进程到新渲染进程告知渲染页面和到旧渲染进程卸载的两个IPC

Service Worker

这个导航过程最近新引入的一个变化是service workder。service workder是一种在应用程序代码中编写网络代理的方法;允许Web开发人员更好地控制本地缓存的内容以及何时从网络获取新数据。如果service workder设置为从缓存中加载页面,则无需向网络请求数据。

需要记住的一个重点是,service workder是运行在渲染进程中的Javascript代码。但是当导航请求到来的时候,浏览器进程如何知道该站点有service workder呢?

当注册了一个service workder后,该service workder的作用域会被作为一个引用保存(你可以在这篇Service Worker生命周期中了解过于作用域的更多信息)。当一个新导航发生时,网络线程根据已注册的service worker作用域来检查新站点的域(domain),如果该域的URL有对应注册的service worker,UI线程会找到一个渲染进程以运行service worker代码。service worker能够从缓存中加载数据,这样就不用从网络上请求数据;或者它也可以从选择从网络上请求资源。

浏览器进程中的网络线程在查到service worker的作用域

浏览器进程中的UI线程启动了一个渲染进程来处理service worker;然后渲染进程中的worker线程从网络请求数据

导航预加载

你可以看到如果service worker最终决定从网络请求数据的话,在浏览器进程和渲染进程之间的这种往返会造成延迟。Navigation Preload是一种通过在service worker时并行加载资源来加速这一过程的机制。它会给这些请求的header中做标记以表示这是由Navigation Preload机制发出的请求,然后服务器可以决定对这些请求返回不同的内容;例如只更新部分数据而不是返回整个HTML文档。

浏览器进程中的UI线程在启动一个渲染进程来执行service worker的同时并行地发出网络请求

总结

在这篇文章中,我们研究了导航期间发生的事情以及你的web应用程序(例如响应header和客户端javascript)是如何与浏览器交互的。了解了浏览器从网络请求数据所经历的步骤,让我们更加明白了为什么要开发出类似Navigation Preload的这些APIs。在下篇文章,我们将深入探讨浏览器是如何解析我们的HTML/CSS/Javascript来渲染页面的。