前端需要知道的网络知识

计算机网络

前沿

网络相关的知识是每个前端工程师都应该具备的,前端从事的工作和网络有很大的关系,前端要负责和后台(服务器)进行交互,其必然得经过网络,所以懂点网络知识是很有必要的

在浏览器输入网址到看到页面经历了哪些过程?

  • 浏览器地址栏输入url
  • 浏览器会先查看浏览器缓存系统缓存路由缓存,如有存在缓存,就直接显示。如果没有,接着第3步
  • 域名解析(DNS)获取相应的 IP
  • 浏览器向服务器发起tcp连接,与浏览器建立tcp三次握手
  • 握手成功,浏览器向服务器发送http请求,请求数据包
  • 服务器请求数据,将数据返回到浏览器
  • 浏览器接收响应,读取页面内容,解析html源码,生成Dom树
  • 解析css样式、浏览器渲染,交互

上面的流程包含了前端必须掌握的网络知识

域名和IP

image.png
由于IP地址不方便记忆,所以同样用具有层次和唯一性的域名和IP一一映射

DNS

  • 客户端向本地域名服务器发出请求,请求www.baidu.com的IP地址
  • 本地DNS服务器向DNS根服务器发出请求,根DNS服务器会告诉本地服务器(.com)的服务器地址
  • 本地DNS服务器会向(.com域)发请求,会得到(baidu.com)的服务器地址
  • 本地DNS服务器会向(baidu.com)发请求,会得到(www.baidu.com)的IP地址61.135.169.125
  • 本地DNS服务器向客户端回复域名(www.baidu.com)对应的IP地址是61.135.169.125

同源策略和跨域

浏览器只对网络请求有同源限制,同源就是协议、域名和端口号一致,不同源的客户端脚本在没有明确授权的情况下,不能读写对方XHR资源,反之不同源脚本读取对方XHR资源就是跨域。以www.a.com/test/index.… 的同源检测举例:

跨域

  • jsonp:只支持GET,不支持POST请求,不安全XSS
  • postMessage:配合使用iframe,需要兼容IE6、7、8、9
  • document.domain:仅限于同一域名下的子域
  • cors:需要后台配合进行相关的设置
  • websocket:需要后台配合修改协议,不兼容,需要使用socket.io
  • proxy:使用代理去避开跨域请求,需要修改nginx、apache等的配置
jsonp
  • 浏览器对script标签src属性、link标签ref属性和img标签src属性没有同源策略限制,利用这个“漏洞”就可以很好的解决跨域请求,JSONP就是利用了script标签无同源限制的特点来实现的。
  • 当向第三方站点请求时,我们可以将此请求放在script标签的src属性里,这就如同请求一个普通的JS脚本,可以自由的向不同的站点请求。
//创建script发送请求
//请求返回执行cb函数,并且删除创建的script
//类似于$ajax中的jsonp
function jsonp(url,params,cb){
    return new Promimse((resolve,reject)=>{
        window[cb] = function(data){
            resolve(data);
            document.body.removeChild(script);
        }
        params={...params,cb},
        let arrs=[];
        for(let key in params){
            arrs.push(`${key}=${params[key]}`)
        }
        let script = document.createElement('script');
        script.src= url + '?'+ arrs.join('&');
        document.body.appendChild(script);
    })
}
jsonp({
    url:'https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su',
    params:{wd:%E8%B7%A8%E5%9F%9F},
    cb:'show'}).then(data=>{
        console.log(data)    
    })
postMessage

