超详细的一次输入URL到页面显示的过程

405 阅读10分钟

浏览器解读资源

前言

其实这个话题也是前端面试中另外的一个面试点,即从浏览器输入URL到页面加载完成的过程分析。在开始下面的正式总结之前,先列出几点我对这此新的认知点。

  • 服务器会存储许多的静态资源,再拿到服务器的IP地址之后,会返回index.html文件,其实了解前端工程化打包的都知道,一个项目在经过webpack打包之后,生成的dist文件夹下的资源入口就是index.html,在浏览器拿到这个html文件之后,会进行解析。遇到被link的css文件之后会到服务器下载对应的css文件;遇到script标签的js文件回到服务器下载对应的script文件。
  • 不同的浏览器有不同的内核,由早期的Gecko、到现如今的Webkit和Blink。
  • 浏览器进行资源解析时,会用到浏览器引擎进行资源解析,不同浏览器的解析引擎是不一样的。浏览器引擎其实也可以看作是浏览器的排版引擎(layout engine),或者叫作页面渲染引擎(render engine)或者样板引擎等。无论被叫做什么,浏览器引擎都是浏览器对资源解析最重要的一环。

输入URL时发生的事

1、输入URL之后,首先进行的第一步就是对浏览器的URL进行解析,解析的时候其实也会有许多注意项。

  • 为什么URL需要解析呢? URL中可能存在一些特殊字符需要进行转义,如果不转义的话,可能导致传输的数据发生错误。常见的特殊符号有@ & = , / ? - _ ~ !
  • URL的编码规则是什么? utf-8
  • 是不是所有的浏览器的URL编码都是utf-8? 应该大多数吧,我看过chrome和百度的搜索引擎都是这样的
  • 如何保证URL的解析都是utf-8编码? 可以使用encodeURLComponent
  • encodeURLComponent和 encodeURL有啥区别? encodeURlComponent的编码范围更广,即对保留字符;,/?:@&=+$,也会进行编码,但是encodeURL不会。

2、DNS解析流程

1.例如URL输入https://www.baidu.com,会取出这个地址对域名,即`www.baidu.com`,首先浏览器会检查自身的缓存中是否存在这个域名解析后对应对ip地址,有则进行返回,解析结束。如果还没命中,则会进入第二点。
2.第二点主要是对用户的系统的host文件中是否存在此域名对应的ip地址,有则返回,解析结束。否则会进入 第三点。
3.进入第三点,就会真正的请求本地的域名服务器(LDNS)进行查询,(LDNS,按照其他资料查询所得,应该是位于用户本地城市的一台域名服务器,这里会缓存许多域名对应的IP地址)一般到此的话域名解析应该都会成功命中了。如果还没有的话,会进入下面的查找。
4.根域名服务器返回给LDNS一个所查询域的主域名服务器(gTLD Server,国际顶尖域名服务器,如 .com .cn .org等)地址。
5.此时LDNS再发送请求给上一步返回的gTLD。
6.接受请求的gTLD查找并返回这个域名对应的Name Server的地址,这个Name Server就是网站注册的域名服务器。
7. Name Server根据映射关系表找到目标ip,返回给LDNS。
8.LDNS缓存这个域名和对应的ip。
9.LDNS把解析的结果返回给用户,用户根据TTL值缓存到本地系统缓存中,域名解析过程至此结束。

附:从第4点到第8点,其实也是CDN的做法。

3、查找到IP之后

查找到IP之后,就是建议与此IP的服务器之间的连接,这里就会涉及到http协议的三次握手和四次挥手。在详细阐述建立连接过程之前,我们先来了解一下位码

位码是tcp的标志位,有6种标识。

  • SYN(synchronous建立联机)
  • ACK(acknowledgement 确认)
  • PSH(push传送)
  • FIN(finish结束)
  • RST(reset重置)
  • URG(urgent紧急)

三次握手

三次握手

  • 第一次握手:建立连接的时候,客户端发送syn包(syn=j)到服务器,客户端进入SYN_SEND状态,等待服务器确认。
  • 第二次握手:服务器收到syn包,必须确认客户端到SYN(ack=j+1),同时自己也发送了一个syn包(syn=k),即SYN+ACK包,此时服务器进入到SYN_RECV状态。
  • 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手,客户端与服务器建立连接,开始传送数据.

为什么需要三次握手而不是二次握手? 其最主要的就是保证双方建立稳定的连接传输通道;如客户端因网络原因断开连接了,那么server在如果不经过第二次握手,就会一直等待客户端发来的请求,这样就会导致资源浪费。

四次挥手

  • 第一次挥手: 客户端送一个带有FIN(结束)的结束码的询问信息。
  • 第二次挥手: 服务端返回一个ack确认收到的信息给客户端。
  • 第三次挥手 服务端返回一个FIN(结束)的结束码给客户端。
  • 第四次挥手 客户端发送一个ack确认收到的信息给服务端,结束这次连接。

4、缓存index.html文件

