瀏覽器出于安全方面的考慮,只允許與本域下的接口交互。不同源的客戶端腳本在沒有明確授權的情況下,不能讀寫對方的資源。
跨域的方式:
JSONP
html中script標簽可以引入其他域下的js,比如引入線上的jquery庫。利用這個特性,可實現跨域訪問接口。需要后端支持。
JSONP看起來和JSON差不多,只不過被包含在函數中調用中的JSON,就像下面這樣:
callback({"name":"Jack"});
以請求高德API天氣接口為例,本地調用高德API數據接口,通過動態<script>
元素來使用,為src屬性指定一個跨域URL。<script>
可以不受限制的從其他域加載資源。
var script = document.createElement('script');
script.src = "http://restapi.amap.com/v3/weather/weatherInfo?key=你的key&output=JSON&extensions=base&callback=showWeather&city="+$('#cityname').value;
document.head.appendChild(script);
//document.head.removeChild(script);
//可以在創建script元素后將它刪除,避免head里script標簽過多,刪除標簽不影響script獲取地址中的資源
//我在url都設定了一個回調函數showWeather;在加載API接口時,返回的數據是這樣的:
showWeather({"status":"1","count":"1","info":"OK","infocode":"10000","lives":[{"province":"上海","city":"上海市","adcode":"310000","weather":"晴","temperature":"33","winddirection":"西","windpower":"4","humidity":"59","reporttime":"2017-07-16 19:00:00"}]})
//返回的數據被包裹在showWeather中,因此我們就可以把showWeather當成一個函數,里面的內容即為傳入函數的內容,只需要將返回的JSON數據解析成JavaScript對象就可以使用了
但是我在將返回數據解析成JavaScript對象時遇到了報錯:
function showWeather(data){
data = JSON.parse(data);
}
Uncaught SyntaxError: Unexpected token o in JSON at position 1
[object Object]
然后我直接打印出data,發現data已經是Object格式的了,并不需要再次解析。
console.log(data);
//{status: "1", count: "1", info: "OK", infocode: "10000", lives: Array(1)}
經查閱資料后發現,Object在作為JSON.parse
的參數時會先轉為string,默認toString實現會將Object轉為"[object Object]"
,JSON.parse
將第一個字符'['理解為數組開始,第二字符'o'就不知道怎么處理了。
如果不確定可以加入這行代碼:
if (typeof data === 'string') {
data = JSON.parse(data);
}
因此只需要直接使用返回的data就可以了,不需要另外再對data進行解析。
另:jQuery中$.ajax()
方法已經對jsonp進行了封裝,它會直接在返回的數據上默認加一個函數頭,在success:function()
中可以直接使用,不需要進行另外的函數設置。
HTML頁面代碼:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>請輸入需要查詢的城市</h1>
<input type="text" id="cityname">
<button id="btn">查詢</button>
<div class="today-weather">
<h1>今天天氣</h1>
<div id="city">城市:</div>
<div id="weather">天氣:</div>
<div id="temperature">溫度:</div>
<div id="winddirection">風向:</div>
<div id="windpower">風力:</div>
<div id="humidity">濕度:</div>
</div>
</body>
<script>
//設置選擇器
function $(id){
return document.querySelector(id);
}
//給btn設置監聽事件
$('#btn').addEventListener('click',function(){
var script = document.createElement('script');
script.src = "http://restapi.amap.com/v3/weather/weatherInfo?key=你的key&output=JSON&extensions=base&callback=showWeather&city="+$('#cityname').value;
document.head.appendChild(script);
document.head.removeChild(script);
})
function showWeather(data){
var weather = data.lives[0];
for(var prop in weather){
if($('#'+prop)){
var span = document.createElement('span');
span.innerHTML = weather[prop];
$('#'+prop).appendChild(span);
}
}
}
JSONP的不足之處:
- JSONP從其他域加載代碼執行,如果其他域不安全,可能會在代碼中夾雜惡意代碼。
- 要確定JSONP請求是否失敗并不容易。
- 只能支持GET方式獲取數據,需要后端支持。
CORS跨域資源共享
CORS 全稱是跨域資源共享(Cross-Origin Resource Sharing),是一種 ajax 跨域請求資源的方式,支持現代瀏覽器,IE支持10以上。 實現方式很簡單,當你使用 XMLHttpRequest 發送請求時,瀏覽器發現該請求不符合同源策略,會給該請求加一個請求頭:Origin,后臺進行一系列處理,如果確定接受請求則在返回結果中加入一個響應頭:Access-Control-Allow-Origin; 瀏覽器判斷該相應頭中是否包含 Origin 的值,如果有則瀏覽器會處理響應,我們就可以拿到響應數據,如果不包含瀏覽器直接駁回,這時我們無法拿到響應數據。所以 CORS 的表象是讓你覺得它與同源的 ajax 請求沒啥區別,代碼完全一樣。
以向高德API請求天氣數據為例,在發送$.ajax()
請求后,我沒有使用JSONP的跨域方法,API一樣會返回數據,進入響應后查看發現,在請求頭中瀏覽器自動加入了請求頭:
Origin:http://127.0.0.1
在響應頭中可以看到:
Access-Control-Allow-Origin:*
服務器默認可以給任何請求返回響應,所有看起來代碼和普通ajax代碼完全相同,沒有做跨域操作依然拿到了數據。CORS主要是服務器端的設置。
<script>
$(function(){
$('#btn').click(function(){
var span = $('span');
var city = $('#cityname').val();
if(span){
span.remove();
}
if(city){
searchWeather();
}
});
function searchWeather(){
$.ajax({
type:'GET',
url:"http://restapi.amap.com/v3/weather/weatherInfo?key=你的key&output=JSON&extensions=base&city="+$('#cityname').val(),
success:function(data){
for(var prop in data.lives[0]){
$('#'+prop).append('<span>'+data.lives[0][prop]+'</span>');
}
},
error:function(jqXHR){
console.log(jqXHR.status);
}
});
}
});
</script>
postMessage
由于同源策略的限制,JavaScript 跨域的問題,一直是一個頗為棘手的問題。HTML5 提供了在網頁文檔之間互相接收與發送信息的功能。使用這個功能,只要獲取到網頁所在窗口對象的實例,不僅同源(域 + 端口號)的 Web 網頁之間可以互相通信,甚至可以實現跨域通信。
要想接收從其他窗口發送來的信息,必須對窗口對象的 onmessage 事件進行監聽,其它窗口可以通過 postMessage 方法來傳遞數據。
語法:
otherWindow.postMessage(message, targetOrigin);
otherWindow
其他窗口的一個引用,比如iframe的contentWindow屬性、執行window.open返回的窗口對象、或者是命名過或數值索引的window.frames。
該方法使用兩個參數:第一個參數為所發送的消息文本,但也可以是任何 JavaScript 對象(通過 JSON 轉換對象為文本),第二個參數為接收消息的對象窗口的 URL 地址,可以在 URL 地址字符串中使用通配符'*'指定全部地。
為了模擬本地跨域,我在hosts里添加了兩個不同的域名:
127.0.0.1 a.com
127.0.0.1 b.com
127.0.0.1 c.com
在父頁面里嵌入了一個子頁面,調用postMessage方法向子頁面發送數據。在子窗口監聽onmessage事件,并顯示到input框內。
父頁面代碼:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>PostMessage A</title>
</head>
<body>
<h1>實現PostMessage實現跨域</h1>
<div class="parent">
<input type="text" placeholder="http://a.com:8080/a.html">
</div>
<iframe src="http://b.com:8080/b.html" frameborder="0"></iframe>
<iframe src="http://c.com:8080/c.html" frameborder="0"></iframe>
</body>
<script>
$('.parent input').addEventListener('input',function(){
//給input輸入框添加事件,當輸入框內容發生變化時就進行監聽,把輸入框中的內容發送到b.html和c.html中
console.log(this.value);
window.frames[0].postMessage(this.value,'http://b.com:8080');
window.frames[1].postMessage(this.value,'http://c.com:8080');
//如果將http://b.com:8080改為*,則表示對所以地址發送信息
})
/*
* frameList是一個frame對象的集合,它類似一個數組,有length屬性且可以使用索引([i])來訪問。
* frameList === window 計算結果為true。
* 在window.frames類數組中的每一項都代表了窗口對應給定對象的<frame>或<iframe>的內容,
而不是(i)frame DOM元素(即window.frames[ 0 ]與document.getElementsByTagName( "iframe" )[ 0 ].contentWindow是相同的)。
*/
function $(id){
return document.querySelector(id);
}
</script>
</html>
子頁面代碼:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>PostMessage B</title>
</head>
<body>
<input type="text" placeholder="http://b.com:8080/b.html" id="input">
<script>
function $(id){
return document.querySelector(id);
}
//接收信息時對window的message進行監聽才可以收到信息
window.addEventListener('message',function(e){
$('#input').value = e.data;
})
</script>
</body>
</html>
在進行跨域的時候,設置好hosts后,啟用本地服務器后,打開a.com時需要加上端口號!!
即在地址欄輸入:a.com:8080/a.html
因為訪問網頁時都是要通過IP地址及端口號進行訪問的,平時我們經常打開的網頁絕大多數是超文本傳輸協議,即http,不要求用戶輸入網頁“http://”的部分。同樣,“80”是超文本傳輸協議文件的常用端口號,因此一般也不必寫明。一般來說用戶只要鍵入統一資源定位符的一部分,比如"www.baidu.com",此處的端口號是默認的80端口。
而我在本地啟用的服務器需要訪問8080端口,因此需要手動輸入,如果不輸入就無法正常訪問。