整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

如何使用JavaScript解析URL

Web 开发中,有许多情况需要解析 URL,这篇主要学习如何使用 URL 对象实现这一点。

开始

创建一个以下内容的 HTML 文件,并在浏览器中打开。

<html> 
 <head> 
 <title>JavaScript URL parsing</title> 
 </head> 
 <body> 
 <script> 
 // 激动人心的代码即将写在这里 
 </script> 
 </body> 
</html> 

如果你想尝试本文中的任何内容,可以将其放在 <script> 标记中,保存,重新加载页面,看看会发生什么! 在本教程中,将使用 console.log 来打印所需要的内容,你可以打开开发都工具,来查看内容。

什么是 URL

这应该是相当简单的,但让我们说清楚。 URL 是网页的地址,可以在浏览器中输入以获取该网页的唯一内容。 可以在地址栏中看到它:



URL 是统一资源定位符,对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。互联网上的每个文件都有一个唯一的 URL,它包含的信息指出文件的位置以及浏览器应该怎么处理它。

此外,如果你不熟悉基本 URL 路径的工作方式,可以查看此文学习。

URL 不都长的一样的

这是一个快速提醒 - 有时 URL 可能非常奇怪,如下:

:1234/page/?a=b

file:///Users/username/folder/file.png

获取当前URL

获取当前页面的 URL 非常简单 - 我们可以使用 window.location。

试着把这个添加到我们形如写的的脚本中:

console.log(window.location); 

查看浏览器的控制台:



不是你想要的?这是因为它不返回你在浏览器中看到的实际 URL 地址——它返回的是一个 URL 对象。使用这个 URL 对象,我们可以解析 URL 的不同部分,接下来就会讲到。

创建 URL 对象

很快就会看到,可以使用 URL 对象来了解 URL 的不同部分。如果你想对任何 URL 执行此操作,而不仅仅是当前页面的 URL,该怎么办? 我们可以通过创建一个新的 URL 对象来实现。 以下是如何创建一个:

var myURL = new URL('https://example.com'); 

就这么简单! 可以打印 myURL 来查看 myURL 的内容:

console.log(myURL); 



出于本文的目的,将 myURL 设置为这个值:

var myURL = new URL('https://example.com:4000/folder/page.html?x=y&a=b#section-2') 

将其复制并粘贴到 <script> 元素中,以便你可以继续操作! 这个 URL 的某些部分可能不熟悉,因为它们并不总是被使用 - 但你将在下面了解它们,所以不要担心!

URL 对象的结构

使用 URL 对象,可以非常轻松地获取 URL 的不同部分。 以下是你可以从 URL 对象获得的所有内容。 对于这些示例,我们将使用上面设置的 myURL。

href

URL 的 href 基本上是作为字符串(文本)的整个 URL。如果你想把页面的 URL 作为字符串而不是 URL 对象,你可以写 window.location.href。

console.log(myURL.href); 
// Output: "https://example.com:4000/folder/page.html?x=y&a=b#section-2" 

协议 (protocol)

URL的协议是一开始的部分。这告诉浏览器如何访问该页面,例如通过 HTTP 或 HTTPS。 但是还有很多其他协议,比如 ftp(文件传输协议)和 ws(WebSocket)。通常,网站将使用 HTTP 或 HTTPS。

虽然如果你的计算机上打开了文件,你可能正在使用文件协议! URL对象的协议部分包括:,但不包括 //。 让我们看看 myURL 吧!

console.log(myURL.protocol); 
// Output: "https:" 

主机名(hostname)

主机名是站点的域名。 如果你不熟悉域名,则它是在浏览器中看到的URL的主要部分 - 例如 google.com 或codetheweb.blog。

console.log(myURL.hostname); 
// Output: "example.com" 

端口(port)

URL 的端口号位于域名后面,用冒号分隔(例如 example.com:1234)。 大多数网址都没有端口号,这种情况非常罕见。

端口号是服务器上用于获取数据的特定“通道” - 因此,如果我拥有 example.com,我可以在多个不同的端口上发送不同的数据。 但通常域名默认为一个特定端口,因此不需要端口号。 来看看 myURL 的端口号:

console.log(myURL.port); 
// Output: "4000" 

主机(host)

主机只是主机名和端口放在一起,尝试获取 myURL 的主机:

console.log(myURL.host); 
// Output: "example.com:4000" 

来源(origin)

origin 由 URL 的协议,主机名和端口组成。 它基本上是整个 URL,直到端口号结束,如果没有端口号,到主机名结束。

console.log(myURL.origin); 
// Output: "https://example.com:4000" 

pathname(文件名)

pathname 从域名的最后一个 “/” 开始到 “?” 为止,是文件名部分,如果没有 “?” ,则是从域名最后的一个 “/” 开始到 “#” 为止 , 是文件部分, 如果没有 “?” 和 “#” , 那么从域名后的最后一个 “/” 开始到结束 , 都是文件名部分。

console.log(myURL.pathname); 
// Output: "/folder/page.html" 

锚点(hash)

从 “#” 开始到最后,都是锚部分。可以将哈希值添加到 URL 以直接滚动到具有 ID 为该值的哈希值 的元素。 例如,如果你有一个 id 为 hello 的元素,则可以在 URL 中添加 #hello 就可以直接滚动到这个元素的位置上。通过以下方式可以在 URL 获取 “#” 后面的值:

console.log(myURL.hash); 
// Output: "#section-2" 

查询参数 (search)

你还可以向 URL 添加查询参数。它们是键值对,意味着将特定的“变量”设置为特定值。 查询参数的形式为 key=value。 以下是一些 URL 查询参数的示例:

?key1=value1&key2=value2&key3=value3 

请注意,如果 URL 也有 锚点(hash),则查询参数位于 锚点(hash)(也就是 ‘#’)之前,如我们的示例 URL 中所示:

console.log(myURL.search); 
// Output: "?x=y&a=b" 

但是,如果我们想要拆分它们并获取它们的值,那就有点复杂了。

使用 URLSearchParams 解析查询参数

要解析查询参数,我们需要创建一个 URLSearchParams 对象,如下所示:

var searchParams = new URLSearchParams(myURL.search); 

然后可以通过调用 searchParams.get('key')来获取特定键的值。 使用我们的示例网址 - 这是原始搜索参数:

?x=y&a=b 

因此,如果我们调用 searchParams.get('x'),那么它应该返回 y,而 searchParams.get('a')应该返回 b,我们来试试吧!

console.log(searchParams.get('x')); 
// Output: "y" 
console.log(searchParams.get('a')); 
// Output: "b" 

扩展

获取 URL 的中参数

方法一:正则法

function getQueryString(name) { 
 var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i'); 
 var r = window.location.search.substr(1).match(reg); 
 if (r != null) { 
 return unescape(r[2]); 
 } 
 return null; 
} 
// 这样调用: 
alert(GetQueryString("参数名1")); 
alert(GetQueryString("参数名2")); 
alert(GetQueryString("参数名3")); 

方法二:split拆分法

function GetRequest() { 
 var url = location.search; //获取url中"?"符后的字串 
 var theRequest = new Object(); 
 if (url.indexOf("?") != -1) { 
 var str = url.substr(1); 
 strstrs = str.split("&"); 
 for(var i = 0; i < strs.length; i ++) { 
 theRequest[strs[i].split("=")[0]] = unescape(strs[i].split("=")[1]); 
 } 
 } 
 return theRequest; 
} 
var Request = new Object(); 
Request = GetRequest(); 
// var 参数1,参数2,参数3,参数N; 
// 参数1 = Request['参数1']; 
// 参数2 = Request['参数2']; 
// 参数3 = Request['参数3']; 
// 参数N = Request['参数N']; 

修改 URL 的中某个参数值

荐阅读:

阿里架构师精选Nginx+Redis+Sping+SpringBoot源码级PDF文档分享

微服务+Docker完美教程,被阿里架构师汇集到这2份文档里面了!

引言

对于面试常问的从浏览器输入 URL 到页面展示过程发生了什么?,我想大家都或多或少能说出一二。但是,其实这个问题很有深度,而你是否回答的有深度,在很大程度上会影响到面试官对你的印象。

并且,网上各种资料都是浅尝辄止地讲解这个过程,经常会出现今天看到这个版本,明天看到另一个版本地情况。所以,这次我们就来深入浅出一下这整个过程~

一、Chrome 多进程架构

首先,在开始讲解整个过程前,我们需要认识一下 Chrome 多进程架构。因为,从浏览器输入 URL 到页面渲染的整个过程都是由 Chrome 架构中的各个进程之间的配合完成。

Chrome 的多进程架构:

  • 浏览器进程,它负责用户界面(地址栏、菜单等等)、子进程的管理(例如,进程间通信和数据传递)、存储等等
  • 渲染进程,它负责将接收到的 HTML 文档和 JavaScript 等转化为用户界面
  • 网络进程,它负责网络资源的请求,例如 HTTP请求、WebSocket 模块
  • GPU(图形处理器)进程,它负责对 UI 界面的展示
  • 插件进程,它负责对插件的管理

二、过程详解

2.1 解析输入

发生这个过程的前提,用户在地址栏中输入了 URL,而地址栏会根据用户输入,做出如下判断:

  • 输入的是非 URL 结构的字符串,则会用浏览器默认的搜索引擎搜索该字符串
  • 输入的是 URL 结构字符串,则会构建完整的 URL 结构,浏览器进程会将完整的 URL 通过进程间通信,即 IPC,发送给网络进程

2.2 请求过程

在网络进程接收到 URL 后,并不是马上对指定 URL 进行请求。首先,我们需要进行 DNS 解析域名得到对应的 IP,然后通过 ARP 解析 IP 得到对应的 MAC(Media Access Control Address)地址。

域名是我们取代记忆复杂的 IP 的一种解决方案,而 IP 地址才是目标在网络中所被分配的节点。MAC 地址是对应目标网卡所在的固定地址。

1. DNS 解析

而 DNS 解析域名的过程分为以下几个步骤:

  • 询问浏览器 DNS 缓存
  • 询问本地操作系统 DNS 缓存(即查找本地 host 文件)
  • 询问 ISP(Internet Service Provider)互联网服务提供商(例如电信、移动)的 DNS 服务器
  • 询问根服务器,这个过程可以进行递归和迭代两种查找的方式,两者都是先询问顶级域名服务器查找

2. 通信过程

首先,建立 TCP 连接,即三次握手过程

  • 客户端发送标有 SYN 的数据包,表示我将要发送请求。
  • 服务端发送标有 SYN/ACK 的数据包,表示我已经收到通知,告知客户端发送请求。
  • 客户端发送标有 ACK 的数据包,表示我要开始发送请求,准备被接受。



然后,利用 TCP 通道进行数据传输

  • 服务端接收到数据包,并发送确认数据包已收到的消息到客户端,不断重复这个过程
  • 客户端在发送一个数据包后,未接收到服务端的确定消息,则重新发送该数据包,即 TCP 的重发机制
  • 当接收完所有的数据包后,接收端会按照 TCP 头中的需要进行排序,形成完整的数据

最后,断开 TCP 连接,即四次握手过程

  • 客户端发送请求,申请断开连接,进入等待阶段,此时不会发送数据,但是会继续接收数据。
  • 服务端接收请求后,告知客户端已明白,此时服务端进入等待状态,不会再接收数据,但是会继续发送数据。
  • 客户端收到后,进入下一阶段等待。
  • 服务端发送完剩余的数据后,告知客户端可以断开连接,此时服务端不会发送和接收数据。
  • 客户端收到后,告知服务端我开始断开连接。
  • 服务端收到后,开始断开连接。



而这整个过程的客户端则是网络进程。并且,在数据传输的过程还可能会发生的重定向的情况,即当网络进程接收到状态码为 3xx 的响应报文,则会根据响应报文首部字段中的 Location 字段的值进行重新向,即会重新发起请求

3. 数据处理

当网络进程接收到的响应报文状态码,进行相应的操作。例如状态码为 200 OK 时,会解析响应报文中的 Content-Type 首部字段,例如我们这个过程 Content-Type 会出现 application/javascript、text/css、text/html,即对应 Javascript 文件、CSS 文件、HTML 文件。

详细的 MIME 类型讲解可以看 MDN

2.3 创建渲染进程

当前需要渲染 HTML 时,则需要创建渲染进程,用于后期渲染 HTML。而对于渲染进程,如果是同一站点是可以共享一个渲染进程,例如 a.abc.com 和 c.abc.com 可以共享一个渲染渲染进程。否则,需要重新创建渲染进程

需要注意的是,同站指的是顶级域名二级域名相等

2.4 开始渲染

在创建完渲染进程后,网络进程会将接收到的 HTML、JavaScript 等数据传递给渲染进程。而在渲染进程接收完数据后,此时用户界面上会发生这几件事:

  • 更新地址栏的安全状态
  • 更新地址栏的 URL
  • 前进后退此时 enable,显示正在加载状态
  • 更新网页



2.5 渲染过程

大家都知道页面渲染的过程也是面试中单独会考的点,并且时常会由这个点延申出另一个问题,即如何避免回流和重绘。

渲染过程,是整个从理器输入 URL 到页面渲染过程的最后一步。而页面渲染的过程可以分为 9 个步骤:

  • 解析 HTML 生成 DOM 树
  • 解析 CSS 生成 CSSOM
  • 加载或执行 JavaScript
  • 生成渲染树(Render Tree)
  • 布局
  • 分层
  • 生成绘制列表
  • 光栅化
  • 显示

2.5.1 构建 DOM 树

由于网络进程传输给渲染进程的是 HTML 字符串,所以,渲染进程需要将 HTML 字符串转化成 DOM 树。例如:



需要注意的是这个 DOM 树不同于 Chrome-devtool 中 Element 选项卡的 DOM 树,它是存在内存中的,用于提供 JavaScript 对 DOM 的操作。

2.5.2 构建 CSSOM

构建 CSSOM 的过程,即通过解析 CSS 文件、style 标签、行内 style 等,生成 CSSOM。而这个过程会做这几件事:

  • 规范 CSS,即将 color: blue 转化成 color: rgb() 形式,可以理解成类似 ES6 转 ES5 的过程
  • 计算元素样式,例如 CSS 样式会继承父级的样式,如 font-size、color 之类的。



CSS Object Model 是一组允许用 JavaScript 操纵 CSS 的 API。详细 API 讲解可以看 MDN

2.5.3 加载 JavaScript

通常情况下,在构建 DOM 树或 CSSOM 的同时,如果也要加载 JavaScript,则会造成前者的构建的暂停。当然,我们可以通过 defer 或 sync 来实现异步加载 JavaScript。虽然 defer 和 sync 都可以实现异步加载 JavaScript,但是前者是在加载后,等待 CSSOM 和 DOM 树构建完后才执行 JavaScript,而后者是在异步加载完马上执行,即使用 sync 的方式仍然会造成阻塞。

而 JavaScript 执行的过程,即编译和运行 JavaScript 的过程。由于 JavaScript 是解释型的语言。所以这个过程会是这样的:

  • 针对每句代码进行分行处理,即 Token 化
  • 根据 Token,生成 AST(Abstract Sytanx Tree) 抽象语法树和创建上下文
  • 解释器解析和执行 AST,生成字节码。
  • 编译器针对需要反复执行的代码,生成对应的机器码,提高运行效率

2.5.4 生成渲染树(Render Tree)

在有了 DOM 树和 CSSOM 之后,需要将两者结合生成渲染树 Render Tree,并且这个过程会去除掉那些 display: node 的节点。此时,渲染树就具备元素和元素的样式信息。

2.5.5 布局

根据 Render Tree 渲染树,对树中每个节点进行计算,确定每个节点在页面中的宽度、高度和位置。

需要注意的是,第一次确定节点的大小和位置的过程称为布局,而第二次才被称为回流

2.5.6 分层

由于层叠上下文的存在,渲染引擎会为具备层叠上下文的元素创建对应的图层,而诸多图层的叠加就形成了我们看到的一些页面效果。例如,一些 3D 的效果、动画就是基于图层而形成的。

值得一提的是,对于内容溢出存在滚轮的情况也会进行分层

2.5.7 生成绘制列表

对于存在图层的页面部分,需要进行有序的绘制,而对于这个过程,渲染引擎会将一个个图层的绘制拆分成绘制指令,并按照图层绘制顺序形成一个绘制列表。

2.5.8 光栅化

有了绘制列表后,渲染引擎中的合成线程会根据当前视口的大小将图层进行分块处理,然后合成线程会对视口附近的图块生成位图,即光栅化。而渲染进程也维护了一个栅格化的线程池,专门用于将图块转为位图。

栅格化的过程通常会使用 GPU 加速,例如使用 wil-change、opacity,就会通过 GPU 加速显示

2.5.9 显示

当所有的图块都经过栅格化处理后,渲染引擎中的合成线程会生成绘制图块的指令,提交给浏览器进程。然后浏览器进程将页面绘制到内存中。最后将内存绘制结果显示在用户界面上。

而这个整个从生成绘制列表、光栅化、显示的过程,就是我们常说的重绘的过程

结语

整个浏览器输入 URL 到页面渲染的过程涉及到的知识点非常广,如 Chrome 多进程的架构、HTTP 通信过程、浏览器解析 JavaScript 过程、浏览器绘制页面过程以及一些计算机的基础知识等等,并且,这整个过程的分析其实和 Chrome-devtools 密切相关,所以很好的使用 Chrome-devtools 是非常重要的,后续应该会出一篇关于使用 Chrome-devtools 的指南。当然,本篇文章仍然存在诸多不足,欢迎提 issue ~


作者:五柳
链接:https://juejin.im/post/5e871ee56fb9a03c832b0013

我们向浏览器的地址栏输入URL的时候,网络会进行一系列的操作,最终获取到我们所需要的文件,如何交给浏览器进行渲染

我们所关注的问题也就是:

  • 如何获取到我们所需要的文件
  • 浏览器是如何渲染的

大致的执行顺序

  • URL解析
  • DNS 解析:缓存判断 + 查询IP地址
  • TCP 连接:TCP 三次握手
  • SSL/TLS四次握手(只有https才有这一步)
  • 浏览器发送请求
  • 服务器响应请求并返回数据
  • 浏览器解析渲染页面
  • 断开连接:TCP 四次挥手

URL解析

浏览器先会判断输入的字符是不是一个合法的URL结构,如果不是,浏览器会使用搜索引擎对这个字符串进行搜索

URL结构组成

https://www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#anchor

  • 协议:https:// 互联网支持多种协议,必须指明网址使用哪一种协议,默认是 HTTP 协议。 也就是说,如果省略协议,直接在浏览器地址栏输入www.example.com,那么浏览器默认会访问http://www.example.com。 HTTPS 是 HTTP 的加密版本,出于安全考虑,越来越多的网站使用这个协议。
  • 主机:www.example.com 主机(host)是资源所在的网站名或服务器的名字,又称为域名。上例的主机是www.example.com。 有些主机没有域名,只有 IP 地址,比如192.168.2.15。
  • 端口:https:// 同一个域名下面可能同时包含多个网站,它们之间通过端口(port)区分。 “端口”就是一个整数,可以简单理解成,访问者告诉服务器,想要访问哪一个网站。 默认端口是80,如果省略了这个参数,服务器就会返回80端口的网站。 端口紧跟在域名后面,两者之间使用冒号分隔,比如www.example.com:80。
  • 路径:/path/to/myfile.html 路径(path)是资源在网站的位置。比如,/path/index.html这个路径,指向网站的/path子目录下面的网页文件index.html 互联网的早期,路径是真实存在的物理位置。现在由于服务器可以模拟这些位置,所以路径只是虚拟位置 路径可能只包含目录,不包含文件名,比如/foo/,甚至结尾的斜杠都可以省略 这时,服务器通常会默认跳转到该目录里面的index.html文件(即等同于请求/foo/index.html),但也可能有其他的处理(比如列出目录里面的所有文件),这取决于服务器的设置 一般来说,访问www.example.com这个网址,很可能返回的是网页文件www.example.com/index.html
  • 查询参数:?key1=value1&key2=value2 查询参数(parameter)是提供给服务器的额外信息。参数的位置是在路径后面,两者之间使用?分隔 查询参数可以有一组或多组。每组参数都是键值对(key-value pair)的形式,同时具有键名(key)和键值(value),它们之间使用等号(=)连接。比如,key1=value就是一个键值对,key1是键名,value1是键值 多组参数之间使用&连接,比如key1=value1&key2=value2
  • 锚点:#anchor 锚点(anchor)是网页内部的定位点,使用#加上锚点名称,放在网址的最后,比如#anchor 浏览器加载页面以后,会自动滚动到锚点所在的位置 锚点名称通过网页元素的id属性命名

DNS解析

DNS(Domain Names System),域名系统,是互联网一项服务,是进行域名和与之相对应的 IP 地址进行转换的服务器

第一步:缓存判断

判断是正确的URL格式之后,DNS会在我们的缓存中查询是否有当前域名的IP地址

基本步骤:

  • 浏览器缓存:浏览器检查是否在缓存中
  • 操作系统缓存:操作系统DNS缓存,去本地的hosts文件查找
  • 路由器缓存:路由器DNS缓存
  • ISP 缓存: ISP DNS缓存(ISP DNS 就是在客户端电脑上设置的首选 DNS 服务器,又称本地的DNS服务器)

在经历上述缓存查找还没有找到的话,就进行下一步查询操作

第二步:查询IP地址

浏览器会去根域名服务器中查找,如果还没有就去顶级域名服务器中查找,最后是权威域名服务器。

找到IP地址后,将它记录在缓存中,供下次使用。

TCP连接:三次握手

简单理解

简单的理解就是:

客户端:hello,你好,你是server吗?
服务端:hello,你好,我是server,你是client吗
客户端:yes,我是client
开始数据传输.....
——————————————————————————————————————————
客户端(男人):我喜欢你,咱俩处对象吧
服务端(女人):我也喜欢你,我答应你
客户端(男人):太棒了,我们现在去看电影吧
开始数据传输.....


然后双方就正确建立连接,开始传输数据

详细分析

  • 第一次握手:客户端发送一个带 SYN=1,Seq=x 的数据包到服务器端口 第一次握手,由浏览器发起,告诉服务器我要发送请求了 SYN(synchronous):请求建立连接 seq(sequence):随机序列号 请注意TCP规定SYN被设置为1的报文段不能携带数据但要消耗掉一个序号。
  • 第二次握手:服务器发回一个带 SYN=1, ACK=1, seq=y, ack= x+1 的响应包以示传达确认信息 第二次握手,由服务器发起,告诉客户端我准备接受了,你赶紧发送吧 ACK(acknowledgement):确认,是一个确定字符 ack:ack=上一次的seq+1,作用是接受上一次远端主机传来的seq,加一然后再传给客户端,提示客户端已经成功接收上一次所有数据 请注意这个报文段不能携带数据,因为它是SYN被设置为一的报文段但同样要消耗掉一个序号
  • 第三次握手:客户端再回传一个带 ACK,seq = x + 1, ack = y + 1 的数据包,代表“握手结束” 第三次握手,由浏览器发送,告诉服务器,我马上就发了,准备接受吧 确认报文段可以携带数据。但如果不携带数据则不消耗序号,在这种情况下所发送的下一个数据报文段的序号仍是x + 1

然后双方就正确建立连接,开始传输数据

SSL/TLS四次握手

HTTPS 建立连接的过程,先进行 TCP 三次握手,再进行 TLS 四次握手(仅对https)

因为 HTTPS 都是基于 TCP 传输协议实现的,得先建立完可靠的 TCP 连接才能做 TLS 握手的事情。

第一次握手:客户端发出请求Client Hello

  • 首先,客户端先向服务器发出加密通信的请求,这被叫做clienthello请求。
  • 在这一步,客户端主要向服务器提供以下信息:
    • 支持的协议版本,比如TLS1.0版本
    • 支持的加密方法,比如RSA公钥加密
    • 一个客户端生成的随机数(client random), 稍后用于生成对话密钥(session key)

第二次握手:服务器回应Server Hello

  • 服务器收到客户端请求后,向客户端发出回应,这叫做serverhello
  • 这一步服务器主要干三件事:
    • 确认使用的加密通信协议版本,比如TLS1.00版本。如果游览器与服务器支持的版本不一致,服务器关闭加密通信
    • 确认使用的加密方法(客户端所支持),比如RSA公钥加密
    • 将服务器证书、非对称加密的公钥,以及一个随机数(Server random)发送给客户端游览器

客户端验证证书

客户端收到服务器回应以后,首先验证服务器证书,验证手段就是执行如下三种检查:

  • 检查证书是否已过期;
  • 检查证书中的域名与实际域名是否一致
  • 检查证书是否是可信机构颁布的

如果,上述过程中有任何一个环节发现问题,那么浏览器就会向访问者显示一个警告,由其选择是否还要继续通信。如果证书受信任,或者是用户接受了不受信的证书,浏览器会生成一串新的随机数(Premaster secret )

第三次握手:客户端回应

此时,浏览器会根据前三次握手中的三个随机数:

  • Client random
  • Server random
  • Premaster secret

通过一定的算法来生成 “会话密钥” (Session Key),这个会话密钥就是接下来双方进行对称加密解密使用的密钥!

第四次握手:服务端回应

服务端收到客户端的回复,利用已知的加密解密方式进行解密,服务器收到客户端的第三个随机数( Premaster secret) 之后,使用同样的算法计算出 “会话密钥” (Session Key)。

至此,整个握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的 HTTP 协议,只不过用 “会话密钥” 加密内容。(非对称加密解密将不再使用,接下来完全由对称加密接手了,因为密钥已经安全的传送给了通信的双方)

总结

  1. 客户端请求建立SSL链接,并向服务发送一个随机数–Client random客户端支持的加密方法,比如RSA公钥加密,此时是明文传输
  2. 服务端回复一种客户端支持的加密方法一个随机数–Server random、授信的服务器证书非对称加密的公钥
  3. 客户端收到服务端的回复后利用服务端的公钥,加上新的随机数–Premaster secret 通过服务端下发的公钥及加密方法进行加密,发送给服务器
  4. 服务端收到客户端的回复,利用已知的加解密方式进行解密,同时利用Client random、Server random和Premaster secret通过一定的算法生成HTTP链接数据传输的对称加密key – session key

浏览器发送请求

连接建立成功之后,浏览器向服务器发送HTTP请求报文,来获取自己想要的数据

请求报文由请求行、请求头、空行、请求体四部分组成

  • 请求行:有请求方法、请求的url、http协议及其版本
  • 请求头:把浏览器的一些基础信息告诉服务器。比如包含了浏览器所使用的操作系统、浏览器内核等信息,以及当前请求的域名信息、浏览器端的 Cookie 信息等
  • 空行:最后一个请求头之后是一个空行,发送回车符和换行符,通知服务器以下不再有请求头
  • 请求体(报文主体/请求中文):当使用POST, PUT等方法时,通常需要客户端向服务器传递数据。这些数据就储存在请求正文中。在请求包头中有一些与请求正文相关的信息,例如: 现在的Web应用通常采用Rest架构,请求的数据格式一般为json。这时就需要设置Content-Type: application/json。

服务端响应请求并返回数据

服务器对http请求报文进行解析,并给客户端发送HTTP响应报文对其进行响应

HTTP响应报文也是由状态行、响应头、空行、响应体四部分组成

  • 响应行/状态行:由 HTTP 版本协议字段、状态码和状态码的描述文本 3 个部分组成
  • 响应头:用于指示客户端如何处理响应体,告诉浏览器响应的类型、字符编码和字节大小等信息
  • 空行:最后一个响应头部之后是一个空行,发送回车符和换行符,通知服务器以下不再有响应头部。
  • 响应体:返回客户端所需数据

这个时候浏览器拿到我们服务器返回的HTML文件,可以开始解析渲染页面

浏览器解析渲染页面

当我们经历了上述的一系列步骤之后,我们的浏览器就拿到了我的HTML文件那么它又是如何解析整个页面并且最终呈现出我们的网页呢?

渲染流程图

简图:从这张图上我们可以得出一个重要的结论:下载CSS文件并不会阻塞HTML的解析

详图

详细解析步骤

解析一:HTML解析过程

默认情况下服务器会给浏览器返回index.html文件,所以解析HTML是所有步骤的开始:解析HTML,会 构建DOM Tree

当遇到我们的script文件的时候,我们是不能进行去构建DOM Tree的。它会停止继续构建,首先下载JavaScript代码,并且执行JavaScript的脚本,只有等到JavaScript脚本执行结束后,才会继续解析HTML,构建DOM树

具体的相关细节看下面的script与页面解析

解析二:生成CSS规则

  • 在解析的过程中,如果遇到CSS的link元素,那么会由浏览器负责下载对应的CSS文件
    • 注意:下载CSS文件是不会影响DOM的解析的
  • 浏览器下载完CSS文件后,就会对CSS文件进行解析,解析出对应的规则树
    • 我们可以称之为 CSSOM (CSS Object Model,CSS对象模型

解析三:构建Render Tree

当有了 DOM Tree 和 CSSOM Tree 后,就可以两个结合来 构建 Render Tree

  • 注意一:link元素不会阻塞DOM Tree的构建过程,但是会阻塞Render Tree的构建过程
    • 因为Render Tree在构建时,需要对应的CSSOM Tree。当我们DOMTree解析完成的时候,如果CSSOM Tree没解析完成就会阻塞。当然一般情况下浏览器会进行优化处理,不会傻傻的等待
  • 注意二:Render Tree和DOMTree并不是一一对应的关系
    • 比如对于display为none的元素,压根不会出现在render tree中

解析四:布局(layout)和绘制(Paint)

  • 第四步是在**渲染树(Render Tree)**上运行 布局(Layout) 以计算每个节点的几何体。 渲染树会 表示 要显示哪些节点以及其他样式,但是 不表示 每个节点的尺寸、位置 等信息 布局的主要目的是为了确定呈现树中所有节点的宽度、高度和位置信息
  • 第五步是将每个节点 绘制(Paint) 到屏幕上 在绘制阶段,浏览器将布局阶段计算的 每个frame转为屏幕上实际的像素点 包括 将元素的可见部分进行绘制,比如文本、颜色、边框、阴影、替换元素(比如img)

特殊解析:composite合成

  • 绘制的过程,可以将布局后的元素绘制到多个合成图层中【这是浏览器的一种优化手段】
  • 默认情况下,标准流中的内容都是被绘制在同一个图层(Layer)中的
  • 而一些特殊的属性,会创建一个新的合成层(Compositinglayer ),并且新的图层可以利用GPU来加速绘制
    • 因为每个合成层都是单独渲染的
  • 那么哪些属性可以形成新的合成层呢?常见的一些属性:
    • 3D transforms
    • video、canvas、iframe
    • opacity动画转换时
    • position: fixed
    • will-change:一个实验性的属性,提前告诉浏览器元素可能发生哪些变化
    • animation或 transition设置了opacity、transform
  • 分层确实可以提高性能,但是它以内存管理为代价,因此不应作为web性能优化策略的一部分过度使用

其他相关概念

回流

  • 回流reflow(也可以称之为重排) 第一次确定节点的大小和位置,称之为布局(layout) 之后对节点的大小、位置修改 重新计算 称之为回流
  • 什么情况下引起回流呢? 比如DOM结构发生改变(添加新的节点或者移除节点) 比如改变了布局(修改了width、height、padding、font-size等值) 比如窗口resize(修改了窗口的尺寸等) 比如调用getComputedStyle方法获取尺寸、位置信息

重绘

  • 重绘repaint【字面理解就是对页面再做绘制】 第一次渲染内容称之为绘制(paint) 之后重新渲染称之为重绘
  • 什么情况下会引起重绘呢? 比如修改背景色、文字颜色、边框颜色、样式等

联系

  • 回流一定会引起重绘,所以回流是一件很消耗性能的事情。
  • 所以在开发中要尽量避免发生回流 修改样式时尽量一次性修改【比如通过cssText修改,比如通过添加class修改】 尽量 避免频繁的操作DOM【我们可以在一个DocumentFragment或者父元素中将要操作的DOM操作完成,再一次性的操作】 尽量 避免通过getComputedStyle获取尺寸、位置等信息 对某些元素使用position的absolute或者fixed【并不是不会引起回流,而是开销相对较小,不会对其他元素造成影响】

script元素

script元素和页面联系

  • 我们现在已经知道了页面的渲染过程,但是JavaScript在哪里呢? 事实上,浏览器在解析HTML的过程中,遇到了 script元素是不能继续构建DOM树的 它会 停止继续构建,首先下载JavaScript代码,并且执行JavaScript的脚本 只有 等到JavaScript脚本执行结束后,才会继续解析HTML,构建DOM树
  • 为什么要这样做呢? 这是 因为JavaScript的作用之一就是操作DOM,并且可以修改DOM 如果我们等到DOM树构建完成并且渲染再执行JavaScript会造成严重的回流和重绘,影响页面的性能 所以会在遇到script元素时,优先下载和执行JavaScript代码,再继续构建DOM树
  • 但是这个也往往会带来新的问题,特别是现代页面开发中: 在目前的开发模式中(比如Vue、React),脚本往往比HTML页面更“重”,处理时间需要更长 所以会造成页面的解析阻塞,在脚本下载、执行完成之前,用户在界面上什么都看不到
  • 为了解决这个问题,script元素给我们提供了两个属性(attribute) : defer和async

defer属性

  • defer属性告诉浏览器 不要等待脚本下载,而继续解析HTML,构建DOM Tree
    • 脚本会 由浏览器来进行下载,但是不会阻塞DOM Tree的构建过程
    • 如果脚本提前下载好了,它会 等待DOM Tree构建完成,在DOMContentLoaded事件之前先执行defer中的代码
  • 所以DOMContentLoaded总是会等待defer中的代码先执行完成
<script src="./foo.js" defer></script>
<script>
	 window.addEventListener("DOMContentLoaded",()=>{
         console.log("DOMContentLoaded");
     })
</script>
  • 多个带defer的脚步是可以保持正确的执行顺序的
  • 从某种角度来说,defer可以提高页面的性能,并且推荐放到head元素中
  • 注意:defer仅适用于外部脚本,对于script默认内容会被忽略

async属性

  • async特性与defer有些类似,它也能够让脚本不阻塞页面
  • async是让一个脚本完全独立的:
    • 浏览器 不会因async 脚本而阻塞(与defer类似)
    • async脚本不能保证顺序,它是独立下载、独立运行,不会等待其他脚本
    • async不会能保证在DOMContentLoaded之前或者之后执行
  • defer通常用于需要在文档解析后操作DOM的JavaScript代码,并且对多个script文件有顺序要求的
  • async通常用于独立的脚本,对其他脚本,甚至DOM没有依赖的

断开连接:TCP 四次挥手

在渲染完成后,浏览器可能会继续加载页面中的其他资源,如异步加载的内容或者通过JavaScript生成的动态内容。

而在此过程中,如果没有其他资源需要加载,浏览器将与服务器之间的TCP连接断开。

简单理解

复制代码主动方:我已经关闭了向你那边的主动通道了,这是我最后一次给你发消息了,之后只能被动接收你的信息了
被动方:收到你通道关闭的信息
被动方:那我也告诉你,我这边向你的主动通道也关闭了
主动方:最后收到你关闭的信息,OK结束
断开连接,结束通讯
————————————————————————————————————————————————————————————————————————————
提出分手的可能是男生(客户端),也可能是女生(服务端)
主动方:分手吧,我不喜欢你了!
被动方:行,你等我忙完手上的工作我在收拾你!
被动方:我忙完了,分手就分手!
主动方:好,好聚好散,拜拜!
断开连接,结束通讯

详细分析

由于TCP连接是全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。

任何一方都可以在数据传送结束后发出连接释放的通知,所有主动发起关闭请求可以是客户端,也可以是服务端

这里我们假设是由客户端先主动发起关闭请求

  • 第一次挥手:TCP客户进程会发送TCP连接释放报文段,并进入终止等待1(FIN-WAIT-1)状态。 FIN:终止位,表示断开TCP连接 TCP规定终止位FIN等于1的报文段即使不携带数据,也要消耗掉一个序号
  • 第二次挥手:TCP服务器进程收到TCP连接释放报文段后,会发送一个普通的TCP确认报文段并进入关闭等待(CLOSE-WAIT)状态。 序号seq字段的值设置为v,与之前收到的TCP连接释放报文段中的确认号匹配 TCP客户进程收到TCP确认报文段后就进入终止等待2(FIN-WAIT-2)状态,等待TCP服务器进程发出的TCP连接释放报文段 这时的TCP连接属于半关闭状态,也就是TCP客户进程已经没有数据要发送了。但如果TCP服务器进程还有数据要发送,TCP客户进程仍要接收,也就是说从TCP服务器进程到TCP客户进程这个方向的连接并未关闭,这个状态可能要持续一段时间。
  • 第三次挥手:TCP服务器进程发送TCP连接释放报文段 假定序号seq字段的值为w,这是因为在半关闭状态下,TCP服务器进程可能又发送了一些数据。 确认号ack字段的值为u+1,这是对之前收到的TCP连接释放报文段的重复确认。
  • 第四次挥手:TCP服务器进程收到确定报文段后就进入关闭状态,而TCP客户进程还要经过2MSL后才能进入关闭状态。

之后断开连接,结束通讯

总结

  • 浏览器先判断是否为合法的url格式,不合法则在搜索引擎中搜索
  • 合法后,DNS解析会先判断缓存中是否有url的ip地址。
  • 缓存的查询顺序是:浏览器缓存 -> 操作系统缓存(本地的hosts文件) -> 路由器缓存 -> 本地的DNS服务器缓存
  • 在缓存中没有的情况,则向服务器发起请求查询ip地址。
  • 查询IP地址的顺序是:根域名服务器 -> 顶级域名服务器 -> 权威域名服务器。直到查找到返回,并将其存储在缓存中下次使用
  • TSP建立连接,也就是三次握手
  • 第一次握手,携带建立连接请求SYN=1和随机序列seq=x
  • 第二次握手,携带确定字段ACK=1、连接请求SYN=1、随机序列seq=y和ack为上一次握手的seq+1,就是x+1
  • 第三次握手,携带确定字段ACK=1、ack=y+1、seq=x+1
  • 如果是https,还有一个TLS四次握手
  • 第一次握手,客户端向服务端发送 支持的协议版本 + 支持的加密方法 + 生成的随机数
  • 第二次握手,服务端向客户端发送 证书 + 公钥 + 随机数
  • 第三次握手前,客户端会先验证证书有没有过期、域名对不对、是否可信机构颁发的。
  • 没有问题或者用户接受不受信的证书,浏览器会生成一个新的随机数
  • 第三次握手,将之前的三个随机数通过一定的算法生成会话秘钥,之后的加密解密都是用这个秘钥
  • 第四次握手,服务端收到回复,是用确定的加密方法进行解密,得到第三个随机数,使用同样的算法计算出会话秘钥
  • 建立连接之后,浏览器发送http请求
  • 请求报文由请求行、请求头、空行和请求体组成
  • 服务器解析请求报文,返回响应报文
  • 响应报文由响应行、响应头、空行和响应体组成,我们需要的html文件就在响应体中
  • 浏览器拿到html文件并开始解析,构建dom tree。遇到css文件,下载并构建CSSOM tree。等到两者都构建完成之后,一起构建Render tree。然后进行布局和绘制
  • 其中遇到了script标签,则停止构建dom tree,等下载完成之后才会继续构建dom tree
  • 当资源传输完毕之后,TSP关闭连接,进行四次挥手的操作,其中四次挥手的操作客户端和服务器都可以发起
  • 第一次挥手,携带断开连接的FIN=1、确定字段ACK=1、随机序列seq=u,ack=v
  • 第二次挥手,携带确定字段ACK=1、随机序列seq=v,ack=u+1
  • 第三次挥手,携带确定字段ACK=1、断开连接FIN=1、随机序列seq=w、ack=u+1
  • 第四次挥手,携带确定字段ACK=1,随机序列seq=u+1,ack=w+1
  • 等待2MSL后进入关闭状态
  • 断开连接,结束通讯


作者:前端实习生鲸落
链接:https://juejin.cn/post/7279093851000242234