建立完成连接之后,就会开始请求index.html文件(一般来说首页都是index.html),这个时候就会看浏览器缓存中是否存在对应的文件,如果存在则直接返回,如果没有就会去服务器中获取。 这其中缓存也是特别重要的一环,常了解到的就是强缓存和协商缓存。因为在页面进行二次加载时,首先要经过强缓存的处理,cache-control是判断是否进入强缓存,如果为no-cache,则进入协商缓存。还有就是如果cache-controle: max-age=xxx;则会将本次请求和上次的成功请求返回的时间差和这个max-age进行比对,没有超过,则命中强缓存,直接从本地文件中读取返回;如果超过了则进入协商缓存。协商缓存主要是在请求资源时,如果向服务器发送header带有If-None-Match和If-Modified-Since的请求,则服务器会比较Etag。 ETag比对:

  • 如果资源没有被更新,则返回304状态码,浏览器读取本地缓存。
  • 如果资源更新,返回200状态码,并且返回的文件中带上新的ETag。

If-Modified-Since比对:

  • 主要是If-Modified-Since的值跟服务器获取到此文件最近改动时间进行比对,如果一致则命中协商缓存。返回304。
  • 如果不一致,则返回新的last-modified文件并返回200.

Etag是URL的Entity Tag,用于标示URL对象是否改变,区分不同语言和 Session等等。具体内部含义是使 服务器控制的,就像 Cookie那样.

补充

启发式缓存:主要是在请求中的expires字段,以服务器时间作为参考系, 其优先级比 Cache-Control:max-age 低, 两者同时出现在响应头时, Expires将被后者覆盖. 如果Expires, Cache-Control: max-age, 或 Cache-Control:s-maxage 都没有在响应头中出现, 并且也没有其它缓存的设置, 那么浏览器默认会采用一个启发式的算法, 通常会取响应头的Date_value - Last-Modified_value值的10%作为缓存时间.

from memory cache、from disk cache、文件本身的大小(如:1.8k)之间的区别 1、from memory cache:不会请求服务器,直接从内存中读取资源;当页面关闭之后,此资源就会被释放,再次重新打开相同的页面时不会出现from memory cache的情况。 2、from disk cache:不会请求服务器,直接从磁盘中读取资源;当页面关闭之后,此资源就不会被释放,再次重新打开相同的页面时会出现from disk cache的情况。 3、1.8k,即资源本身的大小,当http状态码为200时是从浏览器获取资源的大小。当状态码为304时,是与服务端通信的报文的大小,并不是该资源本身的大小,是从本地读取资源的大小。


资源编译

1、index.html文件解析

index.html文件解析过程中在文章的开头已经阐述过了,这里就不再赘述了。

2、浏览器渲染过程

页面渲染的过程图

  • HTML Parser: 是浏览器内核中的HTMlParser将HTML标签转化为DOM Tree
  • CSS Parser:是浏览器内核中的CSSParser将CSS文件转化为样式规则

浏览器内核解析流程: 通过DOM tree和Style Rules进行Attachment(附加)在一起形成了Render Tree(渲染树),再通过Layout(布局引擎)再对Render Tree进行操作,最后通过Painting绘制之后回显到页面进行Display(显示)

在以上解析的过程中,针对生成DOM Tree的过程中,可能会存在许多的DOM操作,有许多的js文件需要执行。但是注意,JavaScript是高级程序语言,浏览器不能直接识别,需要通过JavaScript引擎进行转化为最终的机器指令来执行。(无论js是交给浏览器还是Node执行,其最终都需要通过CPU来进行执行,但是CPU只认识自己的指令集,所以需要借助JS引擎来转化)

浏览器内核: 浏览器内核主要由两部分组成,一个是浏览器的渲染引擎(render engine)和JS引擎。

渲染引擎:在生面图中的HTML Parser和 CSS Parser都是来自渲染引擎中。主要用于页面渲染。

JS引擎:用于对JavaScript代码的编译和解析,现在使用最多的应该就是v8引擎

v8Engine解析流程

其中比较细节的几点:

  • scanner层到parser层中间会经过PreParser层,因为并不是所有的JavaScript代码在初始的时候都会进行执行,如果初始就进行解析,则会消耗大量时间,网页加载速率就会很慢,所以采用lazy parsing(延迟解析)。官网有句话Even though developers can delay such code with async and deferred scripts, that’s not always feasible.

  • 在MachineCode中,有一步是进行Deoptimization(取消优化),即以下这段代码多次调用就会被标记成热点函数,在经过TurboFan转化成优化的机器码,这样做的目的是为了提高代码的执行性能。但是,如果因为js无法做类型校验,如果num1或者num2的类型变为了字符串,那么这个+的符号的意义就变了,之前优化好的机器码不能正常的处理运算,所以需要逆向的转为字节码。

    function sum(num1,num2){
        return num1 + num2 
    }
    
  • AST(Abstract Syntax Tree)抽象语法树,其实这个概念不陌生。例如在vue的template编译创建VNode的时候,就会生成AST。在v8引擎中的,例如const name = 'YDKD',经过AST之后,会形成下面的代码。

{
  "type": "Program",
  "start": 0,
  "end": 18,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 17,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 4,
          "end": 17,
          "id": {
            "type": "Identifier",
            "start": 4,
            "end": 8,
            "name": "name"
          },
          "init": {
            "type": "Literal",
            "start": 11,
            "end": 17,
            "value": "ydkd",
            "raw": "'ydkd'"
          }
        }
      ],
      "kind": "let"
    }
  ],
  "sourceType": "module"
}

附上本文相关参考地址: PreParser(预解析) Scanner(生成Tokens) Ignition(解释器) Ignition(编译器) 缓存相关