Codog

关注微信公众号:Codog代码狗

0%

浏览器输入url到显示页面都发生了什么

一个非常经典的面试题,不同阶段的人的答案相差会很大,一定程度有助于区分面试者水平。初级阶段会说出类似建立tcp连接,发送http请求,服务器接收请求返回数据,浏览器显示页面等;相对高阶的会对每个阶段都详细阐述,如DNS查询、浏览器缓存、资源重定向、浏览器的DOM、CSSOM以及如何渲染等,大神级会说出计算机图形学、浏览器渲染原理之类的。。

另一个经典之处在于,对整个过程了解的越详细,对网站搭建、网络请求、前端性能优化就更有深层次的见解。

本文基于相对完整,且满足大部分面试者需求的角度对整个过程进行描述。

1. 浏览器处理并发送请求

首先会对输入内容进行解析,比如我们只是输入要查询的内容,那么浏览器会自动使用默认搜索引擎进行查询;如果我们输入的是合法的url,比如baidu.com

DNS

首先会进行DNS查询,将我们输入的域名转为一个具体的IP地址,会有多个地方返回查询结果。

  • 浏览器缓存
  • 本地缓存,比如hosts
  • 路由器缓存
  • 最后才是通常意义上的DNS查询,如果本服务器上没有数据,则向上一层级获取,直到根服务器(之前看的全世界只有13台)

经过上面最终会返回baidu.com对应的IP地址,接下来就准备发送HTTP请求了。

发送请求就要建立连接,要经过三次握手:

  • 首先,客户端发出SYN包,等待服务器确认;
  • 然后,服务端接收SYN,并返回SYN+ACK包;
  • 最后,客户端接收SYN+ACK,确认完毕,连接建立完成。

为什么是三次?可以想象一下,如果两个陌生人要互相确认对方的身份会怎么做?首先A会说,你是B吗?B回答道:我是B,你是A吗?A说:我是A。(“确认身份”并不恰当,应该是确认对方接收与发送信息的状态,这里只是举例说明)

多说一下,断开连接会经历四次挥手,分别对应向对方发送断开连接的信息和信息确认。

浏览器会对用一域名下的并发的请求数量做限制,不同浏览器数量可能有差别(Chrome是6个),可以打开一个有n多个图片的网页,实际在pending的请求数有多少个。

这其实就向我们说明要减少并发的请求数量,尽量做到资源合并,静态资源考虑放到CDN服务器上。

网络模型(从上到下)

  • 应用层(HTTP)
  • 表示层
  • 会话层
  • 传输层(TCP/UDP)
  • 网络层(IP)
  • 链路层
  • 物理层

感兴趣的可以具体查阅,现阶段直到每个概念分属第几层就可以了。

2. 后端接受请求并返回结果

现在,就轮到后端处理请求了,具体操作不细说,也不了解。。前后端交互信息的载体就是报文了。

报文结构包括通用头部、请求/响应头部、请求/响应体。

这个我们在浏览器控制台中可以很清晰的看到。

通用头部包括请求地址、请求方式、状态码、请求服务器地址

image

请求方式:

GET,POST,PUT,DELETE,OPTIONS等8种方式

OPTIONS是非简单请求前的预检请求。

什么是简单请求?

  • 请求方式是GET,POST,HEAD之一

  • 请求头部是以下几种:

    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type (but note the additional requirements below)
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
      其中 Content-Type的值是其中之一:
      • application/x-www-form-urlencoded
      • multipart/form-data
      • text/plain

常见状态码:

1xx: 指示信息,表示请求已接收
2xx: 成功请求,常见200
3xx: 重定向,比如302,304(缓存)
4xx: 客户算错误,常见404,资源路径为空;401,鉴权失败
5xx: 服务端错误,常见500,比如服务器繁忙之类

有些面试官还会问些关于https、http2、http3的问题。

https:

https是http的安全版,在http和tcp之间加了SSL/TLS加密协议。

http本身是不安全的,传输采用明文传输,客户端和服务端都不确定对方是否是可信任的,所以会出现中间人攻击(Man-in-the-middle attack, MITM);且服务端对请求来者不拒,所以容易遭受DoS攻击(拒绝服务攻击)

加密方式:

对称密钥加密,即加密和解密使用同一个密钥,但如果被其他人拿到密钥,那么加密也就失去意义了,所以无法保证安全性。

非对称密钥加密,即分为公钥和私钥,公钥任何人都可以获得,私钥只能自己保存。这样发送方使用接收方的公钥加密,接收方收到加密信息后使用私钥解密。

但非对称加密也存在问题,无法保证公钥就是是本想请求服务器的公钥,可能收到的公钥就已经被攻击者替换掉了。

既然双方无法确认对方,那不妨引入第三方机构来做认证,即数字证书认证机构(CA)。

客户端和服务端都认可CA。服务端好人员向CA提出公钥的申请,CA认证身份完成后对公钥进行数字签名,然后分配这个已签名的公钥,并将该公开密钥放入公钥证书后绑定在一起。

服务端将次公钥证书发送给客户端,客户端收到证书后可使用CA的公钥对证书上的数字签名进行验证,验证通过则说明,一:认证服务端公钥的是真实有效的CA,二:服务端的公钥是真实可靠的。

那么如何保证CA的公钥可以安全的到达客户端?大多数浏览器开发商发布版本时都会内置常用CA的公钥。

上面的流程介绍参考自《图解HTTP》,书的内容还是很详细的,图片生动便于理解,建议阅读。

http2

想一下之前提到的http请求特点:每个请求都需要建立一个TCP连接,且有并发数量限制,在http2协议下,一个TCP请求可以请求多个资源,既避免重复建立TCP连接,有解决了并发数量的限制。

总结起来主要有以下几个特点:

  • 多路复用
  • 头部压缩,减小报文体积
  • 服务端推送
  • 。。。

http3

http2也并不完美,虽然使用多路复用,但会导致服务器压力上升,且存在阻塞的问题,且丢包时,整个TCP都要重传。

http3特点:

  • 使用UDP传输
  • 改进的拥塞控制
  • 多路复用
  • TLS1.3加密协议

结构如下图:

image

缓存

缓存是很重要的一部分,可以提升效率,避免资源重复请求造成流量和时间的浪费。

缓存分为强缓存和协商缓存(弱缓存)

强缓存:
控制台常见状态码为200,后面显示disk cache或者memory cache。

请求头部

1
2
http1.1: Cache-Control
http1.0: Pragma/Expires

强缓存规定在给定时间之前,该资源都可以被直接使用,不会发送请求

Cache-Control对应max-age对应的秒级数值,对应客户端本地时间,在这个时间范围内资源都有效。

Expires对应一个具体的时间点,一般对应服务器时间,所以如果客户端时间与服务端不同步就会造成资源始终有效或者缓存始终失效的问题,一般不推荐使用。

如果同时使用二者,那么Cache-Control优先级更高。

协商缓存:

请求头部

1
2
http1.1 If-None-Match/E-Tag
http1.0 If-Modified-Since/Last-Modified

对于http1.0的缓存方式,客户端的请求头部会携带If-Modified-Since,服务端头部为Last-Modified,服务端收到请求后对二者进行比较,如果有效则使用缓存,且请求返回304。

http1.1的缓存方式使用文件指纹(E-Tag),这样更准确,浏览器端头部为E-Tag,服务端为If-None-Match,收到请求后进行比对,如果匹配则使用本地缓存,返回304状态码。

相对Last-Modified来说,E-Tag对文件的控制更为精确,文件内容改变才会相应改变,而对于Last-Modified,服务端文件内容可能没有修改,只是更新时间改变也会造成缓存失效

二者同时存在,则E-Tag优先级更高。

通俗来讲就是http1.1的头部优先级高于http1.0的头部

image

image

3. 浏览器接收并显示

经历一系列流程,数据终于返回到了浏览器。

浏览器的大致工作流程:

  • 处理 HTML 标记并构建 DOM 树。
  • 处理 CSS 标记并构建 CSSOM 树。
  • 将 DOM 与 CSSOM 合并成一个渲染树。
  • 根据渲染树来布局,以计算每个节点的几何信息。
  • 将各个节点绘制到屏幕上。
  • image

对象模型的创建经历了以下步骤:
字节 → 字符 → 令牌 → 节点 → 对象模型。

image

整个流程:

image

回流与重绘

页面并不是一成不变的,某些操作会改变页面样式,分两种情况

回流(reflow):也有叫重排的,一般是元素结构改变,比如大小,定位,位置改变,那么就需要重新计算布局并渲染。

重绘(repaint):一般只是元素某些属性改变,比如背景色、文字颜色之类,这样只需要重新渲染就可以了

回流触发场景:

  • 页面初始化
  • DOM改变
  • Render树改变
  • 窗口大小改变
  • 获取元素属性时,比如offsetWidth/offsetHeight之类,这些都是实时计算的,也会出发页面回流,尽管看不出页面发生了改变。

资源下载

页面并不只有内部数据,还会引用其他外部资源,比如CSS、JS、图片之类的。每个资源都会新开启一个线程负责下载。

CSS资源下载时是异步的,不会阻塞浏览器构建CSSOM,但会阻塞渲染,Render树会等到CSS下载解析完成后再执行。

JS资源下载时会阻塞浏览器的解析,等待完成后在继续解析HTML;但同查那个浏览器做优化时也会继续下载其他资源,但解析过程仍然是阻塞的。

我们可以给外部资源加上async或者defer属性表示脚本是异步的,这样JS下载与解析就并行了。

二者的区别:

image

async会在下载完成后就执行,所以一般是与页面功能无关的操作,比如网站统计之类。

