输入URL后到页面渲染 - 图14 浏览器解析URL

回车之后,浏览器会先解析url格式

正确的URL格式

获取URL信息

浏览器通过url知道下面信息。

  • Protocol ”http”
    使用HTTP协议
  • Resource ”/”
    请求的资源是主页(index)

HSTS过滤

浏览器发起请求前都会检查一个由浏览器维护的HSTS预加载列表,只要请求地址在该列表中,我们就会强制浏览器使用https请求

若列表中没有该地址,且地址访问协议不为 https ,则按照HSTS请求步骤实现页面访问(以访问 baidu.com 为例)

  1. 浏览器时先按照http://www.baidu.com进行访问
  2. 百度服务器发现是http请求,这是不安全的,于是响应一个301/302重定向response让客户端重定向至https://www.baidu.com
  3. 客户端接收到重定向响应信息,于是重定向至https://www.baidu.com让浏览器直接发起HTTPS请求
    此时报文抬头包括,HSTS预加载列表加入该地址
    1. Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

    响应头的 Strict-Transport-Security 给浏览器提供了详细的说明。 从现在开始,每个连接到该网站及其子域的下一年(31536000秒)从这个头被接收的时刻起必须是一个 HTTPS 连接。 HTTP 连接是完全不允许的。 如果浏览器接收到使用 HTTP 加载资源的请求,则必须尝试使用 HTTPS 请求替代。 如果 HTTPS 不可用,则必须直接终止连接

    此外,如果证书无效,将阻止你建立连接。 通常来说,如果 HTTPS 证书无效(如:过期、自签名、由未知 CA 签名等),浏览器会显示一个可以规避的警告。 但是,如果站点有 HSTS,浏览器就不会让你绕过警告。 若要访问该站点,必须从浏览器内的 HSTS 列表中删除该站点


相关知识点输入URL后到页面渲染 - 图15

HSTS优点
  1. 强制使用HTTPS建立安全连接防止重定向劫持以及中间人SSL剥离攻击。
  2. 省略从前http访问服务器,服务器响应,客户端再重定向的步骤,节省时间与资源。
  3. 无HSTS浏览器显示证书错误时用户可以选择忽略警告继续访问,有了HSTS之后证书错误时无目标页链接入口,防止中间人使用SSL劫持攻击。

HSTS缺陷

HSTS无法解决第一次访问时的HTTP请求,只能要求客户端第一次以后每次访问都使用HTTPS请求。

解决HSTS缺陷

浏览器发起请求前都会检查一个由浏览器维护的HSTS预加载列表,所以我们只需要将服务器地址加入到HSTS预加载列表中即可解决HSTS存在的缺陷。

HSTS 是否完全安全?
  • 你第一次访问这个网站,你不受 HSTS 的保护。 如果网站向 HTTP 连接添加 HSTS 头,则该报头将被忽略。 这是因为攻击者可以在中间人攻击(man-in-the-middle attack)中删除或添加头部。 HSTS 报头不可信,除非它是通过 HTTPS 传递的。
  • 目前唯一可用于绕过 HSTS 的已知方法是基于 NTP 的攻击。 如果客户端计算机容易受到 NTP 攻击( NTP-based attack),它可能会被欺骗,使 HSTS 策略到期,并使用 HTTP 访问站点一次。

错误的URL格式

当协议或者主机名不合法时,浏览器会将地址栏中输入的文字传给默认的搜索引擎。

输入URL后到页面渲染 - 图16 浏览器检查缓存

发起请求前,浏览器会先判断所请求的资源是否在缓存里,如果请求的资源在缓存里并且没有失效,那么就直接使用,否则向服务器发起新的请求。

输入URL后到页面渲染 - 图17

缓存策略

强缓存

浏览器不会向服务器发送任何请求,直接从本地缓存中读取文件并返回Status Code: 200 OK。

对于强制缓存来说,响应header中会有两个字段来标明失效规则(Expires/Cache-Control)

  • Expires 的值为服务端返回的到期时间,即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据。

    ExpiresHTTP / 1.0 的产物,表示资源会在到期时间后过期,需要再次请求。并且 Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效

  • Cache-Control 是最重要的规则。常见的取值有private、public、no-cache、max-age,no-store,默认为private。

    Cache-Control 出现于 HTTP / 1.1,优先级高于 Expires 。该属性表示资源会在 max-age 后过期,需要再次请求

  1. private: 客户端可以缓存
  2. public: 客户端和代理服务器都可缓存
  3. max-age=xxx 缓存的内容将在 xxx 秒后失效
  4. s-maxage=xxx s-maxage的优先级比max-age高。s-maxage是代理服务器的缓存时间。
  5. no-cache: 需要使用对比缓存来验证缓存数据(后面介绍)
  6. no-store: 所有内容都不会缓存,强制缓存,对比缓存都不会触发

协商缓存

协商缓存: 向服务器发送请求,服务器会根据这个请求的 Request Header 的一些参数来判断是否命中协商缓存,如果命中,则返回 304 状态码并带上新的 Response Header 通知浏览器从缓存中读取资源;

输入URL后到页面渲染 - 图18

对于协商缓存来说,缓存标识的传递是我们着重需要理解的,它在请求header和响应header间进行传递

Last-Modified / If-Modified-Since

  • Last-Modified:服务器在响应请求时,告诉浏览器资源的最后修改时间。
  • If-Modified-Since:
    再次请求服务器时,通过此字段通知服务器上次请求时,服务器返回的资源最后修改时间。
    服务器收到请求后发现有头If-Modified-Since 则与被请求资源的最后修改时间进行比对。
    若资源的最后修改时间大于If-Modified-Since,说明资源又被改动过,则响应整片资源内容,返回状态码200;
    若资源的最后修改时间小于或等于If-Modified-Since,说明资源无新修改,则响应HTTP 304,告知浏览器继续使用所保存的cache。

Etag / If-None-Match(优先级高于Last-Modified / If-Modified-Since)

  • Etag:服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器决定)。
  • If-None-Match:
    再次请求服务器时,通过此字段通知服务器客户段缓存数据的唯一标识。
    服务器收到请求后发现有头If-None-Match 则与被请求资源的唯一标识进行比对,
    不同,说明资源又被改动过,则响应整片资源内容,返回状态码200;
    相同,说明资源无新修改,则响应HTTP 304,告知浏览器继续使用所保存的cache

相关知识点输入URL后到页面渲染 - 图19

选择合适的缓存策略

对于大部分的场景都可以使用强缓存配合协商缓存解决,但是在一些特殊的地方可能需要选择特殊的缓存策略

  • 对于某些不需要缓存的资源,可以使用 Cache-control: no-store ,表示该资源不需要缓存
  • 对于频繁变动的资源,可以使用 Cache-Control: no-cache 并配合 ETag 使用,表示该资源已被缓存,但是每次都会发送请求询问资源是否更新。
  • 对于代码文件来说,通常使用 Cache-Control: max-age=31536000 并配合策略缓存使用,然后对文件进行指纹处理,一旦文件名变动就会立刻下载新的文件

缓存方式

Memory Cache

Memory Cacha是指内存中的缓存。它是浏览器优先去命中的一种缓存,也是响应速度最快的一种缓存。

但是它的缺点是缓存时间短,关闭tab页面缓存将不复存在,它与浏览器渲染进程紧密联系。

由于浏览器的内存非常有限,浏览器并不会把所有文件都缓存在此处。一般来说会缓存一些体积不大的js或者css文件。

Service Worker Cache

Service Worker 是一种独立于主线程之外的javascript线程。它脱离于浏览器窗体,因此无法直接访问DOM元素。所以这一个独立的线程能够在不干扰主线程的情况下来提升性能。Service Worker 的缓存与浏览器内建的其他缓存机制不一样,它可以让我们自由缓存哪一些文件、如何匹配缓存等,且缓存具有持续性。

Service Worker 可以用来实现离线缓存、消息推送以及网络代理等功能

Server Worker 对协议是有要求的,必须以 https 协议为前提。

实现该缓存一般分为3个步骤:

  • 注册Service Worker

    1. window.navigator.serviceWorker.register('/test.js').then(
    2. function () {
    3. console.log('注册成功')
    4. }).catch(err => {
    5. console.error("注册失败")
    6. })
    7. })
  • 监听install事件就可以缓存我们想要的文件

    1. // Service Worker会监听 install事件,我们在其对应的回调里可以实现初始化的逻辑
    2. self.addEventListener('install', event => {
    3. event.waitUntil(
    4. // 考虑到缓存也需要更新,open内传入的参数为缓存的版本号
    5. caches.open('test-v1').then(cache => {
    6. return cache.addAll([
    7. // 此处传入指定的需缓存的文件名
    8. '/test.html',
    9. '/test.css',
    10. '/test.js'
    11. ])
    12. })
    13. )
    14. })
  • 用户下次访问可以通过拦截请求的方式来获取缓存数据。若没有则会重新获取数据,然后再进行缓存

    1. // Service Worker会监听所有的网络请求,网络请求的产生触发的是fetch事件,我们可以在其对应的监听函数中实现对请求的拦截,进而判断是否有对应到该请求的缓存,实现从Service Worker中取到缓存的目的
    2. self.addEventListener('fetch', event => {
    3. event.respondWith(
    4. // 尝试匹配该请求对应的缓存值
    5. caches.match(event.request).then(res => {
    6. // 如果匹配到了,调用Server Worker缓存
    7. if (res) {
    8. return res;
    9. }
    10. // 如果没匹配到,向服务端发起这个资源请求
    11. return fetch(event.request).then(response => {
    12. if (!response || response.status !== 200) {
    13. return response;
    14. }
    15. // 请求成功的话,将请求缓存起来。
    16. caches.open('test-v1').then(function(cache) {
    17. cache.put(event.request, response);
    18. });
    19. return response.clone();
    20. });
    21. })
    22. );
    23. });

Disk Cache

Disk Cache也就是硬盘缓存。这种缓存的缓存位置在电脑硬盘上,什么文件都可以缓存,就是读取速度慢。所有缓存中,它的覆盖面是最广的,会根据HTTP Header中的字段判断哪一些资源需要缓存,哪些可以不请求直接使用,哪一些已过期需要重新请求。

通常把 大体积文件、系统内存使用率高 放入内存

Push Cache

Push Cache又名推送缓存,是HTTP/2中的内容,只有以上三种缓存未正确命中,它才会使用。仅存在于会话阶段(session),结束就会释放,缓存时间短。

  • Push Cache 是一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放。
  • 不同的页面只要共享了同一个 HTTP2 连接,那么它们就可以共享同一个 Push Cache
  • Push Cache 是缓存的最后一道防线。浏览器均未命中以上的情况下才会去询问 Push Cache。
  • Push Cache中的缓存只能被使用一次。

输入URL后到页面渲染 - 图20 DNS域名解析

检查完若无缓存则封装http请求报文,并通过dns解析获取到目标ip地址

检查缓存

dns解析之前先查找缓存,若无缓存才进行dns解析

1. 浏览器缓存

浏览器会先检查是否在缓存中,没有则调用系统库函数进行查询。

2. 操作系统缓存

操作系统也有自己的 DNS缓存,但在这之前,会向检查域名是否存在本地的 Hosts 文件里,没有则向 DNS 服务器发送查询请求。

3. 路由器缓存

路由器也有自己的缓存。

4. ISP DNS 缓存

ISP DNS 就是在客户端电脑上设置的首选 DNS 服务器,它们在大多数情况下都会有缓存。

DNS解析(基于UDP)

在前面所有步骤没有缓存的情况下,本地 DNS 服务器会将请求转发到互联网上的根域

DNS 协议提供的是一种主机名到 IP 地址的转换服务,就是我们常说的域名系统。它是一个由分层的 DNS 服务器组成的分布式数据库,是定义了主机如何查询这个分布式数据库的方式的应用层协议。DNS 协议运行在 UDP 协议之上,使用 53 号端口。

根据域名的层级结构,管理不同层级域名的服务器,可以分为根域名服务器、顶级域名服务器和权威域名服务器。

  1. 主机名.次级域名.顶级域名.根域名

查询过程

DNS 的查询过程一般为,我们首先将 DNS 请求发送到本地 DNS 服务器,由本地 DNS 服务器来代为请求。

  1. 从”根域名服务器”查到”顶级域名服务器”的 NS 记录和 A 记录( IP 地址)。
  2. 从”顶级域名服务器”查到”次级域名服务器”的 NS 记录和 A 记录( IP 地址)。
  3. 从”次级域名服务器”查出”主机名”的 IP 地址。

比如我们如果想要查询 www.baidu.com 的 IP 地址,我们首先会将请求发送到本地的 DNS 服务器中,

本地 DNS 服务器会判断是否存在该域名的缓存,如果不存在,则向根域名服务器发送一个请求,

根域名服务器返回负责 .com 的顶级域名服务器的 IP 地址的列表。

然后本地 DNS 服务器再向其中一个负责 .com 的顶级域名服务器发送一个请求,负责 .com的顶级域名服务器返回负责 .baidu 的权威域名服务器的 IP 地址列表。

然后本地 DNS 服务器再向其中一个权威域名服务器发送一个请求,最后权威域名服务器返回一个对应的主机名的 IP 地址列表。

查询方式

递归查询指的是查询请求发出后,域名服务器代为向下一级域名服务器发出请求,最后向用户返回查询的最终结果。使用递归查询,用户只需要发出一次查询请求。

迭代查询指的是查询请求后,域名服务器返回单次查询的结果。下一级的查询由用户自己请求。使用迭代查询,用户需要发出多次的查询请求。

一般我们向本地 DNS 服务器发送请求的方式就是递归查询,因为我们只需要发出一次请求,然后本地 DNS 服务器返回给我们最终的请求结果。而本地 DNS 服务器向其他域名服务器请求的过程是迭代查询的过程,因为每一次域名服务器只返回单次查询的结果,下一级的查询由本地 DNS 服务器自己进行。


相关知识点输入URL后到页面渲染 - 图21

DNS 负载平衡

DNS 可以用于在冗余的服务器上实现负载平衡。因为现在一般的大型网站使用多台服务器提供服务,因此一个域名可能会对应多个服务器地址。当用户发起网站域名的 DNS 请求的时候,DNS 服务器返回这个域名所对应的服务器 IP 地址的集合,但在每个回答中,会循环这些 IP 地址的顺序,用户一般会选择排在前面的地址发送请求。以此将用户的请求均衡的分配到各个不同的服务器上,这样来实现负载均衡。

DNS 记录和报文

DNS 服务器中以资源记录的形式存储信息,每一个 DNS 响应报文一般包含多条资源记录。一条资源记录的具体的格式为(Name,Value,Type,TTL)

其中 TTL 是资源记录的生存时间,它定义了资源记录能够被其他的 DNS 服务器缓存多长时间。

常用的一共有四种 Type 的值,分别是 A、NS、CNAME 和 MX ,不同 Type 的值,对应资源记录代表的意义不同。

  1. 如果 Type = A,则 Name 是主机名,Value 是主机名对应的 IP 地址。因此一条记录为 A 的资源记录,提供了标准的主机名到 IP 地址的映射。
  2. 如果 Type = NS,则 Name 是个域名,Value 是负责该域名的 DNS 服务器的主机名。这个记录主要用于 DNS 链式查询时,返回下一级需要查询的 DNS 服务器的信息。
  3. 如果 Type = CNAME,则 Name 为别名,Value 为该主机的规范主机名。该条记录用于向查询的主机返回一个主机名对应的规范主机名,从而告诉查询主机去查询这个主机名的 IP 地址。主机别名主要是为了通过给一些复杂的主机名提供一个便于记忆的简单的别名。
  4. 如果 Type = MX,则 Name 为一个邮件服务器的别名,Value 为邮件服务器的规范主机名。它的作用和 CNAME 是一样的,都是为了解决规范主机名不利于记忆的缺点。

HTML做DNS优化

  1. <meta http-equiv='x-dns-prefetch-control' content='on' />

X-DNS-Prefetch-ControlHTTP 响应头控制 DNS 预取功能通过对用户可以选择跟随,以及通过在文档,包括图片,CSS,JavaScript 和等参考项的 URL 都链接浏览器主动进行域名解析

该预取在后台执行,以便在需要引用项目时 DNS 可能已经解决。这可以减少用户点击链接时的等待时间。

预解析

当浏览器从(第三方)服务器请求资源时,必须先将该跨域域名解析为 IP地址,然后浏览器才能发出请求。此过程称为 DNS解析。DNS 缓存可以帮助减少此延迟,而 DNS解析可以导致请求增加明显的延迟。对于打开了与许多第三方的连接的网站,此延迟可能会大大降低加载性能。

  1. <link rel="dns-prefetch" href="https://fonts.googleapis.com/">

DNS 为什么使用 UDP 协议作为传输层协议?

  1. DNS 使用 UDP 协议作为传输层协议的主要原因是为了避免使用 TCP 协议时造成的连接时延。因为为了得到一个域名的 IP 地址,往往会向多个域名服务器查询,如果使用 TCP 协议,那么每次请求都会存在连接时延,这样使 DNS 服务变得很慢,因为大多数的地址查询请求,都是浏览器请求页面时发出的,这样会造成网页的等待时间过长。
  2. 使用 UDP 协议作为 DNS 协议会有一个问题,由于历史原因,物理链路的最小MTU = 576,所以为了限制报文长度不超过576UDP 的报文段的长度被限制在 512 个字节以内,这样一旦 DNS 的查询或者应答报文,超过了 512 字节,那么基于 UDP DNS 协议就会被截断为 512 字节,那么有可能用户得到的 DNS 应答就是不完整的。这里 DNS 报文的长度一旦超过限制,并不会像 TCP 协议那样被拆分成多个报文段传输,因为 UDP 协议不会维护连接状态,所以我们没有办法确定那几个报文段属于同一个数据,UDP 只会将多余的数据给截取掉。为了解决这个问题,我们可以使用 TCP 协议去请求报文。
  3. DNS 还存在的一个问题是安全问题,就是我们没有办法确定我们得到的应答,一定是一个安全的应答,因为应答可以被他人伪造,所以现在有了 DNS over HTTPS 来解决这个问题。

DNS劫持

本DNS劫持是通过修改客户的DNS主服务器IP和备用服务器IP来达到劫持的效果,只劫持内容,域名不变,也可以跳转新的劫持,也就是说域名和内容都有变化,比如劫持了http://www.xxx1.com劫持到别人的网站,当别人访问http://www.xxx1.com的时候域名还是他原来的域名,但是内容确实我们劫持到的内容了,这个就是叫做只劫持内容其域名不变也可以跳转行的劫持,比如劫持了http://www.xxx2.com那么打开后域名也会跟着随之变化,这个就是叫做跳转性的劫持


DNS优化(CDN)

内容分发网络(Content Delivery Network),简而言之就是对用户请求通过中心平台服务器调度告诉用户当前请求资源距离用户最近,访问速度最快的缓存服务器地址,用户去请求该服务器获取资源。解决了源服务器服务过载网络拥塞(大量TCP连接过来)以及数据传输慢等问题。

CDN流程:

  1. 浏览器访问www.cdnNews.com/index.html
  2. 浏览器使用本地DNS服务对地址进行解析(DNS解析过程)
  3. DNS解析最后将该地址的权威DNS服务器地址返回给本地DNS服务器
  4. 由于访问的地址做了CDN处理,此时的权威DNS服务器其实就是该网站的CDN专用DNS服务器,该服务器通过负载均衡系统解析请求资源,找到对用户响应最快的缓存服务器IP地址返回
  5. 本地DNS服务器接受到CDN专用DNS服务器返回的IP地址交给浏览器。(DNS解析结束)
  6. 浏览器向该IP地址发送请求。
  7. 缓存服务器接收到浏览器发来的请求,先检查本地有没有该资源,有的话直接将资源返回给浏览器。
  8. 无该资源则缓存服务器向源服务器请求资源,获取资源缓存,并将资源返回给浏览器
    输入URL后到页面渲染 - 图22

CDN缓存:

  1. 浏览器向CDN缓存服务器发送数据请求
  2. CDN缓存服务器先检查本地数据有没有过期,没有直接返回改数据
  3. CDN缓存服务器发现本地缓存过期,则向源服务器请求数据,获取请求数据,本地缓存数据,并返回数据给浏览器。

📖相关知识点

CDN的缓存与回源机制解析

CDN (Content Delivery Network,即内容分发网络)指的是一组分布在各个地区的服务器。这些服务器存储着数据的副本,因此服务器可以根据哪些服务器与用户距离最近,来满足数据的请求。 CDN 提供快速服务,较少受高流量影响。

为什么要用 CDN

浏览器存储的相关知识此刻离我们还不太远,大家趁热回忆一下:缓存、本地存储带来的性能提升,是不是只能在“获取到资源并把它们存起来”这件事情发生之后?也就是说,首次请求资源的时候,这些招数都是救不了我们的。要提升首次请求的响应能力,我们还需要借助 CDN 的能力。

CDN的核心功能特写

CDN 的核心点有两个,一个是缓存,一个是回源

这两个概念都非常好理解。对标到上面描述的过程,“缓存”就是说我们把资源 copy 一份到 CDN 服务器上这个过程,“回源”就是说 CDN 发现自己没有这个资源(一般是缓存的数据过期了),转头向根服务器(或者它的上层服务器)去要这个资源的过程。

CDN 是怎么帮助前端的

CDN 往往被用来存放静态资源。上文中我们举例所提到的“根服务器”本质上是业务服务器,它的核心任务在于生成动态页面或返回非纯静态页面,这两种过程都是需要计算的。业务服务器仿佛一个车间,车间里运转的机器轰鸣着为我们产出所需的资源;相比之下,CDN 服务器则像一个仓库,它只充当资源的“栖息地”和“搬运工”。

所谓“静态资源”,就是像 JS、CSS、图片等不需要业务服务器进行计算即得的资源。而“动态资源”,顾名思义是需要后端实时动态生成的资源,较为常见的就是 JSP、ASP 或者依赖服务端渲染得到的 HTML 页面。

什么是“非纯静态资源”呢?它是指需要服务器在页面之外作额外计算的 HTML 页面。具体来说,当我打开某一网站之前,该网站需要通过权限认证等一系列手段确认我的身份、进而决定是否要把 HTML 页面呈现给我。这种情况下 HTML 确实是静态的,但它和业务服务器的操作耦合,我们把它丢到CDN 上显然是不合适的。

CDN 的实际应用

静态资源本身具有访问频率高、承接流量大的特点,因此静态资源加载速度始终是前端性能的一个非常关键的指标。CDN 是静态资源提速的重要手段,在许多一线的互联网公司,“静态资源走 CDN”并不是一个建议,而是一个规定。

CDN 优化细节

如何让 CDN 的效用最大化?这又是需要前后端程序员一起思考的庞大命题。它涉及到 CDN 服务器本身的性能优化、CDN 节点的地址选取等。CDN 的域名选取。

我们注意到淘宝业务服务器的域名是这个:

  1. www.taobao.com

而 CDN 服务器的域名是这个:

  1. g.alicdn.com

Cookie 是紧跟域名的。同一个域名下的所有请求,都会携带 Cookie。大家试想,如果我们此刻仅仅是请求一张图片或者一个 CSS 文件,我们也要携带一个 Cookie 跑来跑去(关键是 Cookie 里存储的信息我现在并不需要),这是一件多么劳民伤财的事情。Cookie 虽然小,请求却可以有很多,随着请求的叠加,这样的不必要的 Cookie 带来的开销将是无法想象的……

  • 同一个域名下的请求会不分青红皂白地携带 Cookie,而静态资源往往并不需要 Cookie 携带什么认证信息。把静态资源和主页面置于不同的域名下,完美地避免了不必要的 Cookie 的出现!
  • 看起来是一个不起眼的小细节,但带来的效用却是惊人的。以电商网站静态资源的流量之庞大,如果没把这个多余的 Cookie 拿下来,不仅用户体验会大打折扣,每年因性能浪费带来的经济开销也将是一个非常恐怖的数字。

谈谈 CDN 服务?

  1. CDN 是一个内容分发网络,通过对源网站资源的缓存,利用本身多台位于不同地域、不同运营商的服务器,向用户提供资就近访问的功能。也就是说,用户的请求并不是直接发送给源网站,而是发送给 CDN 服务器,由 CND 服务器将请求定位到最近的含有该资源的服务器上去请求。这样有利于提高网站的访问速度,同时通过这种方式也减轻了源服务器的访问压力。

什么是正向代理和反向代理?

  1. 我们常说的代理也就是指正向代理,正向代理的过程,它隐藏了真实的请求客户端,服务端不知道真实的客户端是谁,客户端请求的服务都被代理服务器代替来请求。
  2. 反向代理隐藏了真实的服务端,当我们请求一个网站的时候,背后可能有成千上万台服务器为我们服务,但具体是哪一台,我们不知道,也不需要知道,我们只需要知道反向代理服务器是谁就好了,反向代理服务器会帮我们把请求转发到真实的服务器那里去。反向代理器一般用来实现负载平衡。

负载平衡的两种实现方式?

  1. 一种是使用反向代理的方式,用户的请求都发送到反向代理服务上,然后由反向代理服务器来转发请求到真实的服务器上,以此来实现集群的负载平衡。
  2. 另一种是 DNS 的方式,DNS 可以用于在冗余的服务器上实现负载平衡。因为现在一般的大型网站使用多台服务器提供服务,因此一个域名可能会对应多个服务器地址。当用户向网站域名请求的时候,DNS 服务器返回这个域名所对应的服务器 IP 地址的集合,但在每个回答中,会循环这些 IP 地址的顺序,用户一般会选择排在前面的地址发送请求。以此将用户的请求均衡的分配到各个不同的服务器上,这样来实现负载均衡。这种方式有一个缺点就是,由于 DNS 服务器中存在缓存,所以有可能一个服务器出现故障后,域名解析仍然返回的是那个 IP 地址,就会造成访问的问题。

输入URL后到页面渲染 - 图23 建立TCP连接

获得ip地址时,就可以开始建立TCP连接了

输入URL后到页面渲染 - 图24

TCP 协议是面向连接的,提供可靠数据传输服务的传输层协议。

特点:

  1. TCP 协议是面向连接的,在通信双方进行通信前,需要通过三次握手建立连接。它需要在端系统中维护双方连接的状态信息。
  2. TCP 协议通过序号、确认号、定时重传、检验和等机制,来提供可靠的数据传输服务
  3. TCP 协议提供的是点对点的服务,即它是在单个发送方和单个接收方之间的连接。
  4. TCP 协议提供的是全双工的服务,也就是说连接的双方的能够向对方发送和接收数据。
  5. TCP 提供了拥塞控制机制,在网络拥塞的时候会控制发送数据的速率,有助于减少数据包的丢失和减轻网络中的拥塞程度。
  6. TCP 提供了流量控制机制,保证了通信双方的发送和接收速率相同。如果接收方可接收的缓存很小时,发送方会降低发送速率,避免因为缓存填满而造成的数据包的丢失。

TCP连接(三次握手)

  1. 第一次握手,客户端向服务器发送一个 SYN 连接请求报文段,报文段的首部中 SYN 标志位置为 1,序号字段是一个任选的随机数。它代表的是客户端数据的初始序号。
  2. 第二次握手,服务器端接收到客户端发送的 SYN 连接请求报文段后,服务器首先会为该连接分配 TCP 缓存和变量,然后向客户端发送 SYN ACK 报文段,报文段的首部中 SYN 和 ACK 标志位都被置为 1,代表这是一个对 SYN 连接请求的确认,同时序号字段是服务器端产生的一个任选的随机数,它代表的是服务器端数据的初始序号。确认号字段为客户端发送的序号加一。
  3. 第三次握手,客户端接收到服务器的肯定应答后,它也会为这次 TCP 连接分配缓存和变量,同时向服务器端发送一个对服务器端的报文段的确认。第三次握手可以在报文段中携带数据。

TCP 三次握手的建立连接的过程就是相互确认初始序号的过程,告诉对方,什么样序号的报文段能够被正确接收。第三次握手的作用是客户端对服务器端的初始序号的确认。如果只使用两次握手,那么服务器就没有办法知道自己的序号是否已被确认。同时这样也是为了防止失效的请求报文段被服务器接收,而出现错误的情况。

TCP Fast Open(两次握手)

TCP Fast Open 是一种简化 TCP 三次握手的方案

输入URL后到页面渲染 - 图25

如图 所示,TFO的的流程如下:

  1. 用户向Server发送SYN包并请求TFO Cookie;
  2. Server根据用户的IP加密生成Cookie,随SYN-ACK发给用户
  3. 用户储存TFO Cookie

