计算机网络
前沿
网络相关的知识是每个前端工程师都应该具备的,前端从事的工作和网络有很大的关系,前端要负责和后台(服务器)进行交互,其必然得经过网络,所以懂点网络知识是很有必要的
在浏览器输入网址到看到页面经历了哪些过程?
- 浏览器地址栏输入url
- 浏览器会先查看浏览器缓存系统缓存路由缓存,如有存在缓存,就直接显示。如果没有,接着第3步
- 域名解析(DNS)获取相应的 IP
- 浏览器向服务器发起tcp连接,与浏览器建立tcp三次握手
- 握手成功,浏览器向服务器发送http请求,请求数据包
- 服务器请求数据,将数据返回到浏览器
- 浏览器接收响应,读取页面内容,解析html源码,生成Dom树
- 解析css样式、浏览器渲染,交互
上面的流程包含了前端必须掌握的网络知识
域名和IP
由于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.… 的同源检测举例:
- www.a.com/dir/page.ht… —-成功
- child.a.com/test/index.… —-失败,域名不同
- www.a.com/test/index.… —-失败,协议不同(https)
- www.a.com:8080/test/index.… —-失败,端口号不同
跨域
jsonp
:只支持GET
,不支持POST请求,不安全XSSpostMessage
:配合使用iframe,需要兼容IE6、7、8、9document.domain
:仅限于同一域名下的子域cors
:需要后台配合进行相关的设置websocket
:需要后台配合修改协议,不兼容,需要使用socket.ioproxy
:使用代理去避开跨域请求,需要修改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七层协议:应用层,展示层,会话层,传输层,网络层,数据链路层,物理层
是一个概念模型
TCP/IP协议和互联网协议群
TCP/IP协议群(Transmission Control Protocol/Internet Protocol),包含了一系列构成互联网基础的网络协议,是Internet的核心协议。基于TCP/IP的参考模型将协议分成四个层次,它们分别是链路层、网络层、传输层和应用层
应用层: 有FTP、HTTP、TELNET、SMTP、DNS
等协议
传输层: 有TCP和UDP协议(数据封包,增加源端口,和目标端口号)
网络层: 有IP协议、ICMP协议、ARP协议、RARP协议和BOOTP协议
IP协议负责对数据加上IP地址和其他的数据以确定传输的目标;(两个地址之间的运输,增加源ip地址和目标地址头部)
链路层: 是二进制数据,为待传送的数据加入一个以太网协议头,并进行CRC编码
为最后的数据传输做准备。(设备到设备,相当于快递运输过程中的每一个中转站)
TCP协议
为了保证TCP是可靠的、面向连接的协议,具备以下功能:
- 将数据进行分段打包传输,如果不将数据分段打包传输,那么会导致每次传输的数据特别大,而带宽是一定的,所以很容易造成拥塞。想象一下,一辆火车跑在公路上的感觉。
- 对每个数据包编号控制顺序,因为数据进行了分段打包传输,而网络中的路线不止一条,而且某些路线会有延迟的情况,没有编号,那么如何保证到达的数据是原来的模样。想象一下,将一副大拼图从一个地方,分多条路运往另外一个地方,并且没有编号。
- 运输中丢失、重发和丢弃处理,由于网络中的路线会有延迟,并且存在丢包现象,所以会有重发等机制来保证数据的完整性。
- 流量控制避免拥塞,避免发送速率过快,让接收方来不及接收,导致发生丢包。
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协议扩展了以下的功能:
多路复用流
:通过一个tcp连接,可以处理无限多个请求,所有的请求都在一个tcp连接中完成,使得处理效率得到提高。赋予请求优先级
:SPDY可以赋予请求不同的优先级,使得在网络带宽不够时,大量请求也不会使得响应变慢。压缩HTTP首部
:对HTTP首部进行压缩,这样就使得通信产生的数据包和发送的字节数更少。服务器推送功能
:支持服务器主动给客户端发送消息,这样一来,使得服务端不必每次都等待客户端的请求,客户端也不必使用ajax长轮询的方式获得最新数据。服务器提示功能
:服务器可以主动提示客户端请求所需的资源。由于在客户端发现资源之前就可以获知资源的存在,因此在资源已缓存等情况下,以避免发送不必要的请求。
虽然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 连接同时发起多重的请求-响应消息如下图:
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-Since
和 Etag / 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
浏览器的渲染机制
浏览器内核是多线程,在内核控制下各线程相互配合以保持同步,一个浏览器通常由以下常驻线程组成:
以 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] 事件
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 个脚本之前,之间,之后触发)
浏览器在解析到带有 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
之间最大的区别在于它们的执行时机。