一、浏览器通讯部分:使用HTTP协议或者HTTPS协议

# 一、浏览器通讯部分:使用HTTP协议或者HTTPS协议

1、浏览器首先使用HTTP协议或者HTTPS协议,向服务端请求页面;(此之前还有DNS查询解析)

2、把请求回来的HTML代码经过解析,构建成DOM树

3、计算DOM树上的CSS属性

4、最后根据CSS属性对元素逐个进行渲染,得到内存中的位图

5、一个可选的步骤是对位图进行合成,这会极大地增加后续绘制的速度

6、合成之后,再绘制到界面上。

从http请求回来,就产生了流式的数据,后续的DOM树构建、CSS计算、渲染、合成、绘制,都是尽可能地流式处理前一步的产出:即不需要等到上一步步骤完全结束,就开始处理上一步的输出,这样我们在浏览网页时,才会看到逐步出现的页面。

HTTP协议

HTTP协议是基于TCP协议出现的,对TCP协议来说,TCP协议是一条双向的通讯通道,HTTP在TCP的基础上,规定了Request-Response的模式。这个模式决定了通讯必定是由浏览器端首先发起的。

HTTP是纯粹的文本协议,它是规定了使用TCP协议来传输文本格式的一个应用层协议。

http协议 (opens new window)

HTTP Method

浏览器通过地址栏访问页面都是GET方法。表单提交产生POST方法。

HEAD跟GET类似,只返回响应头,多数由JavaScript发起。

PUT和DELETE分别表示添加资源和删除资源,但是实际上这只是语义上的一种约定,并没有强约束。

CONNECT现在多用于HTTPS和WebSocket

OPTIONS和TRACE一般都用于调试,多数线上服务都不支持。

HTTP Status code

1XX:临时回应,表示客户端请继续

2XX:请求成功(200)

3XX: 表示请求的目标有变化,希望客户端进一步处理

​ 301&302:永久性与临时性跳转

​ 304:跟客户端缓存没有更新

4XX:客户端请求错误

​ 403:无权限

​ 404:请求页面不存在

​ 418:一个彩蛋,愚人节玩笑

5XX:服务端请求错误

​ 500:服务端错误

​ 503:服务端暂时性错误,可以一会儿再试

1xx系列的状态码对前端来说比较陌生,原因是1xx状态码被浏览器HTTP库直接处理掉了,不会让上层应用知晓。

2xx系列的状态最熟悉的就是200,通常是网页请求成功的标志。

3xx系列比较复杂,301和302两个状态表示当前资源已经被转移,只不过一个是永久性转移,一个是临时性转移。实际上301更接近于一种报错,提示客户端下次别来了。产生304这个状态的前提是:客户端本地已经有缓存的版本,并且在Request中告诉了服务端,当服务端通过时间或者tag,发现没有更新的时候,就会返回一个不含body的304状态。

HTTP Head

HTTP头可以看作一个键值对。原则上,HTTP头也是一种数据,我们可以自由定义HTTP头和值。不过在HTTP规范中,规定了一些特殊的HTTP头。

Request Header:

http (opens new window)

Response Header

http (opens new window)

HTTP Request Body

HTTP请求的body主要用于提交表单场景。实际上,HTTP请求的body是比较自由的,只要浏览器端发送的body服务端认可就可以了。常见的body格式:

application/json
application/x-www-form-urlencoded
multipart/form-data
text/xml

使用HTML的form标签提交产生的HTML请求,默认会产生application/x-www-form-urlencoded的数据格式,当有文件上传时,则会使用multipart/form-data

HTTPS

在 HTTP 协议的基础上,HTTPS 和 HTTP2 规定了更复杂的内容,但是它基本保持了 HTTP 的设计思想,即:使用上的 Request-Response 模式。

HTTPS 有两个作用,一是确定请求的目标服务端身份,二是保证传输的数据不会被网络中间节点窃听或者篡改。

HTTPS 是使用加密通道来传输 HTTP 的内容。但是 HTTPS 首先与服务端建立一条 TLS 加密通道。TLS 构建于 TCP 协议之上,它实际上是对传输的内容做一次加密,所以从传输内容上看,HTTPS 跟 HTTP 没有任何区别。

HTTP2

HTTP 2.0 最大的改进有两点,一是支持服务端推送,二是支持 TCP 连接复用。

服务端推送能够在客户端发送第一个请求到服务端时,提前把一部分内容推送给客户端,放入缓存当中,这可以避免客户端请求顺序带来的并行度不高,从而导致的性能问题。

TCP 连接复用,则使用同一个 TCP 连接来传输多个 HTTP 请求,避免了 TCP 连接建立时的三次握手开销,和初建 TCP 连接时传输窗口小的问题。

Note:其实很多优化涉及更下层的协议。IP 层的分包情况,和物理层的建连时间是需要被考虑的。

# 二、如何解析请求回来的HTML代码

字符流 --> 状态机 --> 词token --> 栈 --> DOM树

解析代码

关于Response的body部分:

​ HTML 的结构不算太复杂,我们日常开发需要的 90% 的“词”(指编译原理的术语 token,表示最小的有意义的单元),种类大约只有标签开始、属性、标签结束、注释、CDATA 节点几种。

1、词(token)是如何被拆分的

<p class="a">text</p>

如果我们从最小有意义单元的定义来拆分,第一个词(token)是什么呢?显然,作为一个词(token),整个 p 标签肯定是过大了(它甚至可以嵌套)。

那么,只用 p 标签的开头是不是合适吗?我们考虑到起始标签也是会包含属性的,最小的意义单元其实是'<p',所以'<p'就是我们的第一个词(token)。

<p “标签开始“的开始

class="a"属性

>“开始标签”的结束

text文本

</p>结束标签

2、状态机

状态机 (opens new window)

HTML官方文档规定了80个状态,为了理解原理,用这个简单的状态机就足够说明问题了。

状态机的初始状态,我们仅仅区分 “< ”和 “非 <”:

如果获得的是一个非 < 字符,那么可以认为进入了一个文本节点;

如果获得的是一个 < 字符,那么进入一个标签状态。

不过当我们在标签状态时,则会面临着一些可能性。

比如下一个字符是“ ! ” ,那么很可能是进入了注释节点或者 CDATA 节点。

如果下一个字符是 “/ ”,那么可以确定进入了一个结束标签。

如果下一个字符是字母,那么可以确定进入了一个开始标签。

如果我们要完整处理各种 HTML 标准中定义的东西,那么还要考虑“ ? ”“% ”等内容。

用状态机做词法分析,其实正是把每个词的“特征字符”逐个拆开成独立状态,然后再把所有词的特征字符链合并起来,形成一个联通图结构。

实现状态机的方式:我们把每个函数当做一个状态,参数是接受的字符,返回值是下一个状态函数。(状态机真的是一种没有办法封装的东西,所以我们永远不要试图封装状态机。)

3、构建DOM树

要把这些简单的词变成 DOM 树,这个过程我们是使用栈来实现的,任何语言几乎都有栈,为了给你跑着玩,我们还是用 JavaScript 来实现吧,毕竟 JavaScript 中的栈只要用数组就好了。

function HTMLSyntaticalParser(){
    var stack = [new HTMLDocument];
    this.receiveInput = function(token) {
        //……
    }
    this.getOutput = function(){
        return stack[0];
    }
}

我们这样来设计 HTML 的语法分析器,receiveInput 负责接收词法部分产生的词(token),通常可以由 emitToken 来调用。

在接收的同时,即开始构建 DOM 树,所以我们的主要构建 DOM 树的算法,就写在 receiveInput 当中。当接收完所有输入,栈顶就是最后的根节点,我们 DOM 树的产出,就是这个 stack 的第一项。

为了构建 DOM 树,我们需要一个 Node 类,接下来我们所有的节点都会是这个 Node 类的实例。

在完全符合标准的浏览器中,不一样的 HTML 节点对应了不同的 Node 的子类,我们为了简化,就不完整实现这个继承体系了。我们仅仅把 Node 分为 Element 和 Text(如果是基于类的 OOP 的话,我们还需要抽象工厂来创建对象)

function Element(){
    this.childNodes = [];
}
function Text(value){
    this.value = value || "";
}

# 三、DOM树如何构建