全面分析前端的网络请求方式

一、前端进行网络请求的关注点

大多数情况下,在前端发起一个网络请求我们只需关注下面几点:

  • 传入基本参数(  url ,请求方式)

  • 请求参数、请求参数类型

  • 设置请求头

  • 获取响应的方式

  • 获取响应头、响应状态、响应结果

  • 异常处理

  • 携带  cookie 设置

  • 跨域请求

二、前端进行网络请求的方式

  • form 表单、  ifream 、刷新页面

  • Ajax – 异步网络请求的开山鼻祖

  • jQuery – 一个时代

  • fetch –  Ajax 的替代者

  • axiosrequest 等众多开源库

三、关于网络请求的疑问

  • Ajax 的出现解决了什么问题

  • 原生  Ajax 如何使用

  • jQuery 的网络请求方式

  • fetch 的用法以及坑点

  • 如何正确的使用  fetch

  • 如何选择合适的跨域方式

带着以上这些问题、关注点我们对几种网络请求进行一次全面的分析。

四、Ajax的出现解决了什么问题

Ajax 出现之前, web 程序是这样工作的:

这种交互的的缺陷是显而易见的,任何和服务器的交互都需要刷新页面,用户体验非常差, Ajax 的出现解决了这个问题。 Ajax 全称 AsynchronousJavaScript+XML (异步 JavaScriptXML

使用 Ajax ,网页应用能够快速地将增量更新呈现在用户界面上,而不需要重载(刷新)整个页面。

Ajax 本身不是一种新技术,而是用来描述一种使用现有技术集合实现的一个技术方案,浏览器的 XMLHttpRequest 是实现 Ajax 最重要的对象( IE6 以下使用 ActiveXObject )。

尽管 XAjax 中代表 XML , 但由于 JSON 的许多优势,比如更加轻量以及作为 Javascript 的一部分,目前 JSON 的使用比 XML 更加普遍。

五、原生Ajax的用法

这里主要分析 XMLHttpRequest 对象,下面是它的一段基础使用:

下面分别对 XMLHttpRequest 对象常用的的函数、属性、事件进行分析。

函数

open

用于初始化一个请求,用法:

  • method :请求方式,如  getpost

  • url :请求的  url

  • async :是否为异步请求

send

用于发送 HTTP 请求,即调用该方法后 HTTP 请求才会被真正发出,用法:

  • param :http请求的参数,可以为  stringBlob 等类型。

abort

用于终止一个 ajax 请求,调用此方法后 readyState 将被设置为 0 ,用法:

setRequestHeader

用于设置 HTTP 请求头,此方法必须在 open() 方法和 send() 之间调用,用法:

getResponseHeader

用于获取 http 返回头,如果在返回头中有多个一样的名称,那么返回的值就会是用逗号和空格将值分隔的字符串,用法:

属性

readyState

用来标识当前 XMLHttpRequest 对象所处的状态, XMLHttpRequest 对象总是位于下列状态中的一个:

|值|状态|描述 |-|-|-| |0 | UNSENT |代理被创建,但尚未调用 open() 方法。 |1 | OPENED | open() 方法已经被调用。 |2 | HEADERS_RECEIVED | send() 方法已经被调用,并且头部和状态已经可获得。 |3 | LOADING | 下载中; responseText 属性已经包含部分数据。 |4 | DONE | 下载操作已完成。

status

表示 http 请求的状态, 初始值为 0 。如果服务器没有显式地指定状态码, 那么 status 将被设置为默认值, 即 200

responseType

表示响应的数据类型,并允许我们手动设置,如果为空,默认为 text 类型,可以有下面的取值:

|值 |描述| |-|-|-| | "" | 将 responseType 设为空字符串与设置为 "text" 相同, 是默认类型 (实际上是 DOMString )。 | "arraybuffer" | response 是一个包含二进制数据的 JavaScriptArrayBuffer 。 | "blob" | response 是一个包含二进制数据的 Blob 对象 。 | "document" | response 是一个 HTMLDocumentXMLXMLDocument ,这取决于接收到的数据的 MIME 类型。 | "json" | response 是一个 JavaScript 对象。这个对象是通过将接收到的数据类型视为 JSON 解析得到的。 | "text" | response 是包含在 DOMString 对象中的文本。

response

返回响应的正文,返回的类型由上面的 responseType 决定。

withCredentials

ajax 请求默认会携带同源请求的 cookie ,而跨域请求则不会携带 cookie ,设置 xhrwithCredentials 的属性为 true 将允许携带跨域 cookie

事件回调

onreadystatechange

readyState 属性发生变化时,callback会被触发。

onloadstart

ajax 请求发送之前( readyState==1 后, readyState==2 前), callback 会被触发。

onprogress

回调函数可以获取资源总大小 total ,已经加载的资源大小 loaded ,用这两个值可以计算加载进度。

onload

当一个资源及其依赖资源已完成加载时,将触发 callback ,通常我们会在 onload 事件中处理返回值。

异常处理

onerror

ajax 资源加载失败时会触发 callback

ontimeout

当进度由于预定时间到期而终止时,会触发 callback ,超时时间可使用 timeout 属性进行设置。

六、jQuery对Ajax的封装

在很长一段时间里,人们使用 jQuery 提供的 ajax 封装进行网络请求,包括 $.ajax$.get$.post 等,这几个方法放到现在,我依然觉得很实用。

$.ajax 只接收一个参数,这个参数接收一系列配置,其自己封装了一个 jqXHR 对象,有兴趣可以阅读一下jQuary-ajax 源码

常用配置:

url

当前页地址。发送请求的地址。

type

类型: String 请求方式 ( "POST""GET" ), 默认为 "GET" 。注意:其它 HTTP 请求方法,如 PUTDELETE 也可以使用,但仅部分浏览器支持。

timeout

类型: Number 设置请求超时时间(毫秒)。此设置将覆盖全局设置。

success

类型: Function 请求成功后的回调函数。

jsonp

在一个 jsonp 请求中重写回调函数的名字。这个值用来替代在 "callback=?" 这种 GETPOST 请求中 URL 参数里的 "callback" 部分。

error类型: Function 。请求失败时调用此函数。

注意:源码里对错误的判定:

返回值除了这几个状态码都会进 error 回调。

dataType

data

类型: String 使用 JSON.stringify 转码

complete

类型: Function 请求完成后回调函数 (请求成功或失败之后均调用)。

async

类型: Boolean 默认值: true 。默认设置下,所有请求均为异步请求。如果需要发送同步请求,请将此选项设置为 false

contentType

类型: String 默认值: "application/x-www-form-urlencoded" 。发送信息至服务器时内容编码类型。

键值对这样组织在一般的情况下是没有什么问题的,这里说的一般是,不带嵌套类型 JSON ,也就是 简单的 JSON ,形如这样:

但是在一些复杂的情况下就有问题了。 例如在 Ajax 中你要传一个复杂的 json 对像,也就说是对象嵌数组,数组中包括对象,你这样传: application/x-www-form-urlencoded 这种形式是没有办法将复杂的 JSON 组织成键值对形式。

可以用如下方式传递复杂的 json 对象

七、jQuery的替代者

近年来前端 MV* 的发展壮大,人们越来越少的使用 jQuery ,我们不可能单独为了使用 jQueryAjaxapi 来单独引入他,无可避免的,我们需要寻找新的技术方案。

尤雨溪在他的文档中推荐大家用 axios 进行网络请求。 axios 基于 Promise 对原生的 XHR 进行了非常全面的封装,使用方式也非常的优雅。另外, axios 同样提供了在 node 环境下的支持,可谓是网络请求的首选方案。

未来必定还会出现更优秀的封装,他们有非常周全的考虑以及详细的文档,这里我们不多做考究,我们把关注的重点放在更底层的API fetch

FetchAPI 是一个用用于访问和操纵HTTP管道的强大的原生 API。

这种功能以前是使用 XMLHttpRequest实现的。Fetch提供了一个更好的替代方法,可以很容易地被其他技术使用,例如 Service Workers。Fetch还提供了单个逻辑位置来定义其他HTTP相关概念,例如CORS和HTTP的扩展。

可见 fetch 是作为 XMLHttpRequest 的替代品出现的。

使用 fetch ,你不需要再额外加载一个外部资源。但它还没有被浏览器完全支持,所以你仍然需要一个 polyfill

八、fetch的使用

一个基本的 fetch请求:

FetchAPI 提供了一个全局的 fetch() 方法,以及几个辅助对象来发起一个网络请求。

  • fetch()

fetch() 方法用于发起获取资源的请求。它返回一个 promise ,这个 promise 会在请求响应后被 resolve ,并传回 Response 对象。

  • Headers

可以通过 Headers() 构造函数来创建一个你自己的 headers 对象,相当于 response/request 的头信息,可以使你查询到这些头信息,或者针对不同的结果做不同的操作。

  • Request

通过 Request() 构造函数可以创建一个 Request 对象,这个对象可以作为 fetch 函数的第二个参数。

  • Response

fetch() 处理完 promises 之后返回一个 Response 实例,也可以手动创建一个 Response 实例。

九、fetch polyfill源码分析

由于 fetch 是一个非常底层的 API ,所以我们无法进一步的探究它的底层,但是我们可以借助它的 polyfill 探究它的基本原理,并找出其中的坑点。

代码结构

由代码可见, polyfill 主要对 Fetch API提供的四大对象进行了封装:

fetch 封装

代码非常清晰:

  • 构造一个  Promise 对象并返回

  • 创建一个  Request 对象

  • 创建一个  XMLHttpRequest 对象

  • 取出  Request 对象中的请求  url ,请求方发,  open 一个  xhr 请求,并将  Request 对象中存储的  headers 取出赋给xhr

  • xhr onload 后取出  response 的  status 、  headers 、  body 封装  Response 对象,调用  resolve

异常处理

可以发现,调用 reject 有三种可能:

  • 1.请求超时

  • 2.请求失败

注意:当和服务器建立简介,并收到服务器的异常状态码如 404500 等并不能触发 onerror 。当网络故障时或请求被阻止时,才会标记为 reject ,如跨域、 url 不存在,网络异常等会触发 onerror

所以使用fetch当接收到异常状态码都是会进入then而不是catch。这些错误请求往往要手动处理。

  • 3.手动终止

可以在 request 参数中传入 signal 对象,并对 signal 对象添加 abort 事件监听,当 xhr.readyState 变为 4 (响应内容解析完成)后将signal对象的abort事件监听移除掉。

这表示,在一个 fetch 请求结束之前可以调用 signal.abort 将其终止。在浏览器中可以使用 AbortController() 构造函数创建一个控制器,然后使用 AbortController.signal 属性

这是一个实验中的功能,此功能某些浏览器尚在开发中

Headers封装

在header对象中维护了一个 map 对象,构造函数中可以传入 Header 对象、数组、普通对象类型的 header ,并将所有的值维护到 map 中。

之前在 fetch 函数中看到调用了 headerforEach 方法,下面是它的实现:

可见 header 的遍历即其内部 map 的遍历。

另外 Header 还提供了 appenddeletegetset 等方法,都是对其内部的 map 对象进行操作。

Request对象

Request 对象接收的两个参数即 fetch 函数接收的两个参数,第一个参数可以直接传递 url ,也可以传递一个构造好的 request 对象。第二个参数即控制不同配置的 option 对象。

可以传入 credentialsheadersmethodmodesignalreferrer 等属性。

这里注意:

  • 传入的  headers 被当作  Headers 构造函数的参数来构造header对象。

cookie处理

fetch函数中还有如下的代码:

默认的 credentials 类型为 same-origin ,即可携带同源请求的coodkie。

然后我发现这里polyfill的实现和MDN-使用Fetch以及很多资料是不一致的:

mdn: 默认情况下,fetch 不会从服务端发送或接收任何 cookies

于是我分别实验了下使用 polyfill 和使用原生 fetch 携带cookie的情况,发现在不设置 credentials 的情况下居然都是默认携带同源 cookie 的,这和文档的说明说不一致的,查阅了许多资料后都是说 fetch 默认不会携带cookie,下面是使用原生 fetch 在浏览器进行请求的情况:

然后我发现在MDN-Fetch-Request已经指出新版浏览器 credentials 默认值已更改为 same-origin ,旧版依然是 omit

确实MDN-使用Fetch这里的文档更新的有些不及时,误人子弟了…

Response对象

Response 对象是 fetch 调用成功后的返回值:

回顾下 f etch 中对 Response`的操作:

Response 构造函数:

可见在构造函数中主要对 options 中的 statusstatusTextheadersurl 等分别做了处理并挂载到 Response 对象上。

构造函数里面并没有对 responseText 的明确处理,最后交给了 _initBody 函数处理,而 Response 并没有主动声明 _initBody 属性,代码最后使用 Response 调用了 Body 函数,实际上 _initBody 函数是通过 Body 函数挂载到 Response 身上的,先来看看 _initBody 函数:

可见, _initBody 函数根据 xhr.response 的类型( BlobFormDataString... ),为不同的参数进行赋值,这些参数在 Body 方法中得到不同的应用,下面具体看看 Body 函数还做了哪些其他的操作:

Body 函数中还为 Response 对象挂载了四个函数, textjsonblobformData ,这些函数中的操作就是将_initBody中得到的不同类型的返回值返回。

这也说明了,在 fetch 执行完毕后,不能直接在 response 中获取到返回值而必须调用 text()、json() 等函数才能获取到返回值。

这里还有一点需要说明:几个函数中都有类似下面的逻辑:

consumed函数:

每次调用 text()、json() 等函数后会将 bodyUsed 变量变为 true ,用来标识返回值已经读取过了,下一次再读取直接抛出 TypeError('Already read') 。这也遵循了原生 fetch 的原则:

因为Responses对象被设置为了 stream 的方式,所以它们只能被读取一次

十、fetch的坑点

VUE 的文档中对 fetch 有下面的描述:

使用 fetch 还有很多别的注意事项,这也是为什么大家现阶段还是更喜欢 axios 多一些。当然这个事情在未来可能会发生改变。

由于 fetch 是一个非常底层的 API ,它并没有被进行很多封装,还有许多问题需要处理:

  • 不能直接传递  JavaScript 对象作为参数

  • 需要自己判断返回值类型,并执行响应获取返回值的方法

  • 获取返回值方法只能调用一次,不能多次调用

  • 无法正常的捕获异常

  • 老版浏览器不会默认携带  cookie

  • 不支持  jsonp

十一、对fetch的封装

请求参数处理

支持传入不同的参数类型:

cookie携带

fetch 在新版浏览器已经开始默认携带同源 cookie ,但在老版浏览器中不会默认携带,我们需要对他进行统一设置:

异常处理

当接收到一个代表错误的 HTTP 状态码时,从 fetch()返回的 Promise 不会被标记为 reject, 即使该 HTTP 响应的状态码是 404 或 500。相反,它会将 Promise 状态标记为 resolve (但是会将 resolve 的返回值的 ok 属性设置为 false ),仅当网络故障时或请求被阻止时,才会标记为 reject。

因此我们要对 fetch 的异常进行统一处理

返回值处理

对不同的返回值类型调用不同的函数接收,这里必须提前判断好类型,不能多次调用获取返回值的方法:

jsonp

fetch 本身没有提供对 jsonp 的支持, jsonp 本身也不属于一种非常好的解决跨域的方式,推荐使用 cors 或者 nginx 解决跨域,具体请看下面的章节。

fetch封装好了,可以愉快的使用了。

嗯,axios真好用…

十二、跨域总结

谈到网络请求,就不得不提跨域。

浏览器的同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。通常不允许不同源间的读操作。

跨域条件:协议,域名,端口,有一个不同就算跨域。

下面是解决跨域的几种方式:

nginx

使用 nginx 反向代理实现跨域,参考我这篇文章:前端开发者必备的nginx知识

cors

CORS 是一个 W3C 标准,全称是”跨域资源共享” Cross-origin resource sharing 。它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求。

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

jsonp

script 标签的 src 属性中的链接可以访问跨域的 js 脚本,利用这个特性,服务端不再返回 JSON 格式的数据,而是返回一段调用某个函数的 js 代码,在 src 中进行了调用,这样实现了跨域。

jqueryjsonp 的支持:

fetchaxios 等并没有直接提供对 jsonp 的支持,如果需要使用这种方式,我们可以尝试进行手动封装:

postMessage跨域

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

postMessage 跨域适用于以下场景:同浏览器多窗口间跨域通信、 iframe 间跨域通信。

WebSocket

WebSocket 是一种双向通信协议,在建立连接之后, WebSocketserverclient 都能主动向对方发送或接收数据而不受同源策略的限制。

文中如有错误,欢迎在后台私信指正,谢谢阅读。