深入现代浏览器(第二部分)

导航发生了什么

这是四篇博客的第二篇, 来看看Chrome内部如何工作的. 在上一篇文章中, 我们了解到不同的进程和线程控制着浏览器的不同部分. 在这篇文章中, 我们会对于每一个线程和进程在渲染网站的时候的通信方式, 挖掘的更深.

让我们看一个简单的使用浏览器的案例: 你输入一个URL地址到浏览器中, 然后浏览器从网络上取回数据, 展示到页面上. 这篇文章中, 重点放到: 用户请求一个网站, 浏览器准备渲染页面, 也就是众所周知的导航.

通过浏览器进程开始

图1

图1: 上面浏览器的界面, 下图表示浏览器进程中的UI线程, 网络线程和存储线程都包含在浏览器进程中.

我们在第一篇文章中总结了, CPU, GPU, 内存和多进程架构, 所有tab页之外的一切都被浏览器进程控制. 浏览器进程有许多的线程, 比如UI线程: 负责描绘按钮以及浏览器中字段的输入, 网络线程: 从互联网中通过使用网络堆栈接受数据, 存储线程: 控制文件的访问或者更多. 当你输入URL到地址栏中的时候, 你的输入操作就是由UI线程所控制的.

一个简单的导航

第一步: 正在输入

当一个用户开始往地址栏中输入的时候, 第一个UI线程的询问是: "这是一个搜索匹配, 还是URL?". 在Chrome中, 地址栏也是搜索输入框, 所以UI线程需要通过匹配确认, 是将内容发送给搜索引擎, 还是访问你请求的站点.

图1

图1: UI线程判断输入的是搜索内容还是URL.

第二步: 开始导航

当一个用户按下Enter键, UI线程初始化一个网络方法来获取站点内容. 加载标志展示在tab的角落, 网络线程通过适当的协议, 例如DNS解析和为请求建立TLS链接.

图2

图2: UI线程通知网络线程导航到mysite.com

在这点上, 网络线程可能会接受一个服务重定向的头部, 例如HTTP 301. 在这个案例中, 网络线程和UI线程通信的结果是请求重定向, 然后, 另个一URL会被初始化.

第三步: 读取响应内容

图3

图3: 响应的头部确定了类型, 并且payload是真正的数据. 一旦响应体(有效载荷)开始进入, 网络线程就会在必要的时候, 查看流的前几个字节.响应中的Content Type展示了数据的类型, 但那可能已经丢失或者是错误的, MINI Type介绍介绍在这里. 正如源码中评论的那样, 这是一项棘手的任务. 你可以通过阅读评论发现不同的浏览器线程如何处理内容类型和负载对.

如果响应的内容是一个HTML文件, 然后下一步应该是解析数据到渲染引擎, 但如果这是一个zip文件, 或者一些其他的文件, 那就意味着这是一个下载请求, 所以他们需要解析数据到下载管理.

图4

图4: 如果响应的是一个HTML数据, 网络线程会确认是否来自安全站点.

这也就是进行安全浏览检查的地方. 如果主域名和响应的数据似乎已匹配到了恶意站点, 然后网络线程会提醒展示一个危险页面. 此外, Cross Origin Read Blocking(CORB)检查为了确保敏感的跨站点数据不会进入到渲染进程中.

第三步: 发现一个渲染进程

一旦所有的检查都完成了, 网络线程变信任浏览器跳转到请求的站点, 然后网络线程告诉UI线程数据已经准备好了, UI线程找到一个渲染进程进行web页面的渲染.

图5

图5: 网络线程告诉UI线程寻找渲染进程

因为网络请求需要几百毫秒才能得到响应, 这个进程就会采取一个优化策略. 当UI线程在第二步中, 发送一个URL请求到网络中, 就已经得知要导航到的网站. 在进行网络请求的同时, UI线程尝试主动的找到或者开始一个渲染进程. 这种方法, 如果所有的结果是预期那样的话, 当网络线程开始接受数据的时候, 一个徐然线程已经在备用位置准备好了. 如果导航的请求的跨域了, 这个备用线程也许不能用, 事实上, 另一个不同的进程也许用得到.

第四步: 提供导航

现在数据和渲染进程都准备好了, 接着IPC会通知浏览器进程给渲染进程一个导航. 它也会传送数据流,所以渲染进程能够保持接收HTML数据. 一旦浏览器进程确认获取到了渲染进程的提交记录, 导航就完成了, 文件加载格式化就会开始了.

此时, 地址栏, 安全表示和网站设置的UI都会反映出新页面的信息. tab页上的历史操作也会更新, 所以前进后端按钮会走到这个刚刚导航的网站. 为了更快的恢复,当你关闭tab或者window的时候, 历史会话记录会存储在磁盘上.

