前端跨域請(qǐng)求原理及實(shí)踐
一、 跨域請(qǐng)求的含義
瀏覽器的同源策略,出于防范跨站腳本的攻擊,禁止客戶端腳本(如 JavaScript)對(duì)不同域的服務(wù)進(jìn)行跨站調(diào)用。
一般的,只要網(wǎng)站的 協(xié)議名protocol、 主機(jī)host、 端口號(hào)port 這三個(gè)中的任意一個(gè)不同,網(wǎng)站間的數(shù)據(jù)請(qǐng)求與傳輸便構(gòu)成了跨域調(diào)用。這也是我們下面實(shí)踐的理論基礎(chǔ)。我們利用 NodeJs 創(chuàng)建了兩個(gè)服務(wù)器,分別監(jiān)聽 3000、 3001 端口(下面簡(jiǎn)稱 服務(wù)器3000 與 服務(wù)器3001 ),由于端口號(hào)不一樣,這兩個(gè)服務(wù)器以及服務(wù)器上頁(yè)面通信構(gòu)成了跨域請(qǐng)求。
在服務(wù)器3000 上有如下的頁(yè)面:
服務(wù)器3000 上的請(qǐng)求頁(yè)面中包含如下 JavaScript 代碼:
- $(function() {
- $("#submit").click(function() {
- var data = {
- name: $("#name").val(),
- id: $("#id").val()
- };
- $.ajax({
- type: 'POST',
- data: data,
- url: 'http://localhost:3000/ajax/deal',
- dataType: 'json',
- cache: false,
- timeout: 5000,
- success: function(data) {
- console.log(data)
- },
- error: function(jqXHR, textStatus, errorThrown) {
- console.log('error ' + textStatus + ' ' + errorThrown);
- }
- });
- });
- });
服務(wù)器3000 對(duì)應(yīng)的處理函數(shù)為
- pp.post('/ajax/deal', function(req, res) {
- console.log("server accept: ", req.body.name, req.body.id)
- var data = {
- name: req.body.name + ' - server 3000 process',
- id: req.body.id + ' - server 3000 process'
- }
- res.send(data)
- res.end()
- })
請(qǐng)求頁(yè)面返回結(jié)果:
此處數(shù)據(jù)處理成功。
由于數(shù)據(jù)請(qǐng)求一般都是由頁(yè)面發(fā)送數(shù)據(jù)字段,服務(wù)器根據(jù)這些字段作相應(yīng)的處理,如數(shù)據(jù)庫(kù)查詢,字符串操作等等。所以我們這里簡(jiǎn)單的處理數(shù)據(jù)(在數(shù)據(jù)后面加上字符串‘server 3000 process’),并且返回給瀏覽器,表示數(shù)據(jù)經(jīng)過(guò)服務(wù)器端處理。
如果讓 服務(wù)器3000 上的頁(yè)面向 服務(wù)器 3001 發(fā)起請(qǐng)求會(huì)怎樣呢?
將請(qǐng)求頁(yè)面中的 ajax 請(qǐng)求路徑改為:
- $.ajax({
- ...
- url: 'http://localhost:3001/ajax/deal',
- ...
- });
服務(wù)器3001 對(duì)應(yīng)的處理函數(shù)與 服務(wù)器3000 類似:
- app.post('/ajax/deal', function(req, res) {
- console.log("server accept: ", req.body.name, req.body.id)
- var data = {
- name: req.body.name + ' - server 3001 process',
- id: req.body.id + ' - server 3001 process'
- }
- res.send(data)
- res.end()
- })
結(jié)果如下:
結(jié)果證明了我們上面所說(shuō)的端口號(hào)不同,發(fā)生了跨域請(qǐng)求的調(diào)用。
需要注意的是,服務(wù)器 3001 控制臺(tái)有輸出:
- server accept: chiaki 3001
這說(shuō)明跨域請(qǐng)求并非是瀏覽器限制了發(fā)起跨站請(qǐng)求,而是請(qǐng)求可以正常發(fā)起,到達(dá)服務(wù)器端,但是服務(wù)器返回的結(jié)果會(huì)被瀏覽器攔截。
二、 利用 JSONP 實(shí)現(xiàn)跨域調(diào)用
說(shuō)道跨域調(diào)用,可能大家首先想到的或者聽說(shuō)過(guò)的就是 JSONP 了。
2.1 什么是JSONP
JSONP (JSON with Padding or JSON-P) is a JSON extension used by web developers to overcome the cross-domain restrictions imposed by browsers’ same-origin policy that limits access to resources retrieved from origins other than the one the page was served by. In layman’s terms, one website cannot just simply access the data from another website.
It was developed because handling a browsers’ same origin policy can be difficult, so using JSONP abstracts the difficulties and makes it easier.
JSON stands for “JavaScript Object Notation”, a format by which object fields are represented as key-value pairs which is used to represent data.
JSONP 是 JSON 的一種使用模式,可以解決主流瀏覽器的跨域數(shù)據(jù)訪問(wèn)問(wèn)題。其原理是根據(jù) XmlHttpRequest 對(duì)象受到同源策略的影響,而 <script> 標(biāo)簽元素卻不受同源策略影響,可以加載跨域服務(wù)器上的腳本,網(wǎng)頁(yè)可以從其他來(lái)源動(dòng)態(tài)產(chǎn)生 JSON 資料。用 JSONP 獲取的不是 JSON 數(shù)據(jù),而是可以直接運(yùn)行的 JavaScript 語(yǔ)句。
2.2 使用 jQuery 集成的 $.ajax 實(shí)現(xiàn) JSONP 跨域調(diào)用
我們先從簡(jiǎn)單的實(shí)現(xiàn)開始,利用 jQuery 中的 $.ajax 來(lái)實(shí)現(xiàn)上訴的跨域調(diào)用。
依然是上面的例子,我們將 服務(wù)器 3000 上的請(qǐng)求頁(yè)面的 JavaScript 代碼改為:
- // 回調(diào)函數(shù)
- function jsonpCallback(data) {
- console.log("jsonpCallback: " + data.name)
- }
- $("#submit").click(function() {
- var data = {
- name: $("#name").val(),
- id: $("#id").val()
- };
- $.ajax({
- url: 'http://localhost:3001/ajax/deal',
- data: data,
- dataType: 'jsonp',
- cache: false,
- timeout: 5000,
- // jsonp 字段含義為服務(wù)器通過(guò)什么字段獲取回調(diào)函數(shù)的名稱
- jsonp: 'callback',
- // 聲明本地回調(diào)函數(shù)的名稱,jquery 默認(rèn)隨機(jī)生成一個(gè)函數(shù)名稱
- jsonpCallback: 'jsonpCallback',
- success: function(data) {
- console.log("ajax success callback: " + data.name)
- },
- error: function(jqXHR, textStatus, errorThrown) {
- console.log(textStatus + ' ' + errorThrown);
- }
- });
- });
服務(wù)器 3001 上對(duì)應(yīng)的處理函數(shù)為:
- app.get('/ajax/deal', function(req, res) {
- console.log("server accept: ", req.query.name, req.query.id)
- var data = "{" + "name:'" + req.query.name + " - server 3001 process'," + "id:'" + req.query.id + " - server 3001 process'" + "}"
- var callback = req.query.callback
- var jsonp = callback + '(' + data + ')'
- console.log(jsonp)
- res.send(jsonp)
- res.end()
- })
這里一定要注意 data 中字符串拼接,不能直接將 JSON 格式的 data 直接傳給回調(diào)函數(shù),否則會(huì)發(fā)生編譯錯(cuò)誤: parsererror Error: jsonpCallback was not called。
其實(shí)腦海里應(yīng)該有一個(gè)概念:利用 JSONP 格式返回的值一段要立即執(zhí)行的 JavaScript 代碼,所以不會(huì)像 ajax 的 XmlHttpRequest 那樣可以監(jiān)聽不同事件對(duì)數(shù)據(jù)進(jìn)行不同處理。
處理結(jié)果如下所示:
2.3 使用 <script> 標(biāo)簽原生實(shí)現(xiàn) JSONP
經(jīng)過(guò)上面的事件,你是不是覺(jué)得 JSONP 的實(shí)現(xiàn)和 Ajax 大同小異?
其實(shí),由于實(shí)現(xiàn)的原理不同,由 JSONP 實(shí)現(xiàn)的跨域調(diào)用不是通過(guò) XmlHttpRequset 對(duì)象,而是通過(guò) script 標(biāo)簽,所以在實(shí)現(xiàn)原理上,JSONP 和 Ajax 已經(jīng)一點(diǎn)關(guān)系都沒(méi)有了??瓷先バ问较嗨浦皇怯捎?jQuery 對(duì) JSONP 做了封裝和轉(zhuǎn)換。
比如在上面的例子中,我們假設(shè)要傳輸?shù)臄?shù)據(jù) data 格式如下:
- {
- name: "chiaki",
- id": "3001"
- }
那么數(shù)據(jù)是如何傳輸?shù)哪?HTTP 請(qǐng)求頭的第一行如下:
- GET /ajax/deal?callback=jsonpCallback&name=chiaki&id=3001&_=1473164876032 HTTP/1.1
可見,即使形式上是用 POST 傳輸一個(gè) JSON 格式的數(shù)據(jù),其實(shí)發(fā)送請(qǐng)求時(shí)還是轉(zhuǎn)換成 GET 請(qǐng)求。
其實(shí)如果理解 JSONP 的原理的話就不難理解為什么只能使用 GET 請(qǐng)求方法了。由于是通過(guò) script 標(biāo)簽進(jìn)行請(qǐng)求,所以上述傳輸過(guò)程根本上是以下的形式:
- <script src = 'http://localhost:3001/ajax/deal?callback=jsonpCallback&name=chiaki&id=3001&_=1473164876032'></script>
這樣從服務(wù)器返回的代碼就可以直接在這個(gè) script 標(biāo)簽中運(yùn)行了。下面我們自己實(shí)現(xiàn)一個(gè) JSONP:
服務(wù)器 3000請(qǐng)求頁(yè)面的 JavaScript 代碼中,只有回調(diào)函數(shù) jsonpCallback:
- function jsonpCallback(data) {
- console.log("jsonpCallback: "+data.name)
- }
服務(wù)器 3000請(qǐng)求頁(yè)面還包含一個(gè) script 標(biāo)簽:
- <script src = 'http://localhost:3001/jsonServerResponse?jsonp=jsonpCallback'></script>
服務(wù)器 3001上對(duì)應(yīng)的處理函數(shù):
- app.get('/jsonServerResponse', function(req, res) {
- var cb = req.query.jsonp
- console.log(cb)
- var data = 'var data = {' + 'name: $("#name").val() + " - server 3001 jsonp process",' + 'id: $("#id").val() + " - server 3001 jsonp process"' + '};'
- var debug = 'console.log(data);'
- var callback = '$("#submit").click(function() {' + data + cb + '(data);' + debug + '});'
- res.send(callback)
- res.end()
- })
與上面一樣,我們?cè)谒@取的參數(shù)后面加上 “ – server 3001 jsonp process” 代表服務(wù)器對(duì)數(shù)據(jù)的操作。從代碼中我么可以看到,處理函數(shù)除了根據(jù)參數(shù)做相應(yīng)的處理,更多的也是進(jìn)行字符串的拼接。
最終的結(jié)果為:
2.4 JSONP 總結(jié)
至此,我們了解了 JSONP 的原理以及實(shí)現(xiàn)方式,它幫我們實(shí)現(xiàn)前端跨域請(qǐng)求,但是在實(shí)踐的過(guò)程中,我們還是可以發(fā)現(xiàn)它的不足:
只能使用 GET 方法發(fā)起請(qǐng)求,這是由于 script 標(biāo)簽自身的限制決定的。
不能很好的發(fā)現(xiàn)錯(cuò)誤,并進(jìn)行處理。與 Ajax 對(duì)比,由于不是通過(guò) XmlHttpRequest 進(jìn)行傳輸,所以不能注冊(cè) success、 error 等事件監(jiān)聽函數(shù)。
三、 使用 CORS 實(shí)現(xiàn)跨域調(diào)用
3.1 什么是 CORS?
Cross-Origin Resource Sharing(CORS)跨域資源共享是一份瀏覽器技術(shù)的規(guī)范,提供了 Web 服務(wù)從不同域傳來(lái)沙盒腳本的方法,以避開瀏覽器的同源策略,是 JSONP 模式的現(xiàn)代版。與 JSONP 不同,CORS 除了 GET 要求方法以外也支持其他的 HTTP 要求。用 CORS 可以讓網(wǎng)頁(yè)設(shè)計(jì)師用一般的 XMLHttpRequest,這種方式的錯(cuò)誤處理比 JSONP 要來(lái)的好。另一方面,JSONP 可以在不支持 CORS 的老舊瀏覽器上運(yùn)作。現(xiàn)代的瀏覽器都支持 CORS。
3.2 CORS 的實(shí)現(xiàn)
還是以 服務(wù)器 3000 上的請(qǐng)求頁(yè)面向 服務(wù)器 3001 發(fā)送請(qǐng)求為例。
服務(wù)器 3000 上的請(qǐng)求頁(yè)面 JavaScript 不變,如下:
- $(function() {
- $("#submit").click(function() {
- var data = {
- name: $("#name").val(),
- id: $("#id").val()
- };
- $.ajax({
- type: 'POST',
- data: data,
- url: 'http://localhost:3001/cors',
- dataType: 'json',
- cache: false,
- timeout: 5000,
- success: function(data) {
- console.log(data)
- },
- error: function(jqXHR, textStatus, errorThrown) {
- console.log('error ' + textStatus + ' ' + errorThrown);
- }
- });
- });
- });
服務(wù)器 3001上對(duì)應(yīng)的處理函數(shù):
- app.post('/cors', function(req, res) {
- res.header("Access-Control-Allow-Origin", "*");
- res.header("Access-Control-Allow-Headers", "X-Requested-With");
- res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
- res.header("X-Powered-By", ' 3.2.1')
- res.header("Content-Type", "application/json;charset=utf-8");
- var data = {
- name: req.body.name + ' - server 3001 cors process',
- id: req.body.id + ' - server 3001 cors process'
- }
- console.log(data)
- res.send(data)
- res.end()
- })
在服務(wù)器中對(duì)返回信息的請(qǐng)求頭進(jìn)行了設(shè)置。
最終的結(jié)果為:
3.3 CORS 中屬性的分析
- Access-Control-Allow-Origin
The origin parameter specifies a URI that may access the resource. The browser must enforce this. For requests without credentials, the server may specify “*” as a wildcard, thereby allowing any origin to access the resource.
- Access-Control-Allow-Methods
Specifies the method or methods allowed when accessing the resource. This is used in response to a preflight request. The conditions under which a request is preflighted are discussed above.
- Access-Control-Allow-Headers
Used in response to a preflight request to indicate which HTTP headers can be used when making the actual request.
3.4 CORS 與 JSONP 的對(duì)比
CORS 除了 GET 方法外,也支持其它的 HTTP 請(qǐng)求方法如 POST、 PUT 等。
CORS 可以使用 XmlHttpRequest 進(jìn)行傳輸,所以它的錯(cuò)誤處理方式比 JSONP 好。
JSONP 可以在不支持 CORS 的老舊瀏覽器上運(yùn)作。
四、 一些其它的跨域調(diào)用方式
4.1 window.name
window對(duì)象有個(gè)name屬性,該屬性有個(gè)特征:即在一個(gè)窗口 (window) 的生命周期內(nèi),窗口載入的所有的頁(yè)面都是共享一個(gè) window.name 的,每個(gè)頁(yè)面對(duì) window.name 都有讀寫的權(quán)限,window.name 是持久存在一個(gè)窗口載入過(guò)的所有頁(yè)面中的,并不會(huì)因新頁(yè)面的載入而進(jìn)行重置。
4.2 window.postMessage()
這個(gè)方法是 HTML5 的一個(gè)新特性,可以用來(lái)向其他所有的 window 對(duì)象發(fā)送消息。需要注意的是我們必須要保證所有的腳本執(zhí)行完才發(fā)送 MessageEvent,如果在函數(shù)執(zhí)行的過(guò)程中調(diào)用了他,就會(huì)讓后面的函數(shù)超時(shí)無(wú)法執(zhí)行。
參考:https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage