书接上文。
深入理解现代浏览器 阅读笔记之一
ID:6192022-07-17
在这一篇(part2)中,Mariko Kosaka会从一次导航的场景出发,带我们理解浏览器内部各个组成部分如何通力协作将页面呈现出来。这其实就和大家常见的面试题“当你在地址栏输入url到页面呈现之间发生了什么”差不多了,只不过现在我们更加专注于浏览器的部分。
一切要从浏览器进程开始讲起
在上一篇(part1)中提到过:在tab标签页之外的所有东西都是由浏览器进程管理的。浏览器进程中有着负责绘制前进后退按钮和地址栏输入框的UI线程、负责处理网络的网络线程、负责文件访问的储存线程。当输入一个url到地址栏的时候,你的输入会首先被浏览器进程中的UI线程处理。
一次简单的导航
第一步:处理输入
现在的浏览器中,地址栏不只是用来输入地址跳转,还可以直接当作一个搜索框来使用,接入不同的搜索引擎。所以当用户输入一串字符的时候,UI线程会先判断这一串字符到底是叫它干啥。是一串要交给搜索引擎搜索的关键词还是只是一个朴实无华的url。
第二步:开始导航
当用户敲下Enter的时候,UI线程会发起一次网络请求来获取网站的内容。与此同时,一个旋转加载的图标会代替tab标签页头的favicon图标,表示当前正在加载。UI线程发起了网络请求后,就将整个网络请求抛给网络线程了。就像公司里上级提出了一个企划,交给下级完成一样。上级只负责企划,下级负责完成。
在这期间,网络线程可能收到服务器返回的例如HTTP301重定向的信号。这个时候,网络线程就会通知UI线程:“服务器这边叫你再去请求一下新的地址,你再发起一次吧”。就像下级发现这次的企划有些问题的时候就会联系上级:“这企划8太行,你要不再搞份新的”。而下级本身是没有撰写新企划的资格的(悲。
然后一个指向新url的新请求就再次由UI线程发起了。
第三步:阅读(分析)响应
一当响应体(payload)开始被接收时,必要的时候网络线程会查看数据流的前几个字节来判断接受回来的是个什么类型的数据。本来响应头中的Content-Type
字段会告诉我们这个信息,但是由于其可能会缺少或者错误,所以需要再次利用MIME Type sniffing来确认一下接收到的数据究竟是个啥。万一返回的响应头指鹿为马,结果浏览器以错误的方式展示了数据,不仅可能导致一些错误的发生,还有可能造成安全问题。这也被开发者们在源码中被成为“tricky business”。在源码)的备注里面,chrome的开发者们对其余的各个浏览器都做了调查,你可以看到除了chrome以外,不同浏览器在不同的content-type/payload的情况下的应对情况。
一些例子
content-type
字段缺失,但是读取到的响应体数据为HTML
类型的时候,调查结果显示所有浏览器都达成共识将其渲染为HTML
。所以chrome也选择了在这种情况下跟随大流的选择。// HTML payload, no Content-Type header:
// * IE 7: Render as HTML
// * Firefox 2: Render as HTML
// * Safari 3: Render as HTML
// * Opera 9: Render as HTML
//
// Here the choice seems clear:
// => Chrome: Render as HTML
又例如在头部content-type
字段为text/plain
,但是读取到的响应体数据依旧为HTML
类型的时候,就出现了一些分歧。
// HTML payload, Content-Type: "text/plain":
// * IE 7: Render as HTML
// * Firefox 2: Render as text
// * Safari 3: Render as text (Note: Safari will Render as HTML if the URL
// has an HTML extension)
// * Opera 9: Render as text
//
// Here we choose to follow the majority (and break some compatibility with IE).
// Many folks dislike IE's behavior here.
// => Chrome: Render as text
// We generalize this as follows. If the Content-Type header is text/plain
// we won't detect dangerous mime types (those that can execute script).
在一般条件下,除了IE7秉承将最原始的东西呈现给用户的精神,其余的浏览器都选择遵从content-type
的说法,你说是马就是马吧,将其渲染为普通的字符串。
在这里chrome的工程师们也得出了结论,随大流!
这里就举几个例子,更多的场景与应对方式可以再去看源码的注释。
如果最后网络线程认为返回的是一个HTML文件的时候,下一步就是将数据(HTML的内容)传递给渲染进程啦。但是如果这是其他什么别的类型的文件时,以zip文件为例,这就意味着这是一次下载请求,需要将数据传递给下载管理器。
作为一个非常高频的接受外界数据的渠道,安全检查是必不可少的。chrome的 SafeBrowsing 也是在这一阶段发挥作用。如果请求的域名和返回的数据和一些已知的恶意网站匹配上时,网络现场就会提示这是一个危险网站。除此之外,CORB也在这个阶段发光发热,用于保证一些敏感的跨站数据不会传递给渲染进程。
第四步:找到一个渲染进程
一旦所有检查都完成并且网络进程也确定了这个响应是安全的,浏览器可以导航到这个被请求的网站的时候,网络线程就会反馈到UI线程:“我滴任务完成辣”,数据已经准备就绪。
UI线程这个时候回去找到一个渲染进程来承担这个页面的渲染任务。
由于网络请求可能花费上百毫秒才能等到请求返回,这里介绍一个被用于加快这个过程的优化方法。
当UI线程在第二步向网络进程发布请求任务的时候,它实际上已经知道用户要去往哪里了。所以UI线程可以尝试在网络线程在处理网络请求的同时,主动去寻找或者新开一个渲染进程。通过这种方式,正常来说,当网络线程接收到数据的时候,已经有一个渲染进程准备就绪了。当然这个就绪的渲染进程也不一定在这一次请求中派得上用场,返回的是一个301跨站跳转时,就需要别的渲染器进程了。(因为此时这个就绪的进程已经写上了跳转前url的名字了)
第五步:完成导航
现在,数据和渲染进程已经就位,浏览器进程将会通过IPC来通信渲染进程来完成导航。浏览器进程会将数据流传递给渲染进程,保证渲染器进程能持续接受数据。一旦浏览器进程接收到渲染器进程已经确认完成了导航的反馈,那么这次导航就算完成了。这时,文档的加载阶段才刚刚开始。
在这时,地址栏就会更新显示,安全指示器和页面设置的UI也会显示当前页面的数据。
标签页的session历史也会随之更新以便于前进后退按钮可以正常使用。为了实现标签页的session历史可以被恢复,当你关闭一个tab或者关闭浏览器窗口的时候,session历史会被保存在硬盘上。
再来一步:首次加载完成
一旦导航完成了之后,渲染进程会继续加载资源、渲染页面。这中间的细节会在下一篇中介绍,这里先跳过。假如此时渲染进程“完成”了渲染,它会通过IPC反馈给浏览器进程。浏览器进程接收到之后,就会让UI线程把那个加载的图标给换成新页面的favicon图标。
这里的“完成”渲染指的是当所有的onload
事件都被触发并且完成
实际上,因为页面的JS还可以继续从服务器获取资源,重新渲染新视图,所以完成被打上了引号。
小结
这就是在一次简单的导航中,浏览器内部的执行步骤。这只是Inside look at modern web browser (part 2)的前半部分。后半部分包括从当前页面导航到不同站点、Service Worker和导航预加载的相关内容,我会留到下一篇翻译阅读来分享。
在学习这个“一次简单的导航”的过程中,又是一顿醍醐灌顶的感觉。每次遇到一些讲得不清楚的地方,都很想挨个跳进去深入研究一番。最后还是制止住了自己,毕竟浏览器深如海,真跳下去可没个头。还是先从整体出发,先将整体的构造掌握了,再去深究细节吧。
(゜ρ゜)