当连接断掉,重连后的流程如下:

  1. 用户向Server发送SYN包(携带TCP Cookie),同时附带请求;
  2. Server校验Cookie(解密Cookie以及比对IP地址或者重新加密IP地址以和接收到的Cookie进行对比)。
    ``` 如果验证成功,向用户发送SYN+ACK,在用户回复ACK之前,便可以向用户传输数据;

如果验证失败,则丢弃此TFO请求携带的数据,回复SYN-ACK确认SYN Seq,完成正常的三次握手。

如果Cookie在网络传输的过程中被丢弃,Client在RTO后,发起普通的TCP连接

  1. 3. 如果在SYN包中的数据被接受,那么Server可以在收到ClientACK之前回复该请求的响应报文
  2. 4. ClientACKServerSYN。如果Client的数据没有得到ServerACK,那么Client会通过ACK重传该请求。
  3. 5. 随后的操作和普通的TCP连接一致
  4. <a name="2a17de48"></a>
  5. ## TCP可靠传输
  6. ARQ 协议指的是自动重传请求,它通过超时和重传来保证数据的可靠交付,它是 TCP 协议实现可靠数据传输的一个很重要的机制。
  7. <a name="79ae7ae8"></a>
  8. ### 可靠运输机制
  9. TCP 的可靠运输机制是基于**连续 ARQ 协议和滑动窗口协议**的。
  10. ![](https://s2.loli.net/2022/01/10/hpi78LrgejydRwM.png#crop=0&crop=0&crop=1&crop=1&id=deJPV&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
  11. 1. TCP 协议在发送方维持了一个发送窗口,发送窗口以前的报文段是已经发送并确认了的报文段,发送窗口中包含了已经发送但未确认的报文段和允许发送但还未发送的报文段,发送窗口以后的报文段是缓存中还不允许发送的报文段。
  12. 2. 当发送方向接收方发送报文时,会依次发送窗口内的所有报文段,并且设置一个定时器,这个定时器可以理解为是最早发送但未收到确认的报文段。如果在定时器的时间内收到某一个报文段的确认回答,则滑动窗口,将窗口的首部向后滑动到确认报文段的后一个位置,此时如果还有已发送但没有确认的报文段,则重新设置定时器,如果没有了则关闭定时器。如果定时器超时,则重新发送所有已经发送
  13. 3. 但还未收到确认的报文段,并将超时的间隔设置为以前的两倍。当发送方收到接收方的三个冗余的确认应答后,这是一种指示,说明该报文段以后的报文段很有可能发生丢失了,那么发送方会启用快速重传的机制,就是当前定时器结束前,发送所有的已发送但确认的报文段。
  14. 4. 接收方使用的是累计确认的机制,对于所有按序到达的报文段,接收方返回一个报文段的肯定回答。如果收到了一个乱序的报文段,那么接方会直接丢弃,并返回一个最近的按序到达的报文段的肯定回答。使用累计确认保证了返回的确认号之前的报文段都已经按序到达了,所以发送窗口可以移动到已确认报文段的后面。
  15. 5. 发送窗口的大小是变化的,它是由接收窗口剩余大小和网络中拥塞程度来决定的,TCP 就是通过控制发送窗口的长度来控制报文段的发送速率。
  16. 但是 TCP 协议并不完全和滑动窗口协议相同,因为许多的 TCP 实现会将失序的报文段给缓存起来,并且发生重传时,只会重传一个报文段,因此 TCP 协议的可靠传输机制更像是窗口滑动协议和选择重传协议的一个混合体。
  17. <a name="a525fc28"></a>
  18. ### 流量控制机制
  19. TCP 提供了流量控制的服务,这个服务的主要目的是控制发送方的发送速率,保证接收方来得及接收。因为一旦发送的速率大于接收方所能接收的速率,就会造成报文段的丢失。接收方主要是通过接收窗口来告诉发送方自己所能接收的大小,发送方根据接收方的接收窗口的大小来调整发送窗口的大小,以此来达到控制发送速率的目的。
  20. ![](https://s2.loli.net/2022/01/10/d5m49QLcabGlufD.png#crop=0&crop=0&crop=1&crop=1&id=OeMPZ&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
  21. <a name="2e993bb8"></a>
  22. ### 拥塞控制机制
  23. TCP 的拥塞控制主要是根据网络中的拥塞情况来控制发送方数据的发送速率,如果网络处于拥塞的状态,发送方就减小发送的速率,这样一方面是为了避免继续增加网络中的拥塞程度,另一方面也是为了避免网络拥塞可能造成的报文段丢失。
  24. ![](https://s2.loli.net/2022/01/20/xtoBhaYk4RfJSLE.jpg#crop=0&crop=0&crop=1&crop=1&id=stQtO&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
  25. TCP 的拥塞控制主要使用了四个机制,分别是**慢启动、拥塞避免、快速重传和快速恢复**。
  26. - **慢启动的**基本思想是,因为在发送方刚开始发送数据的时候,并不知道网络中的拥塞程度,所以先以较低的速率发送,进行试探,每次收到一个确认报文,就将发动窗口的长度加一,这样每个 RTT 时间后,发送窗口的长度就会加倍。当发送窗口的大小达到一个阈值的时候就进入拥塞避免算法。
  27. - **拥塞避免**算法是为了避免可能发生的拥塞,将发送窗口的大小由每过一个 RTT 增长一倍,变为每过一个 RTT ,长度只加一。这样将窗口的增长速率由指数增长,变为加法线性增长。
  28. - **快速重传**指的是,当发送方收到三个冗余的确认应答时,因为 TCP 使用的是累计确认的机制,所以很有可能是发生了报文段的丢失,因此采用立即重传的机制,在定时器结束前发送所有已发送但还未接收到确认应答的报文段。
  29. - **快速恢复**是对快速重传的后续处理,因为网络中可能已经出现了拥塞情况,所以会将慢启动的阀值减小为原来的一半,然后将拥塞窗口的值置为减半后的阀值,然后开始执行拥塞避免算法,使得拥塞窗口缓慢地加性增大。简单来理解就是,乘性减,加性增。TCP 认为网络拥塞的主要依据是报文段的重传次数,它会根据网络中的拥塞程度,通过调整慢启动的阀值,然后交替使用上面四种机制来达到拥塞控制的目的。
  30. <a name="5f4e6586"></a>
  31. ## TCP七种计时器
  32. <a name="5fe41d66"></a>
  33. ### 建立连接定时器
  34. 顾名思义,这个定时器是在建立连接的时候使用的, 我们知道, TCP建立连接需要3次握手, 如下图所示:<br />![](https://img-blog.csdn.net/20160801135530572#crop=0&crop=0&crop=1&crop=1&id=awgV6&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)<br />   建立连接的过程中,在发送SYN时, 会启动一个定时器(默认应该是3秒),如果SYN包丢失了, 那么3秒以后会重新发送SYN包的(当然还会启动一个新的定时器, 设置成6秒超时),当然也不会一直没完没了的发SYN包, 在/proc/sys/net/ipv4/tcp_syn_retries 可以设置到底要重新发送几次SYN包。
  35. <a name="0c76fc32"></a>
  36. ### 重传定时器
  37. 重传定时器在TCP发送数据时设定,在计时器超时后没有收到返回的确认ACK,发送端就会重新发送队列中需要重传的报文段。使用RTO重传计时器一般有如下规则:
  38. >
  39. > 1. TCP发送了位于发送队列最前端的报文段后就启动这个RTO计时器;
  40. > 2. 如果队列为空则停止计时器,否则重启计时器;
  41. > 3. 当计时器超时后,TCP会重传发送队列最前端的报文段;
  42. > 4. 当一个或者多个报文段被累计确认后,这个或者这些报文段会被清除出队列
  43. >
  44. 重传计时器保证了接收端能够接收到丢失的报文段,继而保证了接收端交付给接收进程的数据始终的有序完整的。因为接收端永远不会把一个失序不完整的报文段交付给接收进程。
  45. <a name="65a52fe7"></a>
  46. ### 延迟应答定时器
  47. 延迟应答也被成为捎带ACK 这个定时器是在延迟应答的时候使用的。 为什么要延迟应答呢? 延迟应答是为了提高网络传输的效率。
  48. 举例说明,比如服务端收到客户端的数据后, 不是立刻回ACK给客户端, 而是等一段时间(一般最大200ms),这样如果服务端要是有数据需要发给客户端,那么这个ACK就和服务端的数据一起发给客户端了, 这样比立即回给客户端一个ACK节省了一个数据包。
  49. <a name="12fea1e6"></a>
  50. ### 坚持定时器
  51. 我们已经知道TCP通过让接收方指明希望从发送方接收的数据字节数(即窗口大小)来进行流量控制。如果窗口大小为 0会发生什么情况呢?这将有效地阻止发送方传送数据,直到窗口变为非0为止。接收端窗口变为非0后,就会发送一个确认ACK指明需要的报文段序号以及窗口大小。
  52. 如果这个确认ACK丢失了,则双方就有可能因为等待对方而使连接终止:接收方等待接收数据(因为它已经向发送方通告了一个非0的窗口),而发送方在等待允许它继续发送数据的窗口更新。为防止这种死锁情况的发生,发送方使用一个坚持定时器 (persist timer)来周期性地向接收方查询,以便发现窗口是否已增大。这些从发送方发出的报文段称为窗口探查 (window probe)。
  53. <a name="ad5dcf57"></a>
  54. ### 保活定时器
  55. TCP连接建立的时候指定了SO_KEEPALIVE,保活定时器才会生效。如果客户端和服务端长时间没有数据交互,那么需要保活定时器来判断是否对端还活着,但是这个其实很不实用,因为默认是2小时没有数据交互才探测,时间实在是太长了。如果你真的要确认对端是否活着, 那么应该自己实现心跳包,而不是依赖于这个保活定时器。
  56. <a name="e7d91726"></a>
  57. ### FIN_WAIT_2定时器
  58. 主动关闭的一端调用完close以后(即发FIN给被动关闭的一端, 并且收到其对FIN的确认ACK)则进入FIN_WAIT_2状态。如果这个时候因为网络突然断掉、被动关闭的一段宕机等原因,导致主动关闭的一端不能收到被动关闭的一端发来的FIN,主动关闭的一段总不能一直傻等着,占着资源不撒手吧?这个时候就需要FIN_WAIT_2定时器出马了, 如果在该定时器超时的时候,还是没收到被动关闭一端发来的FIN,那么不好意思, 不等了, 直接释放这个链接。FIN_WAIT_2定时器的时间可以从/proc/sys/net/ipv4/tcp_fin_timeout中查看和设置。
  59. <a name="4af0425f"></a>
  60. ### TIME_WAIT定时器
  61. TIME_WAIT是主动关闭连接的一端最后进入的状态, 而不是直接变成CLOSED的状态, 为什么呢?第一个原因是万一被动关闭的一端在超时时间内没有收到最后一个ACK 则会重发最后的FIN2MSL(报文段最大生存时间)等待时间保证了重发的FIN会被主动关闭的一段收到且重新发送最后一个ACK;另外一个原因是在2MSL等待时间时,任何迟到的报文段会被接收并丢弃,防止老的TCP连接的包在新的TCP连接里面出现。不可避免的,在这个2MSL等待时间内,不会建立同样(源IP, 源端口,目的IP,目的端口)的连接。
  62. ---
  63. <a name="2dadff0d-1"></a>
  64. ## **📖相关知识点**
  65. <a name="93c50b43"></a>
  66. ### TCP粘包
  67. 粘包:发送方发送两个字符串”hello”+”world”,接收方却一次性接收到了”helloworld”.
  68. > 有时候,TCP为了提高网络的利用率,会使用一个叫做Nagle的算法.该算法是指,发送端即使有要发送的数据,如果很少的话,会延迟发送.如果应用层给TCP传送数据很快的话,就会把两个应用层数据包“粘”在一起,TCP最后只发一个TCP数据包给接收端.
  69. 分包:发送方发送字符串”helloworld”,接收方却接收到了两个字符串”hello”和”world”.
  70. > TCP是以段(Segment)为单位发送数据的,建立TCP链接后,有一个最大消息长度(MSS).如果应用层数据包超过MSS,就会把应用层数据包拆分,分成两个段来发送.这个时候接收端的应用层就要拼接这两个TCP包,才能正确处理数据.
  71. <a name="2e9466e6"></a>
  72. ### 为什么连接的时候是三次握手,关闭的时候却是四次握手?
  73. 因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
  74. <a name="327632f9"></a>
  75. ### 大量time_wait产生原因及解决办法
  76. 原因:爬虫服务器会产生大量的time_wait状态
  77. 解决:让**服务器能够快速回收和重用那些TIME_WAIT的资源。**
  78. <a name="62552f57"></a>
  79. ### TCP协议如何来保证传输的可靠性
  80. - 数据包校验和:发送之前记性计算校验和,接收方收到之后也计算是不是一样的
  81. - 确认应答和序列号
  82. - 丢弃重复数据:对于重复数据丢弃
  83. - 超时重发:TCP发出一个报文后启动一个定时器,等待目的端确认收到这个报文。如果不能及时收到确认,将重发这个报文。
  84. 1. 校验和
  85. 2. 序列号与确认应答
  86. 3. 丢弃重复数据
  87. 4. 重发控制(超时重传)
  88. 5. TCP将对收到的数据进行重新排序
  89. 6. 流量控制、拥塞控制使得网络不好的时候丢包的概率大大减小
  90. <a name="df5b7d44"></a>
  91. ### 三次握手中,第二次握手丢失如何处理
  92. 三次握手协议中,服务器维护一个**未连接队列**,该队列为每个客户端的SYN包开设一个条目,该条目表明服务器已收到SYN包,并向客户发出确认,正在等待客户的确认包。这些条目所标识的连接在服务器处于 SYN_RECV状态,当服务器收到客户的确认包时,删除该条目,服务器进入ESTABLISHED状态。
  93. 服务器发送完SYNACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传,如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。
  94. **半连接存活时间**是指半连接队列的条目存活的最长时间,也即服务器从收到SYN包到确认这个报文无效的最长时间,该时间值是所有重传请求包的最长等待时间总和。有时我们也称半连接存活时间为Timeout时间、SYN_RECV存活时间。
  95. <a name="664748b7"></a>
  96. ### SYN攻击
  97. 概念:伪造大量TCP连接请求,使得被攻击方资源耗尽;攻击客户端在短时间内伪造大量不存在的IP地址,向服务器不断地发送SYN包,服务器回复确认包,并等待客户的确认并等待一段时间之后丢弃这个未完成的连接。由于源地址是不存在的,服务器需要不断的重发直至超时,这些伪造的SYN包将长时间占用未连接队列,正常的SYN请求被丢弃。
  98. 解决办法:
  99. 1. 缩短超时(SYN Timeout)时间
  100. 2. SYNCOOKIE技术:每个请求连接的IP地址分配一个Cookie,如果短时间内收到某个IP重复SYN报文,认定收到了攻击,这个IP地址来的包都被丢弃。
  101. <a name="283b5221"></a>
  102. ### TCP与UDP的区别
  103. 1. TCP是面向连接的,UDP是无连接的,所以udp发送的快一些
  104. 2. TCP可靠,UDP不可靠,udp尽最大努力交付。
  105. 3. TCP支持点对点通信、UDP支持一对一、一对多、多对一、多对多的通信模式
  106. 4. TCP面向字节流,UDP面向报文
  107. 5. TCP可以拥塞控制,UDP没有,所以udp可以更好控制发送时间和发送速度
  108. 6. TCP首部(20个字节)比UDP首部(8字节)大
  109. 7. tcp是全双工的,TCP可靠UDP不可靠,应用不同,udp应用于高速传输和实时性要求较高的通信
  110. <a name="ae80a64b"></a>
  111. ### **TCP为什么会分包**
  112. TCP是以段(Segment)为单位发送数据的,建立TCP链接后,有一个最大消息长度(MSS).如果应用层数据包超过MSS,就会把应用层数据包拆分,分成两个段来发送.这个时候接收端的应用层就要拼接这两个TCP包,才能正确处理数据.
  113. <a name="7b73b0bf"></a>
  114. ### **TCP为什么会粘包**
  115. - **发送方产生的粘包问题**
  116. 有时候,TCP为了提高网络的利用率,会使用一个叫做Nagle的算法.该算法是指,发送端即使有要发送的数据,如果很少的话,会延迟发送.如果应用层给TCP传送数据很快的话,就会把两个应用层数据包“粘”在一起,TCP最后只发一个TCP数据包给接收端.
  117. - 收方产生的粘包问题**
  118. 接收方用户进程不及时接收数据,从而导致粘包现象
  119. **如何处理**
  120. 1. 特殊字符控制
  121. 2. 在包头首都添加数据包的长度。
  122. 3. 关闭nagle算法
  123. > tipsUDP 没有粘包问题,但是有丢包和乱序。不完整的包是不会有的,收到的都是完全正确的包。传送的数据单位协议是 UDP 报文或用户数据报,发送的时候既不合并,也不拆分。
  124. ---
  125. <a name="ace9ef94"></a>
  126. # ![](https://gw.alipayobjects.com/os/lib/twemoji/11.2.0/2/svg/35-20e3.svg#crop=0&crop=0&crop=1&crop=1&id=byiT4&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=&width=18) HTTP系列请求
  127. TCP连接后发起HTTP请求或者HTTPS请求
  128. <a name="669262cd"></a>
  129. ## HTTP请求
  130. 超文本传输协议,定义了客户端和服务器之间交换报文的格式和方式,默认用80端口,使用**TCP**作为传输层协议,保证了数据传输的可靠性。
  131. `HTTP` 是一种**无状态** (stateless) 协议, `HTTP`协议本身不会对发送过的请求和相应的通信状态进行持久化处理。这样做的目的是为了保持HTTP协议的简单性,从而能够快速处理大量的事务, 提高效率。
  132. 然而,在许多应用场景中,我们需要保持用户登录的状态或记录用户购物车中的商品。由于`HTTP`是无状态协议,所以必须引入一些技术来记录管理状态,例如`Cookie`
  133. <a name="c568c1e6"></a>
  134. ### 请求报文
  135. ![](https://s2.loli.net/2022/01/09/Jt5g8xUN2Hj3vYl.png#crop=0&crop=0&crop=1&crop=1&id=btSLS&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
  136. ```http
  137. GET / HTTP/1.1
  138. User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)
  139. Accept: */*

HTTP 请求报文的第一行叫做请求行,后面的行叫做首部行,首部行后还可以跟一个实体主体。请求首部之后有一个空行,这
个空行不能省略,它用来划分首部与实体。

请求行包含三个字段:方法字段、URL 字段和 HTTP 版本字段。

方法字段可以取几种不同的值,一般有 GET、POST、HEAD、PUT 和 DELETE。

  • GET 方法只被用于向服务器获取数据
  • POST 方法用于将实体提交到指定的资源,通常会造成服务器资源的修改
  • HEAD 方法与 GET 方法类似,但是在返回的响应中,不包含请求对象
  • PUT 方法用于上传文件到服务器
  • DELETE 方法用于删除服务器上的对象。虽然请求的方法很多,但更多表达的是一种语义上的区别,并不是说 POST 能做的事情,GET 就不能做了,主要看我们如何选择

首部行

首部可以分为四种首部,请求首部、响应首部、通用首部和实体首部。通用首部和实体首部在请求报文和响应报文中都可以设置,区别在于请求首部和响应首部。

常见的请求首部有

  • Accept 可接收媒体资源的类型
  • Accept-Charset 可接收的字符集
  • Host 请求的主机名

常见的响应首部有

  • ETag 资源的匹配信息
  • Location 客户端重定向的 URI

常见的通用首部有

  • Cache-Control 控制缓存策略
  • Connection 管理持久连接。

常见的实体首部有

  • Content-Length 实体主体的大小
  • Expires 实体主体的过期时间
  • Last-Modified 资源的最后修改时间。

HTTP/1

1996年发布,明文传输安全性差,header特别大。它相对0.9有以下增强:

  • 增加了header(使用元数据与数据解耦)
  • 增加了status code,用于声明请求的结果。
  • content-type可以传输其它文件。
  • 请求头增加了http/1.0版本号

缺点:每请求一次资源就新建一次tcp连接

HTTP/1.1

1997发布,是现在使用最广泛的版本。它相对1.0有以下增强:

  • 可以设置 keepalive 让 http 重用tcp连接(请求必需串行发送)
  • 支持pipeline传输,请求发出后可以继续发送请求
  • 增加了HOST头,让服务端知道用户请求的是哪个域名
  • 增加了type、language、encoding等header

2014年更新了内容:

  • 增加了TLS支持,即https传输
  • 支持四种模型: 短连接,可重用tcp的长链接,服务端push模型(服务端主动将数据推送到客户端cache中),websocket模型
    输入URL后到页面渲染 - 图26

缺点:还是文本协议,客户端服务端都需要利用cpu解压缩

HTTP/2

2015年发布,主要是提升安全性与性能。它相对1.1的增强有:

  • 头部压缩(合并同时发出请求的相同部分)
  • 二进制分帧传输,更方便头部只传输差异部分
  • 流多路复用,同一服务下只需要用一个连接,节省了连接
  • 服务器推送,一次客户端请求服务端可以多次响应。
  • 可以在一个tcp连接中并发发送请求

缺点:基于tcp传输,会有队头阻塞问题(丢包停止窗口滑动),tcp会丢包重传。tcp握手延时长,协议僵化问题。

因为 HTTP/2 使用了多路复用,一般来说同一域名下只需要使用一个 TCP 连接。由于多个数据流使用同一个 TCP 连接,遵守同一个流量状态控制和拥塞控制。只要一个数据流遭遇到拥塞,剩下的数据流就没法发出去,这样就导致了后面的所有数据都会被阻塞。HTTP/2 出现的这个问题是由于其使用 TCP 协议的问题,与它本身的实现其实并没有多大关系。

特点

  • 二进制协议
    HTTP2是一个二进制协议,在1.1的版本中,报文头信息必须是文本(ASCII编码),数据体是文本或者二进制;但在2.0版本中头信息和数据体都是二进制,并统称为 ,分为头信息帧与数据帧;以实现多路复用
  • 多路复用
    在一个TCP连接里,客户端和服务器都可以同时发送多个请求或回应,而且不用按照顺序一一发送,避免队头堵塞问题
  • 数据流
    数据包是不按顺序发送的,同一个连接里面的数据包,可能属于不同请求,因此,必须要对数据包做标记,指名属于哪个请求。2版本将每个请求或回应的所有数据包,成为数据流,数据包发送的时候,都必须标记数据流 ID ,用来区分它属于哪个数据流。
  • 头信息压缩
    HTTP/2 实现了头信息压缩,由于 HTTP 1.1 协议不带有状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重复的,比如 Cookie 和 User Agent ,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。
    HTTP/2 对这一点做了优化,引入了头信息压缩机制。一方面,头信息使用 gzip 或 compress 压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就能提高速度了。
  • 服务器推送
    HTTP/2 允许服务器未经请求,主动向客户端发送资源,这叫做服务器推送。使用服务器推送,提前给客户端推送必要的资源,这样就可以相对减少一些延迟时间。这里需要注意的是 http2 下服务器主动推送的是静态资源,和 WebSocket 以及使用SSE 等方式向客户端发送即时数据的推送是不同的。

Cookie

cookie是服务端发送给客户端一段文本信息,客户端保存起来,下一次在访问该服务端,客户端自动带上这段文本信息发送给服务端。

cookie用途:http协议是无状态的,第一次请求与第二次请求服务器并不知道是同一个客户端发送的请求,但如果第二次请求带上了第一次请求服务器端返回的cookie那么服务端就知道这两次请求是来自同一个客户端。(比如某个网站的第二次登录就不需要手动登录,直接携带cookie发送给服务器,服务器验证cookie信息正确即可)

cookie特性 :

  • 请求自动携带cookie:简而言之cookie在浏览器发出请求会自动携带一并发出
  • 不可跨域名性:该特性由浏览器维护,浏览器会根据请求地址确定携带哪一个域名下的cookie。
    • 二级域名问题:比如a.com与b.a.com,如果期望一级域名a下二级域名b也可以携带a.com的cookie,可以设置cookie.SetDomain(‘.a.com’),这样a.com下的二级域名就可以使用该cookie
    • 路径问题:比如www.a.com/file路径访问是无法携带www.a.com的cookie的,如果我们期望www.a.com/file可以携带www.a.com的cookie,可以设置cookie.setPath(/file/),如果www.a.com+任意路径都可以携带该cookie,则设置cookie.setPath(/)

cookie属性:

  • path&domain:指定域名domain+路径path,浏览器请求url与该指定url相同时才携带cookie。
  • expires:expires服务器指定cookie过期时间戳,到期浏览器不再保存该cookie。
  • max-age:max-age指定cookie存活多少秒,过期浏览器不再保存该cookie。与expires同时出现以max-age为准。如果二者都没出现,那么本次cookie仅在本次对话存在,浏览器窗口关闭,则浏览器不再保存此cookie
  • HttpOnly:服务器指定该字段,则浏览器不能使用document.cookie读取cookie。
  • secure:该属性是个标记没有值,包含secure的cookie只有当https或其他安全协议时浏览器才会携带cookie,如果希望在浏览器端设置该字段,需确保网页为https协议,http是无法设置该字段的。
  • sameSite:该字段有两个值strict与lax,默认strict。
    • strict:不允许跨站发送cookie,只允许当前网站发送cookie。
    • lax:允许跨站请求携带,但只能是get,如果是post,则不会携带cookie。

服务端设置cookie:

  1. ctx.set('Set-cookie', 'name=value;path=/user;domain=localhost;max-age=30; HttpOnly; SameSite=Strict')

浏览器操作cookie:

  • 读cookie:var cookie_ = document.cookie
  • 新建cookie:document.cookie = ‘name1 = value1;path:/;domain:localhost;max-age=30’
  • 替换cookie:使用新建cookie方式保持同名即可替换原有cookie。document.cookie = ‘name1 = value2;path:/;domain:localhost;max-age=30’
  • 删除cookie:删除name3的cookie:document.cookie = ‘name3=value3;max-age=0’,设置cookie的max-age为0即可删除,设置max-age<0则cookie仅存于当前会话,浏览器窗口关闭,cookie消失。

cookie预防XSS与CRSF

  • cookie设置HttpOnly,禁止客户端使用document.cookie访问cookie,预防xss攻击获取cookie。
  • cookie设置sameSite:strict,禁止所有第三方网站携带cookie访问服务器,预防CRSF攻击。

Session

保存在服务端用来记录客户端信息的一块数据。与cookie一样也是为了解决http无状态的特点。

session生成流程:

  1. 客户端第一次访问服务端,服务端为客户端生成一个session与sessionID,session保存客户端信息,sessionID标识session存储位置,并将sessionID返回给客户端(一般放在cookie中)
  2. 客户端下一次访问该服务端会携带cookie
  3. 服务端接受到客户端再一次访问,获取cookie中的sessionID,查找存储中对应的session,获取当前客户端信息。

与cookie区别:

  • session依靠cookie实现,sessionID存放在cookie中(禁用可以使用url重写完成,放在url中)
  • cookie保存在客户端,session保存在服务端
  • cookie一般4kb大小,session无大小限制

📖相关知识点

队头堵塞

HTTP1.1 默认使用持久连接,多个请求可以复用同一个TCP连接,但是在同一个TCP连接里面,数据请求的通讯次序是固定的。服务器只有处理完一个请求连接后,才会进行下一个请求处理,如果前面请求的响应特别慢的话,就会造成许多请求排队等待的情况,这种情况被称为 队头堵塞 。队头堵塞会导致持久连接在到达最大数量时,剩余的资源需要等待其他资源请求完成后才能发起请求。

如何避免队头堵塞

  1. 一个是减少请求数,一个是同时打开多个持久连接。这就是我们对网站优化时,使用雪碧图、合并脚本的原因。

Post 和 Get 的区别?

  1. Post Get HTTP 请求的两种方法。
  2. 1)从应用场景上来说,GET 请求是一个幂等的请求,一般 Get 请求用于对服务器资源不会产生影响的场景,比如说请求一个网页。而 Post 不是一个幂等的请求,一般用于对服务器资源会产生影响的情景。比如注册用户这一类的操作。
  3. 2)因为不同的应用场景,所以浏览器一般会对 Get 请求缓存,但很少对 Post 请求缓存。
  4. 3)从发送的报文格式来说,Get 请求的报文中实体部分为空,Post 请求的报文中实体部分一般为向服务器发送的数据。
  5. 4)但是 Get 请求也可以将请求的参数放入 url 中向服务器发送,这样的做法相对于 Post 请求来说,一个方面是不太安全,因为请求的 url 会被保留在历史记录中。并且浏览器由于对 url 有一个长度上的限制,所以会影响 get 请求发送数据时的长度。这个限制是浏览器规定的,并不是 RFC 规定的。还有就是 post 的参数传递支持更多的数据类型。

http 请求方法 options 方法有什么用?

  1. OPTIONS 请求与 HEAD 类似,一般也是用于客户端查看服务器的性能。这个方法会请求服务器返回该资源所支持的所有 HTTP 请求方法,该方法会用'*'来代替资源名称,向服务器发送 OPTIONS 请求,可以测试服务器功能是否正常。JS XMLHttpRequest 对象进行 CORS 跨域资源共享时,对于复杂请求,就是使用 OPTIONS 方法发送嗅探请求,以判断是否有对指定资源的访问权限。

http1.1 和 http1.0 的区别?

  1. http1.1 相对于 http1.0 有这样几个区别:
  2. 1)连接方面的区别,http1.1 默认使用持久连接,而 http1.0 默认使用非持久连接。http1.1 通过使用持久连接来使多个 http 请求复用同一个 TCP 连接,以此来避免使用非持久连接时每次需要建立连接的时延。
  3. 2)资源请求方面的区别,在 http1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,http1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。
  4. 3)缓存方面的区别,在 http1.0 中主要使用 header 里的 If-Modified-Since,Expires 来做为缓存判断的标准,http1.1 则引入了更多的缓存控制策略例如 EtagIf-Unmodified-SinceIf-MatchIf-None-Match 等更多可供选择的缓存头来控制缓存策略。
  5. 4http1.1 中还新增了 host 字段,用来指定服务器的域名。http1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此,请求消息中的 URL 并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机,并且它们共享一个IP地址。因此有了 host 字段,就可以将请求发往同一台服务器上的不同网站。
  6. 5http1.1 相对于 http1.0 还新增了很多方法,如 PUTHEADOPTIONS 等。

即时通讯的实现,短轮询、长轮询、SSE 和 WebSocket 间的区别?

  1. 短轮询和长轮询的目的都是用于实现客户端和服务器端的一个即时通讯。
  2. 短轮询的基本思路就是浏览器每隔一段时间向浏览器发送 http 请求,服务器端在收到请求后,不论是否有数据更新,都直接进行响应。这种方式实现的即时通信,本质上还是浏览器发送请求,服务器接受请求的一个过程,通过让客户端不断的进行请求,使得客户端能够模拟实时地收到服务器端的数据的变化。这种方式的优点是比较简单,易于理解。缺点是这种方式由于需要不断的建立 http 连接,严重浪费了服务器端和客户端的资源。当用户增加时,服务器端的压力就会变大,这是很不合理的。
  3. 长轮询的基本思路是,首先由客户端向服务器发起请求,当服务器收到客户端发来的请求后,服务器端不会直接进行响应,而是先将这个请求挂起,然后判断服务器端数据是否有更新。如果有更新,则进行响应,如果一直没有数据,则到达一定的时间限制才返回。客户端 JavaScript 响应处理函数会在处理完服务器返回的信息后,再次发出请求,重新建立连接。长轮询和短轮询比起来,它的优点是明显减少了很多不必要的 http 请求次数,相比之下节约了资源。长轮询的缺点在于,连接挂起也会导致资源的浪费。
  4. SSE 的基本思想是,服务器使用流信息向服务器推送信息。严格地说,http 协议无法做到服务器主动推送信息。但是,有一种变通方法,就是服务器向客户端声明,接下来要发送的是流信息。也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 http 协议,目前除了 IE/Edge,其他浏览器都支持。它相对于前面两种方式来说,不需要建立过多的 http 请求,相比之下节约了资源。
  5. 上面三种方式本质上都是基于 http 协议的,我们还可以使用 WebSocket 协议来实现。WebSocket Html5 定义的一个新协议,与传统的 http 协议不同,该协议允许由服务器主动的向客户端推送信息。使用 WebSocket 协议的缺点是在服务器端的配置比较复杂。WebSocket 是一个全双工的协议,也就是通信双方是平等的,可以相互发送消息,而 SSE 的方式是单向通信的,只能由服务器端向客户端推送信息,如果客户端需要发送信息就是属于下一个 http 请求了。

http2如何确保文件传输不会出错

  1. 根据前面介绍的http2帧概念流概念以及多路传输特性,我们知道虽然一个tcp连接内有多个很多帧在传输且属于不同的请求,但是帧头有Stream Identifier字段帮助我们判断当前帧属于哪个流,一个流属于一组请求-回复数据交互过程,所以再接收端按照帧头的Stream Identifier进行帧的组合即可得到原先的完整请求。

什么是http协议,http工作流程

http协议:超文本传输协议(HTTP Hypertext transfer protocol)是基于应用层的协议,规定了浏览器与服务器之间相互通讯的规则。

http工作流程:

  • 浏览器发出http请求报文
  • 建立TCP连接(三次握手)
  • 传输http请求
  • 服务器响应http请求
  • 释放TCP连接(四次挥手)

实现断点续传的原理

  • http1.1推出了range字段,以提供客户端对服务器请求特定部分资源的能力,利用该字段就可以实现断点续传,当然前提是服务器支持range分块传输。在连接建立后,客户端维护当前收到的资源大小,当连接或传输因某些原因中断时,客户端会再次向服务器发起资源请求,在这时的请求报文中range字段写入已收到的资源大小。服务器收到该请求报文后解析range字段就知道需要发给客户端的是哪部分内容,并在响应报文的content-range字段写明是报文实体是哪部分的资源。这样就实现了断点续传。需要注意的是,如果服务器支持断点续传,那么在续传的响应报文状态码会是**206**而不是**200**,如果服务器不支持断点续传也就是**range**字段,那么将会重新返回整个资源并返回**200**状态码。
  • 在这个过程中,如果在客户端发起断点续传时服务器的该资源已经发生改动,那么响应要发生响应变化。一般通过last-modified字段或者eTag字段来确定一个资源有没有被修改。

cookie和localSrorage、session、indexDB

特性 cookie localStorage sessionStorage indexDB
数据生命周期 一般由服务器生成,可以设置过期时间 除非被清理,否则一直存在 页面关闭就清理 除非被清理,否则一直存在
数据存储大小 4K 5M 5M 无限
与服务端通信 每次都会携带在 header 中,对于请求性能影响 不参与 不参与 不参与
作用域 只在同源的同窗口(或标签页)中共享数据,也就是只在当前会话中共享。 在所有同源窗口中都是共享的。 在所有同源窗口中都是共享的。
易用性 需要程序员自己封装,源生的Cookie接口不友好 源生接口可以接受,亦可再次封装来对Object和Array有更好的支持 源生接口可以接受,亦可再次封装来对Object和Array有更好的支持 可以直接存储任何 js 数据,包括blob(其实是支持结构化克隆
的数据)。 可以创建索引,提供高性能的搜索功能! 采用事务,保证数据的准确性和一致性。

从上表可以看到,cookie 已经不建议用于存储。如果没有大量数据存储需求的话,可以使用 localStoragesessionStorage 。对于不怎么改变的数据尽量使用 localStorage 存储,否则可以用 sessionStorage 存储。

  • localStorage 是同步,阻塞的存储机制,
  • IndexDB 是异步的.

对于 cookie,我们还需要注意安全性

属性 作用
value 如果用于保存用户登录态,应该将该值加密,不能使用明文的用户标识
http-only 不能通过 JS
访问 Cookie
,减少 XSS
攻击
secure 只能在协议为 HTTPS
的请求中携带
same-site 规定浏览器不能在跨域请求中携带 Cookie
,减少 CSRF
攻击

HTTPS请求

HTTPS 指的是超文本传输安全协议,HTTPS 是基于 HTTP 协议的,不过它会使用 TLS/SSL 来对数据加密。使用 TLS/SSL 协议,所有的信息都是加密的,第三方没有办法窃听。并且它提供了一种校验机制,信息一旦被篡改,通信的双方会立刻发现。它还配备了身份证书,防止身份被冒充的情况出现。

HTTP 存在的问题

  1. HTTP 报文使用明文方式发送,可能被第三方窃听。
  2. HTTP 报文可能被第三方截取后修改通信内容,接收方没有办法发现报文内容的修改。
  3. HTTP 还存在认证的问题,第三方可以冒充他人参与通信。

TLS/SSL 四次握手

输入URL后到页面渲染 - 图27

  1. 第一步,客户端向服务器发起请求,请求中包含使用的协议版本号、生成的一个随机数、以及客户端支持的加密方法。
  2. 第二步,服务器端接收到请求后,确认双方使用的加密方法、并给出服务器的证书、以及一个服务器生成的随机数。
  3. 第三步,客户端确认服务器证书有效后,生成一个新的随机数,并使用数字证书中的公钥,加密这个随机数,然后发给服务器。并且还会提供一个前面所有内容的 hash 的值,用来供服务器检验。
  4. 第四步,服务器使用自己的私钥,来解密客户端发送过来的随机数。并提供前面所有内容的 hash 值来供客户端检验。
  5. 第五步,客户端和服务器端根据约定的加密方法使用前面的三个随机数,生成对话秘钥,以后的对话过程都使用这个秘钥来加密信息。

加密方式

由对称加密获取非对称加密的密钥

对称加密的方法

首先是对称加密的方法,对称加密的方法是,双方使用同一个秘钥对数据进行加密和解密。但是对称加密的存在一个问题,就是如何保证秘钥传输的安全性,因为秘钥还是会通过网络传输的,一旦秘钥被其他人获取到,那么整个加密过程就毫无作用了。这就要用到非对称加密的方法。

非对称加密的方法

非对称加密的方法是,我们拥有两个秘钥,一个是公钥,一个是私钥。公钥是公开的,私钥是保密的。用私钥加密的数据,只有对应的公钥才能解密,用公钥加密的数据,只有对应的私钥才能解密。我们可以将公钥公布出去,任何想和我们通信的客户,都可以使用我们提供的公钥对数据进行加密,这样我们就可以使用私钥进行解密,这样就能保证数据的安全了。但是非对称加密有一个缺点就是加密的过程很慢,因此如果每次通信都使用非对称加密的方式的话,反而会造成等待时间过长的问题。

数字证书与签名

数字签名是使用数字证书与信息加密技术、用于鉴别电子数据信息的技术,可通俗理解为加盖在电子文件上的“数字指纹”。

数字证书是由权威公证的第三方认证机构(即CA,Certificate Authority)负责签发和管理的、个人或企业的网络数字身份证明。

数字签名是用数字证书对电子文件签名后在电子文件上保留的签署结果,用以证明签署人的签署意愿。所以数字证书是数字签名的基础,数字签名是数字证书的一种应用结果。


📖相关知识点

TLS/SSL 中什么一定要用三个随机数,来生成”会话密钥”?

  1. 客户端和服务器都需要生成随机数,以此来保证每次生成的秘钥都不相同。使用三个随机数,是因为 SSL 的协议默认不信任每个主机都能产生完全随机的数,如果只使用一个伪随机的数来生成秘钥,就很容易被破解。通过使用三个随机数的方式,增加了自由度,一个伪随机可能被破解,但是三个伪随机就很接近于随机了,因此可以使用这种方法来保持生成秘钥的随机性和安全性。

SSL 连接断开后如何恢复?

  1. 一共有两种方法来恢复断开的 SSL 连接,一种是使用 session ID,一种是 session ticket
  2. 使用 session ID 的方式,每一次的会话都有一个编号,当对话中断后,下一次重新连接时,只要客户端给出这个编号,服务器如果有这个编号的记录,那么双方就可以继续使用以前的秘钥,而不用重新生成一把。目前所有的浏览器都支持这一种方法。但是这种方法有一个缺点是,session ID 只能够存在一台服务器上,如果我们的请求通过负载平衡被转移到了其他的服务器上,那么就无法恢复对话。
  3. 另一种方式是 session ticket 的方式,session ticket 是服务器在上一次对话中发送给客户的,这个 ticket 是加密的,只有服务器能够解密,里面包含了本次会话的信息,比如对话秘钥和加密方法等。这样不管我们的请求是否转移到其他的服务器上,当服务器将 ticket 解密以后,就能够获取上次对话的信息,就不用重新生成对话秘钥了。

RSA算法的安全性保障?

  1. 对极大整数做因数分解的难度决定了 RSA 算法的可靠性。换言之,对一极大整数做因数分解愈困难,RSA 算法愈可靠。现在1024位的 RSA 密钥基本安全,2048位的密钥极其安全。

HTTP和HTTPS的区别,以及HTTPS有什么缺点

Http协议运行在TCP之上,明文传输,客户端与服务器端都无法验证对方的身份;Https是身披SSL(Secure Socket Layer)外壳的Http,运行于SSL上,SSL运行于TCP之上,是添加了加密和认证机制的HTTP。

区别:

  • HTTPS是加密传输协议,HTTP是名文传输协议,存在风险;
  • HTTP使用80端口 HTTPS使用的是443端口。
  • https通信需要SSL证书,证书一般需要向认证机构购买,http不需要
  • HTTP工作于应用层,而HTTPS 的安全传输机制工作在传输层

HTTPS 的优缺点?

优点:

  1. 使用 HTTPS 协议可认证用户和服务器,确保数据发送到正确的客户机和服务器;
  2. HTTPS 协议是由 SSL + HTTP 协议构建的可进行加密传输、身份认证的网络协议,要比 HTTP 协议安全,可防止数据在传输过程中不被窃取、改变,确保数据的完整性;
  3. HTTPS 是现行架构下最安全的解决方案,虽然不是绝对安全,但它大幅增加了中间人攻击的成本。

缺点:

  1. HTTPS 协议握手阶段比较费时,会使页面的加载时间延长近 50%,增加 10% 到 20% 的耗电;
  2. HTTPS 连接缓存不如 HTTP 高效,会增加数据开销和功耗,甚至已有的安全措施也会因此而受到影响;
  3. SSL 证书需要钱,功能越强大的证书费用越高,个人网站、小网站没有必要一般不会用;
  4. SSL 证书通常需要绑定 IP,不能在同一 IP 上绑定多个域名,IPv4 资源不可能支撑这个消耗;
  5. HTTPS 协议的加密范围也比较有限,在黑客攻击、拒绝服务攻击、服务器劫持等方面几乎起不到什么作用。最关键的,SSL 证书的信用链体系并不安全,特别是在某些国家可以控制 CA 根证书的情况下,中间人攻击一样可行。

HTTP3.0请求

若使用HTTP3.0版本请求,便无需签名的TCP三次握手连接

2018年发布,基于谷歌的QUIC,底层使用udp代码tcp协议,

这样解决了队头阻塞问题,同样无需握手,性能大大地提升,默认使用tls加密。

输入URL后到页面渲染 - 图28

零 RTT 建立连接

Step1:首次连接时,客户端发送 Inchoate Client Hello 给服务端,用于请求连接;

Step2:服务端生成 g、p、a,根据 g、p 和 a 算出 A,然后将 g、p、A 放到 Server Config 中再发送 Rejection 消息给客户端;

Step3:客户端接收到 g、p、A 后,自己再生成 b,根据 g、p、b 算出 B,根据 A、p、b 算出初始密钥 K。B 和 K 算好后,客户端会用 K 加密 HTTP 数据,连同 B 一起发送给服务端;

Step4:服务端接收到 B 后,根据 a、p、B 生成与客户端同样的密钥,再用这密钥解密收到的 HTTP 数据。为了进一步的安全(前向安全性),服务端会更新自己的随机数 a 和公钥,再生成新的密钥 S,然后把公钥通过 Server Hello 发送给客户端。连同 Server Hello 消息,还有 HTTP 返回数据;

Step5:客户端收到 Server Hello 后,生成与服务端一致的新密钥 S,后面的传输都使用 S 加密。

输入URL后到页面渲染 - 图29

连接迁移

TCP 连接基于四元组(源 IP、源端口、目的 IP、目的端口),切换网络时至少会有一个因素发生变化,导致连接发生变化。当连接发生变化时,如果还使用原来的 TCP 连接,则会导致连接失败,就得等原来的连接超时后重新建立连接,所以我们有时候发现切换到一个新网络时,即使新网络状况良好,但内容还是需要加载很久。如果实现得好,当检测到网络变化时立刻建立新的 TCP 连接,即使这样,建立新的连接还是需要几百毫秒的时间。

QUIC 的连接不受四元组的影响,当这四个元素发生变化时,原连接依然维持。那这是怎么做到的呢?道理很简单,QUIC 连接不以四元组作为标识,而是使用一个 64 位的随机数,这个随机数被称为 Connection ID,即使 IP 或者端口发生变化,只要 Connection ID 没有变化,那么连接依然可以维持。

输入URL后到页面渲染 - 图30

解决队头阻塞

  • QUIC 的传输单元是 Packet,加密单元也是 Packet,整个加密、传输、解密都基于 Packet,这样就能避免 TLS 的队头阻塞问题;
  • QUIC 基于 UDP,UDP 的数据包在接收端没有处理顺序,即使中间丢失一个包,也不会阻塞整条连接,其他的资源会被正常处理。

拥塞控制

TCP 拥塞控制由 4 个核心算法组成:慢启动、拥塞避免、快速重传和快速恢复

QUIC 改进的拥塞控制的特性:

  • 热插拔
  • 前向纠错 FEC
  • 单调递增的 Packet Number
  • ACK Delay

流量控制

TCP 会对每个 TCP 连接进行流量控制,流量控制的意思是让发送方不要发送太快,要让接收方来得及接收,不然会导致数据溢出而丢失,TCP 的流量控制主要通过滑动窗口来实现的。

QUIC 只需要建立一条连接,在这条连接上同时传输多条 Stream,好比有一条道路,两头分别有一个仓库,道路中有很多车辆运送物资。QUIC 的流量控制有两个级别:连接级别(Connection Level)和 Stream 级别(Stream Level),好比既要控制这条路的总流量,不要一下子很多车辆涌进来,货物来不及处理,也不能一个车辆一下子运送很多货物,这样货物也来不及处理。

详情见HTTP/3 原理实战 - 知乎 (zhihu.com)

输入URL后到页面渲染 - 图31 服务器接受并解析

服务器接受请求并解析,将请求转发到服务程序,如虚拟主机使用HTTP Host头部判断请求的服务程序

虚拟主机

虚拟主机:是在网络服务器上划分出一定的磁盘空间供用户放置站点、应用组件等,提供必要的站点功能与数据存放、传输功能。

所谓虚拟主机,也叫“网站空间”就是把一台运行在互联网上的服务器划分成多个“虚拟”的服务器,每一个虚拟主机都具有独立的域名和完整的Internet服务器(支持WWWFTPE-mail等)功能。一台服务器上的不同虚拟主机是各自独立的,并由用户自行管理。但一台服务器主机只能够支持一定数量的虚拟主机,当超过这个数量时,用户将会感到性能急剧下降。

实现原理:虚拟主机是用同一个WEB服务器,为不同域名网站提供服务的技术。Apache、Tomcat等均可通过配置实现这个功能。

相关的HTTP消息头:Host。

例如:Host: [www.baidu.com](http://www.baidu.com/)

客户端发送HTTP请求的时候,会携带Host头,Host头记录的是客户端输入的域名。这样服务器可以根据Host头确认客户要访问的是哪一个域名。

输入URL后到页面渲染 - 图32 服务器处理数据并返回

  1. 服务器检查HTTP请求头是否包含缓存验证信息如果验证缓存新鲜,返回304等对应状态码
  2. 处理程序读取完整请求并准备HTTP响应,可能需要查询数据库等操作
  3. 服务器将响应报文通过TCP连接发送回浏览器

HTTP响应

响应报文

输入URL后到页面渲染 - 图33

  1. HTTP/1.0 200 OK
  2. Content-Type: text/plain
  3. Content-Length: 137582
  4. Expires: Thu, 05 Dec 1997 16:00:00 GMT
  5. Last-Modified: Wed, 5 August 1996 15:55:28 GMT
  6. Server: Apache 0.84
  7. <html>
  8. <body>Hello World</body>
  9. </html>

HTTP 响应报文的第一行叫做状态行,后面的行是首部行,最后是实体主体。

状态行包含了三个字段:协议版本字段、状态码和相应的状态信息。

实体部分是报文的主要部分,它包含了所请求的对象。

状态码

状态码 中文描述
100 继续。客户端应继续其请求
101 切换协议。服务器根据客户端的请求切换协议。只能切换到更高级的协议,例如,切换到HTTP的新版本协议
200 请求成功。一般用于GET与POST请求
201 已创建。成功请求并创建了新的资源
202 已接受。已经接受请求,但未处理完成
203 非授权信息。请求成功。但返回的meta信息不在原始的服务器,而是一个副本
204 无内容。服务器成功处理,但未返回内容。在未更新网页的情况下,可确保浏览器继续显示当前文档
205 重置内容。服务器处理成功,用户终端(例如:浏览器)应重置文档视图。可通过此返回码清除浏览器的表单域
206 部分内容。服务器成功处理了部分GET请求
300 多种选择。请求的资源可包括多个位置,相应可返回一个资源特征与地址的列表用于用户终端(例如:浏览器)选择
301 永久移动。请求的资源已被永久的移动到新URI,返回信息会包括新的URI,浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代替
302 临时移动。与301类似。但资源只是临时被移动。客户端应继续使用原有URI
303 查看其它地址。与301类似。使用GET和POST请求查看
304 未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源
305 使用代理。所请求的资源必须通过代理访问
306 已经被废弃的HTTP状态码
307 临时重定向。与302类似。使用GET请求重定向
400 客户端请求的语法错误,服务器无法理解
401 请求要求用户的身份认证
402 保留,将来使用
403 服务器理解请求客户端的请求,但是拒绝执行此请求
404 服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置”您所请求的资源无法找到”的个性页面
405 客户端请求中的方法被禁止
406 服务器无法根据客户端请求的内容特性完成请求
407 请求要求代理的身份认证,与401类似,但请求者应当使用代理进行授权
408 服务器等待客户端发送的请求时间过长,超时
409 服务器完成客户端的 PUT 请求时可能返回此代码,服务器处理请求时发生了冲突
410 客户端请求的资源已经不存在。410不同于404,如果资源以前有现在被永久删除了可使用410代码,网站设计人员可通过301代码指定资源的新位置
411 服务器无法处理客户端发送的不带Content-Length的请求信息
412 客户端请求信息的先决条件错误
413 由于请求的实体过大,服务器无法处理,因此拒绝请求。为防止客户端的连续请求,服务器可能会关闭连接。如果只是服务器暂时无法处理,则会包含一个Retry-After的响应信息
414 请求的URI过长(URI通常为网址),服务器无法处理
415 服务器无法处理请求附带的媒体格式
416 客户端请求的范围无效
417 服务器无法满足Expect的请求头信息
500 服务器内部错误,无法完成请求
501 服务器不支持请求的功能,无法完成请求
502 作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应
503 由于超载或系统维护,服务器暂时的无法处理客户端的请求。延时的长度可包含在服务器的Retry-After头信息中
504 充当网关或代理的服务器,未及时从远端服务器获取请求
505 服务器不支持请求的HTTP协议的版本,无法完成处理

输入URL后到页面渲染 - 图34 关闭TCP连接

浏览器接收HTTP响应,然后根据情况选择关闭TCP连接或者保留重用,关闭TCP连接的四次握手如下:

为了避免服务器与客户端双方的资源占用和损耗,当双方没有请求或响应传递时,任意一方都可以发起关闭请求。与创建TCP连接的3次握手类似,关闭TCP连接,需要4次握手。

TCP取消连接(四次挥手)

输入URL后到页面渲染 - 图35

  1. 第一次挥手,客户端认为没有数据要再发送给服务器端,它就向服务器发送一个 FIN 报文段,申请断开客户端到服务器端的连接。发送后客户端进入 FIN_WAIT_1 状态。
  2. 第二次挥手,服务器端接收到客户端释放连接的请求后,向客户端发送一个确认报文段,表示已经接收到了客户端释放连接的请求,以后不再接收客户端发送过来的数据。但是因为连接是全双工的,所以此时,服务器端还可以向客户端发送数据。服务器端进入 CLOSE_WAIT 状态。客户端收到确认后,进入 FIN_WAIT_2 状态。
  3. 第三次挥手,服务器端发送完所有数据后,向客户端发送 FIN 报文段,申请断开服务器端到客户端的连接。发送后进入 LAST_ACK 状态。
  4. 第四次挥手,客户端接收到 FIN 请求后,向服务器端发送一个确认应答,并进入 TIME_WAIT 阶段。该阶段会持续一段时间,这个时间为报文段在网络中的最大生存时间,如果该时间内服务端没有重发请求的话,客户端进入 CLOSED 的状态。如果收到服务器的重发请求就重新发送确认报文段。服务器端收到客户端的确认报文段后就进入 CLOSED 状态,这样全双工的连接就被释放了。

📖相关知识点

为什么客户端在TIME-WAIT阶段要等2MSL?

为的是确认服务器端是否收到客户端发出的ACK确认报文

当客户端发出最后的ACK确认报文时,并不能确定服务器端能够收到该段报文。所以客户端在发送完ACK确认报文之后,会设置一个时长为2MSL的计时器。MSL指的是Maximum Segment Lifetime:一段TCP报文在传输过程中的最大生命周期。2MSL即是服务器端发出为FIN报文和客户端发出的ACK确认报文所能保持有效的最大时长。

服务器端在1MSL内没有收到客户端发出的ACK确认报文,就会再次向客户端发出FIN报文;

如果客户端在2MSL内,再次收到了来自服务器端的FIN报文,说明服务器端由于各种原因没有接收到客户端发出的ACK确认报文。客户端再次向服务器端发出ACK确认报文,计时器重置,重新开始2MSL的计时;否则客户端在2MSL内没有再次收到来自服务器端的FIN报文,说明服务器端正常接收了ACK确认报文,客户端可以进入CLOSED阶段,完成“四次挥手”。

所以,客户端要经历时长为2SML的TIME-WAIT阶段;这也是为什么客户端比服务器端晚进入CLOSED阶段的原因


输入URL后到页面渲染 - 图36 浏览器解析报文

  1. 浏览器检查响应状态吗:是否为1XX,3XX, 4XX, 5XX,这些情况处理与2XX不同
  2. 如果资源可缓存,进行缓存
  3. 对响应进行解码(例如gzip压缩)
  4. 根据资源类型决定如何处理(假设资源为HTML文档)

资源解码

HTTP 压缩:

HTTP 压缩是一种内置到网页服务器和网页客户端中以改进传输速度和带宽利用率的方式。在使用 HTTP 压缩的情况下,HTTP 数据在从服务器发送前就已压缩:兼容的浏览器将在下载所需的格式前宣告支持何种方法给服务器;不支持压缩方法的浏览器将下载未经压缩的数据。最常见的压缩方案包括 Gzip 和 Deflate

HTTP 压缩就是以缩小体积为目的,对 HTTP 内容进行重新编码的过程

Gzip 的内核就是 Deflate,目前我们压缩文件用得最多的就是 Gzip。可以说,Gzip 就是 HTTP 压缩的经典例题。

Gzip 压缩

具体的做法非常简单,只需要你在你的 request headers 中加上这么一句:

  1. accept-encoding:gzip

适用于非超小型文件, Gzip 是高效的,压缩后通常能帮我们减少响应 70% 左右的大小。

但并不保证针对每一个文件的压缩都会使其变小。

Gzip 压缩背后的原理,是在一个文本文件中找出一些重复出现的字符串、临时替换它们,从而使整个文件变小。根据这个原理,文件中代码的重复率越高,那么压缩的效率就越高,使用 Gzip 的收益也就越大。反之亦然。

webpack和服务端的 Gzip

  1. 一般来说,Gzip 压缩是服务器的活儿:服务器了解到我们这边有一个 Gzip 压缩的需求,它会启动自己的 CPU 去为我们完成这个任务。而压缩文件这个过程本身是需要耗费时间的,大家可以理解为我们**以服务器压缩的时间开销和 CPU 开销(以及浏览器解析压缩文件的开销)为代价,省下了一些传输过程中的时间开销。**
  2. 既然存在着这样的交换,那么就要求我们学会权衡。服务器的 CPU 性能不是无限的,如果存在大量的压缩需求,服务器也扛不住的。服务器一旦因此慢下来了,用户还是要等。**Webpack Gzip 压缩操作的存在,事实上就是为了在构建过程中去做一部分服务器的工作,为服务器分压。**
  3. 因此,这两个地方的 Gzip 压缩,谁也不能替代谁。它们必须和平共处,好好合作。作为开发者,我们也应该**结合业务压力的实际强度情况,去做好这其中的权衡。**

🔟浏览器解析HTML

  1. 构建DOM树:
    1. Tokenizing:根据HTML规范将字符流解析为标记
    2. Lexing:词法分析将标记转换为对象并定义属性和规则
    3. DOM construction:根据HTML标记关系将对象组成DOM树
  2. 解析过程中遇到图片、样式表、js文件,启动下载
  3. 构建CSSOM树:
    1. Tokenizing:字符流转换为标记流
    2. Node:根据标记创建节点
    3. CSSOM:节点创建CSSOM树
  4. 根据DOM树和CSSOM树构建渲染树:
    1. 从DOM树的根节点遍历所有可见节点,不可见节点包括:
      • script,meta这样本身不可见的标签。
      • 被css隐藏的节点,如display: none
    2. 对每一个可见节点,找到恰当的CSSOM规则并应用

HTML 文档的解析和渲染过程中,CSS和JS 顺序执行、并发加载

HTML 解析

输入URL后到页面渲染 - 图37

HTML 解释器

HTML 解释器的工作就是将网络或者本地磁盘获取的 HTML 网页和资源从字节流解释成 DOM 树结构。

输入URL后到页面渲染 - 图38

从资源的字节流到 DOM 树

通过上图可以清楚的了解这一过程:首先是字节流,经过解码之后是字符流,然后通过词法分析器会被解释成词语(Tokens),之后经过语法分析器构建成节点,最后这些节点被组建成一颗 DOM 树。

在这个过程中,每一个环节都会调用对应的类去处理

  • 词法分析: HTMLTokenizer 类
  • 词语验证:XSSAuditor 类
  • 从词语到节点: HTMLDocumentParser 类、 HTMLTreeBuilder 类
  • 从节点到 DOM 树: HTMLConstructionSite 类

对于线程化的解释器,字符流后的整个解释、布局和渲染过程基本会交给一个单独的渲染线程来管理(不是绝对的)。由于 DOM 树只能在渲染线程上创建和访问,所以构建 DOM 树的过程只能在渲染线程中进行。但是,从字符串到词语这个阶段可以交给单独的线程来做,Chromium 浏览器使用的就是这个思想。在解释成词语之后,Webkit 会分批次将结果词语传递回渲染线程。

解析的过程可以分为四个步骤:

1. 解码(encoding)

传输回来的其实都是一些二进制字节数据,浏览器需要根据文件指定编码(例如UTF-8)转换成字符串,也就是HTML 代码。

2. 预解析(pre-parsing)

预解析做的事情是提前加载资源,减少处理时间,它会识别一些会请求资源的属性,比如img标签的src属性,并将这个请求加到请求队列中。

3. 符号化(Tokenization)

符号化是词法分析的过程,将输入解析成符号,HTML 符号包括,开始标签、结束标签、属性名和属性值。

它通过一个状态机去识别符号的状态,比如遇到<,>状态都会产生变化。

4. 构建树(tree construction)

注意:符号化和构建树是并行操作的,也就是说只要解析到一个开始标签,就会创建一个 DOM 节点。

在上一步符号化中,解析器获得这些标记,然后以合适的方法创建DOM对象并把这些符号插入到DOM对象中。

图片格式

时下应用较为广泛的 Web 图片格式有 JPEG/JPG、PNG、WebP、Base64、SVG 等,

JPEG/JPG

关键字:有损压缩、体积小、加载快、不支持透明

JPG 的优点

JPG 最大的特点是有损压缩。这种高效的压缩算法使它成为了一种非常轻巧的图片格式。另一方面,即使被称为“有损”压缩,JPG的压缩方式仍然是一种高质量的压缩方式:当我们把图片体积压缩至原有体积的 50% 以下时,JPG 仍然可以保持住 60% 的品质。此外,JPG 格式以 24 位存储单个图,可以呈现多达 1600 万种颜色,足以应对大多数场景下对色彩的要求,这一点决定了它压缩前后的质量损耗并不容易被我们人类的肉眼所察觉——前提是你用对了业务场景。

使用场景

  • JPG 适用于呈现色彩丰富的图片,在我们日常开发中,JPG 图片经常作为大的背景图、轮播图或 Banner 图出现。
  • 两大电商网站对大图的处理,是 JPG 图片应用场景的最佳写照:

使用 JPG 呈现大图,既可以保住图片的质量,又不会带来令人头疼的图片体积,是当下比较推崇的一种方案。

JPG 的缺陷

有损压缩在上文所展示的轮播图上确实很难露出马脚,但当它处理矢量图形Logo 等线条感较强、颜色对比强烈的图像时,人为压缩导致的图片模糊会相当明显。

此外,JPEG 图像不支持透明度处理,透明图片需要召唤 PNG 来呈现。

PNG-8/PNG-24

关键字:无损压缩、质量高、体积大、支持透明

PNG 的优点

  • PNG(可移植网络图形格式)是一种无损压缩的高保真的图片格式。8 和 24,这里都是二进制数的位数。按照我们前置知识里提到的对应关系,8 位的 PNG 最多支持 256 种颜色,而 24 位的可以呈现约 1600 万种颜色。
  • PNG 图片具有比 JPG 更强的色彩表现力,对线条的处理更加细腻,对透明度有良好的支持。它弥补了上文我们提到的 JPG 的局限性,唯一的 BUG 就是体积太大

PNG-8 与 PNG-24 的选择题

  • 什么时候用 PNG-8,什么时候用 PNG-24,这是一个问题。
  • 理论上来说,当你追求最佳的显示效果、并且不在意文件体积大小时,是推荐使用 PNG-24 的。
  • 但实践当中,为了规避体积的问题,我们一般不用PNG去处理较复杂的图像。当我们遇到适合 PNG 的场景时,也会优先选择更为小巧的 PNG-8。
  • 如何确定一张图片是该用 PNG-8 还是 PNG-24 去呈现呢?好的做法是把图片先按照这两种格式分别输出,看 PNG-8 输出的结果是否会带来肉眼可见的质量损耗,并且确认这种损耗是否在我们(尤其是你的 UI 设计师)可接受的范围内,基于对比的结果去做判断。

应用场景

  • 前面我们提到,复杂的、色彩层次丰富的图片,用 PNG 来处理的话,成本会比较高,我们一般会交给 JPG 去存储。
  • 考虑到 PNG 在处理线条和颜色对比度方面的优势,我们主要用它来呈现小的 Logo、颜色简单且对比强烈的图片或背景等。
  • 颜色简单、对比度较强的透明小图也在 PNG 格式下有着良好的表现

SVG

关键字:文本文件、体积小、不失真、兼容性好

SVG(可缩放矢量图形)是一种基于 XML 语法的图像格式。它和本文提及的其它图片种类有着本质的不同:SVG 对图像的处理不是基于像素点,而是是基于对图像的形状描述。

SVG 的特性

和性能关系最密切的一点就是:SVG 与 PNG 和 JPG 相比,文件体积更小,可压缩性更强

当然,作为矢量图,它最显著的优势还是在于图片可无限放大而不失真这一点上。这使得 SVG 即使是被放到视网膜屏幕上,也可以一如既往地展现出较好的成像品质——1 张 SVG 足以适配 n 种分辨率。

此外,SVG 是文本文件。我们既可以像写代码一样定义 SVG,把它写在 HTML 里、成为 DOM 的一部分,也可以把对图形的描述写入以 .svg 为后缀的独立文件(SVG 文件在使用上与普通图片文件无异)。这使得 SVG 文件可以被非常多的工具读取和修改,具有较强的灵活性

SVG 的局限性主要有两个方面,一方面是它的渲染成本比较高,这点对性能来说是很不利的。另一方面,SVG 存在着其它图片格式所没有的学习成本(它是可编程的)。

应用场景

SVG 是文本文件,我们既可以像写代码一样定义 SVG,把它写在 HTML 里、成为 DOM 的一部分,也可以把对图形的描述写入以 .svg 为后缀的独立文件(SVG 文件在使用上与普通图片文件无异)。

Base64

关键字:文本文件、依赖编码、小图标解决方案

Base64 并非一种图片格式,而是一种编码方式。Base64 和雪碧图一样,是作为小图标解决方案而存在的。

Base64 图片的出现,也是为了减少加载网页图片时对服务器的请求次数,从而提升网页性能。Base64 是作为雪碧图的补充而存在的。

Base64 是一种用于传输 8Bit 字节码的编码方式,通过对图片进行 Base64 编码,我们可以直接将编码结果写入 HTML 或者写入 CSS,从而减少 HTTP 请求的次数。

使用方式:

按照一贯的思路,我们加载图片需要把图片链接写入 img 标签:

  1. <img src="https://user-gold-cdn.xitu.io/2018/9/15/165db7e94699824b?w=22&h=22&f=png&s=3680">
  • 浏览器就会针对我们的图片链接去发起一个资源请求。
  • 但是如果我们对这个图片进行 Base64 编码,我们会得到一个这样的字符串:
  1. data:image/png;base64,iVBORw0KGgoAAAA.... // 过长省略

字符串比较长,我们可以直接用这个字符串替换掉上文中的链接地址。你会发现浏览器原来是可以理解这个字符串的,它自动就将这个字符串解码为了一个图片,而不需再去发送 HTTP 请求。

应用场景:

  • 图片的实际尺寸很小(大家可以观察一下使用的页面的 Base64 图,几乎没有超过 2kb 的)
  • 图片无法以雪碧图的形式与其它小图结合(合成雪碧图仍是主要的减少 HTTP 请求的途径,Base64 是雪碧图的补充)
  • 图片的更新频率非常低(不需我们重复编码和修改文件内容,维护成本较低)

webp

关键字:年轻的全能型选手

WebP 是今天在座各类图片格式中最年轻的一位,它于 2010 年被提出, 是 Google 专为 Web 开发的一种旨在加快图片加载速度的图片格式,它支持有损压缩和无损压缩。

webp 的优点

WebP 像 JPEG 一样对细节丰富的图片信手拈来,像 PNG 一样支持透明,像 GIF 一样可以显示动态图片——它集多种图片文件格式的优点于一身。

WebP 的官方介绍对这一点有着更权威的阐述:

与 PNG 相比,WebP 无损图像的尺寸缩小了 26%。在等效的 SSIM 质量指数下,WebP 有损图像比同类 JPEG 图像小 25-34%。 无损 WebP 支持透明度(也称为 alpha 通道),仅需 22% 的额外字节。对于有损 RGB 压缩可接受的情况,有损 WebP 也支持透明度,与 PNG 相比,通常提供 3 倍的文件大小。

webp的缺点

  • webp的局限性:(现在感觉已经没有什么局限性了,主要针对18年以前的浏览器不兼容)
  • webp还会增加服务器的负担——和编码 JPG 文件相比,编码同样质量的 WebP 文件会占用更多的计算资源。

CSS解析

CSS 样式表会阻塞渲染树的构建,但 DOM 树依然继续构建(除非遇到 script 标签且 css 文件此时仍未加载完成),但不会渲染绘制到页面上。

理论上来说,应用样式表不会更改 DOM 树,因此似乎没有必要等待样式表并停止文档解析。但这涉及到一个问题,就是脚本在文档解析阶段会请求样式信息。如果当时还没有加载和解析样式,脚本就会获得错误的回复,这样显然会产生很多问题。这看上去是一个非典型案例,但事实上非常普遍。Firefox 在样式表加载和解析的过程中,会禁止所有脚本。而对于 WebKit 而言,仅当脚本尝试访问的样式属性可能受尚未加载的样式表影响时,它才会禁止该脚本。

🟦JS解析

在 HTML 文档的解析过程中,解析器遇到 <script> 标记时会立即解析并执行脚本,HTML 文档的解析将被阻塞,直到脚本执行完毕。如果脚本是外部的,那么解析过程会停止,直到从网络抓取资源并解析和执行完成后,再继续解析后续内容。

js解析如下:

  1. 浏览器创建Document对象并解析HTML,将解析到的元素和文本节点添加到文档中,此时document.readystate为loading
  2. HTML解析器遇到没有async和defer的script时,将他们添加到文档中,然后执行行内或外部脚本。这些脚本会同步执行,并且在脚本下载和执行时解析器会暂停。这样就可以用document.write()把文本插入到输入流中。同步脚本经常简单定义函数和注册事件处理程序,他们可以遍历和操作script和他们之前的文档内容
  3. 当解析器遇到设置了async属性的script时,开始下载脚本并继续解析文档。脚本会在它下载完成后尽快执行,但是解析器不会停下来等它下载。异步脚本禁止使用document.write(),它们可以访问自己script和之前的文档元素
  4. 当文档完成解析,document.readState变成interactive
  5. 所有defer脚本会按照在文档出现的顺序执行,延迟脚本能访问完整文档树,禁止使用document.write()
  6. 浏览器在Document对象上触发DOMContentLoaded事件
  7. 此时文档完全解析完成,浏览器可能还在等待如图片等内容加载,等这些内容完成载入并且所有异步脚本完成载入和执行,document.readState变为complete,window触发load事件

JS 脚本会阻塞 HTML 文档的解析,包括 DOM 树的构建和渲染树的构建

输入URL后到页面渲染 - 图39

1. 词法分析

JS 脚本加载完毕后,会首先进入语法分析阶段,它首先会分析代码块的语法是否正确,不正确则抛出“语法错误”,停止执行。

几个步骤:

  • 分词,例如将var a = 2,,分成var、a、=、2这样的词法单元。
  • 解析,将词法单元转换成抽象语法树(AST)。
  • 代码生成,将抽象语法树转换成机器指令。

2. 预编译

JS 有三种运行环境:

  • 全局环境
  • 函数环境
  • eval

每进入一个不同的运行环境都会创建一个对应的执行上下文,根据不同的上下文环境,形成一个函数调用栈,栈底永远是全局执行上下文,栈顶则永远是当前执行上下文。

创建执行上下文

创建执行上下文的过程中,主要做了以下三件事:

  • 创建变量对象
    • 参数、函数、变量
  • 建立作用域链
    • 确认当前执行环境是否能访问变量
  • 确定 This 指向

3. 执行

JS 线程

输入URL后到页面渲染 - 图40

虽然 JS 是单线程的,但实际上参与工作的线程一共有四个:

其中三个只是协助,只有 JS 引擎线程是真正执行的

  • JS 引擎线程:也叫 JS 内核,负责解析执行 JS 脚本程序的主线程,例如 V8 引擎
  • 事件触发线程:属于浏览器内核线程,主要用于控制事件,例如鼠标、键盘等,当事件被触发时,就会把事件的处理函数推进事件队列,等待 JS 引擎线程执行
  • 定时器触发线程:主要控制setInterval和setTimeout,用来计时,计时完毕后,则把定时器的处理函数推进事件队列中,等待 JS 引擎线程。
  • HTTP 异步请求线程:通过XMLHttpRequest连接后,通过浏览器新开的一个线程,监控readyState状态变更时,如果设置了该状态的回调函数,则将该状态的处理函数推进事件队列中,等待JS引擎线程执行。

相关知识点输入URL后到页面渲染 - 图41

渲染过程中遇到 JS 文件怎么处理?(浏览器解析过程)

  1. JavaScript 的加载、解析与执行会阻塞文档的解析,也就是说,在构建 DOM 时,HTML 解析器若遇到了 JavaScript,那么它会暂停文档的解析,将控制权移交给 JavaScript 引擎,等 JavaScript 引擎运行完毕,浏览器再从中断的地方恢复继续解析文档。
  2. 也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性。

async 和 defer 的作用是什么?有什么区别?(浏览器解析过程)

  1. 1)脚本没有 defer async,浏览器会立即加载并执行指定的脚本,也就是说不等待后续载入的文档元素,读到就加载并执行。
  2. 2defer 属性表示延迟执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未停止解析,这两个过程是并行的。当整个 document 解析完毕后再执行脚本文件,在 DOMContentLoaded 事件触发之前完成。多个脚本按顺序执行。
  3. 3async 属性表示异步执行引入的 JavaScript,与 defer 的区别在于,如果已经加载好,就会开始执行,也就是说它的执行仍然会阻塞文档的解析,只是它的加载过程不会阻塞。多个脚本的执行顺序无法保证。

什么是文档的预解析?(浏览器解析过程)

  1. Webkit Firefox 都做了这个优化,当执行 JavaScript 脚本时,另一个线程解析剩下的文档,并加载后面需要通过网络加载的资源。这种方式可以使资源并行加载从而使整体速度更快。需要注意的是,预解析并不改变 DOM 树,它将这个工作留给主解析过程,自己只解析外部资源的引用,比如外部脚本、样式表及图片。

🟦HTML渲染

通过解析,已经获取到 DOM 树和 CSSOM 规则树构建渲染树

html渲染如下:

  • 根据 DOM 树和 CSSOM 规则树构建渲染树。渲染树的节点被称为渲染对象,渲染对象是一个包含有颜色和大小等属性的矩形,渲染对象和 DOM 元素相对应,但这种对应关系不是一对一的,不可见的 DOM 元素不会被插入渲染树。还有一些 DOM元素对应几个可见对象,它们一般是一些具有复杂结构的元素,无法用一个矩形来描述。
  • 当渲染对象被创建并添加到树中,它们并没有位置和大小,所以当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为“自动重排”。(根据渲染树来布局,计算每个节点的位置)
  • 布局阶段结束后是绘制阶段,遍历渲染树并调用渲染对象的 paint 方法将它们的内容显示在屏幕上,绘制使用 UI 基础组件。(调用**GPU** 绘制,合成图层,显示在屏幕上)

值得注意的是,这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的html 都解析完成之后再去构建和布局 render 树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。

输入URL后到页面渲染 - 图42

在构建 CSSOM 树时,会阻塞渲染,直至 CSSOM 树构建完成。并且构建 CSSOM 树是一个十分消耗性能的过程,所以应该尽量保证层级扁平,减少过度层叠,越是具体的 CSS 选择器,执行速度越慢

当 HTML 解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件。并且 CSS 也会影响 JS 的执行,只有当解析完样式表才会执行 JS,所以也可以认为这种情况下,CSS 也会暂停构建 DOM

图层

一般来说,可以把普通文档流看成一个图层。特定的属性可以生成一个新的图层。不同的图层渲染互不影响,所以对于某些频繁需要渲染的建议单独生成一个新图层,提高性能。但也不能生成过多的图层,会引起反作用

通过以下几个常用属性可以生成新图层

  • 3D变换:translate3dtranslateZ
  • will-change
  • videoiframe 标签
  • 通过动画实现的 opacity 动画转换
  • position: fixed

重绘(Repaint)和回流(Reflow)

重绘: 当渲染树中的一些元素需要更新属性,而这些属性只是影响元素的外观、风格,而不会影响布局的操作,比如 background-color,我们将这样的操作称为重绘。

重绘是当节点需要更改外观而不会影响布局的,比如改变 color 就叫称为重绘

回流:当渲染树中的一部分(或全部)因为元素的规模尺寸、布局、隐藏等改变而需要重新构建的操作,会影响到布局的操作,这样的操作我们称为回流。

回流是布局或者几何属性需要改变就称为回流

常见引起回流属性和方法: 任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发回流。

(1)添加或者删除可见的 DOM 元素;
(2)元素尺寸改变——边距、填充、边框、宽度和高度
(3)内容变化,比如用户在 input 框中输入文字
(4)浏览器窗口尺寸改变——resize事件发生时
(5)计算 offsetWidth 和 offsetHeight 属性
(6)设置 style 属性的值
(7)当你修改网页的默认字体时。

回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变深层次的节点很可能导致父节点的一系列回流

所以以下几个动作可能会导致性能问题:

  • 改变 window 大小
  • 改变字体
  • 添加或删除样式
  • 文字改变
  • 定位或者浮动
  • 盒模型

重绘和回流其实和 Event loop 有关

  • Event loop 执行完 Microtasks 后,会判断 document 是否需要更新。因为浏览器是 60Hz的刷新率,每 16ms才会更新一次。
  • 然后判断是否有 resize 或者 scroll ,有的话会去触发事件,所以 resizescroll 事件也是至少 16ms 才会触发一次,并且自带节流功能。
  • 判断是否触发了media query,更新动画并且发送事件
  • 判断是否有全屏操作事件,执行 requestAnimationFrame 回调
  • 执行 IntersectionObserver 回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好
    更新界面
  • 以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行 requestIdleCallback 回调

减少重绘和回流

  • 使用 translate 替代 top
  • 使用 visibility 替换display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
  • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
  • CSS 选择符从右往左匹配查找,避免 DOM 深度过深
  • 将频繁运行的动画变为图层,图层能够阻止该节点回流影响别的元素。比如对于 video标签,浏览器会自动将该节点变为图层
  • 把 DOM 离线后修改。如:使用 documentFragment 对象在内存里操作 DOM
  • 不要一条一条地修改 DOM 的样式。与其这样,还不如预先定义好 css 的 class,然后修改 DOM 的 className。
  • 不要把节点的属性值放在一个循环里当成循环里的变量

常见引起重绘属性和方法:

输入URL后到页面渲染 - 图43

常见引起回流属性和方法:

输入URL后到页面渲染 - 图44

浏览器渲染进程

  1. 图形用户界面GUI渲染线程
    • 负责渲染浏览器界面,包括解析HTML、CSS、构建DOM树、Render树、布局与绘制等
    • 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
  2. JS引擎线程
    • JS内核,也称JS引擎,负责处理执行javascript脚本
    • 等待任务队列的任务的到来,然后加以处理,浏览器无论什么时候都只有一个JS引擎在运行JS程序
  3. 事件触发线程

    • 听起来像JS的执行,但是其实归属于浏览器,而不是JS引擎,用来控制时间循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
    • 当JS引擎执行代码块如setTimeout时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
    • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理

      注意:由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)

  4. 定时触发器线程

    • setIntervalsetTimeout所在线程
    • 定时计时器并不是由JS引擎计时的,因为如果JS引擎是单线程的,如果JS引擎处于堵塞状态,那会影响到计时的准确
    • 当计时完成被触发,事件会被添加到事件队列,等待JS引擎空闲了执行

      注意:W3C的HTML标准中规定,setTimeout中低与4ms的时间间隔算为4ms

  5. 异步HTTP请求线程

    • 在XMLHttpRequest在连接后新启动的一个线程
    • 线程如果检测到请求的状态变更,如果设置有回调函数,该线程会把回调函数添加到事件队列,同理,等待JS引擎空闲了执行

渲染的不良现象

FOUC:主要指的是样式闪烁的问题,由于浏览器渲染机制(比如firefox),在 CSS 加载之前,先呈现了 HTML,就会导致展示出无样式内容,然后样式突然呈现的现象。会出现这个问题的原因主要是 css 加载时间过长,或者 css 被放在了文档底部。

白屏:有些浏览器渲染机制(比如chrome)要先构建 DOM 树和 CSSOM 树,构建完成后再进行渲染,如果 CSS 部分放在 HTML 尾部,由于 CSS 未加载完成,浏览器迟迟未渲染,从而导致白屏;也可能是把 js 文件放在头部,脚本的加载会阻塞后面文档内容的解析,从而页面迟迟未渲染出来,出现白屏问题。

🟦浏览器加载(JS执行)

渲染完毕后可能会出发JS的loda事件。

  • Load 事件触发代表页面中的 DOMCSSJS,图片已经全部加载完毕。
  • DOMContentLoaded 事件触发代表初始的 HTML 被完全加载和解析,不需要等待 CSSJS,图片加载

DOMContentLoaded 事件和 Load 事件的区别?

  1. 当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,而无需等待样式表、图像和子框架的加载完成。
  2. Load 事件是当所有资源加载完成后触发的。

JS引擎

负责处理执行javascript脚本

单线程

浏览器是多进程的,浏览器每一个 tab 标签都代表一个独立的进程,其中浏览器渲染进程(浏览器内核)属于浏览器多进程中的一种,主要负责页面渲染,脚本执行,事件处理等
其包含的线程有:GUI 渲染线程(负责渲染页面,解析 HTML,CSS 构成 DOM 树)、JS 引擎线程、事件触发线程、定时器触发线程、http 请求线程等主要线程。

主线程:也就是 js 引擎执行的线程,这个线程只有一个,页面渲染、函数处理都在这个主线程上执行。

工作线程:也称幕后线程,这个线程可能存在于浏览器或js引擎内,与主线程是分开的,处理文件读取、网络请求等异步事件。

可以看出,异步操作都是放到事件循环队列里面,等待主执行栈来执行的,并没有专门的异步执行线程,所以说js是单线程

js设计为单线程还是跟他的用途有关,试想一下 如果js设计为多线程 那么同时修改和删除同一个dom 浏览器又该如何执行?(容易矛盾)

事件循环 Event Loop

js用此来解决单线程运行带来的问题

如果有很多任务需要执行,不外乎三种解决方法。

(1)排队。因为一个进程一次只能执行一个任务,只好等前面的任务执行完了,再执行后面的任务。

(2)新建进程。使用fork命令,为每个任务新建一个进程。

(3)新建线程。因为进程太耗费资源,所以如今的程序往往允许一个进程包含多个线程,由线程去完成任务。(进程和线程的详细解释,请看这里。)

多线程不仅占用多倍的系统资源,也闲置多倍的资源,这显然不合理。

Event Loop就是为了解决这个问题而提出的。Wikipedia这样定义:

Event Loop是一个程序结构,用于等待和发送消息和事件。(a programming construct that waits for and dispatches events or messages in a program.)”

简单说,就是在程序中设置两个线程:一个负责程序本身的运行,称为”主线程”;另一个负责主线程与其他进程(主要是各种I/O操作)的通信,被称为”Event Loop线程”(可以译为”消息线程”)。输入URL后到页面渲染 - 图45

  • JS 分为同步任务和异步任务
  • 同步任务都在JS引擎线程上执行,形成一个 执行栈
  • 事件触发线程管理一个任务队列,异步任务触发条件达成,将回调事件放到 任务队列中
  • 执行栈中所有同步任务执行完毕,此时JS引擎线程空闲,系统会读取 任务队列,将可运行的异步任务回调事件添加到 执行栈中,开始执行

输入URL后到页面渲染 - 图46

总结一下:

  • JS引擎线程只执行执行栈中的事件
  • 执行栈中的代码执行完毕,就会读取事件队列中的事件
  • 事件队列中的回调事件,是由各自线程插入到事件队列中的
  • 如此循环

宏任务和微任务

宏任务

我们可以将每次执行栈执行的代码当做是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)

每一个宏任务会从头到尾执行完毕,不会执行其他。

我们前文提到过 JS引擎线程和 GUI渲染线程是互斥的关系,浏览器为了能够使 宏任务和 DOM任务有序的进行,会在一个 宏任务执行结果后,在下一个 宏任务执行前, GUI渲染线程开始工作,对页面进行渲染。

  1. 宏任务-->渲染-->宏任务-->渲染-->渲染.
  • script
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI rendering

微任务

我们已经知道 宏任务结束后,会执行渲染,然后执行下一个 宏任务,

而微任务可以理解成在当前 宏任务执行后立即执行的任务。

也就是说,当 宏任务执行完,会在渲染前,将执行期间所产生的所有 微任务都执行完

  • process.nextTick
  • promise
  • Object.observe
  • MutationObserver

总结:

  • 执行一个 宏任务(栈中没有就从 事件队列中获取)
  • 执行过程中如果遇到 微任务,就将它添加到 微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前 微任务队列中的所有 微任务(依次执行)
  • 当前 宏任务执行完毕,开始检查渲染,然后 GUI线程接管渲染
  • 渲染完毕后, JS线程继续接管,开始下一个 宏任务(从事件队列中获取)
  • 输入URL后到页面渲染 - 图47

所以正确的一次 Event loop 顺序是这样的

  • 执行同步代码,这属于宏任务
  • 执行栈为空,查询是否有微任务需要执行
  • 执行所有微任务
  • 必要的话渲染 UI
  • 然后开始下一轮 Event loop,执行宏任务中的异步代码

通过上述的 Event loop 顺序可知,如果宏任务中的异步代码有大量的计算并且需要操作 DOM 的话,为了更快的响应界面响应,我们可以把操作 DOM 放入微任务中

Web Workers

因为JS引擎是单线程的,当JS执行时间过长会页面阻塞。后来HTML5中支持了 Web Worker

来自MDN的官方解释

Web Workers 使得一个Web应用程序可以在与主执行线程分离的后台线程中运行一个脚本操作。这样做的好处是可以在一个单独的线程中执行费时的处理任务,从而允许主(通常是UI)线程运行而不被阻塞/放慢。

注意点:

  • WebWorker可以想浏览器申请一个子线程,该子线程服务于主线程,完全受主线程控制。
  • JS引擎线程与worker线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)

所以,如果需要进行一些高耗时的计算时,可以单独开启一个WebWorker线程,这样不管这个WebWorker子线程怎么密集计算、怎么阻塞,都不会影响JS引擎主线程,只需要等计算结束,将结果通过postMessage传输给主线程就可以了。

另外,还有个东西叫 SharedWorker,与WebWorker在概念上所不同。

  • WebWorker 只属于某一个页面,不会和其他标签页的Renderer进程共享,WebWorker是属于Renderer进程创建的进程。
  • SharedWorker 是由浏览器单独创建的进程来运行的JS程序,它被所有的Renderer进程所共享,在浏览器中,最多只能存在一个SharedWorker进程。

SharedWorker由进程管理,WebWorker是某一个Renderer进程下的线程。

🟦浏览器通信

浏览器加载过程中可能存在浏览器通信一些相关问题

同源策略

同源策略是一个概念,就一句话。有什么限制,就三句话。能说出来即可。

同源策略:限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的关键的安全机制。(来自MDN官方的解释)

具体解释:

  1. 包括三个部分:协议、域名、端口(http协议的默认端口是80)。如果有任何一个部分不同,则不同,那就是跨域了。
  2. 限制:这个源的文档没有权利去操作另一个源的文档。这个限制体现在:(要记住)
    • CookieLocalStorageIndexDB无法获取。
    • 无法获取和操作DOM
    • 不能发送Ajax请求。我们要注意,Ajax只适合同源的通信

浏览器标签页通信

  • 使用 WebSocket,通信的标签页连接同一个服务器,发送消息到服务器后,服务器推送消息给所有连接的客户端。
  • 使用 SharedWorker (只在 chrome 浏览器实现了),两个页面共享同一个线程,通过向线程发送数据和接收数据来实现标签页之间的双向通行。
  • 可以调用 localStorage、cookies 等本地存储方式,localStorge 另一个浏览上下文里被添加、修改或删除时,它都会触发一个 storage 事件,我们通过监听 storage 事件,控制它的值来进行页面信息通信;
  • 如果我们能够获得对应标签页的引用,通过 postMessage 方法也是可以实现多个标签页通信的。

前后端通信

  • Ajax:不支持跨域。
  • WebSocket:不受同源策略的限制,支持跨域
  • CORS:不受同源策略的限制,支持跨域。一种新的通信协议标准。可以理解成是:同时支持同源和跨域的Ajax
  • fetch: 可实现cors通信

解决跨域

方式如下:

  1. JSONP
  2. WebSocket
  3. CORS
  4. Hash
  5. postMessage

JSONP

面试会问:JSONP的原理是什么?怎么实现的?

在CORS和postMessage以前,我们一直都是通过JSONP来做跨域通信的。

JSONP的原理:通过<script>标签的异步加载来实现的。比如说,实际开发中,我们发现,head标签里,可以通过<script>标签的src,里面放url,加载很多在线的插件。这就是用到了JSONP。

JSONP的实现:

比如说,客户端这样写:

  1. <script src="http://www.smyhvae.com/?data=name&callback=myjsonp"></script>

上面的src中,data=name是get请求的参数,myjsonp是和后台约定好的函数名。 服务器端这样写:

  1. myjsonp({
  2. data: {}
  3. })

于是,本地要求创建一个myjsonp 的全局函数,才能将返回的数据执行出来。

WebSocket

WebSocket的用法如下:

  1. var ws = new WebSocket('wss://echo.websocket.org'); //创建WebSocket的对象。参数可以是 ws 或 wss,后者表示加密。
  2. //把请求发出去
  3. ws.onopen = function (evt) {
  4. console.log('Connection open ...');
  5. ws.send('Hello WebSockets!');
  6. };
  7. //对方发消息过来时,我接收
  8. ws.onmessage = function (evt) {
  9. console.log('Received Message: ', evt.data);
  10. ws.close();
  11. };
  12. //关闭连接
  13. ws.onclose = function (evt) {
  14. console.log('Connection closed.');
  15. };

面试一般不会让你写这个代码,一般是考察你是否了解 WebSocket概念,知道有这么回事即可。

CORS

CORS 可以理解成是既可以同步、也可以异步的Ajax。

fetch是一个比较新的API,用来实现CORS通信。用法如下:

  1. // url(必选),options(可选)
  2. fetch('/some/url/', {
  3. method: 'get',
  4. }).then(function (response) {
  5. //类似于 ES6中的promise
  6. }).catch(function (err) {
  7. // 出错了,等价于 then 的第二个参数,但这样更好用更直观
  8. });

另外,如果面试官问:“CORS为什么支持跨域的通信?”

答案:跨域时,浏览器会拦截Ajax请求,并在http头中加Origin。

Hash

url的#后面的内容就叫Hash。Hash的改变,页面不会刷新。这就是用 Hash 做跨域通信的基本原理。

补充:url的?后面的内容叫Search。Search的改变,会导致页面刷新,因此不能做跨域通信。

使用举例:

场景:我的页面 A 通过iframe或frame嵌入了跨域的页面 B。

现在,我这个A页面想给B页面发消息,怎么操作呢?

输入URL后到页面渲染 - 图48 首先,在我的A页面中:

  1. //伪代码
  2. var B = document.getElementsByTagName('iframe');
  3. B.src = B.src + '#' + 'jsonString'; //我们可以把JS 对象,通过 JSON.stringify()方法转成 json字符串,发给 B

输入URL后到页面渲染 - 图49然后,在B页面中:

  1. // B中的伪代码
  2. window.onhashchange = function () { //通过onhashchange方法监听,url中的 hash 是否发生变化
  3. var data = window.location.hash;
  4. };

postMessage()方法

H5中新增的postMessage()方法,可以用来做跨域通信。既然是H5中新增的,那就一定要提到。

场景:窗口 A (http:A.com)向跨域的窗口 B (http:B.com)发送信息。步骤如下

输入URL后到页面渲染 - 图50在A窗口中操作如下:向B窗口发送数据:

  1. // 窗口A(http:A.com)向跨域的窗口B(http:B.com)发送信息
  2. Bwindow.postMessage('data', 'http://B.com'); //这里强调的是B窗口里的window对象

输入URL后到页面渲染 - 图51在B窗口中操作如下:

  1. // 在窗口B中监听 message 事件
  2. Awindow.addEventListener('message', function (event) { //这里强调的是A窗口里的window对象
  3. console.log(event.origin); //获取 :url。这里指:http://A.com
  4. console.log(event.source); //获取:A window对象
  5. console.log(event.data); //获取传过来的数据
  6. }, false);

🔴优化

网络优化

DNS预解析

  1. <meta http-equiv='x-dns-prefetch-control' content='on' />

X-DNS-Prefetch-ControlHTTP 响应头控制 DNS 预取功能通过对用户可以选择跟随,以及通过在文档,包括图片,CSS,JavaScript 和等参考项的 URL 都链接浏览器主动进行域名解析

该预取在后台执行,以便在需要引用项目时 DNS 可能已经解决。这可以减少用户点击链接时的等待时间。

预解析

当浏览器从(第三方)服务器请求资源时,必须先将该跨域域名解析为 IP地址,然后浏览器才能发出请求。此过程称为 DNS解析。DNS 缓存可以帮助减少此延迟,而 DNS解析可以导致请求增加明显的延迟。对于打开了与许多第三方的连接的网站,此延迟可能会大大降低加载性能。

  1. <link rel="dns-prefetch" href="https://fonts.googleapis.com/">

缓存机制

缓存对于前端性能优化来说是个很重要的点,良好的缓存策略可以降低资源的重复加载提高网页的整体加载速度

相同的资源可以通过强缓存或者协商缓存从浏览器获取

HTTP2.0 (3.0还没有怎么普及)

因为浏览器会有并发请求限制,在 HTTP / 1.1 时代,每个请求都需要建立和断开,消耗了好几个 RTT 时间,并且由于 TCP 慢启动的原因,加载体积大的文件会需要更多的时间

HTTP / 2.0 中引入了多路复用,能够让多个请求使用同一个 TCP 链接,极大的加快了网页的加载速度。并且还支持 Header 压缩,进一步的减少了请求的数据大小

3.0 实现了0RTT的信息传输,以及基于UPD减少了握手请求的繁琐

CDN缓存加速

  1. 更快地传递内容
    由于CDN最靠近用户放置,因此当您的内容需要移动的距离更短时,可以减少延迟。CDN可以使您的网站加载速度更快。例如,如果您的网站位于英国并且您从美国获得流量,则您的CDN提供商可能在美国拥有服务器并将该服务器用于您的网站。
  2. 更多同步用户
    CDN可以确保网络具有高数据阈值。因此,大量用户可以在没有延迟的情况下同时访问网络。通过实现高流量,CDN允许来自世界各地的人们同时访问您的网站。
  3. 持续可用性
    CDN中的服务器始终在运行,即使服务器已关闭,您的网站也可以访问。如果没有CDN,您的服务器有时可能会关闭,这意味着您必须等到主机解决问题。如果您使用CDN,则不会发生这种情况。如果服务器崩溃,CDN将使用您的缓存页面。
  4. 可靠的内容传递
    如果您使用CDN,则内容的传送更加一致。特别是,这涉及高分辨率内容,如视频和图像。CDN提供高质量的内容交付,因此对您网站的性能产生重大影响。由于54%的客户希望在您的网站上看到视频,因此您可以以高分辨率提供这些视频对您的业务至关重要。
  5. 控制资产交付
    当CDN监控您的资产交付时,运营商可以根据实时统计数据确定需要额外容量的位置。如果某个地区的服务器出现过载,运营商可以提供额外的带宽,以确保一切顺利运行。
  6. 防止流量高峰
    如果您的网站突然出现大量流量,您可以从CDN服务中受益。大型服务器网络可确保这些资源在所有情况下都可用且可扩展。许多企业的噩梦是他们将获得大幅增加的流量,但他们的网站无法处理它。

预加载

在开发中,可能会遇到这样的情况。有些资源不需要马上用到,但是希望尽早获取,这时候就可以使用预加载

预加载其实是声明式的 fetch ,强制浏览器请求资源,并且不会阻塞 onload 事件,可以使用以下代码开启预加载

  1. <link rel="preload" href="http://example.com">

预加载可以一定程度上降低首屏的加载时间,因为可以将一些不影响首屏但重要的文件延后加载,唯一缺点就是兼容性不好

预渲染

可以通过预渲染将下载的文件预先在后台渲染,可以使用以下代码开启预渲染

  1. <link rel="prerender" href="http://poetries.com">

预渲染虽然可以提高页面的加载速度,但是要确保该页面百分百会被用户在之后打开,否则就白白浪费资源去渲染

文件优化

图片优化

对于如何优化图片,有 2 个思路

  • 减少像素点
  • 减少每个像素点能够显示的颜色

图片加载优化

  • 不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用 CSS 去代替。
  • 对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片
  • 小图使用 base64格式
  • 将多个图标文件整合到一张图片中(雪碧图)
  • 选择正确的图片格式:
    • 对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好
    • 小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替
    • 照片使用 JPEG

其他文件优化

  • CSS文件放在 head
  • 服务端开启文件压缩功能
  • script 标签放在 body 底部,因为 JS 文件执行会阻塞渲染。当然也可以把 script 标签放在任意位置然后加上 defer ,表示该文件会并行下载,但是会放到 HTML 解析完成后顺序执行。对于没有任何依赖的 JS文件可以加上 async ,表示加载和渲染后续文档元素的过程将和 JS 文件的加载与执行并行无序进行。 执行 JS代码过长会卡住渲染,对于需要很多时间计算的代码
  • 可以考虑使用 WebworkerWebworker可以让我们另开一个线程执行脚本而不影响渲染。
  • 启用Gizp压缩,降低资源传输的压力。gizp在压缩比、压缩时间方面处理的很好,对一些动态资源效果明显。

懒加载

懒加载就是将不关键的资源延后加载

懒加载的原理就是只加载自定义区域(通常是可视区域,但也可以是即将进入可视区域)内需要加载的东西。对于图片来说,先设置图片标签的 src 属性为一张占位图,将真实的图片资源放入一个自定义属性中,当进入自定义区域时,就将自定义属性替换为 src 属性,这样图片就会去下载资源,实现了图片懒加载

懒加载不仅可以用于图片,也可以使用在别的资源上。比如进入可视区域才开始播放视频等

Webpack

  • 对于 Webpack4,打包项目使用 production 模式,这样会自动开启代码压缩
  • 使用 ES6 模块来开启 tree shaking,这个技术可以移除没有使用的代码
  • 优化图片,对于小图可以使用 base64 的方式写入文件中
  • 按照路由拆分代码,实现按需加载
  • 给打包出来的文件名添加哈希,实现浏览器缓存文件

输入URL后到页面渲染 - 图52 总结

网络优化主要是以下两方面

  • 获取资源:
    • 缓存获取资源:先强制后协商
    • 提前获取资源:预解析预加载预渲染
    • 延迟获取资源:懒加载
  • 文件压缩:HTTP2.0请求压缩,文件压缩
  • CDN减少信息远距离传输

性能优化

HTML优化

  1. 减少iframse使用(会阻塞父文档的加载,要用的话延迟加载,通过JS加src)
  2. 压缩空白符
  3. 避免节点深层级的嵌套(越深导致dom的遍历消耗更多的时间)
  4. 避免tabel布局
  5. 删除注释
  6. CSS&Javascript尽量外链
  7. 删除元素的默认属性
  8. 使用html5语义化标签🏷

可以借助 html-minifier 工具

CSS优化

css选择器的过滤是右向左的,避免使用一长串的选择器去确定一个元素(现在的差别已经很微小了)

  1. // good
  2. .high-light-list
  3. // bad
  4. .list:nth-last-child(1) > #box a

降低CSS对渲染的阻塞

  • 尽早加载CSS,减少CSS加载的大小
  • 利用GPU进行动画的优化
  • 使用contain属性进行优化CSS属性计算消耗的时间

输入URL后到页面渲染 - 图53

比如你需要在一个长列表中为某一项添加一个内容,但是浏览器并不知道会不会影响到其他元素。这时候可以使用contain属性来告诉浏览器不会变

  1. contain: layout // 告诉浏览器这块元素内容的变化不会影响到其他
  • 使用 font-display 属性进行优化,减少字体闪动出现的可能性

减少回流与重绘

  • 使用 translate 替代 top
  • 使用 visibility 替换display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
  • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
  • CSS 选择符从右往左匹配查找,避免 DOM 深度过深
  • 将频繁运行的动画变为图层,图层能够阻止该节点回流影响别的元素。比如对于 video标签,浏览器会自动将该节点变为图层
  • 把 DOM 离线后修改。如:使用 documentFragment 对象在内存里操作 DOM
  • 不要一条一条地修改 DOM 的样式。与其这样,还不如预先定义好 css 的 class,然后修改 DOM 的 className。
  • 不要把节点的属性值放在一个循环里当成循环里的变量

复合与图层

复合(类似photoshop的改变)

复合是把页面拆分成不同的图层,当我们页面发生变化的时候有可能只会影响到某些图层,其他图层不会被影响。最后不同的图层叠加成一个页面,提高了绘制的效率。

以下这些属性把某些元素提取到单独的图层,使用GPU单独的处理。当这些图层变化时只会出现复合而不会导致页面大范围回流重绘。

  • Position (位置): transform:translate
  • Scale (大小): transform:scale
  • Rotation (旋转): transform:rotate
  • Opacity (透明度): opacity

小技巧:如何把当前元素分离到单独的图层

  1. /* 关键字值 */
  2. will-change: auto;
  3. will-change: scroll-position;
  4. will-change: contents;
  5. will-change: transform; /* <custom-ident>示例 */
  6. will-change: opacity; /* <custom-ident>示例 */
  7. will-change: left, top; /* 两个<animateable-feature>示例 */
  8. /* 全局值 */
  9. will-change: inherit;
  10. will-change: initial;
  11. will-change: unset;

will-change 是css提供的一个新属性,可以用于创建图层。在will-change之前我们要开启GPU加速通常是使用Z轴,如使用translate3d、scalez但是这些实际上是一种 hack 的做法,我们实际上是并不需要z轴的,will-change的出现我们可以用来告诉浏览器我要创建独立的渲染图层。

注意事项:

  1. dom.onmousedown = function() {
  2. target.style.willChange = 'transform';
  3. };
  4. dom.onclick = function() {
  5. // target动画哔哩哔哩...
  6. };
  7. target.onanimationend = function() {
  8. // 动画结束回调,移除will-change
  9. this.style.willChange = 'auto';
  10. };

最好不要在默认属性加上 will-change ,而将要触发动画才添加,否则GPU一直处于待命状态反而会引起不必要的浪费。

JS优化

  • 脚本流
  • 字节码缓存
  • 懒解析

函数优化

lazy parsing 懒解析(函数声明)、 eager parsing 饥饿解析(函数调用)

  1. export default () => {
  2. const add = () => a + b // 声明的代码其实并没有调用浏览器,此时只会进行懒解析
  3. const num1 = 1;
  4. const num2 = 2;
  5. add(num1,num2) // 饥饿解析
  6. }

可以使用一对圆括号来告诉浏览器这段代码很快就要执行,需要饥饿解析。但是我们在使用压缩工具的时候很有可能这对括号会被去掉。(webpack4已经解决了)

  1. export default () => {
  2. const add = (() => a + b) // 用圆括号,要饥饿解析
  3. const num1 = 1;
  4. const num2 = 2;
  5. add(num1,num2) // 饥饿解析
  6. }

利用 Optimize.js 优化初次加载时间(原理是把括号加回来)

对象优化

可以做哪些优化呢?

  • 以相同的顺序初始化对象成员,避免隐藏类调整(为什么呢?JS是一种弱类型的语言并不会声明类型,但是对编译器还是需要知道类型,编译器遇到会对类型进行推断有二十几种(隐藏类型hidden class))若果按照相同的顺序初始化,上一次生成的 hidden class 可以被重用
  1. // good
  2. class Demo{ // HC0
  3. constructor() {
  4. this.l = l;
  5. this.w = w;
  6. }
  7. }
  8. const r1 = nwe Demo(3,4)
  9. const r2 = nwe Demo(3,4)
  10. // bad
  11. cosnt car = {color: "red"}// HCO
  12. car.name = "xxx" //HC1
  13. cosnt car = {color: "red"}// HC2
  14. car.useTime = 1 // HC3
  • 实例化后避免添加新的属性
  1. const car = {color: "red"} // In-object 属性
  2. car.sears = 4 // Normal/Fast 属性,存储property store里面,需要描述数组间接查找
  • 尽量使用 Array 代替 array-like 对象,转成数组再遍历效率可能会更高
  1. Array.prototype.forEach.call(arrObj, () => {
  2. console.log()
  3. })
  4. // good
  5. cosnt add = Array.prototype.slice.call(arrObj,0) // 转化的代价更小(相比直接用)
  • 越界比较,避免读取超过数组长度(没有的话会触发原型链查找的机制,效率降低)

输入URL后到页面渲染 - 图54

  • 数组优化:PACKED_SMI_ELEMENTS()
  1. const array = [1,2,3];
  2. array.push(4.4); // 类型不同优化失效

输入URL后到页面渲染 - 图55

代码拆分

本质:按需加载

Tree shaking

本质:代码减重,去除无用代码

减少主线程工作量

  • 避免长任务
  • 避免超过1k的行间脚本
  • 使用rAF和rIC进行时间调度

渐进式启动

核心:尽可能减少首屏需要加载的资源量

防抖节流

搜索框请求等等场景

而对于某些频繁的事件处理会造成性能的损耗,我们就可以通过防抖和节流来限制事件频繁的发生

webpack优化

优化的配置是很多的,我们很难记住所有相应的loader、plugin,因此webpack引入了mode,针对不同的环境选用模式。

Tree-shaking

什么是Tree-shaking?

tree-shaking是webpack提供的生产环境的优化方式,可以去除未被引用的代码(不会被输出),在生产模式下自动开启。注意使用tree-shaking必须使用的是 ESModules

原理 :根据entry进行依赖扫描,分析依赖关系,把里面引用没使用的代码去掉

作用: 去除未引用的代码(dead-code)

  1. // 用于集中配置webpack的一些优化功能
  2. optimization: {
  3. usedExports: true, // 只导出使用过的成员,用于标记哪些成员未被使用
  4. minimize: true, //开启压缩,用于压缩掉未被使用的代码
  5. }

要是把代码比作一棵树, usedExports 负责将未被使用的树叶进行标记,而 minimize 则负责将他们摇掉

使用合并模块函数进一步优化,这里我们可以借助 concatenateModules 。普通的模块打包,会把模块单独放到一个函数中,如果我们模块很多那么输出的结果中会有很多的模块函数。

使用模块合并既能提升输出效率,又能减少输出代码的体积。

  1. optiimization: {
  2. concatenateModules: true // 尽可能的将所有模块合并到一起输出到一个函数中
  3. }

缺点:基于ES6模块化,影响全局作用域或者有其他副作用需要使用sideEffects声明哪些有副作用

问题

tree-shaking & babel-loader冲突问题?

babel-loader使用的插件集合 @babel/preset-env 会将代码转换为以CommonJS模块化组织的代码,而要使用tree-shaking的话代码必须使用ESM因此tree-shaking会失效,但是新版的babel-loader已经为我们自动关闭了模块的转换,因此两者不会冲突。

输入URL后到页面渲染 - 图56

sideEffects

webpack4中新增的特性,允许我们通过配置去标识我们的代码有没有副作用,从而为tree-shaking提供更大的压缩空间。所谓的副作用是指:除了导出成员之外做的事情,比如某些模块仅仅只是做了某些逻辑但是并没有向外导出模块

有什么用❓

sideEffects 一般用于npm包的标记是否有副作用。假设有一个入口文件集中导出了该目录下的所有组件,我们在另一个文件中引用其中的某一个组件,打包后会发现所有文件都被打包进来了,使用 sideEffects 可以解决这个问题

  1. optimization: {
  2. sideEffects: true // 尽可能的将所有模块合并到一起输出到一个函数中
  3. }
  4. // package.json
  5. {
  6. ...
  7. sideEffects: false // 表示所有模块都没有副作用,没用到的将不会被打包
  8. // 若有部分模块有副作用这里可以改成一个数组[有副作用模块的路径]
  9. ...
  10. }

JS压缩

使用生产 production 模式时默认启用了 terser-webpack-plugin ,减少JS文件的体积

作用域提升

在我们引入一些方法如果没有启用作用域提升,代码会在需要的时候再通过require去引入。而使用作用域提升可以把代码合并到调用的代码里面使得代码更加精简,如下面的例子🌰

输入URL后到页面渲染 - 图57

该特性在生产模式下会默认开启

作用

  • 函数合并,代码体积减少
  • 提高执行效率

注意

  1. babel的 @proset/env 的module需要配置成fasle(使用ES6Module)

Babel7优化

  1. 需要的地方打pollyfill,用于兼容低版本的代码,pollyfill通常很大,我们希望能仅仅引入我们使用到的 pollyfill ,可以在 babel.config.js 中配置 useBuiltIns
  2. 辅助函数按需引入,可以借助 @babel/plugin-transform-runtime
  3. 根据浏览器按需转换代码
  1. presets: [
  2. [
  3. '@babel/preset-env',
  4. {
  5. modules: false,
  6. "targets": {
  7. "browsers": [">0.25%"] //对市场份额超过0.25% ,根据浏览器按需转换
  8. },
  9. "useBuiltIns": "usage", // 仅仅引入需要的pollyfill
  10. "bugfixes": true
  11. }
  12. ],
  13. '@babel/preset-react'
  14. ],
  15. plugins: [
  16. '@babel/plugin-proposal-class-properties',
  17. "@babel/plugin-transform-runtime",
  18. ]
  19. };

代码分割

默认情况下webpack会把所有东西打包成一个包,把代码合理分包,能够极大的减少屏幕白屏时间。

随着前端项目的不断变大,打包后的项目体积2、3m也是经常的,这就导致了bundle的体积太过于庞大了,导致应用加载慢。但是很多时候启动时并不是所有的模块都需要加载进来的。

什么是代码分割?

按照需求将代码分别打包到不同的文件,按需请求对应的模块。

如何实现?

多入口打包

目前实现代码分割的方案主要是有两种,一种是 entry ,另一种是动态导入(ESModule)

  • 多入口打包:多入口打包一般使用与传统的多页面应用程序,简单来说就是:一个一面对应一个入口,公共的部分抽离到公共的文件📃
  1. // 多入口打包
  2. entry: {
  3. "pageA": "路径",
  4. "pageB": "路径"
  5. }
  6. output: {
  7. filename: [name].bundle.js
  8. },
  9. plugins: [
  10. new HtmlWebpackPlugin({
  11. name: "pageA",
  12. template: "xxx",
  13. filename: "pageA.html",
  14. chunk:"a"
  15. }),
  16. new HtmlWebpackPlugin({
  17. name: "pageB",
  18. template: "xxx",
  19. filename: "pageB.html",
  20. chunk:
  21. })
  22. ]
  • 公共部分抽离,不同的模块在不同的入口中被应用,会造成重复引用导致应用的体积过于庞大,这是我们可以使用 optimazation 将相同引用的部分抽离到单独文件中去
  1. optimizations: {
  2. splitChunks: {
  3. chunk: "all"
  4. }
  5. }

注意:

  • html-webpack-plugins 默认会注入所有的打包结果,因此需要使用 chunk 属性手动地指定,注入的bundle.js

代码拆分

我们还可以通过 optimizationsplitChunks 来配置要拆分的包

  1. optimization: {
  2. splitChunks: {
  3. // chunk分组
  4. cacheGroups: {
  5. // 依赖包
  6. vendor: {
  7. name: 'vendor', // chunk包名
  8. test: /[\\/]node_modules[\\/]/,
  9. minSize: 0, // chunk最小体积
  10. minChunks: 1, // 最小要拆分多少段
  11. priority: 10, // 优先级
  12. chunks: 'initial' // 同步加载
  13. },
  14. // 公用代码,类似一些公共组件
  15. common: {
  16. name: 'common',
  17. test: /[\\/]src[\\/]/,
  18. chunks: 'all', // 即考虑动态引入又考虑同步加载
  19. minSize: 0,
  20. minChunks: 2
  21. }
  22. }
  23. }
  24. },

动态导入

动态导入的模块会被自动分包,相比于多页面方案更加灵活。根据路由判断当前的页面,使用ESM提供的动态引入API import 适时引入文件📃,返回的是一个promise。在React中可以结合 SuspenseLazy ,来进行组件的动态引入。

  1. import React, {Suspense, lazy} from 'react';
  2. class Home extends React.Component {
  3. render() {
  4. let cards = [];
  5. // 多次添加更多的卡片,展示懒加载
  6. for (let i = 0; i < 100; i++) {
  7. cards.push(model.map(panel => (
  8. // fallback是未被加载好的占位内容
  9. <Suspense fallback={<div>Loading...</div>}>
  10. <Card key={panel.name} image={panel.image} title={panel.name}
  11. route={panel.route} description={panel.body}/>
  12. </Suspense>
  13. )));
  14. }
  15. return (
  16. <main className={this.props.classes.root}>
  17. <title className={this.props.classes.title}>
  18. <span>正在出售</span>
  19. </title>
  20. {cards}
  21. </main>
  22. );
  23. }
  24. }

输入URL后到页面渲染 - 图58

魔法注释Magic Comments

默认动态引入导入的文件📃仅仅是一个序号,如果我们要让它正常显示名字则需要使用webpack提供的魔法注释来实现自定义chunkname,若设置两个模块的chunkname相同则会被打包到一起

输入URL后到页面渲染 - 图59

首屏优化

除了以上优化等等,还有一些优化用于首屏渲染的

懒执行与懒加载

懒执行

懒执行就是将某些逻辑延迟到使用时再计算。该技术可以用于首屏优化,对于某些耗时逻辑并不需要在首屏就使用的,就可以使用懒执行。懒执行需要唤醒,一般可以通过定时器或者事件的调用来唤醒

懒加载

懒加载就是将不关键的资源延后加载

懒加载的原理就是只加载自定义区域(通常是可视区域,但也可以是即将进入可视区域)内需要加载的东西。对于图片来说,先设置图片标签的 src 属性为一张占位图,将真实的图片资源放入一个自定义属性中,当进入自定义区域时,就将自定义属性替换为 src 属性,这样图片就会去下载资源,实现了图片懒加载

懒加载不仅可以用于图片,也可以使用在别的资源上。比如进入可视区域才开始播放视频等

react按需加载

目前按需加载结合路由是一种主流的方式,我们可以借助 React-ReLoadable 高阶组件 动态加载。

  1. import React from 'react';
  2. import {createMuiTheme, MuiThemeProvider} from '@material-ui/core/styles';
  3. import {BrowserRouter as Router, Route, Switch} from 'react-router-dom';
  4. import Header from './Header.jsx';
  5. import Home from './Home.jsx';
  6. import loadable from '@loadable/component';
  7. const primary = '#30929b';
  8. const theme = createMuiTheme({
  9. palette: {
  10. primary: {
  11. main: primary,
  12. contrastText: '#fff'
  13. },
  14. secondary: {
  15. main: '#000000',
  16. contrastText: primary
  17. }
  18. }
  19. });
  20. // 使用React-Loadable动态加载组件
  21. const LoadableAbout = loadable(() => import('./About.jsx'), {
  22. fallback: '<div>loading...</div>'
  23. });
  24. class App extends React.Component {
  25. constructor(props) {
  26. super(props);
  27. // this.calculatePi(1500); // 测试密集计算对性能的影响
  28. // test(); // 测试函数lazy parsing, eager parsing
  29. }
  30. calculatePi(duration) {
  31. const start = new Date().getTime();
  32. while (new Date().getTime() < start + duration) {
  33. // TODO(Dereck): figure out the Math problem
  34. }
  35. }
  36. render() {
  37. return (
  38. <Router>
  39. <Switch>
  40. <MuiThemeProvider theme={theme}>
  41. <div>
  42. <Header/>
  43. <Route exact path="/" component={Home}/>
  44. <Route path="/about" component={LoadableAbout}/>
  45. </div>
  46. </MuiThemeProvider>
  47. </Switch>
  48. </Router>
  49. );
  50. }
  51. }
  52. export default App;

SEO优化

客户端渲染

客户端渲染模式下,服务端会把渲染需要的静态文件发送给客户端,客户端加载过来之后,自己在浏览器里跑一遍 JS,根据 JS 的运行结果,生成相应的 DOM。这种特性使得客户端渲染的源代码总是特别简洁,往往是这个德行:

  1. <!doctype html>
  2. <html>
  3. <head>
  4. <title>我是客户端渲染的页面</title>
  5. </head>
  6. <body>
  7. <div id='root'></div>
  8. <script src='index.js'></script>
  9. </body>
  10. </html>

根节点下到底是什么内容呢?你不知道,我不知道,只有浏览器把 index.js 跑过一遍后才知道,这就是典型的客户端渲染。

页面上呈现的内容,你在 html 源文件里里找不到——这正是它的特点。

服务端渲染

服务端渲染是一个相对的概念,它的对立面是“客户端渲染”。在运行机制解析这部分,我们会借力客户端渲染的概念,来帮大家理解服务端渲染的工作方式。基于对工作方式的了解,再去深挖它的原理与优势。

服务端渲染的模式下,当用户第一次请求页面时,由服务器把需要的组件或页面渲染成 HTML 字符串,然后把它返回给客户端。客户端拿到手的,是可以直接渲染然后呈现给用户的 HTML 内容,不需要为了生成 DOM 内容自己再去跑一遍 JS 代码。

使用服务端渲染的网站,可以说是“所见即所得”,页面上呈现的内容,我们在 html 源文件里也能找到

事实上,很多网站是出于效益的考虑才启用服务端渲染,性能倒是在其次。

假设 A 网站页面中有一个关键字叫“前端性能优化”,这个关键字是 JS 代码跑过一遍后添加到 HTML 页面中的。那么客户端渲染模式下,我们在搜索引擎搜索这个关键字,是找不到 A 网站的——搜索引擎只会查找现成的内容,不会帮你跑 JS 代码。A 网站的运营方见此情形,感到很头大:搜索引擎搜不出来,用户找不到我们,谁还会用我的网站呢?为了把“现成的内容”拿给搜索引擎看,A 网站不得不启用服务端渲染。

但性能在其次,不代表性能不重要。服务端渲染解决了一个非常关键的性能问题——首屏加载速度过慢。在客户端渲染模式下,我们除了加载 HTML,还要等渲染所需的这部分 JS 加载完,之后还得把这部分 JS 在浏览器上再跑一遍。这一切都是发生在用户点击了我们的链接之后的事情,在这个过程结束之前,用户始终见不到我们网页的庐山真面目,也就是说用户一直在等!相比之下,服务端渲染模式下,服务器给到客户端的已经是一个直接可以拿来呈现给用户的网页,中间环节早在服务端就帮我们做掉了,用户岂不“美滋滋”?

应用实例:

万变不离其宗。强调的只有两点:一是这个 renderToString() 方法;二是把转化结果“塞”进模板里的这一步

这两个操作是服务端渲染的灵魂操作。在虚拟 DOM“横行”的当下,服务端渲染不再是早年 JSP 里简单粗暴的字符串拼接过程,它还要求这一端要具备将虚拟 DOM 转化为真实 DOM 的能力。与其说是“把 JS 在服务器上先跑一遍”,不如说是“把 Vue、React 等框架代码先在 Node 上跑一遍”

应用场景

根据我们前面的描述,不难看出,服务端渲染本质上是本该浏览器做的事情,分担给服务器去做。这样当资源抵达浏览器时,它呈现的速度就快了。乍一看好像很合理:浏览器性能毕竟有限,服务器多牛逼!能者多劳,就该让服务器多干点活!

但仔细想想,在这个网民遍地的时代,几乎有多少个用户就有多少台浏览器。用户拥有的浏览器总量多到数不清,那么一个公司的服务器又有多少台呢?我们把这么多台浏览器的渲染压力集中起来,分散给相比之下数量并不多的服务器,服务器肯定是承受不住的。

这样分析下来,服务端渲染也并非万全之策。在实践中,我一般会建议大家先忘记服务端渲染这个事情——服务器稀少而宝贵,但首屏渲染体验和 SEO 的优化方案却很多——我们最好先把能用的低成本“大招”都用完。除非网页对性能要求太高了,以至于所有的招式都用完了,性能表现还是不尽人意,这时候我们就可以考虑向老板多申请几台服务器,把服务端渲染搞起来了

vercel/next.js: The React Framework (github.com)

https://www.yuque.com/docs/share/9acbe277-a045-447d-ad80-f865337fadf1?#YKgQ0

💠总结

  1. 当输入url回车之后,浏览器会先进行一个url的判断
    • 如果是一个合法地址,则进行下一步
    • 如果不是则交给搜索引擎处理
  2. 解析url,获取url的协议与地址
    • 获取如果用户输入url带有协议,会根据该协议取请求服务器
    • 若没有,先查看HSTS缓存列表
      • 若存在缓存,则自动填充https请求;
      • 不存在则填充http请求;
  3. http请求前先检查浏览器资源缓存
    • 若有该资源则判断资源类型并调用
      • 强缓存不需要问服务器
      • 协商缓存需要发个缓存是否更新的请求
  4. 没有缓存则开始进行DNS缓存检查以及DNS解析;获取ip地址
  5. 有了ip地址后,开始建立TCP三次握手连接
  6. 开始HTTP报文请求,获取HTML
    • HTTP 请求
    • HTTPS TLS四次握手加密请求
    • HTTP3.0 0RTT握手请求(没有TCP连接)
  7. 服务器接受请求并解析,将请求转发到服务程序
  8. 服务器处理数据并返回
    • 正常处理: 200ok
    • 重定向:301/302 避免http不安全请求,告诉客户端请求https
    • 协商缓存资源无变化: 304 资源无变化,浏览器可以之间从缓存调取资源
  9. 若接下来没有请求,则关闭TCP连接,四次挥手
  10. 浏览器解析报文
    • 若服务器使用gzip压缩,则浏览器需要一个解压过程
  11. 浏览器解析HTML
    • HTML 解析,生成DOM树
    • CSS 解析,生成CSSOM树
    • JS 解析,生成语法树,预编译,或者执行
    • 资源请求
  12. HTML渲染
    • 根据DOM树和CSSOM树结合生成渲染树,进行布局与渲染
  13. JS执行
    • js执行Load事件