URL是如何变成一个网页的?

191 阅读8分钟

在浏览器输入一个URL,按下回车键后,网页是如何显示在屏幕上的?大概流程如下:

  1. 浏览器使用http协议向服务端发送请求
  2. 把请求回来的html代码经过解析,构建成DOM树
  3. 计算DOM树上的css属性
  4. 根据css属性进行渲染,得到内存的位图
  5. 对位图进行合成,增加后续会值的速度
  6. 合成后,再绘制到界面上

6391573a276c47a9a50ae0cbd2c5844c.webp

从http请求回来以后,就产生了流式的数据,也就是说DOM树构建,css计算、渲染、合成、绘制,不需要等到上一步骤完成结束,就可以输出,所以网页都是逐步出现的。

那么下面我们就来具体了解一下每个步骤。

http协议

http是纯粹的文本协议,它规定了使用tcp协议来传输文本格式的一个应用层协议,以request-response的模式出现。request和response的格式都是由line,head,body组成。request的line是由method,path,version组成的:最常用到的method就是GET和POST,浏览器通过地址栏访问页面都是GET方法,表单提交产生POST方法;path是请求的路径完全由服务器来定义;version几乎都是固定的字符串。同样的我们看看response line是由version,status code,status text组成的,一般来说状态码和状态文都是跟随出现的。状态码一共分为五种大的类型:

  1. 1xx,以1开头,临时回应,表示客户端请继续。实际很少会看到,因为1xx的状态被浏览器http库直接处理掉了,不会让上层应用知晓
  2. 2xx,以2开头表示请求成功,喜闻乐见
  3. 3xx,以3开头表示请求的目标有变化,希望客户端进一步处理。比如301当前资源被永久性转移,302暂时性转移。304比较复杂一点,客户端本地已经有缓存的版本,并且在request中告诉了服务端,当服务端使用标识对比,发现此资源没有更新的时候,就会返回一个不含body的304
  4. 4xx,以4开头的标识客户端的请求错误,例如403:无权限,404:page not found
  5. 5xx,以5开头的是表示服务端的请求错误。

了解了line之后,就需要知道什么是http头,其实http头也是很多组键值对组成的数据,可以被自由定义,但是在规范中也有一些特殊的定义。

Request Header
Accept:浏览器端接受的格式
Accept-Encoding:浏览器端接受的编码方式
Accept-Language:浏览器端接受的语言,用于服务器判断多语言
Cache-Control:控制缓存的时效性
Connection:连接方式,如果是keep-alive,且服务端支持,则会复用
Host:http访问使用的域名
If-Modified-Since:上次访问时的更改时间,如果服务端认为此时间后自己没有更新,则会给出304响应
If-None-Match:上次访问时使用的E-tag,通常是页面的信息摘要,这个比更改时间更准确
User-Agent:客户端标识
Cookie:客户端存储的cookie字符串

Response Header
Cache-Control:缓存控制,用于通知各级缓存保存的时间
Connection:链接类型,keep-alive表示可复用
Content-Encoding:内容编码方式
Content-Length:内容长度,有利于浏览器判断内容是否已经结束
Content-Type:内容类型,所有请求网页都是text/html
Date:当前的服务器日期
ETag:页面的信息摘要,用于判断是否需要重新到服务器取回页面
Expires:过期时间,用于判断下次请求是否需要到服务端取回页面
Keep-Alive:保持链接不断时需要的一些信息
Last-Modified:页面上次修改的时间
Server:服务端软件的类型
Set-Cookie:设置cookie,可以存多个
Via:服务端的请求链路,对一些调试场景至关重要

http请求的body主要是用于表单提交,一般只要是浏览器端发送的body服务端都会认可,常用的格式就是application/json,application/x-www-form-urlencoded,multipart/form-data,text/xml。

https是使用加密通道来传输内容,https首先与服务端建立一条TLS加密通道,TLS构建与TCP协议之上,它实际上是对传输到的内容做一次加密,所以传输内容上看,https和http没有任何区别。https主要有两个作用,一是确认请求的目标服务端身份,而是保证传输的数据不会被网络中间节点窃听或者篡改。

构建DOM树

构建DOM树一共分为两个步骤,一个是解析代码,而是构建DOM树,那么我们可以看看这两步具体是怎么实现的。

第一步我们需要把字符流拆分成token,这里拆分的方法是使用状态机。使用状态机做词法分析,其实就是把每一个词的“特征字符”逐个拆开成独立状态,然后再把所有词的特征字符链合并起来,形成一个联通图结构。那么一般一个html标签都是被如何拆分成token的呢?会被拆分成五个token,开始标签的开始(左尖括号加标签名),标签的属性(比如class),开始标签的接受(右肩括号),文本内容,结束标签。

第二步使用栈来把第一步的token构建成DOM树。栈是最佳的用于匹配开始和结束标签的方案。具体流程如下:

  • 栈顶元素是当前节点
  • 遇到属性就添加到当前节点
  • 遇到文本节点,填到当前节点;如果当前节点就是文本节点,那就合并
  • 遇到注释节点,作为当前节点的子节点
  • 遇到开始标签就入栈一个节点,当前节点就是这个节点的父节点
  • 遇到结束标签就出栈一个节点

css属性渲染

上文提到其实整个步骤是流式的,所以在DOM构造的同时,css属性也会被同时计算出来。上一步构造好的元素,去检查它匹配到了哪些规则,再根据规则的优先级,做覆盖和调整。那么我们就可以看到css选择器的一个特点,选择器的出现顺序必定跟构建DOM树的顺序一致,也就是说选择器在DOM树构建到当前节点时,已经可以准确判断是否匹配,不需要后继节点的信息。

在我的实践过程中,常常在考虑有没有一种方式可以针对子节点的条件来勾挂父节点的css属性?那么到这里我就知道答案是否定的,未来也不会出现“父元素选择器”这种东西,因为父元素选择器要求根据当前节点的子节点,来判断当前节点是否被选中,而父节点会先于子节点构建,所以要解决这一类问题的方法还是只能使用js。

在DOM树上添加了css属性之后,就是要开始确定每一个元素的位置了,具体的步骤参见我之前写的CSS如何排版布局?

渲染,合成,绘制

渲染

渲染的过程就是把每一个元素对应的盒变成位图,过程十分复杂,大概可以分为两大类:图形和文字。盒的背景、边框、svg元素、阴影等特性,都是需要绘制图形类,这一部分需要一个底层库来支持。盒中的文字,也是需要字体库来提供读取字体文件的基本能力,它能根据字符的码点抽取出字形。

合成

合成这一步是非必需的,主要是为了性能考虑,把一部分子元素渲染到合成的位图上面,因为在渲染时,是不会把子元素渲染到合成的位图上面的。那么从性能的角度考虑,我们建立的原则就是最大限度减少绘制次数,猜测可能变化的元素,把它排到合成之外。主流浏览器一般是根据position、transform等属性来猜测这些元素未来可能发生的变化来决定合成策略,灵活运用这些特性可以大大提升合成策略的效果。

绘制

绘制就是把位图最终绘制到屏幕上,变成肉眼可见的图像。一般来说浏览器不需要处理,只要把要显示的位图交给操作系统或者驱动就可以了。

那么现在你知道了在浏览器输入一个URL,按下回车键后,网页是显示在屏幕上大概是怎么回事了吧!


Reference:
https://time.geekbang.org/column/article/80240 https://time.geekbang.org/column/article/80260 https://time.geekbang.org/column/article/80311 https://time.geekbang.org/column/article/81730 https://time.geekbang.org/column/article/82397