配合iframes使用,假设a.html位于服务`localhost:3000,b.html位于服务器localhost:4000

javascript
复制代码
//a.html
<body>
    <iframe id="frame"  src="http://localhost:4000/b.html" frameborder="0" onload="load()"></iframe>
    <script>
        function load(){

            let frame = document.getElementById('frame');

            frame.contentWindow.postMessage('我很帅','http://localhost:4000');
            window.onmessage =function (e){
                console.log(e.data);
            }
        }
    </script>
</body>
//otherWindow.postMessage(message, targetOrigin);
//otherWindow:指目标窗口,也就是给哪个window发消息,是 window.frames 属性的成员或者由 window.open 方法创建的窗口
//message:是要发送的消息,类型为 String、Object (IE8、9 不支持)
//targetOrigin: 是限定消息接收范围,不限制请使用'*'
//注意otherWindow和targetOrigin的区别
//b.html
<body>

    <script>

        //data:消息
        //origin:消息来源地址
        //source:源DOMWindow 对象
        window.onmessage =function (e){
            console.log(e.data);
            e.source.postMessage('不要脸',e.origin);
        }

    </script>

</body>

document.domain
//a.html
<body>

    helloa
    <iframe id="frame"  src="http://www.kongbz.com/b.html" frameborder="0" onload="load()"></iframe>
    <script>
        document.domain = 'kongbz.com';//设置domain
        function load(){

            let frame = document.getElementById('frame');

            console.log(frame.contentWindow.a)
        }

    </script>

</body>

<body>
    hellob
    <script>

        document.domain = 'kongbz.com';//设置domain
        var a = 'isB'
    </script>
</body>
websocket

客户端发送信息给服务端,如果想实现客户端向客户端通信,只能通过页面->服务端->另一个页面

//客户端
<body>
    hellob
    <script>
        let socket = new WebSocket('ws://localhost:3000');
        socket.onopen = function(){
            socket.send('我很帅')
        }
        socket.onmessage = function(e){
            console.log(e.data)
        }
    </script>
</body>
//服务端
let express = require('express');
let Websocket = require('wss');
let wss= new WebSocket.Server({port:3000})
wss.on('connection',function(ws){
    ws.on('message',function(data){
        console.log(data);
        ws.send('不要脸');
    })
})
let app = new express();
app.listen(3000)
cors
const http = require('http')
const whitList = ['http://localhost:4000'];
http.createServer(function (req, res) {
  let origin = req.headers.origin;
  //在白名单中的域名才能访问
  if(whitList.includes(origin)){
    //允许的域名(* 所有域),*不能和Access-Control-Allow-Credentials一起使用
    res.header("Access-Control-Allow-Origin", "*");
    //允许携带哪个头访问,不设置不能携带参数
    res.header("Access-Control-Allow-Headers","ContentType");
    //允许的方法,不设置默认支持GET、HEAD、POST,其他类型必须设置才能处理请求
    res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
    //运行携带cookie,设置之后还能服务器才能接受cookie
    res.header("Access-Control-Allow-Credentials",true);
    //允许前端获取哪个头,不设置浏览器不能解析后台返回的参数
    res.header("Access-Control-Allow-Expose-Headers",'ContentType');
    if(req.method=== 'OPTIONS'){
        res.end()
    }
  }
}).listen(9000, function () {
    console.log('server is runing at 9000')
})
proxy

反向代理

  • 大家都有过这样的经历,拨打10086客服电话,可能一个地区的10086客服有几个或者几十个,你永远都不需要关心在电话那头的是哪一个,叫什么,男的,还是女的,漂亮的还是帅气的,你都不关心,你关心的是你的问题能不能得到专业的解答,你只需要拨通了10086的总机号码,电话那头总会有人会回答你,只是有时慢有时快而已。那么这里的10086总机号码就是我们说的反向代理。客户不知道真正提供服务人的是谁。
  • 反向代理隐藏了真实的服务端,当我们请求 www.baidu.com 的时候,就像拨打10086一样,背后可能有成千上万台服务器为我们服务,但具体是哪一台,你不知道,也不需要知道,你只需要知道反向代理服务器是谁就好了,www.baidu.com 就是我们的反向代理服务器,反向代理服务器会帮我们把请求转发到真实的服务器那里去。Nginx就是性能非常好的反向代理服务器,用来做负载均衡。
    反向代理隐藏了真实的服务端,当我们请求 www.baidu.com 的时候,就像拨打10086一样,背后可能有成千上万台服务器为我们服务,但具体是哪一台,你不知道,也不需要知道,你只需要知道反向代理服务器是谁就好了,www.baidu.com 就是我们的反向代理服务器,反向代理服务器会帮我们把请求转发到真实的服务器那里去。Nginx就是性能非常好的反向代理服务器,用来做负载均衡。

正向代理
A同学在大众创业、万众创新的大时代背景下开启他的创业之路,目前他遇到的最大的一个问题就是启动资金,于是他决定去找马云爸爸借钱,可想而知,最后碰一鼻子灰回来了,情急之下,他想到一个办法,找关系开后门,经过一番消息打探,原来A同学的大学老师王老师是马云的同学,于是A同学找到王老师,托王老师帮忙去马云那借500万过来,当然最后事成了。不过马云并不知道这钱是A同学借的,马云是借给王老师的,最后由王老师转交给A同学。这里的王老师在这个过程中扮演了一个非常关键的角色,就是代理,也可以说是正向代理,王老师代替A同学办这件事,这个过程中,真正借钱的人是谁,马云是不知道的,这点非常关键。

  我们常说的代理也就是只正向代理,正向代理的过程,它隐藏了真实的请求客户端,服务端不知道真实的客户端是谁,客户端请求的服务都被代理服务器代替来请求,知名的科学上网工具shadowsocks 扮演的就是典型的正向代理角色。在天朝用浏览器访问 www.google.com 时,被残忍的拒绝了,于是你可以在国外搭建一台代理服务器,让代理帮我去请求google.com,代理把请求返回的相应结构再返回给我。

OSI七层模型

OSI七层协议应用层,展示层,会话层,传输层,网络层,数据链路层,物理层 是一个概念模型
image.png

TCP/IP协议和互联网协议群

TCP/IP协议群(Transmission Control Protocol/Internet Protocol),包含了一系列构成互联网基础的网络协议,是Internet的核心协议。基于TCP/IP的参考模型将协议分成四个层次,它们分别是链路层、网络层、传输层和应用层
image.png

应用层:FTP、HTTP、TELNET、SMTP、DNS等协议

传输层:TCP和UDP协议(数据封包,增加源端口,和目标端口号)

网络层:IP协议、ICMP协议、ARP协议、RARP协议和BOOTP协议IP协议负责对数据加上IP地址和其他的数据以确定传输的目标;(两个地址之间的运输,增加源ip地址和目标地址头部)

链路层: 是二进制数据,为待传送的数据加入一个以太网协议头,并进行CRC编码 为最后的数据传输做准备。(设备到设备,相当于快递运输过程中的每一个中转站)

TCP协议

为了保证TCP是可靠的、面向连接的协议,具备以下功能:

  1. 将数据进行分段打包传输,如果不将数据分段打包传输,那么会导致每次传输的数据特别大,而带宽是一定的,所以很容易造成拥塞。想象一下,一辆火车跑在公路上的感觉。
  2. 对每个数据包编号控制顺序,因为数据进行了分段打包传输,而网络中的路线不止一条,而且某些路线会有延迟的情况,没有编号,那么如何保证到达的数据是原来的模样。想象一下,将一副大拼图从一个地方,分多条路运往另外一个地方,并且没有编号。
  3. 运输中丢失、重发和丢弃处理,由于网络中的路线会有延迟,并且存在丢包现象,所以会有重发等机制来保证数据的完整性。
  4. 流量控制避免拥塞,避免发送速率过快,让接收方来不及接收,导致发生丢包。

node 可以非常方便的搭建网络服务器,node 的内置模板 net、dgram、http、https 分别对应处理 TCP、UDP、HTTP、HTTPS

const net = require('net');
const server = net.createServer((socket) => { 
    socket.on('data', (data) => { socket.write('data hello'); })
    socket.on('end', (data) => { socket.write('end'); }) 
    socket.write('tcp hello'); 
}) 
    
    server.listen(8082, () => {
    console.log('tcp server bound'); }) 
    console.log("Hello World"); 
    
    我们通过 net.createServer(listener) 创建了一个 TCP 服务器,listener 是连
    接事件 connection 的监听器,也可以使用下面的方式进行侦听
    const net = require('net'); 
    const server = net.createServer(); 
    server.on('connection', function (socket) { // 新的连接 }); 
    server.listen(8082);

UDP协议

UDP 的全称是 用户数据报协议(UDP,User Datagram Protocol),UDP 为应用程序提供了一种无需建立连接就可以发送封装的 IP 数据包的方法。如果应用程序开发人员选择的是 UDP 而不是 TCP 的话,那么该应用程序相当于就是和 IP 直接打交道的。

从应用程序传递过来的数据,会附加上多路复用/多路分解的源和目的端口号字段,以及其他字段,然后将形成的报文传递给网络层,网络层将运输层报文段封装到 IP 数据报中,然后尽力而为的交付给目标主机。最关键的一点就是,使用 UDP 协议在将数据报传递给目标主机时,发送方和接收方的运输层实体间是没有握手的。正因为如此,UDP 被称为是无连接的协议。

// UDP 服务器
const dgram = require('dgram');
const server = dgram.createSocket('udp4');

server.on('error', (err) => {
  console.log(`Server error:\n${err.stack}`);
  server.close();
});

server.on('message', (msg, rinfo) => {
  console.log(`Received: ${msg} from ${rinfo.address}:${rinfo.port}`);
  // 将消息发送回客户端
  server.send(`Echo: ${msg}`, rinfo.port, rinfo.address);
});

server.on('listening', () => {
  const address = server.address();
  console.log(`Server listening ${address.address}:${address.port}`);
});
// 开始监听
server.bind(8080);

// UDP 客户端
const client = dgram.createSocket('udp4');
const message = Buffer.from('Hello, server! Love, Client.');

client.send(message, 8080, 'localhost', (err) => {
  if (err) throw err;
  console.log('Message sent to server');
});

client.on('message', (msg, rinfo) => {
  console.log(`Received: ${msg} from ${rinfo.address}:${rinfo.port}`);
  client.close();
});

client.on('close', () => {
  console.log('Client closed');
});

UDP 特点

UDP 协议一般作为流媒体应用、语音交流、视频会议所使用的传输层协议,我们大家都知道的 DNS 协议底层也使用了 UDP 协议,这些应用或协议之所以选择 UDP 主要是因为以下这几点

  • 速度快,采用 UDP 协议时,只要应用进程将数据传给 UDP,UDP 就会将此数据打包进 UDP 报文段并立刻传递给网络层,然后 TCP 有拥塞控制的功能,它会在发送前判断互联网的拥堵情况,如果互联网极度阻塞,那么就会抑制 TCP 的发送方。使用 UDP 的目的就是希望实时性。
  • 无须建立连接,TCP 在数据传输之前需要经过三次握手的操作,而 UDP 则无须任何准备即可进行数据传输。因此 UDP 没有建立连接的时延。如果使用 TCP 和 UDP 来比喻开发人员:TCP 就是那种凡事都要设计好,没设计不会进行开发的工程师,需要把一切因素考虑在内后再开干!所以非常靠谱;而 UDP 就是那种上来直接干干干,接到项目需求马上就开干,也不管设计,也不管技术选型,就是干,这种开发人员非常不靠谱,但是适合快速迭代开发,因为可以马上上手!
  • 无连接状态,TCP 需要在端系统中维护连接状态,连接状态包括接收和发送缓存、拥塞控制参数以及序号和确认号的参数,在 UDP 中没有这些参数,也没有发送缓存和接受缓存。因此,某些专门用于某种特定应用的服务器当应用程序运行在 UDP 上,一般能支持更多的活跃用户
  • 分组首部开销小,每个 TCP 报文段都有 20 字节的首部开销,而 UDP 仅仅只有 8 字节的开销。

这里需要注意一点,并不是所有使用 UDP 协议的应用层都是不可靠的,应用程序可以自己实现可靠的数据传输,通过增加确认和重传机制。所以使用 UDP 协议最大的特点就是速度快。

HTTP 和 HTTPS

超文本传输协议(HTTP,HyperText Transfer Protocol)
是互联网上应用最为广泛的一种网络协议。设计 HTTP 最初的目的是为了提供一种发布和接收 HTML 页面的方法。它可以使浏览器更加高效。HTTP 协议是以明文方式发送信息的,如果黑客截取了 Web 浏览器和服务器之间的传输报文,就可以直接获得其中的信息。

http1.0
HTTP/1.0版本是1996正式提出的技术,但是现在在很多地方依旧有着应用,特别是代理服务器领域。HTTP/1.0是一种默认短连接的协议,就是说一个请求结束之后就断开连接,下次请求时重新建立新的连接,这种设计在最开始是为了提高服务器的效率,因为保持连接状态是需要服务器开销的,但是随着人们对互联网需求的不断增大,并且网络带宽不断增加的情况下,这种无法复用连接的方式成为了制约HTTP/1.0的主要因素之一,因为现在的网络请求可能是同时间的大量请求,例如我们访问网站时,一次可能需要同时请求网站的css、js、html和各种图片,如果每次请求都需要建立连接,那么在建立连接上就需要消耗大量资源。

HTTP/1.0另一个被人诟病的原因就是head of line blocking(队头阻塞),在HTTP/1.0版本中,上次请求完成后才能发送下一次请求,也就导致了,如果某次请求耗时特别长,会使得后续的请求都被阻塞而不能发出。

HTTP 1.0总结:
无状态,无连接,短连接,每次发送请求都要重新建立tcp请求,即三次握手,非常浪费性能;无host头域,也就是http请求头里的host;不允许断点续传,而且不能只传输对象的一部分,要求传输整个对象

HTTP/1.1

为了应对HTTP/1.0版本中出现的各种问题,1996年5月,HTTP诞生了1.1版本,HTTP/1.1是现在应用最广泛的HTTP协议,其主要是为了解决HTTP/1.0中的两大问题。

首先HTTP/1.1默认的是建立长连接,也就是说,当一次连接建立之后,短时间发出的请求都可以使用这一个连接发出,减少了建立连接时的资源消耗,同时,HTTP/1.1还允许客户端在不接收上次请求的响应的情况下发送新请求,但是在服务端,会依次对客户端发送的请求按次序回送响应,保证客户端能够区分每次请求的响应内容。

此外,HTTP/1.1在其它方面也做了很多优化,HTTP/1.1版本加入了更多的首部字段扩张HTTP协议的功能,比如HTTP/1.1加入了Host头域实现了在一台WEB服务器上可以在同一个IP地址和端口号上使用不同的主机名来创建多个虚拟WEB站点。HTTP 1.1还提供了与身份认证、状态管理Cache缓存等机制相关的请求头和响应头,并且还提供了断点续传功能

HTTP/1.1解决了1.0版本中出现的很多问题,但是,随着新的需求的出现,HTTP/1.1的瓶颈也逐渐显露,例如,我们熟悉的微博,一个微博用户的粉丝可能是成千上万,如果一个博主发送了一篇微博,为了在所有粉丝的客户端接收到最新信息,在客户端就必须发送大量的请求去询问是否有资源更新,如果没有资源更新,那么就会发送很多无用的请求。

HTTP/1.1的瓶颈主要有以下个方面:

  • 请求只能从客户端开始。客户端不可以接收除响应以外的指令。
  • 请求 / 响应首部未经压缩就发送。首部信息越多延迟越大。
  • 发送冗长的首部。每次互相发送相同的首部造成的浪费较多

基于以上问题,在2012年,谷歌提出了SPDY的设计方案,大大的提升了HTTP协议的性能。
SPDY并不是对HTTP协议的改写,而是在应用层之下加入了一个会话层,通过这个会话层来控制数据的流动。
使用SPDY之后,HTTP协议扩展了以下的功能:

  1. 多路复用流:通过一个tcp连接,可以处理无限多个请求,所有的请求都在一个tcp连接中完成,使得处理效率得到提高。
  2. 赋予请求优先级:SPDY可以赋予请求不同的优先级,使得在网络带宽不够时,大量请求也不会使得响应变慢。
  3. 压缩HTTP首部:对HTTP首部进行压缩,这样就使得通信产生的数据包和发送的字节数更少。
  4. 服务器推送功能:支持服务器主动给客户端发送消息,这样一来,使得服务端不必每次都等待客户端的请求,客户端也不必使用ajax长轮询的方式获得最新数据。
  5. 服务器提示功能:服务器可以主动提示客户端请求所需的资源。由于在客户端发现资源之前就可以获知资源的存在,因此在资源已缓存等情况下,以避免发送不必要的请求。

虽然SPDY的出现解决了很大一部分HTTP协议的问题,但是SPDY并没有被大规模退推广,其主要的问题是SPDY强制使用SSL,但是不是每一家网站都有证书,毕竟证书不便宜,所以到目前为之,也只有谷歌和一些稍大的公司开启了SPDY。

HTTP 1.1总结:
长连接流水线,使用connection:keep-alive使用长连接;请求管道化;增加缓存处理(新的字段如cache-control)增加Host字段,支持断点传输等;由于长连接会给服务器造成压力

HTTP/2.0

服务器推送:当我们对支持HTTP2.0的web server请求数据的时候,服务器会顺便把一些客户端需要的资源一起推送到客户端,免得客户端再次创建连接发送请求到服务器端获取。这种方式非常合适加载静态资源。
服务器推送可以缓存,并且在遵循同源的情况下,不同页面之间可以共享缓存。

因此当客户端需要的数据已缓存时,客户端直接从本地加载这些资源就可以了,不用走网络,速度自然是快很多的。

首部压缩:HTTP1.1不支持header数据的压缩,HTTP2.0使用HPACK算法对header的数据进行压缩,这样数据体积小了,在网络上传输就会更快。

二进制分帧:关键之一就是在应用层(HTTP/2)和传输层(TCP or UDP)之间增加一个二进制分帧层。在二进制分帧层中, HTTP/2 会将所有传输的信息分割为帧(frame),并对它们采用二进制格式的编码 ,其中 首部信息会被封装到 HEADER frame,而相应的 Request Body 则封装到 DATA frame 里面。HTTP 性能优化的关键并不在于高带宽,而是低延迟。TCP 连接会随着时间进行自我「调谐」,起初会限制连接的最大速度,如果数据成功传输,会随着时间的推移提高传输的速度。这种调谐则被称为 TCP 慢启动。由于这种原因,让原本就具有突发性和短时性的 HTTP 连接变的十分低效。

多路复用:HTTP/2是完全多路复用的,而非有序并阻塞的——只需一个连接即可实现并行
多路复用允许单一的 HTTP/2 连接同时发起多重的请求-响应消息如下图:
image.png

HTTP 2.0总结:
二进制分帧,头部压缩,双方各自维护一个header的索引表,使得不需要直接发送值,通过发送key缩减头部大小;多路复用(或连接共享),使用多个stream,每个stream又分帧传输,使得一个tcp连接能够处理多个http请求;服务器推送(Sever push)HTTP/2采用二进制格式而非文本格式

HTTP 3.0

  • 基于google的QUIC协议,而quic协议是使用udp实现的
  • 减少了tcp三次握手时间,以及tls握手时间
  • 解决了http 2.0中前一个stream丢包导致后一个stream被阻塞的问题
  • 优化了重传策略,重传包和原包的编号不同,降低后续重传计算的消耗
  • 连接迁移,不再用tcp四元组确定一个连接,而是用一个64位随机数来确定这个连接更合适的流量控制

http缓存
缓存有哪些好处?

  • 缓解服务器压力,不用每次都去请求某些数据了。
  • 提升性能,打开本地资源肯定会比请求服务器来的快。
  • 减少带宽消耗,当我们使用缓存时,只会产生很小的网络消耗

Web缓存种类: 数据库缓存,CDN缓存,代理服务器缓存,浏览器缓存。

所谓浏览器缓存其实就是指在本地使用的计算机中开辟一个内存区,同时也开辟一个硬盘区作为数据传输的缓冲区,然后用这个缓冲区来暂时保存用户以前访问过的信息。

浏览器缓存过程: 强缓存,协商缓存。

浏览器缓存位置一般分为四类: Service Worker-->Memory Cache-->Disk Cache-->Push Cache

强制缓存(存储持久不变的资源):不去服务器对比(缓存生效不再发生请求)

如何设置强缓存?

我们第一次进入页面,请求服务器,然后服务器进行应答,浏览器会根据response Header来判断是否对资源进行缓存,如果响应头中expires、pragma或者cache-control字段,代表这是强缓存,浏览器就会把资源缓存在memory cache 或 disk cache中。

协商缓存

协商缓存的标识也是在响应报文的HTTP头中和请求结果一起返回给浏览器的,控制协商缓存的字段分别有:Last-Modified / If-Modified-SinceEtag / If-None-Match,其中Etag / If-None-Match的优先级比Last-Modified / If-Modified-Since高。同时存在则只有Etag / If-None-Match生效;

强制缓存优先于协商缓存进行,若强制缓存(Expires和Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,重新获取请求结果,再存入浏览器缓存中;生效则返回304,继续使用缓存

Last-Modified是服务器响应请求时,返回该资源文件在服务器最后被修改的时间

If-Modified-Since则是客户端再次发起该请求时,携带上次请求返回的Last-Modified值,通过此字段值告诉服务器该资源上次请求返回的最后被修改时间。服务器收到该请求,发现请求头含有If-Modified-Since字段,则会根据If-Modified-Since的字段值与该资源在服务器的最后被修改时间做对比,若服务器的资源最后被修改时间大于If-Modified-Since的字段值,则重新返回资源,状态码为200;否则则返回304,代表资源无更新,可继续使用缓存文件

Etag是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成)在node中通过 app.set(‘etag’, true);设置协商缓存
If-None-Match是客户端再次发起该请求时,携带上次请求返回的唯一标识Etag值,通过此字段值告诉服务器该资源上次请求返回的唯一标识值。服务器收到该请求后,发现该请求头中含有If-None-Match,则会根据If-None-Match的字段值与该资源在服务器的Etag值做对比,一致则返回304,代表资源无更新,继续使用缓存文件;不一致则重新返回资源文件,状态码为200

浏览器的渲染机制

浏览器内核是多线程,在内核控制下各线程相互配合以保持同步,一个浏览器通常由以下常驻线程组成:
image.png
以 Chrome 为例,浏览器不仅有多个线程,还有多个进程,如渲染进程、GPU 进程和插件进程等。而每个 tab 标签页都是一个独立的渲染进程,所以一个 tab 异常崩溃后,其他 tab 基本不会被影响。作为前端开发者,主要重点关注其渲染进程,渲染进程下包含了 JS 引擎线程、HTTP 请求线程和定时器线程等,这些线程为 JS 在浏览器中完成异步任务提供了基础。

浏览器的解析渲染过程,解析DOM生成DOM Tree,解析CSS生成CSSOM Tree,两者结合生成render tree渲染树,最后浏览器根据渲染树渲染至页面。由此可以看出DOM Tree的解析和CSSOM Tree的解析是互不影响的,两者是并行的。因此CSS不会阻塞页面DOM的解析,但是由于render tree的生成是依赖DOM Tree和CSSOM Tree的,因此CSS必然会阻塞DOM的渲染。
更为严谨一点的说,CSS会阻塞render tree的生成,进而会阻塞DOM的渲染。

总结:CSS不会阻塞DOM解析,但是会阻塞DOM渲染,严谨一点则是CSS会阻塞render tree的生 成,进而会阻塞DOM的渲染;

首先浏览器无法知晓JS的具体内容,倘若先解析DOM,万一JS内部全部删除掉DOM,那么浏览器就白忙活了,所以就干脆暂停解析DOM,等到JS执行完成再继续解析。

总结:JS会阻塞DOM解析;

其实这样做也是有道理的,设想JS脚本中的内容是获取DOM元素的CSS样式属性,如果JS想要获取到DOM最新的正确的样式,势必需要所有的CSS加载完成,否则获取的样式可能是错误或者不是最新的。因此要等到JS脚本前面的CSS加载完成,JS才能再执行,并且不管JS脚本中是否获取DOM元素的样式,浏览器都要这样做。

总结:CSS会阻塞JS的执行

浏览器解析DOM时,虽然会一行一行向下解析,但是它会预先加载具有引用标记的外部资源(例如带有src标记的script标签),而在解析到此标签时,则无需再去加载,直接运行,以此提高运行效率。所以就会有上述两个输出结果间隔2s的情况,而不是4s,因为浏览器预先就一起加载了两个script脚本,第一个script脚本加载完成时,第二个script脚本还剩大概2s加载完成。

而这个结论才是解释为何CSS会阻塞JS的执行的真正原因,浏览器无法预先知道脚本的具体内容,因此在碰到script标签时,只好先渲染一次页面,确保script脚本内能获取到DOM的最新的样式。倘若在决定渲染页面时,还有尚未加载完成的CSS样式,只能等待其加载完成再去渲染页面。

总结:浏览器遇到script标签且没有defer或async属性时会触发页面渲染;

script defer

先看一下 MDN 上的解释:

这个布尔属性被设定用来通知浏览器该脚本将在文档完成解析后,触发 DOMContentLoaded 事件前执行。

有 defer 属性的脚本会阻止 DOMContentLoaded 事件,直到脚本被加载并且解析完成。

文档是直接总结了他的特性,我们先看看下面的代码,展开说说细节,加深一下理解。

<!DOCTYPE html>

<html lang="zh">

  <head>

    <title>Hi</title>

    <script>

      console.log("Howdy ~");

    </script>

    <script defer src="https://unpkg.com/vue@3.2.41/dist/vue.global.js"></script>
    <script defer src="https://unpkg.com/vue-router@4.1.5/dist/vue-router.global.js"></script>
  </head>

  <body>

    Hello ?? ~

  </body>

</html>

他的执行顺序是:

  • 在控制台打印:Howdy ~
  • 在页面中展示:Hello ?? ~
  • 请求并执行 vue.global.js
  • 请求并执行 vue-router.global.js
  • 触发 DOMContentLoaded[3] 事件

Image
script defer 加载逻辑

如果在 script 标签上设置了 defer 属性,那么在浏览器解析到这里时,会默默的在后台开始下载此脚本,并继续解析后面的 HTML,并不会阻塞解析操作。

等到 HTML 解析完成之后,浏览器会立即执行后台下载的脚本,脚本执行完成之后,才会触发 DOMContentLoaded 事件。

看起来还是蛮好理解的吧?咱们再来讨论 2 个小细节:

Q1:  如果 HTML 解析完成之后,设置了 defer 属性的脚本还没下载完成,会怎样?

A1:  浏览器会等脚本下载完成之后,再执行此脚本,执行完成之后,再触发 DOMContentLoaded 事件。

Q2:  如果有多个设置了 defer 属性的脚本,那浏览器会如何处理?

A2:  浏览器会并行的在后台下载这些脚本,等 HTML 解析完成,并且所有脚本下载完成之后,再按照他们在 HTML 中出现的相对顺序执行,等所有脚本执行完成之后,再触发 DOMContentLoaded 事件。

最佳实践:

建议所有的外联脚本都默认设置此属性,因为他不会阻塞 HTML 解析,可以并行下载 JavaScript 资源,还可以按照他们在 HTML 中的相对顺序执行,确保有依赖关系的脚本运行时,不会缺少依赖。

在 SPA 的应用中,可以考虑把所有的 script 标签加上 defer 属性,并且放到 body 的最后面。在现代浏览器中,可以并行下载提升速度,也可以确保在老浏览器中,不阻塞浏览器解析 HTML,起到降级的作用。

注意:

  • defer 属性仅适用于外部脚本,如果 script 脚本没有 src,则会忽略 defer 特性。
  • defer 属性对模块脚本(script type=’module'[4])无效,因为模块脚本就是以 defer 的形式加载的。

script async

按照惯例,先看一下 MDN 上的解释:

对于普通脚本,如果存在 async 属性,那么普通脚本会被并行请求,并尽快解析和执行。

对于模块脚本,如果存在 async 属性,那么脚本及其所有依赖都会在延缓队列中执行,因此它们会被并行请求,并尽快解析和执行。

该属性能够消除解析阻塞的 Javascript。

解析阻塞的 Javascript 会导致浏览器必须加载并且执行脚本,之后才能继续解析。

感觉这段描述的已经蛮清晰了,不过咱们还是先看看下面的代码,展开说说细节,加深一下理解。

<!DOCTYPE html>

<html lang="zh">

  <head>

    <title>Hi</title>

    <script>

      console.log("Howdy ~");

    </script>

    <script async src="https://google-analytics.com/analytics.js"></script>
    <script async src="https://ads.google.cn/ad.js"></script>
  </head>

  <body>

    Hello ?? ~

  </body>

</html>

他的执行顺序是:

  • 在控制台打印:Howdy ~
  • 并行请求 analytics.js 和 ad.js
  • 在页面中展示:Hello ?? ~
  • 根据网络的实际情况,以下几项会无序执行
    • 执行 analytics.js(下载完后,立即执行)
    • 执行 ad.js(下载完后,立即执行)
    • 触发 DOMContentLoaded 事件(可能在在上面 2 个脚本之前,之间,之后触发)

Image

浏览器在解析到带有 async 属性的 script 标签时,也不会阻塞页面,同样是在后台默默下载此脚本。当他下载完后,浏览器会暂停解析 HTML,立马执行此脚本。

看起来还是蛮好理解的吧?咱们再来讨论 2 个小细节:

Q1: 如果设置了 async 属性的 script 下载完之后,浏览器还没解析完 HTML,会怎样?

A1: 浏览器会暂停解析 HTML,立马执行此脚本,等执行完之后,再继续解析 HTML。

Q2: 如果有多个 async 属性的 script 标签,那等他们下载完成之后,会按照代码顺序执行吗?

A2: 不会。执行顺序是:谁先下载完成,谁先执行。async 的特点是「完全独立」,不依赖其他内容。

最佳实践:

当我们的项目,需要集成其他独立的第三方库时,可以使用此属性,他们不依赖我们,我们也不依赖于他们。通过设置此属性,让浏览器异步下载并执行他,是个不错的优化方案。

注意:

  • async 特性仅适用于外部脚本,如果 script 脚本没有 src,则会忽略 async 特性。

defer

  • 不阻塞浏览器解析 HTML,等解析完 HTML 之后,才会执行 script
  • 会并行下载 JavaScript 资源。
  • 会按照 HTML 中的相对顺序执行脚本。
  • 会在脚本下载并执行完成之后,才会触发 DOMContentLoaded 事件。
  • 在脚本执行过程中,一定可以获取到 HTML 中已有的元素。
  • defer 属性对模块脚本无效。
  • 适用于:所有外部脚本(通过 src 引用的 script)。

async

  • 不阻塞浏览器解析 HTML,但是 script 下载完成后,会立即中断浏览器解析 HTML,并执行此 script
  • 会并行下载 JavaScript 资源。
  • 互相独立,谁先下载完,谁先执行,没有固定的先后顺序,不可控。
  • 由于没有确定的执行时机,所以在脚本里面可能会获取不到 HTML 中已有的元素。
  • DOMContentLoaded 事件和 script 脚本无相关性,无法确定他们的先后顺序。
  • 适用于:独立的第三方脚本。

另外:async 和 defer 之间最大的区别在于它们的执行时机。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYVlPBbJ' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片