图6

图6: 浏览器进程发送IPC给渲染进程, 请求渲染页面.

额外步骤: 初始加载完成

一旦导航提交了, 渲染进程继续加载资源, 并开始渲染页面. 我们会在下一章中来回顾这个步骤里发生的细节. 一旦渲染进程"完成"渲染, 会发送一个IPC返回给浏览器进程(页面中所有frames都触发了onload事件,并完成执行). 在这点上, UI线程停在在tab页上的loading图标.

我说"完成", 因为客户端这边的JavaScript可以继续加载额外的资源并在这点之后, 继续渲染新的视图.

图7

图7: 渲染进程向浏览器进程发送一个页面上的IPC通知, 表示加载完成.

导航到不同站点

简单的导航到此就完成了. 但是如果用户再次放了一个不同的URL到地址栏, 会发生什么呢? 没错, 浏览器进程会通过一个相同的步骤导航到新的网站. 但在他能做之前, 需要简称当前渲染的网站, 是否订阅了beforeunload事件.

beforeunload事件能够, 当你尝试跳转或者关闭tab页时, 创建"确认离开网站"的提示. 每一个tab页里面都包含的js代码, 都是通过渲染进程处理的, 所以有新的导航请求进来时, 浏览器进程会检查当前的渲染进程.

不要在beforeunload中添加无意义的处理函数. 因为每一次导航开始的时候, 都会执行, 会造成更多的延迟.
这个事件应该按需添加, 例如用户离开当前页会丢失数据的时候, 需要进行提醒.

图8

图8: 浏览器进程通过IPC告诉渲染进程, 将要跳转到不同的站点.

如果导航是渲染进程创建的(比如用户点击了某个链接, 或者js执行了window.location = "https://newsite.com"), 渲染进程会首先检查beforeunload回调. 然后, 会执行和浏览器进程一样的过程. 唯一不同的时候, 导航的请求是从渲染进程到浏览器进程反向进行的.

当新的导航达到新的站点时, 会调用一个新的渲染进程来处理新的导航, 之前的渲染进程被留作处理, 类似与unload的事件. 其他的, 查看页面生命周期状态, 已经你可以使用页面生命周期API调用事件.

图9

图9: 浏览器进程通过IPCs告告诉新的渲染进程渲染页面, 告诉旧的渲染进程执行unload

服务工作举例

导航过程最近的一次变化, 就是引入了服务工作者. 服务工作者是一种在你app代码中写入代理的方式, 允许开发者获得更多的控制权, 例如对于本地缓存什么, 以及什么时候通过网络获取新的数据. 如果服务工作者设置了从缓存中加载页面, 那么就不需要从网络中请求数据.

重要的地方是, 记录运行在当前渲染进程中js代码中的服务工作者. 但触发导航请求的时候, 浏览器进程如何知道网站中含有服务工作者呢?

图10

图10: 浏览器进程中的网络线程正在查看服务工作者空间.

当服务工作者被注册, 服务工作者的空间被作为一个引用进行保存(你可以阅读服务工作者生命周期文章来了解更多关于空间的内容). 当导航事件发生的时候, 网络线程会对照已经注册的服务工作者空间来检查域, 如果一个服务工作者注册了这个URL, UI线程会找到渲染进程, 来按照顺序执行服务工作者的代码. 服务进程可能会从缓存中加载数据, 清除需要从网络中请求的数据, 或者它可以从网络中请求新的资源

图10

图10: 浏览器进程中的UI线程启动一个渲染进程处理服务工作者; 然后, 渲染进程中的工作线程从网络中请求数据.

导航预加载

如果服务工作者最终确定从网络中获取数据, 那么你可以看到浏览器进程和渲染进程的一个循环过程导致了延迟. 导航预加载是一种通过启动服务工作者同时加载资源, 来提高速度的方式. 它可以让请求带上头部信息, 允许服务决定对这个请求发送不同的请求, 比如, 只是对整个文档替换数据.

图12

图12: 浏览器进程中的UI线程开启一个渲染进程处理服务工作的同时, 开始了网络请求.

总结

在这篇文章中, 我们聊了导航中发生的, 以及你的web app代码, 例如响应头部, 和客户端JS在浏览器中的运行. 通过了解浏览器如何从网络中一步步的获取数据, 来更加容易的理解为什么开发使用预加载API. 在下一章中, 我们会介绍浏览器如何使用HTML/CSS/JavaScript来渲染页面.

啦啦啦啦, 今天是我生日, 也是第一次纹身.

posted @ 2019-07-22 11:22  张润昊  阅读(230)  评论(0编辑  收藏  举报