本文著作權(quán)歸饑人谷_Lyndon和饑人谷所有膀估,轉(zhuǎn)載請(qǐng)注明出處参袱。
這是一篇對(duì)于跨域的總結(jié)答朋,將涵蓋跨域的四種方法:
- jsonp
- cors
- 降域
- postMessage
在回顧每種方法時(shí)都會(huì)結(jié)合自己的實(shí)踐续镇。
>>> 什么是跨域镀裤?
在介紹跨域之前首先要了解何為“同源策略”(Same Origin Policy)卿樱,瀏覽器(注意:主體是瀏覽器)出于安全方面的考慮僚害,只允許與本域(同協(xié)議、同域名繁调、同端口)下的數(shù)據(jù)接口進(jìn)行交互萨蚕,不同源的客戶端腳本在沒有授權(quán)的情況下,是不能讀寫對(duì)方資源的蹄胰。
可以設(shè)想一下:如果沒有同源策略岳遥,如果我自己建了一個(gè)網(wǎng)站,然后在沒有支付寶客戶端腳本授權(quán)的情況下輕松操控支付寶的腳本裕寨,隨意傳入我的個(gè)人信息浩蓉,或者獲得其他用戶支付寶的數(shù)據(jù)派继,那將是非常危險(xiǎn)的。同源策略有效地阻止了諸如此類的危險(xiǎn)行為捻艳。
但是請(qǐng)?jiān)O(shè)想這樣一種場(chǎng)景:我自己建設(shè)了一個(gè)網(wǎng)站驾窟,這時(shí)候需要在網(wǎng)站上建設(shè)一個(gè)天氣控件,背后的數(shù)據(jù)我必須從一些天氣網(wǎng)站或者數(shù)據(jù)接口中進(jìn)行獲取认轨,但是由于同源策略的限制绅络,我無法實(shí)現(xiàn)這一目標(biāo)。因此跨域就應(yīng)運(yùn)而生了嘁字。JS在不同域之間進(jìn)行數(shù)據(jù)傳輸或者通信恩急,譬如AJAX向一個(gè)不同源的服務(wù)端去請(qǐng)求數(shù)據(jù),或者利用JS獲取頁面中不同域的iframe數(shù)據(jù)拳锚,從而實(shí)現(xiàn)不同域數(shù)據(jù)的相互訪問假栓,這些情境歸根結(jié)底都是跨域。
>>> 跨域方法1:jsonp
jsonp全稱:json with padding霍掺,這個(gè)名稱非常地形象匾荆。意思就是異步請(qǐng)求跨域服務(wù)端時(shí),不直接返回?cái)?shù)據(jù)杆烁,而是返回一個(gè)JS方法牙丽,數(shù)據(jù)是其中的參數(shù)。其實(shí)就相當(dāng)于數(shù)據(jù)變成了餡料兔魂,填充(padding)在一個(gè)方法里面烤芦,然后返回并運(yùn)行。
為什么會(huì)用這么巧妙的一種方法呢析校?實(shí)際上构罗,在書寫HTML時(shí)如果需要引用JQuery,只需要在頁面中加上<script src = "http://code.jquery.com/xxx"></script>
就可以了智玻,之后在HTML中就能調(diào)用JQuery中已經(jīng)封裝好的各種方法遂唧,但是code.jquery.com
與請(qǐng)求頁面的域名肯定不一樣,jsonp正是借鑒了這一點(diǎn)來實(shí)現(xiàn)跨域的數(shù)據(jù)訪問吊奢。
我的電腦是Windows系統(tǒng)盖彭,首先我在我的host文件中添加以下新域名:
# New Hosts
127.0.0.1 a.com
127.0.0.1 b.com
127.0.0.1 a.lyndon.com
127.0.0.1 b.lyndon.com
為何要在host文件中添加這些?因?yàn)樵跒g覽器地址欄中輸入域名后页滚,需要根據(jù)域名去尋找對(duì)應(yīng)的IP地址召边,這就是所謂的DNS解析,首先是在瀏覽器的緩存中尋找裹驰,如果沒有找到隧熙,就去系統(tǒng)的host文件中尋找,再?zèng)]有找到邦马,就去路由器緩存中找贱鼻,再往深處就是ISP DNS宴卖,根域名服務(wù)器滋将。
我在本地啟動(dòng)server-mock邻悬,最原始的客戶端頁面和服務(wù)端頁面代碼如下:
<div class="container">
<p class="show">0000</p>
<button class="btn">change</button>
</div>
<script>
function $(id){
return document.querySelector(id);
}
$(".btn").addEventListener("click", function(){
var xhr = new XMLHttpRequest();
xhr.open("get", "/change", true);
xhr.send();
xhr.onreadystatechange = function(){
if(xhr.readyState === 4 && xhr.status === 200){
console.log(JSON.parse(xhr.responseText));
append(JSON.parse(xhr.responseText));
}
}
});
function append(data){
$(".show").innerText = data.data[0];
}
</script>
app.get('/change', function(req, res){
array = [
"1111",
"2222",
"3333",
"4444",
"5555"
];
var data = [];
data.push(array[parseInt(Math.random() * array.length)]);
res.send({
data: data
});
});
在這種情境下,是能夠進(jìn)行正常請(qǐng)求的随闽,因?yàn)檎?qǐng)求頁面請(qǐng)求的是同域服務(wù)端的數(shù)據(jù)父丰。
但是當(dāng)我稍對(duì)客戶端頁面的代碼做更改,就會(huì)出現(xiàn)不一樣的結(jié)果掘宪。
xhr.open("get", "http://b.lyndon.com:8080/change", true);
因?yàn)?code>http://a.com:8080和http://b.lyndon.com:8080
不同域蛾扇,瀏覽器限制了我的跨域請(qǐng)求。
這時(shí)候使用jsonp的思路來做一些調(diào)整魏滚,這時(shí)候我就不再使用AJAX方法镀首,而是加入一個(gè)script
標(biāo)簽,點(diǎn)擊“change”按鈕時(shí)鼠次,script
的src
屬性將直接從服務(wù)端返回一個(gè)方法(回調(diào)函數(shù))更哄,數(shù)據(jù)將作為其中的參數(shù)⌒瓤埽客戶端頁面和服務(wù)端頁面代碼如下:
function $(id){
return document.querySelector(id);
}
// jsonp
$(".btn").addEventListener("click", function(){
var script = document.createElement("script");
script.src = "http://b.lyndon.com:8080/change?callback=process";
document.head.appendChild(script);
// 及時(shí)刪除成翩,防止加載過多的JS
document.head.removeChild(script);
});
function process(data){
$(".show").innerText = data[0];
}
app.get('/change', function(req, res){
array = [
"1111",
"2222",
"3333",
"4444",
"5555"
];
var data = [];
data.push(array[parseInt(Math.random() * array.length)]);
res.send(req.query.callback + "(" + JSON.stringify(data) + ")");
});
因?yàn)樵诳蛻舳思尤肓嘶卣{(diào)函數(shù),因此在服務(wù)端稍作更改即可赦役,返回的是一個(gè)function_name(data)
麻敌,這樣一來,即使脫離了server-mock
掂摔,也可以愉快地執(zhí)行了术羔。
- 客戶端域名為:
a.com:8080
- 單獨(dú)執(zhí)行html
>>> 跨域方法2:CORS
使用CORS方法和AJAX原代碼幾近類似,主要工作是在服務(wù)端加上響應(yīng)頭res.header("Access-Control-Allow-Origin", "xxx")乙漓,只要響應(yīng)頭中包含了請(qǐng)求頭(Origin)级历,就可以實(shí)現(xiàn)跨域,相當(dāng)于數(shù)據(jù)請(qǐng)求的決定權(quán)在于服務(wù)端是否同意簇秒,因此CORS對(duì)于代碼的修改也只需修改服務(wù)端代碼即可鱼喉。
客戶端和服務(wù)端的代碼如下:
<div class="ct">
<ul class="nums">
<li>111</li>
<li>222</li>
<li>333</li>
</ul>
<button class="btn">換一組</button>
</div>
<script>
function $(id){
return document.querySelector(id);
}
$(".btn").addEventListener("click", function(){
var xhr = new XMLHttpRequest();
xhr.open("get", "http://b.com:8080/getNums", true);
xhr.send();
xhr.onreadystatechange = function(){
if(xhr.readyState === 4 && xhr.status === 200){
appendHtml(JSON.parse(xhr.responseText));
}
}
})
function appendHtml(nums){
var html = "";
for(var i = 0; i < nums.length; i++){
html += "<li>" + nums[i] + "</li>";
}
console.log(html);
$(".nums").innerHTML = html;
}
</script>
app.get('/getNums', function(req, res) {
var array = [
"444",
"555",
"666",
"777",
"888",
"999",
"000"
]
var data = [];
for(var i = 0; i < 3; i++){
data.push(array[parseInt(Math.random() * array.length)]);
array.splice(parseInt(Math.random() * array.length), 1);
}
res.header("Access-Control-Allow-Origin", "http://b.com:8080");
res.send(data);
});
在以上的服務(wù)端代碼中,設(shè)定的允許域?yàn)?code>http://b.com:8080趋观,在進(jìn)行訪問時(shí)扛禽,如果打開localhost:8080
,雖然存在數(shù)據(jù)交換但是無法更新頁面皱坛。
將訪問頁的域名改為http://b.com:8080
即可正常訪問编曼。
如果為了方便,希望來自所有域的請(qǐng)求都可以自由獲取服務(wù)端的數(shù)據(jù)剩辟,那么只需要改為:res.header("Access-Control-Allow-Origin", "*");
即可掐场。
>>> 跨域方法3:降域
降域使得處于不同域的兩個(gè)HTML文件實(shí)現(xiàn)相互訪問或相互操作成為可能往扔。一個(gè)非常典型的使用場(chǎng)景:在一個(gè)頁面中存在一個(gè)iframe
,但是iframe
中的網(wǎng)頁與包含網(wǎng)頁不同域熊户,使用降域的方法可以實(shí)現(xiàn)兩個(gè)頁面內(nèi)容的同步更改萍膛,因?yàn)橹挥刑幱谕驐l件才能使用JS操作其中的元素。
需要注意的一點(diǎn)是:降域的使用是存在限制的嚷堡,域名中需要有一致的父級(jí)域名才可以使用降域蝗罗。
比如:a.lyndon.com
和b.lyndon.com
,它們擁有一致的父級(jí)域名:lyndon.com
蝌戒,因此可以進(jìn)行降域從而實(shí)現(xiàn)跨域串塑,而a.com
和b.com
無法進(jìn)行降域,同理北苟,類似于a.jrg.com
和b.lik.com
也不行桩匪。
降域的實(shí)現(xiàn)很簡(jiǎn)單,以剛才提及的使用場(chǎng)景為例:只需要在兩個(gè)html文件的script
中加入共同的代碼document.domain="lyndon.com";
即可友鼻。
以下展現(xiàn)a.html和b.html的代碼:
<div class="main">
<input type="text" placeholder="http://a.lyndon.com:8080/a.html">
</div>
<iframe src="http://b.lyndon.com:8080/b.html" frameborder="0"></iframe>
<script>
document.querySelector(".main input").addEventListener("input", function(){
console.log(this.value);
window.frames[0].document.querySelector("#input").value = this.value;
});
document.domain = "lyndon.com";
</script>
<input type="text" id="input" placeholder="http://b.lyndon.com:8080/b.html">
<script>
document.querySelector("#input").addEventListener("input", function(){
console.log(this.value);
window.parent.document.querySelector("input").value = this.value;
});
document.domain = "lyndon.com";
</script>
這里的window.frames
返回的是一個(gè)類數(shù)組對(duì)象傻昙,成員為頁面內(nèi)所有的框架,包括frame元素和iframe元素桃移,window.frames
內(nèi)的每個(gè)成員是框架內(nèi)的窗口(框架的window對(duì)象)屋匕,如果需要獲取每個(gè)框架的DOM樹,就需要像以上代碼一樣寫成window.frames[0].document
的形式借杰。
在第二段(b.html)的代碼中过吻,iframe內(nèi)部使用的window.parent
指向的是父頁面。因此第二段代碼中的window.parent.document.querySelector("input")
對(duì)應(yīng)的是第一段代碼中的input
蔗衡,這樣的做法在兩個(gè)代碼文件中建立起了相互的連接纤虽。
實(shí)際效果如下:
>>> 跨域方法4:postMessage(window對(duì)象才有postMessage方法)
介紹postMessage之前,需要明確一點(diǎn):iframe元素遵守同源政策绞惦,只有當(dāng)父頁面與框架頁面來自同一個(gè)域名逼纸,兩者之間才可以用腳本通信,否則只有使用window.postMessage方法济蝉。
因此可以明確得知:postMessage的使用范圍是更加廣闊的杰刽,且當(dāng)降域不可行時(shí)(如:a.com和b.com無法降域)時(shí),使用postMessage會(huì)是一個(gè)不錯(cuò)的選擇王滤。
這里依然以頁面與嵌套的iframe消息傳遞這一場(chǎng)景為例贺嫂。postMessage(data, origin)方法接受兩個(gè)參數(shù):
-
data
:要傳遞的數(shù)據(jù),為了讓所有瀏覽器都能正常解析雁乡,建議使用:JSON.stringify()
方法將對(duì)象參數(shù)序列化 -
origin
:目標(biāo)窗口的源第喳,postMessage()方法會(huì)將message傳遞給指定窗口,同CORS中一樣踱稍,如果將origin
設(shè)置為*
曲饱,就可以將message傳遞給任意窗口
與postMessage(發(fā)送消息)對(duì)應(yīng)的是接收消息悠抹,因此與postMessage相互搭配的是監(jiān)聽window的message事件。
以下給出兩份添加注釋的html代碼:
<div class="ct">
<input type="text" placeholder="http://a.lyndon.com:8080/a.html">
</div>
<iframe src="http://localhost:8080/b.html" frameborder="0"></iframe>
<script>
// 將輸入的信息傳遞給頁面上的不同域的iframe(b.html)
document.querySelector(".ct input").addEventListener("input", function(){
console.log(this.value);
window.frames[0].postMessage(this.value, "http://localhost:8080/b.html");
});
// 監(jiān)聽b.html(本頁面上的iframe)是否有message傳遞過來扩淀,如果有楔敌,將輸入框中的內(nèi)容換成iframe中input里的輸入內(nèi)容
window.addEventListener("message", function(e){
document.querySelector(".ct input").value = e.data;
console.log(e.data);
});
</script>
<input type="text" id="input" placeholder="http://b.lyndon.com:8080/b.html">
<script>
// 當(dāng)有輸入的文字時(shí),向父頁面(a.html)發(fā)出message
document.querySelector("#input").addEventListener("input", function () {
window.parent.postMessage(this.value, "http://a.lyndon.com:8080/a.html");
});
// 監(jiān)聽a.html是否有message傳遞過來引矩,如果有梁丘,將iframe輸入框中的內(nèi)容換成a.html中input里的輸入內(nèi)容
window.addEventListener("message", function(e){
document.querySelector("#input").value = e.data;
console.log(e.data);
});
</script>
所以歸根結(jié)底侵浸,postMessage就是一個(gè)信息交叉的過程旺韭。實(shí)際執(zhí)行效果是:
>>> 附加一個(gè)自己的實(shí)踐:使用jsonp獲取百度聯(lián)想詞
- 首先在Console中Network查看百度搜索詞的聯(lián)想詞獲取地址
聯(lián)想詞的數(shù)據(jù)地址為:https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd=%E6%BC%82%E4%BA%AE%E7%9A%84&json=1&p=3&sid=1452_21099_18559_21673&req=2&csor=3&pwd=%20&cb=jQuery110208414170774720962_1486043984005&_=1486043984013
精簡(jiǎn)URL,可以發(fā)現(xiàn):"https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd=" + string
即可返回聯(lián)想詞掏觉。后面需要加上callback"cb=" + function name
來進(jìn)行返回結(jié)果的處理区端。
- 動(dòng)態(tài)獲取跨域數(shù)據(jù)
function $(id){
if(document.querySelectorAll(id).length > 1){
return document.querySelectorAll(id);
}else{
return document.querySelector(id);
}
}
var txt = $("#txt"),
ul = $("#baidusug"),
script = null;
txt.onkeyup = function (){
ul.innerHTML = "";
if (script) {
document.body.removeChild(script);
}
script = document.createElement("script");
script.src = "https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd=" + txt.value + "&cb=process";
document.body.appendChild(script);
};
function process(json){
for(var i = 0; i < json["s"].length; i++){
var li = document.createElement("li");
li.innerHTML = json.s[i];
ul.appendChild(li);
}
}
- 最后的結(jié)果
>>> 總結(jié)
在今后的使用過程中,只需要辨清場(chǎng)景澳腹,然后按照因地制宜的原則選擇一種跨域方法就好织盼,沒有必要完全依賴于一種特定的方法。一言以蔽之:沒有最正確的酱塔,只有最適合的沥邻。