首頁  >  文章  >  web前端  >  AJAX原理與CORS跨域的方法

AJAX原理與CORS跨域的方法

一个新手
一个新手原創
2017-10-25 14:24:384850瀏覽

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。它擁有原型鏈上和本身的方法和屬性,現在看下我們常用的方法:
AJAX原理與CORS跨域的方法

#我們解釋下它的幾個主要方法,我們在創建了新的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');
// 接受一个请求主体发送的数据,如果不需要,传入null
xhr.send(data);

當請求的類型為get/head時, send()的參數會被忽略並置為null,send()傳遞的參數會影響到我們請求的頭部content-type的預設值,該欄位代表傳回的資源內容的類型,用於瀏覽器處理,如果沒有設定或在一些場景下,瀏覽器會進行MIME嗅探來決定怎麼處理回傳的資源。

XHR2級中定義了FormData數據,用於常見的類別表單資料序列化:

// 直接传入表单id
var 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:
------WebKitFormBoundaryjn3q2KKRYrEH55Vz
Content-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,deflate
Accept-Language: 浏览器当前设置的语言。// zh-CN,zh;q=0.8,en;q=0.6
Connection: 浏览器与服务器之间连接的类型。// keep-alive
Cookie: 当前页面设置的任意Cookie。// JlogDataSource=jomodb
Host: 发出请求的页面所在域。// gzhxy-cdn-oss-06.gzhxy.baidu.com:8090
Referer: 发出请求的页面URI。// http://gzhxy-cdn-oss-06.gzhxy.baidu.com:8090/jomocha/index.php?r=tools/offline/index
User-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/html
xhr.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    // 发起响应的URL
responseType    // 响应的类型,用于浏览器强行重置响应数据的类型
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分钟,单位为ms
xhr.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这个网站上查询兼容性问题:
AJAX原理與CORS跨域的方法


以上是AJAX原理與CORS跨域的方法的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn