AJAX原理与CORS跨域

ajax作为前端开发必需的基础能力之一,你可能会使用它,但并不一定懂得其原理,以及更深入的服务器通信相关的知识。在最近两天的整理过程中,看了大量的文章,发现自己的后端能力已经限制自己在网络通信相关的知识领域的探索,还是应该尽快补齐短板。

下面我们来聊一聊ajax相关的东西,包括xhr/xdr/ajax/cors/http的一部分内容,其中会抛弃一些被弃用的历史包袱,如IE6/7等。

Ajax的出现

2005年,Jesse James Garrett提出了Ajax的技术,其全称为Asynchronous Javascript and XML,Ajax的核心是XMLHttpRequest对象,简称XHR,它用于使浏览器向服务器请求额外的数据而不卸载页面,极大的提高了用户体验。在此之前,其实这种技术已经存在并被一些人实现,但并没有流行也没有被浏览器支持。不过在此之后,IE5第一次引入XHR对象,并支持ajax技术,后续被所有浏览器支持。

XMLHttpRequest对象和请求

XHR是一个API,为客户端提供服务端和客户端之间通信的功能,并且不会刷新页面。它并不仅仅能取回XML类型的数据,而能取回所有类型的数据,除了http协议,还支持file和ftp协议。我们可以通过其构造函数来创建一个新的XHR对象,这个操作需要在其它所有操作之前完成:

var xhr = new XMLHttpRequest();

通过控制台我们可以很方便看到XHR的原型链:Object -> EventTarget -> XMLHttpRequestEventTarget -> XMLHttpRequest。它拥有原型链上和本身的方法和属性,现在看下我们常用的方法:

我们解释下它的几个主要方法,我们在创建了新的xhr对象之后,首先要调用它的open()方法:

// 第一个参数可以为get/post等,表示该请求的类型// 第二个参数是请求的url,可以为相对路径或绝对路径// 第三个参数代表是否异步,为true时异步,为false时同步// 第四五个参数为可选的授权使用的参数,因为安全性不推荐明文使用xhr.open('get', 'example.php', true, username, password);

在这里受同源策略的影响,当第二个参数url跨域的时候会被浏览器报安全错误。同源策略指的是当前页面和目标url协议、域名和端口均相同。后面也会讲到,除IE之外的浏览器通过XHR对象实现跨域请求,只需将url设置为绝对url即可。

当初始化请求完成后,我们调用send()方法发送请求:

var data = new FormData();data.append('name', 'Nicholas');// 接受一个请求主体发送的数据,如果不需要,传入nullxhr.send(data);

当请求的类型为get/head时,send()的参数会被忽略并置为null,send()传递的参数会影响到我们请求的头部content-type的默认值,该字段代表返回的资源内容的类型,用于浏览器处理,如果没有设置或在一些场景下,浏览器会进行MIME嗅探来确定怎么处理返回的资源。

在XHR2级中定义了FormData数据,用于常见的类表单数据序列化:

// 直接传入表单idvar data = new FormData(document.getElementById('user-form'));// 创建类表单数据var data = new FormData();data.append('name', 'Nicholas');// `FormData`可以直接被send()调用,会自动修改xhr的content-type头部xhr.send(data);// 请求头部的content-type: multipart/form-data; boundary=----WebKitFormBoundaryjn3q2KKRYrEH55Vz// 请求的上传数据 Request Payload:------WebKitFormBoundaryjn3q2KKRYrEH55VzContent-Disposition: form-data; name="name"Nicholas------WebKitFormBoundaryjn3q2KKRYrEH55Vz--

FormData常用的方法有

append/delete/entries/forEach/get/getAll/has/keys/set/values,都是常用的跟数组类似的方法,不再解释。

请求方法

GET是最常见的请求类型,可以将查询字符串参数添加到URL尾部,对XHR而言,该查询字符串必须经过正确编码,每个键值对必须使用encodeURIComponent()进行编码,键值对之间由&分割:

// 封装序列化键值对function addURLParam(url, name, value) { url += (url.indexOf('?') === -1 ? '?' : '&'; url += encodeURIComponent(name) + '=' + encodeURIComponent(value); return url; }

POST请求使用频率仅次于GET请求,通常发送较多数据,且格式不限,数据传递给send()作为参数。

HTTP一共规定了九种请求方法,每一个动词代表不同的语义,但是常用的只有上面两种:

- OPTIONS:返回服务器针对特定资源所支持的HTTP请求方法。也可以利用向Web服务器发送'*'的请求来测试服务器的功能性。 - HEAD:向服务器索要与GET请求相一致的响应,只不过响应体将不会被返回。这一方法可以在不必传输整个响应内容的情况下,就可以获取包含在响应消息头中的元信息。 - GET:向特定的资源发出请求。 - POST:向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的创建和/或已有资源的修改。 - PUT:向指定资源位置上传其最新内容。 - DELETE:请求服务器删除Request-URI所标识的资源。 - TRACE:回显服务器收到的请求,主要用于测试或诊断。 - CONNECT:HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。 - PATCH: 用于对资源进行部分修改

HTTP头部信息

每个HTTP请求和响应都带有头部信息,xhr对象允许我们操作部分头部信息。我们可以通过xhr.setRequestHeader()方法来设置自定义的头部信息或者修改浏览器默认的正常头部信息。常用的请求头部:

// 下面的实例是从我本地的一次请求取出的Accept: 浏览器能够处理的内容类型。// */*Accept-Charset: 浏览器能够显示的字符集。// 未取到Accept-Encoding: 浏览器能够处理的压缩编码。// gzip,deflateAccept-Language: 浏览器当前设置的语言。// zh-CN,zh;q=0.8,en;q=0.6Connection: 浏览器与服务器之间连接的类型。// keep-aliveCookie: 当前页面设置的任意Cookie。// JlogDataSource=jomodbHost: 发出请求的页面所在域。// gzhxy-cdn-oss-06.gzhxy.baidu.com:8090Referer: 发出请求的页面URI。// http://gzhxy-cdn-oss-06.gzhxy.baidu.com:8090/jomocha/index.php?r=tools/offline/indexUser-Agent: 浏览器的用户代理字符串。// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36

我们一般不修改浏览器正常的头部信息,可能会影响到服务器响应。如果需要可以通过xhr.setRequestHeader()进行修改:

// 传入头部键值对,键值不区分大小写,如果多次设置,则追加// 此时请求头部的content-type: application/json, text/htmlxhr.setRequestHeader('content-type', 'application/json');xhr.setRequestHeader('content-type', 'application/json');

设置头部信息需要在open()之后,send()之前进行调用。响应的头部信息在后端处理,不在此处讲解。有一部分请求头部信息不允许设置,如Accept-Encoding, Cookie等。

在请求返回后,我们可以获取到响应头部:

// 获取指定项的响应头xhr.getResponseHeader('content-type'); // application/json;charset=utf-8// 获取所有的响应头部信息xhr.getAllResponseHeaders();

这里简单说下content-type值,指的是请求和响应的HTTP内容类型,影响到服务器和浏览器对数据的处理方式,默认为text/html,常用的如:

// 包含资源类型,字符编码, 边界字符串三个参数,可选填text/html;charset=utf-8 // html标签文本text/plain // 纯文本text/css // css文件text/javascript // js文件// 普通的表单数据,可以通过表单标签的enctype属性指定application/x-www-form-urlencode// 发送文件的POST包,包过大需要分片时使用`boundary`属性分割数据作边界multipart/form-data; boundary=something// json数据格式application/json// xml类型的标记语言application/xml

XHR对象的响应

我们现在对请求的发起很了解了,接着看下如何拿到响应数据。如果我们给open()传递的第三个参数是true,则代表为同步请求,那么js会被阻塞直到拿到响应,而如果为false则是异步请求,我们只需要绑定xhr.onreadystatechange()事件监听响应即可。最上面的图已经说明了readystate的值含义,所以我们可以:

// xhr v1 的写法,检测readystate的值,为4则说明数据准备完毕,需要在open()前定义 xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if (xhr.status === 200 || xhr.status === 304) { console.log(xhr.responseText); } else { console.log(xhr.statusText); } }  } // xhr v2 的写法,onload()事件说明数据准备完毕 xhr.onload = function () { if (xhr.status === 200 || xhr.status === 304) { console.log(xhr.responseText); } else { console.log(xhr.statusText); } }

xhr对象的响应数据中包含几个属性:

response // 响应的数据responseURL // 发起响应的URLresponseType // 响应的类型,用于浏览器强行重置响应数据的类型responseText // 如果为普通文本,则在这显示responseXML // 如果为xml类型文本,在这里显示

数据会出现在responseText/responseXML中的哪一个,取决于服务器返回的MIME类型,当然我们也有一些方式在浏览器端设置如何处理这些数据:

// xhr v1 的写法,设置响应资源的处理类型 xhr.overrideMimeType('text/xml');// xhr v2 的写法, 可用值为 arraybuffer/blob/document/json/text xhr.responseType = 'document';

响应数据相关的属性默认为null / '',只有当请求完成并被正确解析的时候才会有值,取决于responseType的值,来确定

response/responseText/responseXML谁最终具有值。

XHR的高级功能

在xhr v2里提供了超时和进度事件。

超时

xhr.timeout = 1000; // 1分钟,单位为msxhr.ontimeout = function () {};

在请求send()之后开始计时,等待timeout时长后,如果没有收到响应,则触发ontimeout()事件,超时会将readystate=4,直接触发onreadystatechange()事件。

请求进度

像上图所示,xhr v2定义了不同的进度事件:

loadstart/progress/error/abort/load/loadend,这其中我们已经说过了onload()事件为内容加载完成可用。现在说一下onprogress()进度事件:

xhr.onprogress = function (event) { if (event.lengthComputable) { console.log(event.loaded / event.total); } }

该事件会接收一个event对象,其target属性为该xhr对象,lengthComputable属性为total size是否已知,即是否可用进度信息,loaded属性为已经接收的字节数,total为总字节数。该事件会在数据接收期间不断触发,但间隔不确定。

跨域CORS

提到XHR对象,我们就会讲到跨域问题,它是为了预防某些恶意行为的安全策略,但有时候我们需要跨域来实现某些功能。需要注意的是跨域并不仅仅是前端单方面的事情,它需要后端代码进行配合,我们只是通过一些方式跳过了浏览器的阻拦。

对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。

CORS(Cross-Origin Resource Sharing, 跨域资源共享)的思想是浏览器和服务端通过头部信息来进行沟通确认是否给予响应。如:

Origin: http://www.baidu.com // 浏览器的头部信息// 如果服务端认可这个域名的跨域请求,如下设置就可跨域访问资源Access-Control-Allow-Origin: http://www.baidu.com

如上就可以实现最简单的跨域访问,但是此时不能携带任何的cookie,如果我们需要传递cookie进行身份认证,需要设置:

xhr.withCredentials = true; // 浏览器端Access-Control-Allow-Credentials: true; // 服务端

这样我们就可以传递认证信息了,但如果允许认证,

Access-Control-Allow-Origin不能设置为*,而一定是具体的域名信息。

现在的浏览器都对CORS有了实现,如IE使用XDomainRequest对象,其它浏览器使用XMLHttpRequest对象。所以在此之前有很多奇技淫巧,如通过jsonp/图像 Ping方法都不再详述,而且其都需要服务端配合并且有很多局限性。

IE实现: XDomainRequest

var xdr = new XDomainRequest(); xdr.open('get', 'http://www.site.com/page'); xdr.send(null);

XDR区别于XHR:

不能传输cookie

只能设置请求头部的content-type

不能访问响应头部信息

只支持get/post方法

通过这些区别可以阻止一部分的CSRF(Cross-Site Request Forgery,跨站点请求伪造)和XSS(Cross-Site Scripting,跨站点脚本)。

XDR与XHR非常相似,区别有几点:

open()方法只接受两个参数,请求类型和URL

只允许异步请求

响应完成触发onload()事件,但我们只能访问responseText原始文本,并且无法获取响应的status.

异常事件都会触发error事件,并且无错误信息可用。

其余浏览器实现: XMLHttpRequest

其余浏览器通过XHR对象直接实现了CORS,你只需要做的就是open()方法中传入一个绝对URL。

xhr.open('get', 'http://www.site.com/page', true);

相对于普通的XHR对象,CORS-XHR依然有部分限制:

不能使用setRequestHeader()定义头部

不能传递cookie

调用getAllResponseHeaders(),结果为空

其余跨域方法

上面的两种方法已经很成熟了,但是仍然有一部分方法可以跨域,比如图像Ping:

var img = new Image(); img.onload = img.onerror = function () { console.log('done'); } img.src = 'http://www.site.com/test?name=Nicholas';

这种方式常用于服务端统计广告的点击次数,其缺陷为:

只能是GET请求

单向通信,无法获取响应文本

另外还有JSONP:

function handleResponse(response) { console.log(response.ip, response.city); }var script = document.createElement('script'); script.src = 'http://freegeoip.net/json?callback=handleResponse';document.body.insertBefore(script, document.body.firstChild);

这种方式通过和服务器配合,跨域请求一个js文件并被服务器处理后传回:

handleResponse({'name': 'Nicholas'});

然后直接在浏览器调用了该函数,传回的数据被当做response形参进行处理。但它也有一些缺陷:

访问的方式是请求js,所以如果域名不安全,则很容易被恶意代码直接执行并攻击

无法检测是否错误,因为js不支持这样的接口事件,只能超时判断

上面两种方式很容易看出,我们在支持CORS之前,使用的方法只不过是采用img/css/js等不受跨域访问限制的对象,变相拿到了响应数据,但都有缺陷,所以如果没有历史包袱,建议采用XDR或XHR对象来实现跨域访问。

XHR的兼容性

我们可以直接到Can I use宇晨PHP培训:这个网站上查询兼容性问题:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容