defer更像是我们推荐的将script标签放在body末尾的写法,且会按加载顺序执行(看了挺多人的说法并不一定会按照顺序,使用须谨慎,实际场景乖乖地放在body最后就好了)

图片的下载不会阻塞解析,是异步下载,完成后替换img实际内容即可,但图片资源往往比较大且多,通常优化方式包括懒加载、雪碧图(小图片合成一张,减少请求数量)等。。

DOMContentLoaded和load

HTML解析完成会发出这两个事件,但也是不同意义上的“完成”。

DomContentLoaded:当浏览器加载完html内容,DOM构建完成后触发,但此时外部资源(比如图片)可能还未加载完成。

load:在DomContentLoaded之后,等待外部资源加载完成。

BFC

经常听说但实际可能遇到的较少,最频繁的就是“margin塌陷”,比如左边的元素设置margin-right: 10px; ,右边的元素设置margin-left: 20px,那么实际两个元素之间的间隔为20px而非30px。

解决这个现象的方法也很简单,给元素设置overflow: hidden就可以了(这可能也是最常用的创建BFC的方式),实际上这也是BFC的条件之一。

创建BFC需满足以下几个条件:

1、float的值不是none。
2、position的值不是static或者relative。
3、display的值是inline-block、table-cell、flex、table-caption或者inline-flex
4、overflow的值不是visible

JS

垃圾回收(GC)

(摘自MDN

从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法本身和它对“对象是否不再需要”的简化定义。

这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。

这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。

这个算法比引用计数要好,因为“有零引用的对象”总是不可获得的,但是相反却不一定,参考“循环引用”。

事件循环(Event Loop)

JS是单线程,为实现主线程不阻塞,但又具备异步的能力,Event Loop方案应运而生。

JS执行任务分为

  • task:主任务,即我们的同步代码
  • microtask:微任务,常见promise, process.nextTick, MutationObserver
  • macrotask:宏任务,常见setTimeout, setInterval, I/O,UI事件等

整个运行机制用图表示就是这样的:

image

需要注意的是,setTimeout的执行机制不是等待多久执行,而是等待相应时间后加入消息队列;Promise的执行是同步的,then方法的调用才是异步的。

事件循环的机制是等待主线程空闲时,再按照一定规则执行消息队列中的任务,即主任务 > 微任务 > 宏任务。

以一段代码为例:

摘自一篇文章:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
console.log('1');

setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})

setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})

根据上述执行规则,试着回答一下输入结果是什么

答案是(Node 8环境):1,7,6,8,2,4,3,5,9,11,10,12。

注意的是微任务是按顺序执行,宏任务执行时会等待所有微任务都完成后在执行。

所以1,7是同步任务;6,8是第一阶段微任务,2,4,3,5是第一个setTimeout的执行顺序,9,11,10,12是第二个setTimeout的执行顺序。

其他

整体过程大概就是这样,还有一些特殊但经常遇到的问题。

跨域(CORS)

请求地址与当前页面不同源(域名、协议、端口、子域都相同)就会触发跨域,解决方法一般有以下几种:

  • 服务端配置Access-Control-Allow-*头部
  • 通过jsonp跨域,但能接收GET请求
  • document.domain + iframe跨域
  • location.hash + iframe
  • window.name + iframe跨域
  • postMessage跨域
  • 跨域资源共享(CORS)
  • nginx代理跨域
  • nodejs中间件代理跨域
  • WebSocket协议跨域

方式比较多,具体不细说,遇到过的就是后端设置头部,poseMessage方式,具体可以查看这篇文章

至于怎么检测跨域,浏览器对于请求都会发送,再检查返回头部,如果没有相关跨域头部则直接丢弃并提示跨域错误。

How does Access-Control-Allow-Origin header work?

安全相关

XSS

跨站脚本攻击,一般是用户输入或URL上携带了恶意代码。

永远不要相信用户的输入。

防范方式也对症下药,即对用户输入内容进行过滤或转码。

image

CSRF

跨站请求伪造,指的是访问恶意网站hacker.com,页面提供了诱导链接,比如某个银行网站的转账操作a.bank/transfer?userId=hakerxxx&money=9999,而该用户又保持着银行网站a.bank的登录状态,那么就存在资金损失的风险。

防范措施就在于对登录状态做校验,而登录态一般由cookie控制,cookie字段中可以设置same-site属性,即无法从其他域名下携带此cookie;另外,可以检查请求头的字段:Origin和Refer,前者包含域名信息,后者包含具体路径信息

还可以将接口改为POST方式,因为一般CSRF是GET请求,比如点击链接或者打开图片


说了这么多又侧重也有简述,旨在希望读者对整个流程有个基本的概念,也包括实际开发和学习过程中的指导,形成知识图谱,只要载体仍旧是浏览器,那么在未来相当长的一段时间内都不会有太大变化。

上面某些点是值得深入了解的,比如浏览器渲染原理,知道原理就知道如何优化以及避免页面卡顿。

附上一些参考链接: