前端网络请求的方式主要有 Ajax ,jQuery封装的 Ajax,fetch,axios、request 等开源库等。
这里插一下http response status
,指示一个 http 请求是否成功完成,具体分为五类:
(1) informational responses,1XX,临时回应,表示客户端请继续:
100 Continue: 代表目前一切正常,客户端应该继续等待请求完成,或者直接忽略如果请求已经完成。
101 Switching Protocol: 对客户端发送的 Upgrade请求头的回应,请求者已要求服务器切换协议,服务器已确认并准备切换。
102 Processing: 代表服务器已经收到请求并正在处理,不过此时仍没有响应返回。
103 Early Hints: 主要用于 Link 头以允许客户端开始预加载资源,而此时服务器仍在准备一个响应。
(2) successful responses:
200 OK: 请求成功,但具体含义依请求方法而异,具体参考MDN
201 Created: 请求成功,并且因该请求而生成了新的资源,通常是由于发送了 POST 或者 PUT 请求。
202 Accepted: 请求已被收到,但是没有据此进行进一步操作,这表示无法返回一个异步响应来代表请求处理的结果,这通常出现在另一个进程或者服务器处理请求或者批处理中。
203 Non-Authoritative Information: 非权威性信息,表示返回的元信息并非源于目标服务器,而来自本地或者第三方副本。
204 No Content: 对于请求没有可返回的内容,即没有新文档,浏览器应该继续显示原来的文档。不过返回的响应头可能有用,客户端可以据此更新对应资源的缓存头。
205 Reset Content:没有新的内容,但浏览器应该重置它所显示的内容。用来强制浏览器清除表单输入内容。
206 Partial Content: 因为客户端发送了一个带有Range头的请求,而Ranger头用于将下载分为多个流。
还有207、208、226自己查资料吧;
(3) redirects,3xx,表示请求的目标有变化,希望客户端进一步处理。
300 Multiple Choice: 请求可以有多个响应,客户端自己挑一个。即客户端请求的资源可以在多个位置找到,这些位置已经在返回的文档内列出。如果服务器要提出优先选择,则应该在Location应答头指明。
301 Moved Permanently: 这意味着被请求资源对应的URI已被永久改变了,旧地址A上的资源已被永久移除了而不可访问了。在可通过 Location 响应头给出新地址的URL。
302 Found:这意味着被请求资源对应的URI已被暂时改变了,旧地址A的资源仍可以访问,重定向只是临时地从地址A跳转到地址B(详细请看这篇文章).
(Note: 关于301、302状态码,参考这篇文章,它们都表示重定向,即浏览器在拿到服务器返回的这个状态码后,会自动跳转到一个新的URL(可从响应的Location头部中获取,用户看到的效果就是他输入的地址A变成了另一个地址B),尽量使用301跳转)
303 See Other: 服务器返回该状态码指示客户端通过GET方法使用另一个URI去获取被请求的资源。
304 Not Modified: 出于缓存目的,告诉客户端被请求的资源没被修改,可以继续使用对应的缓存。即客户端本地已经有缓存的版本,并且在 Request 中告诉了服务器,当服务器通过 if-modified-since 或 e-tag 发现对应资源没更新时,就会返回一个不包含响应体的304状态。 (这是在ajax请求中,唯一一个可以成功处理的重定向类状态码,其他3XX都不行)
307 Temporary Redirect: 语义上与 302一致,只不过限制了客户端不能改变使用的请求方法类型,例如第一次请求使用的是 POST 方法,那么后续请求只能使用 POST 方法。
308 Permanent Redirect: 语义上与 301 一致,不过限制了客户端不能改变使用的请求方法类型。
(4) client errors:
400 Bad Request: 由于语法错误,服务器无法理解请求。
401 Unauthorized: 语义上应该是"unauthenticated",未经验证的,也有“未授权”的叫法,要求身份验证。对于需要登录的网页,服务器可能返回该响应(服务器:你要我提供服务,先告诉我你是谁)。
403 Forbidden: 客户端无权限获取相应内容,即未得到授权因此服务器拒绝返回相应内容。与401不同,服务器知道客户端的身份,但不好意思,人家就是不想给你提供服务。
404 Not Found: 这个太常见了,服务器找不到被请求的资源(亲,我这边没有你要的东西哦)。不过服务器有时候也会用404代替403返回,以此来向无权限的客户端隐藏对应资源的存在(我有,但骗你说没有,哈哈)。
405 Method Not Allowed: 服务器知道请求使用的方法,但不允许。例如禁止 Delete 方法防止删除资源。不过 GET 和 HEAD 这两个方法不可以被禁止,即使用这两个方法去请求资源,不应该被返回 405.
406 Not Acceptable: 无法根据客户端给定的规则找到内容。
407 Proxy Authentication Required: 与401类似,但是需要通过代理来完成验证。
408 Request Timeout: 某些服务器会对空闲的连接(即没有传输任务在进行)发送该响应,即使客户端之前并未发送任何请求(即该响应可以是服务器主动发送的),意味着服务器想要关闭该连接。
还有还多4XX,这里就不介绍了。
(5) servers eerors:
500 Internal Server Error: 服务器内部错误。
501 Not Implemented: 请求使用的方法类型不被服务器所支持而无法处理。不过服务器一般都支持 get 、head 方法。
502 Bad Gateway: 服务器作为网关或代理,从上游服务器收到无效响应。
503 Service Unavailable: 服务器目前无法使用(由于超载或停机维护),通常只是暂时状态。
504 Gateway Timeout: 网关超时,服务器作为网关或者代理,没有及时从上游服务器收到请求。
505 HTTP Version Not Supported: 请求使用的HTTP版本不被服务器所支持。
1.原生 Ajax
Ajax: asynchoronous javascript and xml,这种技术能够向服务器请求数据而无须重新加载整个页面,带来更好的用户体验,关于ajax的超详细介绍请移步这里
本人画了下面这张示意图
说明:
(1)发出请求(xhr.send())之前的语句都是同步执行,从 send 方法内部开始, 浏览器为将要发生的网络请求创建新的http请求线程(这个线程独立于js引擎线程)。网络请求异步被发送出去后,js 引擎并不会等待 ajax 发起的http请求收到结果, 而是直接顺序往下执行。
(2)当ajax请求被服务器响应并且收到 response 后,浏览器事件触发线程捕获到了ajax的相应事件,并将 onreadystatechange (也可能是 onload 或者 onerror 等等)对应的事件处理程序添加到 任务队列 的末尾。
(3)一次ajax请求, 并非所有的部分都是异步进行的,例如 "readyState==1"的 onreadystatechange 回调以及 onloadstart 回调就是同步执行的代码。
xhr.send()刚开始执行,onloadstart 回调方法就被触发。
使用原生 ajax 的代码如下:
let xhr=new XMLHttpRequest();
//状态监听
xhr.onreadystatechange=function () {
console.log(xhr.readyState);
if(xhr.readyState===4){
if(xhr.status>=200 && xhr.status<300 || xhr.status==304){
console.log(xhr.responseText);
}
}
}
//异常处理
xhr.onerror=function(){
console.log("Ajax request failed.");
}
//设置相关事件回调方法
// onloadstart 在开始执行 xhr.send()就会被触发,属于同步执行。
xhr.onloadstart=function(){
console.log('loadstart');
}
//超时处理
xhr.ontimeout=function(){
console.log("请求超时!")
}
//处理请求参数
postData={"name1":"value1","name2":"value2"};
postData=(function (value) {
let dataString="";
for(let key in value){
dataString+=key+"="+value[key]+"&";
}
return dataString;
})(postData);
xhr.open('get','https://www.qq.com',true);
//设置请求头,要在 open 与 send 之间
// 推测是设置请求头的时候,xhr需要已经完成初始化
xhr.setRequestHeader('Content-type','application/x-www-form-urlencoded');
//跨域携带cookie
xhr.withCredentials=true;
// 发出请求,之前的语句都是同步执行
// 从send方法内部开始, 浏览器为将要发生的网络请求创建了新的http请求线程,
xhr.send(postData);
2.jQuery对 Ajax 的封装
$.ajax({
dataType: 'json', // 设置返回值类型
contentType: 'application/json', // 设置参数类型
headers: {'Content-Type','application/json'},// 设置请求头
xhrFields: { withCredentials: true }, // 跨域携带 cookie
data: JSON.stringify({a: [{b:1, a:1}]}), // 传递参数
error:function(xhr,status){ // 错误处理
console.log(xhr,status);
},
success: function (data,status) { // 获取结果
console.log(data,status);
}
})
3.fetch
Fetch API是一个用用于访问和操纵 HTTP 管道的强大的原生 API。
这种功能以前是使用 XMLHttpRequest 实现的。Fetch 提供了一个更好的替代方法,可以很容易地被其他技术使用,例如 Service Workers。Fetch 还提供了单个逻辑位置来定义其他 HTTP 相关概念,例如 CORS 和 HTTP 的扩展。
fetch是作为XMLHttpRequest的替代品出现的。
优点:
(1)符合关注分离,没有将输入、输出和用事件来跟踪的状态混杂在一个对象里;
(2)更好更方便的写法;
(3)基于标准 Promise 实现,支持 async/await;
(4)更加底层,提供的API丰富(request, response);
(5)脱离了XHR,是ES规范里新的实现方式。
使用fetch,你不需要再额外加载一个外部资源。但它还没有被浏览器完全支持,所以仍然需要一个polyfill。
一个基本的fetch请求如下:
const options = {
method: "POST", // 请求参数
headers: { "Content-Type": "application/json"}, // 设置请求头
body: JSON.stringify({name:'123'}), // 请求参数
credentials: "same-origin", // cookie 设置
mode: "cors", // 跨域
}
// fetch方法返回的是一个 promise
// fetch(input, init)
fetch('http://www.xxx.com', options)
.then(function(response) {
return response.json();
})
.then(function(myJson) {
console.log(myJson); // 响应数据
})
.catch(function(err){
console.log(err); // 异常处理
})
fetch polyfill 源码
polyfill 主要对 fetch API 的 Headers, Request, Response, fetch 进行了封装。
(1) fetch
(a)构造一个Promise对象并返回;
(b)创建一个Request对象;
(c)创建一个XMLHttpRequest对象(注意 原生 fetch 并未使用 XHR 对象);
(d)取出Request对象中的请求url,请求方发,open一个xhr请求,并将Request对象中存储的headers取出赋给 xhr;
(e)xhr onload后取出response的status、headers、body封装Response对象,调用resolve。
// fetch 方法的封装
function fetch(input, init) {
return new Promise(function (resolve, reject) {
let request=new Request(input,init);
let xhr=new XMLHttpRequest();
xhr.open(request.method,request.url,true);
//响应获取完毕
xhr.onload=function () {
let options={
status:xhr.status,
statusText:xhr.statusText,
headers:parseHeaders(xhr.getAllResponseHeaders() || '')
}
options.url='responseURL' in xhr? xhr.responseURL:options.headers.get('X-Request-URL');
let body='response' in xhr? xhr.response: xhr.responseText;
resolve(new Response(body,options));
}
//异常处理
function abortXhr(){
xhr.abort();
}
// 1.请求失败,网络故障或请求被阻止才触发,
//而收到服务器异常状态码如404,500不会触发
xhr.onerror=function(){
reject(new TypeError('Network request failed.'));
}
//2.请求超时
xhr.ontimeout=function(){
reject(new TypeError('请求超时'));
}
xhr.onabort=function(){
reject(new DOMException('Aborted','AbortError'));
}
//3.手动终止
if(request.signal){
request.signal.addEventListener('abort',abortXhr);
xhr.onreadystatechange=function () {
if(xhr.readyState===4){
request.signal.removeEventListener('abort',abortXhr);
}
}
}
// forEach 的具体实现在 Headers.prototype 中
request.headers.forEach(function (value, name) {
xhr.setRequestHeader(name,value);
});
xhr.send();
})
}
(2) Request
Request对象接收的两个参数即fetch函数接收的两个参数,第一个参数可以直接传递url,也可以传递一个构造好的request对象。第二个参数即控制不同配置的option对象。
// 2.Request对象封装
function Request(input, options) {
options=options || {}
let body=options.body
if(input instanceof Request){
this.url=input.url
this.method=input.method
//...
}
else{
this.url=String(input)
}
this.credentials=options.credentials || this.credentials || 'same-origin'
if(options.headers || !this.headers){
this.headers=new Headers(options.headers)
}
this.method=normalizeMethod(options.method || this.method || 'GET')
this.mode=options.mode || this.mode || null
this.signal=options.signal || this.signal
this.referrer=null
//...
this._initBody(body)
}
(3) Headers
// 3.Headers 封装
function Headers(headers) {
this.map={}
// (1)传入的参数headers本身就是 Headers 的实例
if(headers instanceof Headers){
headers.forEach(function (value, name) {
this.append(name,value)
},this)
}
// (2)传入的参数headers是数组类型(二维)
else if(Array.isArray(headers)){
headers.forEach(function (header) {
this.append(header[0],header[1])
},this)
}
// (3)传入的参数headers是普通对象
else if(headers){
Object.getOwnPropertyNames(headers).forEach(function (name) {
this.append(name,headers[name])
})
}
}
在 Headers 中维护了一个map对象,构造函数中可以传入Headers对象、数组、普通对象类型的header,并将所有的值维护到map中。
fetch及上段代码中都有 headers.forEach,这个 forEach 方法的具体实现如下:
Headers.prototype.forEach=function (callback, thisArg) {
for(let name in this.map){
if(this.map.hasOwnProperty(name)){
callback.call(thisArg,this.map[name],name,this)
}
}
}
可见header的遍历即其内部map的遍历。
另外Header还提供了append、delete、get、set等方法,都是对其内部的map对象进行操作。
(4) Response
在 fetch中有对 Response 的操作:
xhr.onload=function(){
···
resolve(new Response(body,options))
}
function Response(bodyInit,options) {
options=options || {}
this.type='default'
this.status=options.status===undefined? 200 : options.status
this.ok=this.status>=200 && this.status<300
this.statusText='statusText' in options?options.statusText:'OK'
this.headers=new Headers(options.headers)
this.url=options.url || ''
// _initBody 是在 Body 函数中挂载到 Response.prototype 对象上的
this._initBody(bodyInit)
}
Body.call(Response.prototype)
可见在构造函数中主要对options中的status、statusText、headers、url等分别做了处理并挂载到Response对象上。
构造函数里面并没有对responseText的明确处理,最后交给了_initBody函数处理,而Response并没有主动声明_initBody属性,代码最后使用Response调用了Body函数,实际上_initBody函数是通过Body函数挂载到Response身上的,先来看看Body函数:
function Body() {
this.bodyUsed=false;
// 这就是 Response 中的 _initBody
this._initBody=function (body) {
//具体代码见后面
}
this.text=function () {
// _bodyText...
}
this.json=function () {
this.text().then(JSON.parse)
}
if(support.blob){
this.blob=function () {
//_bodyBlob...
}
}
if(support.formData){
this.formData=function () {
//_bodyFormData...
}
}
return this
}
Body函数中还为Response对象挂载了四个函数,text、json、blob、formData,这些函数中的操作就是将 _initBody 中得到的不同类型的返回值返回。
这也说明了,在fetch执行完毕后,不能直接在response中获取到返回值而必须调用text()、json()等函数才能获取到返回值。
再来看看_initBody函数
function Body() {
this.bodyUsed=false;
// 这就是 Response 中的 _initBody
this._initBody=function (body) {
if(!body){
this._bodyText=''
}
else if(typeof body==='string'){
this._bodyText=body
}
else if(support.blob && Blob.prototype.isPrototypeOf(body)){
this._bodyBlob=body
}
else if(support.formData && FormData.prototype.isPrototypeOf(body)){
this._bodyFormData=body
}
// else if ...
else{
this._bodyText=body=Object.prototype.toString.call(body)
}
}
// ...
}
可见,_initBody函数根据xhr.response的类型(Blob、FormData、String...),为不同的参数进行赋值,这些参数在Body方法中得到不同的应用.
这里还有一点需要说明:Response相关的几个函数中都有类似下面的逻辑:
var rejected = consumed(this)
if (rejected) {
return rejected
}
function consumed(body) {
if (body.bodyUsed) {
return Promise.reject(new TypeError('Already read'))
}
body.bodyUsed = true
}
每次调用text()、json()等函数后会将bodyUsed变量变为true,用来标识返回值已经读取过了,下一次再读取直接抛出TypeError('Already read')。这也遵循了原生fetch的原则:
因为 Responses 对象被设置为了 stream 的方式,所以它们只能被读取一次。
fetch 的缺点
(1)不能直接传递JavaScript对象作为参数;
(2)需要自己判断返回值类型,并执行响应获取返回值的方法;
(3)获取返回值方法只能调用一次,不能多次调用;
(4)无法正常的捕获异常(服务器返回 400,500 错误码时并不会 reject,而是会将 resolve 的返回值的 ok 属性设置为 false;仅当网络故障时或请求被阻止时,才会标记为 reject。);
(5)老版浏览器不会默认携带cookie;
(6)不支持jsonp;
(7)fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费;
(8)fetch没有办法原生监测请求的进度,而XHR可以.
所以说半天, fetch并不是那么好用,偏底层,想要用的顺手还要自己封装,进行诸如 请求参数处理、cookie携带、异常处理、返回值处理等。
4.jsonp
fetch本身没有提供对jsonp的支持,jsonp本身也不属于一种非常好的解决跨域的方式。不过呢,多了解一种方式也不是坏事。jsonp 本身很简单,就是利用 script
标签的 src 属性不受同源策略约束,可进行跨域请求,不过服务端需要进行对应的设置才行。
// 服务器端
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
//数据
List<Student> studentList = getStudentList();
JSONArray jsonArray = JSONArray.fromObject(studentList);
String result = jsonArray.toString();
//前端传过来的回调函数名称
String callback = request.getParameter("callback");
//用回调函数名称包裹返回数据,这样,返回数据就作为回调函数的参数传回去了
result = callback + "(" + result + ")";
response.getWriter().write(result);
}
上段代码就是服务器端对 jsonp 请求的简单处理(摘自)
下面就是对 jsonp 的封装。
(function (window, document) {
"use strict";
let jsonp=function (url, data, callback) {
// 1.将传入的data数据转化为url字符串形式
// {id:1,name:'Jack'} => id=1&name=Jack
let dataString=url.indexOf('?')==-1? '?': '&';
for(let key in data){
dataString+=key+'='+data[key]+'&';
}
// 2.处理url中的回调函数
// cbFuncName 回调函数的名字 :my_json_cb_ 名字的前缀 + 随机数(把小数点去掉)
let cbFuncName='my_json_cb_'+
Math.random().toString().replace('.','');
dataString+='callback='+cbFuncName;
// 3.创建一个 script 标签
let scriptEle=document.createElement('script');
scriptEle.src=url+dataString;
// 4.挂载回调函数(cbFuncName是变量,不能直接作为回调函数名)
window[cbFuncName]=function (data) {
callback(data);
// 处理完回调函数的数据之后,删除 jsonp 的 script 标签
document.body.removeChild(scriptEle);
}
document.body.appendChild(scriptEle);
}
// 通过给全局window对象添加$jsonp属性,可在 此‘定义并立即调用匿名函数’执行后,
// 在其他地方使用封装好的jsonp
window.$jsonp=jsonp;
})(window,document)
//调用
let url='https://www.xxx.com';
let cb=function(data){console.log(data)}
window.$jsonp(url,null,cb);
5.axios
Vue大佬尤雨溪推荐大家用axios替换JQuery封装的ajax.
axios 是一个基于Promise,可用于浏览器和 nodejs 的 HTTP 客户端,本质上也是对原生XHR的封装,只不过它是Promise的实现版本,符合最新的ES规范,它本身具有以下特征:
1.从浏览器中创建 XMLHttpRequest
2.支持 Promise API
3.客户端支持防止CSRF
4.提供了一些并发请求的接口(重要,方便了很多的操作)
5.从 node.js 创建 http 请求
6.拦截请求和响应
7.转换请求和响应数据
8.取消请求
9.自动转换JSON数据
axios既提供了并发的封装,也没有fetch的各种问题,而且体积也较小,你值得拥有!
axios使用示例:
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
Reference
1.全面分析前端的网络请求方式
2.ajax知识体系大梳理
3.ajax和axios、fetch的区别
4.jsonp原理学习笔记
网友评论