跳到主要内容

认识

2024年03月21日
柏拉文
越努力,越幸运

前言

“在浏览器里,从输入 URL 到页面展示,这中间发生了什么? ”这是一道经典的面试题,能比较全面地考察应聘者知识的掌握程度,其中涉及到了网络、操作系统、Web 等一系列的知识。

一、URL 解析


用户输入内容,进行URL 解析(编码)

  • 如果是文本,则拼接成默认搜索引擎加关键字的 URL 进行搜索 •
  • 如果是 URL 就进行页面访问请求,并加上协议头(http、https 的区别)

1.1 问题

二、查询缓存


在真正发起网络请求之前,浏览器会先在浏览器缓存中查询是否有要请求的文件。当浏览器发现请求的资源已经在浏览器缓存中存有副本,它会拦截请求,返回该资源的副本,并直接结束请求,而不会再去源服务器重新下载。这样做的好处有:

  • 缓解服务器端压力,提升性能(获取资源的耗时更短了);
  • 对于网站来说,缓存是实现快速资源加载的重要组成部分。

如果缓存查找失败,就会进入网络请求过程了。

2.1 问题

三、DNS 解析


HTTP 发送网络请求之前,浏览器需要通过TCP与服务器建立连接。而TCP连接需要用到IP端口号,所以浏览器会请求DNS返回域名对应的IP请求DNS的过程中首先查看是否有DNS缓存,如果没有DNS缓存,那么开始进行DNS解析。

3.1 问题

四、等待TCP队列

通过DNS解析获取到IP后,接下来就需要获取端口号了。通常情况下,如果URL没有特别指明端口号,那么HTTP协议默认是80端口。IP端口号都准备好后,进入TCP队列

如果使用 http/1.1: 一个tcp同时只能处理一个请求,浏览器会为每个域名维护6tcp连接! 但是每个tcp连接是可以复用的,也就是处理完一个请求之后,不断开这个tcp连接,可以用来处理下个http请求。如果在同一个域名下同时有 10 个请求发生,那么其中 4 个请求会进入排队等待状态,直至进行中的请求完成。如果当前请求数量少于 6,会直接进入下一步,建立 TCP 连接。

如果使用 http/2.0: 采取多路复用,在一个tcp连接,客户端和浏览器都可以同时发送多个请求或回应。所以如果使用http2,浏览器只会为每个域名维护一个tcp连接。

五、建立TCP连接

5.1 问题

六、发送 HTTP 请求


七、服务器处理请求


7.1 Node BFF

很多项目并非纯前端, 比如我们之前的一个项目 UMU 学习互动平台, 它通过 Node 充当 BFF (Backend for frontend,即服务于前端的后端), 做了许多数据数据, 工作流如下:

  1. Node 收到请求后, 向服务端发起请求, 服务端响应数据

  2. Node 基于数据, 进行权限判断, 这时候需要服务端鉴权

  3. 如果有权限, 读取页面模版(前端项目中每一个页面配有 .tpl 文件模版), 获取当前语种多语数据, 并注入数据, 最后渲染模版

  4. 最终生成 HTML 文件, 返回 HTML

八、服务端响应请求


九、断开TCP连接


通常情况下,一旦服务器向客户端返回了请求数据,它就要关闭 TCP 连接。不过如果浏览器或者服务器在其头信息中加入了:

Connection:Keep-Alive

那么 TCP 连接在发送后将仍然保持打开状态,这样浏览器就可以继续通过同一个 TCP 连接发送请求。保持 TCP 连接可以省去下次请求时需要建立连接的时间,提升资源加载速度。

十、处理响应


服务器接收到请求信息后,会根据请求信息生成响应数据(包括响应行、响应头和响应体等信息),并发给网络进程。等网络进程接收了响应行和响应头之后,就开始解析响应头的内容了。

10.1 解析响应行状态码

在接收到服务器返回的响应行后,网络进程开始解析响应行,

  • 服务器返回的响应行的状态码是301或者302,那么说明服务器需要浏览器重定向到其他 URL。这时网络进程会从响应头的 Location 字段里面读取重定向的地址,然后再发起新的 HTTP 或者 HTTPS 请求,一切又重头开始了。在导航过程中,如果服务器响应行的状态码包含了 301、302 一类的跳转信息,浏览器会跳转到新的地址继续导航;
  • 服务器返回的响应行的状态码是 200,这是告诉浏览器一切正常,可以继续往下处理该请求了。

10.2 解析响应头头部字段

解析完响应行之后,浏览器通过Content-Type来决定如何显示响应体的内容。

  • Content-type 字段的值是text/html: 告诉浏览器,服务器返回的数据是HTML格式,浏览器开始渲染
  • Content-Type 的值是application/octet-stream: 显示数据是字节流类型的,通常情况下,浏览器会按照下载类型来处理该请求。

十一、构建DOM树


11.1 什么是DOM

从网络传给渲染引擎的HTML文件字节流是无法直接被渲染引擎理解的,所以要将其转化为渲染引擎能够理解的内部结构,这个结构就是DOMDOM提供了对HTML文档结构化的表述。在渲染引擎中,DOM有三个层面的作用。

  • 从页面的视角来看:DOM是生成页面的基础数据结构。
  • 从 JavaScript 脚本视角来看: DOM提供给JavaScript脚本操作的接口,通过这套接口,JavaScript可以对DOM结构进行访问,从而改变文档的结构、样式和内容。
  • 从安全视角来看DOM 是一道安全防护线,一些不安全的内容在 DOM 解析阶段就被拒之门外了。

简言之,DOM是表述HTML的内部数据结构,它会将Web页面和JavaScript脚本连接起来,并过滤一些不安全的内容。

11.2 DOM树构建过程

在渲染引擎内部,有一个叫HTML 解析器(HTMLParser)的模块

HTML 解析器(HTMLParser)的工作: 将网络或者本地磁盘获取的HTML 网页和资源从字节流解释成DOM树结构。

HTML 解析器(HTMLParser)的工作原理:

网络进程接收到响应头之后,会根据响应头中的content-type字段来判断文件的类型,比如 content-type 的值是“text/html”,那么浏览器就会判断这是一个HTML类型的文件,然后为该请求选择或者创建一个渲染进程。渲染进程准备好之后,网络进程和渲染进程之间会建立一个共享数据的管道。网络进程接收到数据后就往这个管道里面放,而渲染进程则从管道的另外一端不断地读取数据,并同时将读取的数据“喂”给HTML解析器。所以HTML解析器 解析数据与网络进程加载数据同时进行,并不是等着整个HTML文档加载完成后在开始解析。

从网络传输过来是HTML文档字节流的形式,后续字节流转换为DOM的过程为:

Preview
  • 一、通过分词器将字节流转换为 Token

    HTML解析器 通过分词器将字节流转换为一个个Token。解析后的Token分为Tag Token文本 Token,Tag Token又分为StartTagEndTag

  • 二、ToKen词语生成之后,随之被XSSAuditor过滤

    渲染引擎还有一个安全检查模块叫XSSAuditor,是用来检测词法安全的。在分词器解析出来Token之后,它会检测这些模块是否安全,比如是否引用了外部脚本,是否符合 CSP 规范,是否存在跨站点请求等。如果出现不符合规范的内容,XSSAuditor会对该脚本或者下载任务进行拦截。

  • 三、需要将 Token 解析为 DOM 节点

  • 四、将 DOM 节点添加到 DOM 树中

HTML解析器维护了一个Token栈结构,该Token栈主要用来计算节点之间的父子关系,在第一个阶段中生成的 Token 会被按照顺序压到这个栈中。具体的处理规则如下所示:

  • HTML 解析器开始工作时,会默认创建了一个根为document的空 DOM 结构,同时会将一个StartTag documentToken压入栈底。
  • 如果经过分词器解析出来是StartTag Token,通过XSSAuditor安全过滤之后,将StartTag Token压入栈中。HTML 解析器会为Start Token创建一个DOM节点,然后将该节点加入到DOM树中,它的父节点就是栈中相邻的那个元素生成的节点。
  • 如果经过分词器解析出来是文本 Token,通过XSSAuditor安全过滤之后,那么会生成一个文本节点,然后将文本节点加入到DOM树中,文本 Token是不需要压入到栈中,它的父节点就是当前栈顶Token所对应的DOM节点
  • 如果经过分词器解析出来是EndTag Token,通过XSSAuditor安全过滤之后,比如是EndTag divHTML 解析器会查看Token栈顶的元素是否是 StarTag div,如果是,就将StartTag div 从栈中弹出,表示该 div 元素解析完成。

通过分词器产生的新Token就这样不停地压栈和出栈,整个解析过程就这样一直持续下去,直到分词器将所有字节流分词完成。

DOM 树构建完成之后,浏览器触发DOMContentLoaded事件,注册再该事件上的JavaScript函数会被调用。当所有资源加载完之后,浏览器会触发onload事件。

11.3 DOM树构建中的JavaScript

  • 一、如果在HTML中插入一段Script标签。分词器解析到Strat Tokenscript时,渲染引擎判断这是一段脚本。因为接下来的JavaScript脚本可能要修改当前已经生成的DOM 结构HTML解析器暂停解析。所以JavaScript代码的执行会阻碍后面DOM节点的创建同时也会阻碍后面的资源下载。如果遇到外联脚本JavaScript执行之前还需等待下载完成。
  • 二、在执行JavaScript之前,需要先解析JavaScript语句之上所有的CSS 样式。所以如果代码里引用了外部的CSS 文件,那么在执行JavaScript之前,还需要等待外部的CSS文件下载完成,并解析生成CSSOM对象之后,才能执行JavaScript脚本。而JavaScript引擎在解析JavaScript之前,是不知道JavaScript是否操纵了CSSOM的,所以渲染引擎在遇到JavaScript脚本时,不管该脚本是否操纵了CSSOM,都会执行CSS文件下载,解析操作,再执行JavaScript脚本。
  • 三、同样,在执行JavaScript之前,浏览器通过预先扫描器来扫描script之后的词语,如果发现有其他资源,那么浏览器会通过预资源加载器来发送请求,来提前下载资源。
  • 四、检测JavaScript的引入方式
    • 如果JavaScript脚本时内嵌的,JavaScript引擎直接执行JavaScript代码。脚本执行完成之后,HTML解析器恢复解析过程,继续解析后续的内容,直至生成最终的DOM
    • 如果JavaScript脚本时外部引入
      • 下载JavaScript脚本: JavaScript 文件的下载过程会阻塞 DOM 解析,而通常下载又是非常耗时的,会受到网络环境、JavaScript 文件大小等因素的影响。JavaScript 下载的优化策略:
        • 浏览器预解析: 当渲染引擎收到字节流之后,会开启一个预解析线程,用来分析 HTML 文件中包含的 JavaScript、CSS 等相关文件,解析到相关文件之后,预解析线程会提前下载这些文件。
        • 使用CDN来加速JavaScript文件的加载
        • 压缩JavaScript文件的体积
        • 如果JavaScript文件中没有操作DOM相关代码,就可以将该JavaScript脚本设置为异步加载,通过asyncdefer来标记代码前往查看具体对比
        • script元素放在body元素的最后,这样它就不会阻碍DOM节点的创建以及其他资源的下载

11.4 DOM树构建中的预解析

JavaScript 文件的下载过程执行过程会阻塞 DOM 解析。而执行JavaScript 文件之前都会下载 Css 文件。而通常下载又是非常耗时的,会受到网络环境、JavaScriptCss 文件大小等因素的影响。Chrome 浏览器做了很多优化,其中一个主要的优化是预解析操作

当渲染引擎收到字节流之后,会开启一个预解析线程,用来分析 HTML 文件中包含的 JavaScriptCSS 等相关文件,解析到相关文件之后,预解析线程会提前下载这些文件。

11.5 问题

  • load 事件DOMContentLoaded 区别?

    DOM 树构建完成之后,浏览器触发DOMContentLoaded事件,注册再该事件上的JavaScript函数会被调用。当所有资源加载完之后,浏览器会触发onload事件。

十二、样式计算


为什么要进行样式计算?

样式计算的目的是为了计算出 DOM 节点中每个元素的具体样式,这个阶段大体可分为三步来完成。

  1. 把 CSS 转换为浏览器能够理解的结构
  2. 转换样式表中的属性值,使其标准化
  3. 计算出 DOM 树中每个节点的具体样式

12.1 把 CSS 转换为浏览器能够理解的结构 CSSOM

Preview

从图中可以看出,CSS 样式来源主要有三种:

  • 通过 link 引用的外部 CSS 文件
  • <style>标记内的 CSS
  • 元素的 style 属性内嵌的 CSS

什么是 CSSOM?为什么需要 CSSOM 呢?

HTML一样,渲染引擎也是无法直接理解CSS文件内容的,所以需要将其解析成渲染引擎能够理解的结构,这个结构就是CSSOM。和DOM一样,CSSOM也具有两个作用:

  • 第一个是提供给 JavaScript 操作样式表的能力
  • 第二个是为布局树的合成提供基础的样式信息

CSSOM体现在DOM中就是document.styleSheets。可以在Chrome 控制台中查看其结构,只需要在控制台中输入 document.styleSheets,然后就看到如下图所示的结构:

Preview

12.2 转换样式表中的属性值,使其标准化

把现有的CSS文本转化为浏览器可以理解的结构了,那么接下来就要对其进行属性值的标准化操作。

body { font-size: 2em }
p {color:blue;}
span {display: none}
div {font-weight: bold}
div p {color:green;}
div {color:red; }

可以看到上面的 CSS 文本中有很多属性值,如 2em、blue、bold,这些类型数值不容易被渲染引擎理解,所以需要将所有值转换为渲染引擎容易理解的、标准化的计算值,这个过程就是属性值标准化。标准之后的样子:

Preview

12.3 计算出 DOM 树中每个节点的具体样式


现在样式的属性已被标准化了,接下来就需要计算 DOM 树中每个节点的样式属性了,如何计算呢?

  • 继承

    首先是 CSS 继承。CSS 继承就是每个 DOM 节点都包含有父节点的样式。

  • 层叠

样式计算过程中的第二个规则是样式层叠层叠是 CSS 的一个基本特征,它是一个定义了如何合并来自多个源的属性值的算法。它在 CSS 处于核心地位,CSS 的全称“层叠样式表”正是强调了这一点。

总之,样式计算阶段的目的是为了计算出DOM节点中每个元素的具体样式,在计算过程中需要遵守CSS的继承和层叠两个规则。这个阶段最终输出的内容是每个DOM节点的样式,并被保存在ComputedStyle的结构内。

12.4 样式计算阶段细节思考

  • 如果下载 CSS 文件阻塞了,会阻塞 DOM 树的合成吗?会阻塞页面的显示吗?

十三、布局阶段


现在,我们有DOM 树DOM 树中元素的样式,但这还不足以显示页面,因为我们还不知道DOM 元素的几何位置信息。那么接下来就需要计算出 DOM 树中可见元素的几何位置,我们把这个计算过程叫做布局。在布局阶段需要完成两个任务:创建布局树布局计算

13.1 创建布局树

你可能注意到了DOM 树还含有很多不可见的元素,比如head 标签,还有使用了display:none 属性的元素。所以在显示之前,我们还要额外地构建一棵只包含可见元素布局树

布局树的构造过程:

Preview

为了构建布局树,浏览器大体上完成了下面这些工作:

  • 遍历 DOM 树中的所有可见节点,并把这些节点加到布局树中;
  • 而不可见的节点会被布局树忽略掉,如 head 标签下面的全部内容,再比如 body.p.span 这个元素,因为它的属性包含 dispaly:none,所以这个元素也没有被包进布局树。

13.2 布局计算

现在我们有了一棵完整的布局树。那么接下来,就要计算布局树节点的坐标位置了。

在执行布局操作的时候,会把布局运算的结果重新写回布局树中,所以布局树既是输入内容也是输出内容,这是布局阶段一个不合理的地方,因为在布局阶段并没有清晰地将输入内容和输出内容区分开来。针对这个问题,Chrome 团队正在重构布局代码,下一代布局系统叫 LayoutNG,试图更清晰地分离输入和输出,从而让新设计的布局算法更加简单。

十四、分层


现在有了布局树,而且每个元素的具体位置信息都计算出来了,此时还是不可以绘制页面了。因为页面中有很多复杂的效果,如一些复杂的3D 变换页面滚动,或者使用z-indexingz 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)

要想直观地理解什么是图层,你可以打开 Chrome 的“开发者工具”,选择“Layers”标签,就可以可视化页面的分层情况,如下图所示:

Preview

通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。那么需要满足什么条件,渲染引擎才会为特定的节点创建新的图层呢?通常满足下面两点中任意一点的元素就可以被提升为单独的一个图层。

  • 1.拥有层叠上下文属性的元素会被提升为单独的一层

    • 什么是层叠上下文?

      页面是个二维平面,但是层叠上下文能够让 HTML 元素具有三维概念,这些 HTML 元素按照自身属性的优先级分布在垂直于这个二维平面的 z 轴上。层叠上下文的层级是 HTML 元素层级的一个子级,因为只有某些元素才会创建层叠上下文。可以这样说,没有创建自己的层叠上下文的元素会被父层叠上下文同化。
    • 感受下层叠上下文

      Preview
    • 层叠上下文属性如下: (文档中的层叠上下文由满足以下任意一个条件的元素形成:)

      • 文档根元素(<html>
      • position 值为 absolute(绝对定位)或 relative(相对定位)且 z-index 值不为 auto 的元素
      • position 值为 fixed(固定定位)或 sticky(粘滞定位)的元素(沾滞定位适配所有移动设备上的浏览器,但老的桌面浏览器不支持)
      • flex (flexbox (en-US)) 容器的子元素,且 z-index 值不为 auto
      • grid (grid) 容器的子元素,且 z-index 值不为 auto
      • opacity 属性值小于 1 的元素
      • mix-blend-mode 属性值不为 normal 的元素
      • 以下任意属性值不为 none 的元素:
        • transform
        • filter
        • perspective
        • clip-path
        • mask / mask-image / mask-border
      • isolation 属性值为 isolate 的元素
      • -webkit-overflow-scrolling 属性值为 touch 的元素
      • will-change 值设定了任一属性而该属性在 non-initial 值时会创建层叠上下文的元素
      • contain 属性值为 layoutpaint 或包含它们其中之一的合成值(比如 contain: strict、contain: content)的元素
    • 层叠上下文注意事项:

      在层叠上下文中,子元素同样也按照上面解释的规则进行层叠。 重要的是,其子级层叠上下文的 z-index 值只在父级中才有意义。子级层叠上下文被自动视为父级层叠上下文的一个独立单元。
    • 层叠上下文总结:

      • 层叠上下文可以包含在其他层叠上下文中,并且一起创建一个层叠上下文的层级。
      • 每个层叠上下文都完全独立于它的兄弟元素:当处理层叠时只考虑子元素。
      • 每个层叠上下文都是自包含的:当一个元素的内容发生层叠后,该元素将被作为整体在父级层叠上下文中按顺序进行层叠。
  • 2.需要剪裁(clip)的地方也会被创建为图层

    • 什么是剪裁?

      在这里我们把div的大小限定为200 * 200像素,而div里面的文字内容比较多,文字所显示的区域肯定会超出200 * 200的面积,这时候就产生了剪裁,渲染引擎会把裁剪文字内容的一部分用于显示在 div 区域。出现这种裁剪情况的时候,渲染引擎会为文字部分单独创建一个层,如果出现滚动条,滚动条也会被提升为单独的层。

十五、绘制


在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制,那么接下来我们看看渲染引擎是怎么实现图层绘制*的?

渲染引擎实现图层的绘制与之类似,会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表,如下图所示:

Preview

从图中可以看出,绘制列表中的指令其实非常简单,就是让其执行一个简单的绘制操作,比如绘制粉色矩形或者黑色的线等。而绘制一个元素通常需要好几条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制。所以在图层绘制阶段,输出的内容就是这些待绘制列表。

你也可以打开“开发者工具”的“Layers”标签,选择“document”层,来实际体验下绘制列表,如下图所示:

Preview

十六、分块


绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。当图层绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程.在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。基于这个原因,合成线程会将图层划分为图块(tile),这些图块的大小通常是 256x256 或者 512x512。

十七、光栅化


合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的。通常,栅格化过程都会使用GPU来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。

十八、合成


一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。浏览器进程里面有一个叫viz的组件,用来接收合成线程发过来的DrawQuad命令,然后根据DrawQuad命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。到这里,经过这一系列的阶段,编写好的 HTML、CSS、JavaScript 等文件,经过浏览器就会显示出漂亮的页面了。

参考资料


阿里面试官的”说一下从url输入到返回请求的过程“问的难度就是不一样!