网络传输(个人整理 多网站)

网络传输

留一个问题:浏览器是怎么解析同源的?

js脚本的同源判断 document.domain(浏览器基础安全策略)

脚手架代理的同端口webpack请求代理实现?

跨域以及解决跨域的方法(常规)

同源策略(产生)

同源:"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源

同源的意义是:不同信任度的来源链接不会污染源页面,也不会拿到客户端资源信息

限制内容有

Cookie、LocalStorage、IndexedDB 等存储性内容//都有domain 和path的观念
DOM 节点 //限制了不同源的DOC对象或者JS脚本读取设置当前脚本属性
AJAX 请求发送后,结果被浏览器拦截了//发送可以 但不能接收跨域报文 同时携带的cookie也是严格按照发送同源进行携带

三个标签可以允许加载跨域资源:

<\img src=‘xxx’>

<\link href=‘xxx’>

<\script src=‘xxx’>//都是需要资源外链的跨域引入 原生属性

本质:请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了//比对报文头

跨域解决方案:

jsonp、cors、postMessage、websocket、Node中间件代理(两次跨域)、nginx反向代理、window.name + iframe、location.hash + iframe、document.domain + iframe

CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方案,JSONP只支持GET请求,JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。不管是Node中间件代理还是nginx反向代理,主要是通过同源策略对服务器不加限制。日常工作中,用得比较多的跨域方案是cors和nginx反向代理

逐一解析
1.jsonp

原理:利用

优缺点:JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持get方法具有局限性, 不安全可能会遭受XSS攻击。//默认发送形式导致形式单一,引入资源不安全会导致引入不安全XSS注入脚本

实现流程:就是通过script的src中预设一些传递参数,通过服务端检验回复报文,JSONP由两部分组成有回调函数和数据,执行返回报文的回调字符串。

jsonp封装实现跨域请求

解决发送传输的字符串为服务器所识别

同时要考虑客户端创建接收函数的重复性

手写jsonp
2.cors

解释:跨源资源共享 Cross-Origin Resource Sharing(CORS) ,它新增的一组HTTP首部字段,允许服务端其声明哪些源站有权限访问哪些资源,它允许浏览器向声明了 CORS 的跨域服务器,发出 XMLHttpReuest 请求,从而克服 Ajax 只能同源使用的限制。//预先设置防止拦截的形式。

实现条件:Cors需要浏览器和后端同时的支持(IE 8 和 9 需要通过XDomainRequest 来实现),浏览器必须首先使用OPTION方法发起一个预检请求,获知服务端是否允许该跨域请求。

开启方式:服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。

Access-Control-Allow-Origin: <origin> | *//哪些域访问资源
Access-Control-Allow-Methods: <method>[, <method>]*//预检的响应,指明实际请求允许使用的HTTP方法
Access-Control-Allow-Headers: <field-name>[, <field-name>]*//允许携带的首部字段
Access-Control-Max-Age: <delta-seconds>//预检缓存
Access-Control-Allow-Credentials: true//是否可以发送cookie
3.postMessage

解释含义:postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可以用来解决很多问题,窗口之间的跨域问题:

  • 页面和其打开的新窗口的数据传递//window上
  • 多窗口之间消息传递
  • 页面与嵌套的iframe消息传递
  • 上面三个场景的跨域数据传递

解决:postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递

4.websocket

实现跨域的原理是采用了不同的网络传输方式,实现了新协议

网站为了实现推送技术,在早期使用的是短长轮询的形式进行(前端定时器),这样的访问形式会造成大量的无用报文消耗,于是HTML5定义了WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

占用端口:WebSocket 与 HTTP 和 HTTPS 使用相同的 TCP 端口,可以绕过大多数防火墙的限制。大部分情况采用80端口,在TLS安全协议上运行之时,则采用443端口。

实现原理:实现双工通信,允许服务端主动向客户端推送数据

5.Node中间件代理(两次跨域)

实现原理:通过代理服务器转发的形式,架设一个同源的服务器,用于接收转发报文。

nginx反向代理:搭建一个nginx中转服务器,用于转发请求

实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。

反向代理:

反向代理的工作原理是,代理服务器来接受客户端的网络访问连接请求,然后服务器将请求有策略的转发给网络中实际工作的业务服务器,并将从业务服务器处理的结果,返回给网络上发起连接请求的客户端。

通俗来将就是采用服务端之间的信息沟通方式过滤一遍信息后再进行客户端传递。

正向代理:

普通的中间服务器的报文转发

nginx配置

1.第一部分是全局块区域:配置运行 Nginx服务器的用户(组)、允许生成的 worker process 数,进程 PID存放路径、日志存放路径和类型以及配置文件的引入等。

2.第二部分是events块,涉及的是网络连接:是否开启对多 work process下的网络连接进行序列化,是否允许同时接收多个网络连接,选取哪种事件驱动模型来处理连接请求,每个 work process 可以同时支持的最大连接数等

3.第三部分是HTTP块

代理、缓存和日志定义等绝大多数功能和第三方模块的配置都在这里。需要注意的是:http块也可以包括 http全局块、server块。

  • http全局块:http全局块配置的指令包括:文件引入、MIME-TYPE 定义、日志自定义、连接超时时间、单链接请求数上限等。
  • server块:这块和虚拟主机有密切关系,从用户角度看,虚拟主机和一台独立的硬件主机是完全一样的,该技术的产生是为了节省互联网服务器硬件成本。

MIME (Multipurpose Internet Mail Extensions) 是描述消息内容类型的标准,用来表示文档、文件或字节流的性质和格式。MIME 的组成结构非常简单,由类型与子类型两个字符串中间用 分隔而组成,不允许有空格。

6.window属性+iframe

通过iframe引入时window的内置信息(如.name属性等)进行信息的传递

  1. iframe标签的跨域能力
  2. window.name属性值在文档刷新后依旧存在的能力
7.location.hash+iframe

通过地址栏的location.hash(hash后方的数据更新不会引起页面刷新,页面之间的沟通可以通过轮询调度)

8.document.domain + iframe

实现一个主域相同,子域不同下的强行domain设置,截断子域操作

前后端实时通讯的方法:

  • WebSocket: IE10以上才支持,Chrome16, FireFox11,Safari7以及Opera12以上完全支持,移动端形势大
  • event-source: IE完全不支持(注意是任何版本都不支持),Edge76,Chrome6,Firefox6,Safari5和Opera以上支持, 移动端形势大好
  • AJAX轮询: 用于兼容低版本的浏览器
  • 永久帧( forever iframe)可用于兼容低版本的浏览器
  • flash socket 可用于兼容低版本的浏览器

1.WebSocket详解

参考:万字长文,一篇吃透WebSocket:概念、原理、易错常识、动手实践 - 云+社区 - 腾讯云 (tencent.com)

基础相关:

1.诞生背景:网站为了实现推送技术,在早期使用的是短长轮询的形式进行(前端定时器),这样的访问形式会造成大量的无用报文的消耗,于是HTML5定义了WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

2.协议头修改:采用的是ws或者wss(使用了TLS的websocket)的URL。

3.占用端口:WebSocket 与 HTTP 和 HTTPS 使用相同的 TCP 端口,可以绕过大多数防火墙的限制。大部分情况采用80端口,在TLS安全协议上运行之时,则采用443端口。

4.优点:

  • *1)*较少的控制开销:在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小;
  • *2)*更强的实时性:由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于 HTTP 请求需要等待客户端发起请求服务端才能响应,延迟明显更少;
  • *3)*保持连接状态:与 HTTP 不同的是,WebSocket 需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息;
  • *4)*更好的二进制支持:WebSocket 定义了二进制帧,相对 HTTP,可以更轻松地处理二进制内容;
  • *5)*可以支持扩展:WebSocket 定义了扩展,用户可以扩展协议、实现部分自定义的子协议
API学习

1.构造函数:new WebSocket(url [, protocols]);//url表示连接的URL,protocols字段标识的是协议字符串或者一个包含协议字符串的数组。

2.属性:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AzO2e3HE-1665836300465)(C:\Users\86189\Pictures\笔记图\image-20220604161152811.png)]

属性解析:

  • *1)*binaryType:使用二进制的数据类型连接;
  • *2)*bufferedAmount(只读):未发送至服务器的字节数;
  • *3)*extensions(只读):服务器选择的扩展;
  • *4)*onclose:用于指定连接关闭后的回调函数;
  • *5)*onerror:用于指定连接失败后的回调函数;
  • *6)*onmessage:用于指定当从服务器接受到信息时的回调函数;
  • *7)*onopen:用于指定连接成功后的回调函数;
  • *8)*protocol(只读):用于返回服务器端选中的子协议的名字;
  • *9)*readyState(只读):返回当前 WebSocket 的连接状态,共有 4 种状态://状态参
  • - CONNECTING — 正在连接中,对应的值为 0;
  • - OPEN — 已经连接并且可以通讯,对应的值为 1;
  • - CLOSING — 连接正在关闭,对应的值为 2;
  • - CLOSED — 连接已关闭或者没有连接成功,对应的值为 3
  • *10)*url(只读):返回值为当构造函数创建 WebSocket 实例对象时 URL 的绝对路径。
  • 11)方法close 关闭wb的连接
  • 12)方法send 加入传输队列的api

3.wb对象的事件()函数执行阶段的回调

上面介绍的属性中有提到–可以自行添加事件监听,也可以调用它本身的事件监听调用生命周期。

const wb=new WebSocket('ws://cloud.tencent.com')
        console.log(wb.readyState)
        wb.onerror=function (data){
            console.log(data)
        }

4.发送二进制数据

利用js全局的构造函数Blob传入字符串流和生成相应的二进制大对象。

WebSocket连接的生命周期

preload

在使用 WebSocket 实现全双工通信之前,客户端与服务器之间需要先进行握手(Handshake)且他是一次HTTP协议的握手形式

此次握手携带了一些参数信息:信息的传输率,字母表,奇偶校验,中断过程,和其他特性

WebSocket握手协议

WebSocket 通过 HTTP/1.1 协议的 101 状态码进行握手。为什么要利用好HTTP来完成报文握手呢?

WebSocket其实是HTTP协议上的一种补充,他们有交集但并不是全部,一旦web服务器和客户端建立起websocket协议的通信连接,之后所有的通信都依靠这个专用连接进行。

1.请求

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
  • *1)*Connection:必须设置 Upgrade,表示客户端希望连接升级;
  • *2)*Upgrade:字段必须设置 websocket,表示希望升级到 WebSocket 协议;
  • *3)*Sec-WebSocket-Version:表示支持的 WebSocket 版本。RFC6455 要求使用的版本是 13,之前草案的版本均应当弃用;
  • *4)*Sec-WebSocket-Key:是随机的字符串,服务器端会用这些数据来构造出一个 SHA-1 的信息摘要;
  • *5)*Sec-WebSocket-Extensions:用于协商本次连接要使用的 WebSocket 扩展:客户端发送支持的扩展,服务器通过返回相同的首部确认自己支持一个或多个扩展;
  • *6)*Origin:字段是可选的,通常用来表示在浏览器中发起此 WebSocket 连接所在的页面,类似于 Referer。但是,与 Referer 不同的是,Origin 只包含了协议和主机名称。

2.握手的报文响应是,服务器返回状态码 101 Switching Protocols的响应。

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
  • 101 响应码确认升级到 WebSocket 协议;
  • 设置 Connection 头的值为 “Upgrade” 来指示这是一个升级请求(HTTP 协议提供了一种特殊的机制,这一机制允许将一个已建立的连接升级成新的、不相容的协议);
  • Upgrade 头指定一项或多项协议名,按优先级排序,以逗号分隔。这里表示升级为 WebSocket 协议;
  • 签名的键值验证协议支持。

3.通信时不再使用HTTP的数据帧,而是使用websocket独立的数据帧。

WebSocket的数据帧格式

preload

手写websocket服务器万字长文,一篇吃透WebSocket:概念、原理、易错常识、动手实践 - 云+社区 - 腾讯云 (tencent.com)

判断是否断开

WebSocket心跳,通过心跳帧的形式通过隔段时间的定时服务端发送判断是否客户端已经断开连接。ping帧和pong帧)

2. server-sent-event

它是服务器推送的一个网络事件的接口,一个 EventSource 实例会对 HTTP 服务开启一个持久化的连接,它会一直以text/event-stream 格式发送事件,会一直保持开启直到被要求关闭。

一旦连接开启,来自服务端传入的消息会以事件的形式分发至你代码中。如果接收消息中有一个事件字段,触发的事件与事件字段的值相同。如果没有事件字段存在,则将触发通用事件。

  • 优点:(1)只需一次请求,便可以stream的方式多次传送数据,节约资源和带宽 (2)相对WebSocket来说简单易用 (3)内置断线重连功能(retry)
  • 缺点: (1)是单向的,只支持服务端->客户端的数据传送,客户端到服务端的通信仍然依靠AJAX,没有”一家人整整齐齐“的感觉(2)兼容性令人担忧,IE浏览器完全不支持

3.AJAX轮询

长轮询和短轮询的区别:长轮询是阻塞请求直到有数据传递或超时才返回。 客户端JavaScript响应处理函数会在处理完服务器返回的信息后,再次发出请求,客户端再次建立连接,周而复始。

短轮询每隔一段事件进行一次访问

4.Flash Socket

5.永久帧

Forever Iframe

Forever Iframe(永存的Iframe)技术涉及了一个置于页面中的隐藏Iframe标签,该标签的src属性指向返回服务器端事件的servlet路径。每次在事件到达时,servlet写入并刷新一个新的script标签,该标签内部带有JavaScript代码,iframe的内容被附加上这一script标签,标签中的内容就会得到执行。

  1. 优点:实现简单,在所有支持iframe的浏览器上都可用。

  2. 缺点: 没有方法可用来实现可靠的错误处理或是跟踪连接的状态,因为所有的连接和数据都是由浏览器通过HTML标签来处理的,因此你没有办法知道连接何时在哪一端已被断开了。

ajax篇及ajax扩展

ajax指的是Asynchronous Javascript And XML(异步的js代码和xml)

为什么要使用ajax,ajax可以减少请求的数据量进行精准请求,节省网络带宽。

ajax的工作原理:

Ajax相当于在用户和服务器之间加了一个中间层,使用户操作与服务器响应异步化。并不是所有的用户请求都提交给服务器,像一些数据验证和数据处理等都交给Ajax引擎自己来做,只有确定需要从服务器读取新数据时再由Ajax引擎代为向服务器提交请求。

Ajax的原理简单来说通过XmlHttpRequest对象来向服务器发送异步请求,从服务器获得数据,然后用JavaScript来操作DOM而更新页面。(添加回调函数执行)

其中最关键的就是从服务器获得请求的数据:

XMLHttpRequest是ajax的核心机制,它是在IE5中首先引入的,是一种支持异步请求的技术。简单的说,也就是JavaScript可以及时向服务器提出请求和处理响应,而不阻塞用户。达到无刷新的效果。

ajax的简单使用

创建核心对象xhr

xhr

XMLHttpRequest也经历过特性变革:

在1.0中:它不能发送二进制文件,只能发送纯文本数据,且只能分析数据发送的完成态,受同源策略的限制不能发送跨域的请求

在2.0中:它实现了跨域请求发送,只是服务端的返回会在客户端再接受校验,同时它也支持发送和接受二进制的数据,它新增了formData对象的数据体,支持发送表单数据。并且可以支持(进度条的实现),可以设置请求的超时时间。

xhr对象可以编辑请求头:

Accept:客户端可以处理的内容类型。比如:Accept: */*

Accept-Charset:客户端可以处理的字符集类型。比如:Accept-Charset: utf8

Accept-Encoding:客户端可以处理的压缩编码。比如:Accept-Encoding: gzip, deflate, br

Accept-Language:客户端当前设置的语言。比如:Accept-Language: zh-CN,zh;q=0.9,en;q=0.8

Connection:客服端与服务器之间连接的类型。比如:Connection: keep-alive

Cookie:当前页面设置的任何Cookie

Host:发出请求页面所在的域。

Referer:表示当前请求页面的来源页面的地址,即当前页面是通过此来源页面里的链接进入的。

User-Agent:客户端的用户代理字符串。一般包含浏览器、浏览器内核和操作系统的版本型号信息。

Content-Type:客户端告诉服务器实际发送的数据类型。比如:Content-Type: application/x-www-form-urlencoded

xhr也实现了各种API来实现对于请求方法等的编写:

1.open()方法只是初始化请求不会真正发送send()方法用于发送HTTP请求

2.setRequestHeader()可以自定义请求头(部分)。

3.readyState属性和onreadystatechange事件:表示请求响应会话状态的属性值。(五个属性,可以使用onreadystatechange绑定钩子函数)(异步实现)

4.timeout属性和ontimeout事件:可以判定一个请求是否成果了。

5.overrideMimeType()方法,方法能够重写服务器返回的MIME类型,从而让浏览器进行不一样的处理。(防止浏览器自动解析失误,拿到原始文本)

6.withCredentials属性是一个布尔值,表示跨域请求时是否协带凭据信息(cookie、HTTP认证及客户端SSL证明等)。默认为false。

常见的Post请求数据方式

1.application/x-www-form-urlencoded:浏览器原生的表单发送形式

2.multipart/form-data表单发送时,让表单的enctype等于multipart/form-data

3.application/json:发送ajax请求时,告诉服务端这是序列化后的JSON字符串。

4.text/xml:使用HTTP作为传输协议,XML作为编码方式的远程调用规范。

xhr的响应的属性和内容

响应头方面:

1.Content-Type:服务器告诉客户端响应内容的类型和采用字符编码。比如:Content-Type: text/html; charset=utf-8

2.Content-Length:服务器告诉客户端响应实体的大小。比如:Content-Length: 8368

3.Content-Encoding:服务器告诉客户端返回的的压缩编码格式。比如:Content-Encoding: gzip, deflate, br

属性和方法方面:

1.status属性可以看出服务器回应的状态码,对http的标识

2.statusText属性属性返回一个字符串,表示服务器发送的状态说明

3.response属性属性表示服务器返回的数据。它可以是任何数据类型,比如字符串、对象、二进制对象等

4.getResponseHeader()方法方法返回HTTP头信息指定字段的值,如果还没有收到服务器的响应或者指定字段不存在,返回null。

5.getAllResponseHeaders()方法:getAllResponseHeaders()方法返回一个字符串,表示服务器发来的所有HTTP头信息。

注意:常见的xhr请求和其封装的方法已经出现了一些问题,引入了fetch的解决方式。

fetch

Fetch的理解和使用_叨唠的博客-CSDN博客_fetch函数的功能

定义

Fetch本质上是一种标准,该标准定义了请求、响应和绑定的流程。 Fetch标准还定义了Fetch () JavaScript API,它在相当低的抽象级别上公开了大部分网络功能,我们今天讲的主要是Fetch API。Fetch API 提供了一个获取资源的接口(包括跨域)。它类似于 XMLHttpRequest ,但新的API提供了更强大和灵活的功能集。 Fetch 的核心在于对 HTTP 接口的抽象,包括 Request,Response,Headers,Body,以及用于初始化异步请求的 global fetch。

fetch的使用

fetch(input?: Request | string, init?: RequestInit): Promise<Response>//fetch对资源的请求会返回promise对象在内部请求完成后被resolve 可以使用async来实现异步转同步

参数:接收两个参数input和options

这些都是抽象底层的操作,通过配置项的形式传入:

method: 请求使用的方法,如 GET、POST。
headers: 请求的头信息,包含与请求关联的Headers对象。
body: 请求的 body 信息。注意 GET 或 HEAD 方法的请求不能包含 body 信息
mode: 请求的模式,如 cors、 no-cors 或者 same-origin。
credentials: 请求的 credentials,如 omit、same-origin 或者 include。为了在当前域名内自动发送 cookie , 必须提供这个选项。

cache: 请求的 cache 模式: default、 no-store不缓存、 reload 、 no-cache有缓存但仍需要去服务器获取资源 、 force-cache 或者 only-if-cached 。
redirect: 可用的 redirect 模式: follow (自动重定向), error (如果产生重定向将自动终止并且抛出一个错误), 或者 manual (手动处理重定向). 在Chrome中默认使用follow(Chrome 47之前的默认值是manual)。
referrer: 一个 USVString 可以是 no-referrer、client或一个 URL。默认是 client。
referrerPolicy: 指定了HTTP头部referer字段的值。可能为以下值之一: no-referrer、 no-referrer-when-downgrade、 origin、 origin-when-cross-origin、 unsafe-url 。

fetch的错误处理

fetch只有在网络错误的情况下,会将返回的promise调用reject值。调用resolve的成果返回中含有404.

fetch的特别处理

1.credentials 设置:控制发送请求时是否带上cookie

2.中止:可以通过AbortController中止fetch请求,将创建abort对象的signal属性传递给fetch的配置项(应该是什么轮询机制吧)

3.分离出了Request Response Header对象

4.Headers对象可以用于配置请求头中的参数键对值(get append等方法),Response对象中.header属性指向其响应头。

URl拼接

URLSearchParams构造函数的使用:在对不同的对象处理后生成正确的url参数携带形式。

关于axios

封装条件下的ajax库,集成了些譬如参数拆解请求,以及promise对象体返回的过程操作,使得代码更加简便。

它有几个特点:1.基于promise 2.支持响应拦截器 3.支持请求取消 4批量发送和请求/响应数据转换。

源码解析
axios                                                             
├─ lib                                                             // 项目源码目录
│  ├─ adapters                                                     // 请求适配器
│  │  ├─ http.js                                                   // http适配器
│  │  └─ xhr.js                                                    // xhr适配器
│  ├─ axios.js                                                     // axios的实例
│  ├─ cancel                                                       // 请求取消模块
│  ├─ core                                                         // 核心模块
│  │  ├─ Axios.js                                                  // Axios对象
│  │  ├─ buildFullPath.js                                          // url构建
│  │  ├─ createError.js                                            // 自定义异常相关
│  │  ├─ dispatchRequest.js                                        // 请求封装
│  │  ├─ enhanceError.js                                           // 自定义异常相关
│  │  ├─ InterceptorManager.js                                     // 拦截器类
│  │  ├─ mergeConfig.js                                            // 配置合并工具
│  │  ├─ settle.js                                                 // promise处理工具
│  │  └─ transformData.js                                          // 数据转换工具
│  ├─ defaults.js                                                  // 默认配置
│  ├─ helpers                                                      // 各种工具函数         
│  └─ utils.js

Axios的一次基本使用流程:

1.初始化axios实例(利用传入配置初始化预设函数内的参数,同时可以绑定自己的拦截器等等)

2.执行请求拦截器

3.根据当前环境选择合适的网络适配器(adapter)

4.处理响应数据

5.执行响应拦截器

在五个阶段出现以后,我们更深入的了解一下这五个阶段的源码层面

createInstance(这个函数是用来初始化axios实例的)

function createInstance(defaultConfig) {
  // 根据默认配置构建个上下文对象,包括默认配置和请求、响应拦截器对象  可能有mergeoptions参与
  var context = new Axios(defaultConfig);//原生类
  // 创建实例 bind后返回的是一个函数,并且上下文指向context (bind会使得原型方法上的this指向为context生成的新函数为实例对象)
  var instance = bind(Axios.prototype.request, context);
  // 拷贝prototype到实例上 类似于把Axios的原型上的方法(例如: request、get、post...)继承到实例上,this指向为context
  utils.extend(instance, Axios.prototype, context);
  // 拷贝上下文对象属性(默认配置和请求、相应拦截器对象)到实例上, this指向为context
  utils.extend(instance, context);
  
  // 创建axios实例,一般axios封装 应该都会用到 (我们把一些默认、公共的配置都放到一个实例上,复用实例,无需每次都重新创建实例)//复用实例方法(因为原有实例保留有自身默认配置项)
  instance.create = function create(instanceConfig) {
    // 这里mergeConfig 就是用来深度合并的
    return createInstance(mergeConfig(defaultConfig, instanceConfig));
  };
     return instance;
}

关于bind函数:

module.exports = function bind(fn, thisArg) {
  return function wrap() {
    var args = new Array(arguments.length);
    for (var i = 0; i < args.length; i++) {
      args[i] = arguments[i];
    }
    return fn.apply(thisArg, args);//apply函数改变了this指向使得fn函数的上下文改变成为thisArg 本质上函数只是改变了this 通过新函数调用原函数
  };
};

关于utils.extend() - 实例的复制

utils.extend()也是一个工具函数:

function extend(a, b, thisArg) {
  forEach(b, function assignValue(val, key) {//foreach遍历b中所有属性值,将其键队值传入函数中
    if (thisArg && typeof val === 'function') {
      a[key] = bind(val, thisArg);//判断上下文对象存在且val为函数的话 就把创建给a一个以thisArg为this指向的方法
    } else {
      a[key] = val;//直接复制属性
    }
  });
  return a;//返回复制完成对象
}

关于Foreach:

function forEach(obj, fn) {
  // Don't bother if no value provided
  if (obj === null || typeof obj === 'undefined') {
    return;
  }

  // Force an array if not already something iterable
  if (typeof obj !== 'object') {
    /*eslint no-param-reassign:0*/
    obj = [obj];
  }

  if (isArray(obj)) {
    // Iterate over array values
    for (var i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj);
    }//遍历对象键值 调用call方法 成功对每一个fn使用
  } else {
    // Iterate over object keys
    for (var key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        fn.call(null, obj[key], key, obj);
      }
    }
  }
}

两个extend

 utils.extend(instance, Axios.prototype, context);
 utils.extend(instance, context);

可以将上下文对象实例 以及pro上的所有方法和属性扩展到实例上

扩展内容

在axios最后将被返回时,它内部又添加了几个静态的方法。

axios.create方法(创建新实例的工厂)可以通过调用mergeConfig()//深拷贝结合)来合并用户配置和默认配置。从而我们可以得到一个自定义的instance

axios.all方法(可以同时执行多个请求)直接调用了Promise的all方法进行了一次封装

axios.spread(用于调用函数并且扩展参数数组的语法糖)

module.exports = function spread(callback) {//在then处执行
  return function wrap(arr) {//被then调用接收了返回值结果
    return callback.apply(null, arr);//callback方法接收到了该结果可以操作更多
  };
};

axios.Cancel(主动取消请求)用户可以调用该接口取消请求。

const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('xxxxxxxxx', {
  cancelToken: source.token//配置里传入了新建对象的token  这里是用他的工厂方法得到的
}).catch(function(thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
     // 处理错误
  }
});
source.cancel('请求被用户取消!');//source调用cancel 便会通知已经配置过的停止异步操作 并且抛出thrown的语句

//方法二
const CancelToken = axios.CancelToken;
let cancel;
axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {//这个函数传入了executor等待调用 会给cancel赋值 他在cancletoken执行时调用 让外界拿到了内部初始化的函数
    cancel = c;
  })
});
cancel();

关于CancelToken.source()是一个工厂方法,它生产了一个对象,包含CancelToken()和一个默认的cancel()

cancel()对象 内部重写了toString方法和自己的message属性原型上的__CANCEL__ = true 利用这个原型对象上的值判断是否是cancel对象

module.exports = function isCancel(value) {
  return !!(value && value.__CANCEL__);
};

canceltoken请求取消操作的对象,它的内部需要传入一个executor

function CancelToken(executor) {
  //如果executor不是function,抛出错误
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }
  // 初始化外部的resolvePromise
  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    // 让外部的 resolvePromise 指向 resolve
    // 这样的话,我们执行 resolvePromise() 就相当于将Promise resolve
    resolvePromise = resolve;
  });
  // 获取上下文 这里的token即 CancelToken对象
  var token = this;
  // 当我们的executor()被执行时,我们的resolvePromise的状态也从pending变成了resolved
  executor(function cancel(message) {
    if (token.reason) {
      // 如果CancelToken对象上已经存在reason,说明已经取消,多余的取消函数将失去作用
      return;
    }
    
    // 为cancelToken设置reason(一个Cancel对象)
    token.reason = new Cancel(message);//获得一个新reason
    
    // 在我们resolve时,触发了adapter的resolve事件。adapter
    resolvePromise(token.reason);
  });
}

// 判断请求是否已经被取消,就抛出this.reason, 也就是上面的 Cancel 对象
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    throw this.reason;
  }
};

// CancelToken的工厂方法
CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};

module.exports = CancelToken;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3YKZRYal-1665836300468)(C:\Users\86189\Pictures\笔记图\image-20221014152715117.png)]

外界利用暴露的cancel方法来resolve该处promise 实现abort()中断发送

梳理一遍:

首先用户配置了cancleToken这个配置 并且使用CancelToken这个类的构造器 把executor函数传入其中做为参数,该函数在调用时会把,调用处传入的中断函数c传递给用户cancel,由此用户便能拿到中断函数。

于是我们又进入到了类构造器CancelToken中,这个函数内部实例化了自己的一个属性名叫promise,这个Promise的resolve方法被构造器函数本地变量resolve获取,在resolve调用时pormise改变状态,又用token获取自身,之后执行executor传出自己定义的cancel函数,这个cancel函数会判断该canceltoken类是否已经取消了(判断reason),否则为reason复制为Cancel对象并且传入用户参数,在cancel执行时,resolve也执行,并且传入了这个reason也就是新建的Cancel对象。

然后在请求发起前,会访问该配置项的ct对象,判断是否中断,通过其.promise的then方法等待用户执行传入上一步末尾的cancel,在请求期间resolve的调用都会导致request.abort的触发。

讲解完createInstense函数后,我们回到对于Axios对象的解析中,因为在之前创建实例的过程中,只是调用了该函数并没有拆解。

Axios对象

function Axios(instanceConfig) {
  this.defaults = instanceConfig;//构造函数模式,属性挂载了用户传入的默认配置(或者时merge配置)
  // 初始化拦截器
  this.interceptors = {//采用InterceptorManager()对象做为拦截器使用
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

重点介绍拦截器:interceptors(InterceptorManager对象)

function InterceptorManager() {
  this.handlers = [];
}

InterceptorManager.prototype.use = function use(fulfilled, rejected) {//use会拿到传入的两个函数参数,分别为请求函数和错误函数
  this.handlers.push({//handlers栈会把这一组拦截器压入栈中
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;//返回栈长
};

InterceptorManager.prototype.eject = function eject(id) {//用于移除拦截器
  if (this.handlers[id]) {
    this.handlers[id] = null;//有些粗糙
  }
};

InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};//遍历调用

到目前为止只是为我们的实例对象初始化了拦截器与拦截器栈,但是它是如何生效调用的呢。

Axios.prototype.request = function request(config) {
  if (typeof config === 'string') {//如果是string
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {//传入了url 需要解构config重新merge合并
    config = config || {};
  }
  config = mergeConfig(this.defaults, config);//整合配置
  // 设置config.method
  if (config.method) {//如果配置了方法
    config.method = config.method.toLowerCase();//将方法名字变为小写
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();//默认转换小写
  } else {
    config.method = 'get';
  }
  // 连接拦截器中间件
  var chain = [dispatchRequest, undefined];//它首先会调用这两个函数 为什么在下面解释
  // 实例化一个Promise
  var promise = Promise.resolve(config);

  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });//遍历拦截器调用这两个函数 将其添加到流程链chain中
    //如果是请求拦截器,我们将它放到流程链的前面。
//如果是响应拦截器,我们将它放在流程链的后面。

  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());//链式调用 直到排空chain栈
  }

  return promise;
};//简单来说就是调用的时候将预先配置先执行

上面讲了拦截器的调用原理,和调用过程:我们重新推演一下:

axios在传入配置时,通过argument整合了一些配置项方面的问题,然后合并了自身的默认配置,然后初始化了自己的拦截中间件chain栈,以及以个已经解决过的promise实例,为其两个拦截器forEach调用其中压入栈的操作,最后在chain中采用链式调用把config传入两个方法中。返回的是执行后的结果。

但是chain栈排空的第一步执行了dispatchRequest方法【undefined因为默认了resolve解决】

关于上面遗留的一点dispatchRequest在下面进行讲解(使用配置的适配器向服务器发送请求//说明的)

module.exports = function dispatchRequest(config) {
  throwIfCancellationRequested(config);//经过try catch捕获后 抛出异常 内部原理是通过配置内的reason
  //请求取消否  
  config.headers = config.headers || {};
	//确认请求头
  // Transform request data
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );//转换请求数据 transformData的原理暂时不清楚
  // Flatten headers
  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers
  );//合并去重请求头
  utils.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
  );//删除多余请求头

  var adapter = config.adapter || defaults.adapter;//适配器初始化方法?adapter方法
//初始化适配器
  return adapter(config).then(function onAdapterResolution(response) {//执行adapter,根据是否成功调用后面的两个函数
    throwIfCancellationRequested(config);//是否取消
    // Transform response data
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );
    return response;//交给下一个拦截器的response 同理可得request
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }

    return Promise.reject(reason);
  });
};

这里的具体就是dispatch调用了adapter的适配器方法,传入了配置,如果成功或者失败会进入下一个拦截器的promise调用链。//适配器执行了函数配置内的所有操作

下面就进入适配器adapter的讲解:

适配器的返回值是一个promise,方便拦截器的链式调用

XHR适配器

XHR适配器封装了一个Promise,面向浏览器的XHR请求,本质上是封装了浏览器内置的XMLHttpRequest()。

// xhr.js
// axios 的 xhr 适配器

module.exports = function xhrAdapter(config) {//传入配置参数
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    // 初始化数据和请求头
    var requestData = config.data;
    var requestHeaders = config.headers;

    // 对于FormData,Content-Type 由浏览器自行设定
    if (utils.isFormData(requestData)) {
      delete requestHeaders['Content-Type']; // Let the browser set it
    }

    // 通过 XMLHttpRequest 构造函数 生成 XHR 对象
    var request = new XMLHttpRequest();


    // HTTP basic authentication,后面会讲
    if (config.auth) {
      var username = config.auth.username || '';
      var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
      requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
    }

    // 根据BaseUrl和后缀来拼接URL
    var fullPath = buildFullPath(config.baseURL, config.url);


    // 准备了一个异步的 request(只是准备、没有发送)
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);//已经传入了发送方法和发送路径


    // 设置超时时间 (单位为毫秒)
    request.timeout = config.timeout;


    // 每次 readyState 从一个值变成另一个值,都会触发 readystatechange 事件
    // 监听 readystatechange 事件
    request.onreadystatechange = function handleLoad() {

      // 响应阶段,细节信息我们在下面会提到
      if (!request || request.readyState !== 4) {//调用已经完成
        return;
      }

      // 该请求出错了,我们没有得到响应, 将由 onerror 处理
      // 本地文件传输协议除外,它即使请求成功,也会返回状态为 0
      if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {//非本地传输 或者只是创建
        return;
      }

      // 获取响应头
      var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;//利用该方法获取响应头 具备选择性

      // 获取响应数据
      var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;//是否为文本

      // 响应
      var response = {//匹配
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config: config,
        request: request
      };

      // 处理 promise
      settle(resolve, reject, response);//返回的是该函数
        {//移动到这儿方便看
            module.exports = function settle(resolve, reject, response) {
              var validateStatus = response.config.validateStatus;
              if (!response.status || !validateStatus || validateStatus(response.status)) {
                resolve(response);
              } else {
                reject(createError(
                  'Request failed with status code ' + response.status,
                  response.config,
                  null,
                  response.request,
                  response
                ));
              }
            };
        }

      // 清理请求数据
      request = null;
    };

    // 处理下面的状态:
    // 请求被取消,更确切地来说,是非正常的取消(相对于手动取消)
    request.onabort = function handleAbort() {
      if (!request) {
        return;
      }

      reject(createError('Request aborted', config, 'ECONNABORTED', request));

      // 清理请求数据
      request = null;//非正常中止报错
    };

    // 处理一些底层的网络错误,这些错误的具体内容被浏览器所隐藏 抛出一个笼统的 Network Error
    request.onerror = function handleError() {
      // Real errors are hidden from us by the browser
      // onerror should only fire if it's a network error
      reject(createError('Network Error', config, null, request));

      // 清理请求数据
      request = null;
    };

    // 处理超时
    request.ontimeout = function handleTimeout() {
      var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded';
      if (config.timeoutErrorMessage) {
        timeoutErrorMessage = config.timeoutErrorMessage;
      }
      reject(createError(timeoutErrorMessage, config, 'ECONNABORTED',
        request));

      // 清理请求数据
      request = null;
    };

    // 添加xsrf头
    // 仅在标准浏览器环境中运行时才能执行此操作。
    // 例如 web worker 和 react-native 之类,则不会

    if (utils.isStandardBrowserEnv()) {
      // 添加xsrf头
      var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
        cookies.read(config.xsrfCookieName) :
        undefined;

      if (xsrfValue) {
        requestHeaders[config.xsrfHeaderName] = xsrfValue;
      }
    }

    // 添加请求头
    // 对每个请求头执行 setRequestHeader 函数
    if ('setRequestHeader' in request) {
      utils.forEach(requestHeaders, function setRequestHeader(val, key) {
        if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
          // 如果数据未定义,则删除Content-Type
          delete requestHeaders[key];
        } else {
          // 否则,将标头添加到请求中
          request.setRequestHeader(key, val);
        }
      });
    }

    // 默认情况下,跨源请求不提供凭据( cookie、 HTTP 认证和客户端 SSL 证书)。可以通过将
    // withCredentials 属性设置为 true 来表明请求会发送凭据。
    if (!utils.isUndefined(config.withCredentials)) {
      request.withCredentials = !!config.withCredentials;
    }

    // 如果用户设置了响应类型,则处理之。
    // 主要的响应类型列举如下:
    // "arraybuffer" "blob" "document" "json" "text";
    if (config.responseType) {
      try {
        request.responseType = config.responseType;
      } catch (e) {
        // 浏览器预期抛出的DOMException不兼容 XMLHttpRequest Level 2。
        // 但是,对于“json”类型,可以取消此设置,因为可以默认使用transformResponse功能对其进行解析。
        if (config.responseType !== 'json') {
          throw e;
        }
      }
    }
    
    // 下载事件  我们可以通过这个属性来实现对下载进度的监控。
    if (typeof config.onDownloadProgress === 'function') {
      request.addEventListener('progress', config.onDownloadProgress);
    }

    // 上传事件,注意: 并非所有浏览器都支持上传事件
    // 我们可以通过这个属性来实现对上传进度的监控。
    if (typeof config.onUploadProgress === 'function' && request.upload) {
      request.upload.addEventListener('progress', config.onUploadProgress);
    }

    
    if (config.cancelToken) {
      // 处理取消行为
      config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
          return;
        }

        request.abort();
        reject(cancel);
        // 清理请求
        request = null;
      });
    }

    if (!requestData) {
      // 清理请求
      requestData = null;
    }

    // 发送请求
    request.send(requestData);
  });
};

基本都由适配器绑定了关于本次网络请求的所有监听器等,通过配置,生成了自己的requestpromise responsepromise的返回值。上面便是axios的具体请求流程,一些工厂方式创建,拓展,和封装的xhr ajax对象。

HTTP basic authentication:在HTTP中,Basic Authorization基本认证是一种用来允许Web浏览器或其他客户端程序在请求时提供用户名和口令形式的身份凭证的一种登录验证方式。

requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);

关于HTTP适配器:

http适配器封装了一个Promise,用于执行http请求,服务于Node.js环境。

module.exports = function httpAdapter(config) {
  return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {
    // 初始化配置
    var resolve = function resolve(value) {
      resolvePromise(value);
    };
    var reject = function reject(value) {
      rejectPromise(value);
    };
    var data = config.data;
    var headers = config.headers;

    // 设置默认的用户代理(某些服务器需要)
    if (!headers['User-Agent'] && !headers['user-agent']) {
      headers['User-Agent'] = 'axios/' + pkg.version;
    }

    // 二进制数据流
    if (data && !utils.isStream(data)) {
      // Buffer代表一个缓冲区,主要用于操作二进制数据流
      if (Buffer.isBuffer(data)) {
        // Nothing to do...
      } else if (utils.isArrayBuffer(data)) {
        data = Buffer.from(new Uint8Array(data));
      } else if (utils.isString(data)) {
        data = Buffer.from(data, 'utf-8');
      } else {
        // 转换条件错误
        // 转换后的数据必须是字符串,ArrayBuffer,Buffer 或 Stream
        return reject(createError(
          'Data after transformation must be a string, an ArrayBuffer, a Buffer, or a Stream',
          config
        ));
      }

      // 如果数据存在,添加 Content-Length 标头
      headers['Content-Length'] = data.length;
    }

    // HTTP Basic Auth, 用于权限相关的处理
    var auth = undefined;
    if (config.auth) {
      var username = config.auth.username || '';
      var password = config.auth.password || '';
      // 拼接auth字符串
      auth = username + ':' + password;
    }

    // 处理请求url, 通过baseURL(如果有的话),和后缀url来进行拼接
    // 仅当请求的URL不是绝对URL时,
    // 才通过将baseURL与请求的URL组合在一起来创建新的URL。
    var fullPath = buildFullPath(config.baseURL, config.url);

    // 处理 fullPath(这里的 parse 调用了一个第三方的的url处理库,细节代码比较复杂)
    var parsed = url.parse(fullPath);

    // 处理协议相关
    var protocol = parsed.protocol || 'http:';

    // 当url中带有权限相关内容时,我们重写 auth 变量
    if (!auth && parsed.auth) {
      var urlAuth = parsed.auth.split(':');
      var urlUsername = urlAuth[0] || '';
      var urlPassword = urlAuth[1] || '';
      auth = urlUsername + ':' + urlPassword;
    }

    // 如果我们通过赋值auth对象的方式来生成Authorization请求头,那么我们删除默认的请求头
    if (auth) {
      delete headers.Authorization;
    }

    // 运用正则表达式 /https:?/ 进行https的判断
    var isHttpsRequest = isHttps.test(protocol);

    // `httpAgent` 和 `httpsAgent` 分别在 node.js 中用于定义在执行 http 和 https 时使用的自定义代理
    var agent = isHttpsRequest ? config.httpsAgent : config.httpAgent;

    // 汇总配置
    var options = {
      // 这里的build url 在url 的末尾附加了一系列参数
      // 如果用户没有传入自定义的 config.paramsSerializer(序列化函数) 那么序列化将按照默认的方式进行
      path: buildURL(parsed.path, config.params, config.paramsSerializer).replace(/^\?/, ''),
      method: config.method.toUpperCase(),
      headers: headers,
      agent: agent,
      agents: {http: config.httpAgent, https: config.httpsAgent},
      auth: auth
    };

    if (config.socketPath) {
      options.socketPath = config.socketPath;
    } else {
      // 配置主机名和端口
      options.hostname = parsed.hostname;
      options.port = parsed.port;
    }

    // 代理配置 'proxy' 定义代理服务器的主机名称和端口
    var proxy = config.proxy;
    if (!proxy && proxy !== false) {
      var proxyEnv = protocol.slice(0, -1) + '_proxy';
      var proxyUrl = process.env[proxyEnv] || process.env[proxyEnv.toUpperCase()];
      if (proxyUrl) {
        var parsedProxyUrl = url.parse(proxyUrl);
        var noProxyEnv = process.env.no_proxy || process.env.NO_PROXY;
        var shouldProxy = true;

        if (noProxyEnv) {
          var noProxy = noProxyEnv.split(',').map(function trim(s) {
            return s.trim();
          });

          shouldProxy = !noProxy.some(function proxyMatch(proxyElement) {
            if (!proxyElement) {
              return false;
            }
            if (proxyElement === '*') {
              return true;
            }
            if (proxyElement[0] === '.' &&
              parsed.hostname.substr(parsed.hostname.length - proxyElement.length) === proxyElement) {
              return true;
            }

            return parsed.hostname === proxyElement;
          });
        }


        if (shouldProxy) {
          proxy = {
            host: parsedProxyUrl.hostname,
            port: parsedProxyUrl.port
          };

          if (parsedProxyUrl.auth) {
            var proxyUrlAuth = parsedProxyUrl.auth.split(':');
            proxy.auth = {
              username: proxyUrlAuth[0],
              password: proxyUrlAuth[1]
            };
          }
        }
      }
    }

    if (proxy) {
      options.hostname = proxy.host;
      options.host = proxy.host;
      options.headers.host = parsed.hostname + (parsed.port ? ':' + parsed.port : '');
      options.port = proxy.port;
      options.path = protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path;

      // 基本代理授权
      if (proxy.auth) {
        var base64 = Buffer.from(proxy.auth.username + ':' + proxy.auth.password, 'utf8').toString('base64');
        options.headers['Proxy-Authorization'] = 'Basic ' + base64;
      }
    }

    var transport;

    // https 代理配置
    var isHttpsProxy = isHttpsRequest && (proxy ? isHttps.test(proxy.protocol) : true);
    if (config.transport) {
      transport = config.transport;
    } else if (config.maxRedirects === 0) {
      transport = isHttpsProxy ? https : http;
    } else {
      // 定义在 node.js 中 follow 的最大重定向数目
      if (config.maxRedirects) {
        options.maxRedirects = config.maxRedirects;
      }

      // 使用重定向功能包装协议
      transport = isHttpsProxy ? httpsFollow : httpFollow;
    }

    if (config.maxBodyLength > -1) {
      options.maxBodyLength = config.maxBodyLength;
    }

    // 创建请求 调用了nodejs原生的API 即 http.request(options[,callback])
    var req = transport.request(options, function handleResponse(res) {
      if (req.aborted) return;

      // uncompress the response body transparently if required
      var stream = res;

      // return the last request in case of redirects
      var lastRequest = res.req || req;


      // if no content, is HEAD request or decompress disabled we should not decompress
      // 如果没有内容,体现为状态码204 -- 对于一些提交到服务器处理的数据,只需要返回是否成功的情况下,可以考虑使用状态码204
      // head 请求 -- 检查资源的有效性,也是没有消息体的
      // 解压缩被禁用
      if (res.statusCode !== 204 && lastRequest.method !== 'HEAD' && config.decompress !== false) {
        // 获取内容的编码方式
        switch (res.headers['content-encoding']) {
          /*eslint default-case:0*/
          case 'gzip':
          case 'compress':
          case 'deflate':
            // add the unzipper to the body stream processing pipeline
            stream = stream.pipe(zlib.createUnzip());

            // remove the content-encoding in order to not confuse downstream operations
            delete res.headers['content-encoding'];
            break;
        }
      }

      // 响应配置
      var response = {
        status: res.statusCode,
        statusText: res.statusMessage,
        headers: res.headers,
        config: config,
        request: lastRequest
      };

      // 流式文件
      if (config.responseType === 'stream') {
        response.data = stream;
        // 这个函数根据响应状态解决或拒绝Promise:
        // 它会 查看response的 status 根据状态来决定 resolve 还是 reject
        settle(resolve, reject, response);

      } else {
        var responseBuffer = [];
        // 获取数据事件
        stream.on('data', function handleStreamData(chunk) {
          responseBuffer.push(chunk);

          // 如果指定 maxContentLength ,请确保内容长度不超过maxContentLength, 否则我们执行reject
          if (config.maxContentLength > -1 && Buffer.concat(responseBuffer).length > config.maxContentLength) {
            stream.destroy();
            reject(createError('maxContentLength size of ' + config.maxContentLength + ' exceeded',
              config, null, lastRequest));
          }
        });

        // 错误事件
        stream.on('error', function handleStreamError(err) {
          if (req.aborted) return;
          reject(enhanceError(err, config, null, lastRequest));
        });

        // 请求结束事件
        stream.on('end', function handleStreamEnd() {

          // 连接缓冲区数组
          // 如果列表中有多个项目,则创建一个新的缓冲区。
          var responseData = Buffer.concat(responseBuffer);

          if (config.responseType !== 'arraybuffer') {
            // 转换成字符串
            responseData = responseData.toString(config.responseEncoding);
            if (!config.responseEncoding || config.responseEncoding === 'utf8') {
              responseData = utils.stripBOM(responseData);
            }
          }

          response.data = responseData;
          // 在上面的文章里已经提到了这个函数
          // 这个函数根据响应状态解决或拒绝Promise:
          // 它会 查看response的 status 根据状态来决定 resolve 还是 reject
          settle(resolve, reject, response);
        });
      }
    });

    // 处理错误
    req.on('error', function handleRequestError(err) {
      if (req.aborted && err.code !== 'ERR_FR_TOO_MANY_REDIRECTS') return;
      reject(enhanceError(err, config, null, req));
    });

    // 处理请求超时
    if (config.timeout) {
      // 有时,响应将非常缓慢,并且没有响应,connect事件将被事件循环系统阻止。
      // 并且将触发计时器回调,并在连接之前调用abort(),然后获取“套接字挂起”和代码ECONNRESET。
      // 这时,如果我们有大量请求,nodejs将在后台挂断一些套接字。而且这个数字会不断增加。
      // 然后这些挂断的套接字将逐渐使CPU变得更糟。
      // ClientRequest.setTimeout将在指定的毫秒内触发,并可以确保在连接后将触发abort()。
      req.setTimeout(config.timeout, function handleRequestTimeout() {
        req.abort();
        reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED', req));
      });
    }

    if (config.cancelToken) {
      // Handle cancellation
      config.cancelToken.promise.then(function onCanceled(cancel) {
        if (req.aborted) return;

        req.abort();
        reject(cancel);
      });
    }

    // 发送请求
    if (utils.isStream(data)) {
      data.on('error', function handleStreamError(err) {
        reject(enhanceError(err, config, null, req));
      }).pipe(req);
    } else {
      req.end(data);
    }
  });
};

CDN:

cdn加速原理:CDN的全称是Content Delivery Network,即内容分发网络。其目的是通过在现有的internet中增加一层新的网络架构,将网站的内容发布到最接近用户的网络边缘,使用户可以就近取得所需的内容,提高用户访问网站的响应速度。CDN有别于镜像,因为它比镜像更智能,或者可以做这样一个比喻:CDN=更智能的镜像+缓存+流量导流。因而,CDN可以明显提高Internet网络中信息流动的效率。从技术上全面解决由于网络带宽小、用户访问量大、网点分布不均等问题,提高用户访问网站的响应速度。

cdn请求的流程是:用户在自己的浏览器中输入要访问的网站的域名,浏览器向本地DNS请求对该域名的解析,本地DNS将请求发到网站的主DNS,主DNS根据一系列的策略确定当时最适当的CDN节点,并将解析的结果(IP地址)发给用户,用户向给定的CDN节点请求相应网站的内容。(需要网站DNS智能分析当前用户最佳访问ip)

  1. 当用户点击网站页面上的url时,经过本地dns系统解析,dns系统会将域名的解析权给交cname指向的cdn专用dns服务器。
  2. cdn的dns服务器将cdn的全局负载均衡设备ip地址返回给用户。
  3. 用户向cdn的全局负载均衡设备发起内容url访问请求。
  4. cdn全局负载均衡设备根据用户ip,以及用户请求的内容url,选择一台用户所属区域的区域负载均衡设备
  5. 区域负载均衡设备会为用户选择一台合适的缓存服务器提供服务,选择的依据包括:根据用户IP地址,判断哪一台服务器距用户最近;根据用户所请求的URL中携带的内容名称,判断哪一台服务器上有用户所需内容;查询各个服务器当前的负载情况,判断哪一台服务器尚有服务能力。基于以上这些条件的综合分析之后,区域负载均衡设备会向全局负载均衡设备返回一台缓存服务器的IP地址全局负载均衡设备把服务器的IP地址返回给用户。
  6. 用户向缓存服务器发起请求,缓存服务器响应用户请求,将用户所需内容传送到用户终端。如果这台缓存服务器上没有用户想要的内容,而区域均衡设备依然将它分配给了用户,那么这台服务器 就要向它的上一级缓存服务器发起请求内容,直至追溯到网站的源服务器将内容拉回给用户

CDN网络是在用户和服务器之间增加Cache层,主要是通过接管DNS实现,将用户的请求引导到Cache上获得源服务器的数据,从而降低网络的访问的速度。

增加了一次cache并且运算后智能分配了ip值。

浏览器请求

同域请求的并发数限制的原因?

浏览器的并发请求数目限制是针对同一域名的,同一时间针对同一域名下的请求有一定数量限制,超过限制数目的请求会被阻塞(chorme和firefox的限制请求数都是6个)。

限制其数量的原因是:基于浏览器端口的限制和线程切换开销的考虑,浏览器为了保护自己不可能无限量的并发请求,如果一次性将所有请求发送到服务器,也会造成服务器的负载上升。(限制并发数,是因为浏览器线程受限)

URL 路径包含什么, URI 是什么?

一个完整的url分为4部分:

协议 例 Http(超文本传输协议) 、Https、
域名 例www.baidu.com为网站名字。 baidu.com为一级域名,www是服务
端口 不填写的话默认走的是80端口号
路径 http://www.baidu.com/路径1/路径1.2。/表示根目录
查询参数 http://www.baidu.com/路径1/路径1.2?name=“man”(可有可无)
URI 是什么

URI是一个用于标识互联网资源名称的字符串。 该种标识允许用户对网络中(一般指万维网)的资源通过特定的协议进行交互操作。URI的最常见的形式是统一资源定位符(URL),经常指定为非正式的网址。更罕见的用法是统一资源名称(URN),其目的是通过提供一种途径。用于在特定的命名空间资源的标识,以补充网址。

扩展:

URL和URN是URI的子集,URI属于URL更高层次的抽象,一种字符串文本标准

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值