關(guān)于跨域大概可以分為 iframe 的跨域和純粹的跨全域請求。
3種跨全域方法:
1荣瑟、JSONP
全稱:JSON with Padding,可用于解決主流瀏覽器的跨域數(shù)據(jù)訪問的問題。
Web 頁面上調(diào)用 js 文件不受瀏覽器同源策略的影響橱赠,所以通過 Script 便可以進(jìn)行跨域的請求:(頁面中兩個script標(biāo)簽厚棵,其中一個是函數(shù)聲明call蕉世,另一個是請求成功后的函數(shù)調(diào)用call())
代碼如下
html部分
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>跨域</title>
<script src="index1.js"></script>
</head>
<body>
</body>
</html>
js部分蔼紧,文件名為index1.js
//創(chuàng)建script標(biāo)簽婆硬,設(shè)置屬性,將script標(biāo)簽添加到head中奸例,至此head中有兩個script標(biāo)簽
function loadScript(url) {
var script = document.createElement('script')
script.src = url
script.type = 'text/javascript'
var head = document.querySelector('head')
head.appendChild(script)
}
//設(shè)置回調(diào)函數(shù)的函數(shù)聲明
function jsonpCallback(data) {
console.log(JSON.stringify(data))
}
loadScript('http://127.0.0.1:3000?callback=jsonpCallback')
后端邏輯彬犯,文件名為index2.js
var url = require('url')//獲取url模塊
var http = require('http')//獲取http模塊
http.createServer(function (req, res) {//創(chuàng)建服務(wù)器并監(jiān)聽127.0.0.1的3000端口
//設(shè)置數(shù)據(jù)
var data = {
name: "dolby"
};
//url.parse()方法將一個url字符串轉(zhuǎn)換為URL對象,true的意思是query 屬性將始終設(shè)置為由 querystring 模塊的 parse() 方法返回的對象(參考https://stackoverflow.com/questions/17184791/node-js-url-parse-and-pathname-property)查吊。.query方法獲取的是不含問號的參數(shù)鍵值對谐区,callback為鍵名,不固定逻卖,只要前后端約定好就可以
//所以callbackName為回調(diào)函數(shù)的名字宋列,即前段index1.js文件中的jsonpCallback
var callbackName = url.parse(req.url, true).query.callback
//設(shè)置響應(yīng)頭為200,即OK狀態(tài)评也,一般也要提前約定好炼杖,不能亂寫
res.writeHead(200)
//res.end()即發(fā)送后端相應(yīng)結(jié)果,${callbackName}為函數(shù)名盗迟,${JSON.stringify(data)}表示JSON字符串格式的實(shí)際參數(shù)坤邪,合起來為函數(shù)調(diào)用
res.end(`${callbackName}(${JSON.stringify(data)})`)
}).listen(3000, '127.0.0.1')
//打印這句話只是方便在終端中查看代碼是否生效
console.log('啟動服務(wù),監(jiān)聽 127.0.0.1:3000')
- 前端先設(shè)置好回調(diào)函數(shù)罚缕,并將函數(shù)名作為url的參數(shù)艇纺;將此url插入到新創(chuàng)建的script標(biāo)簽的src屬性中,并將此script標(biāo)簽添加到頁面的head中
- 服務(wù)端接收到請求后邮弹,通過該參數(shù)獲得回調(diào)函數(shù)名jsonpCallback黔衡,并將js格式的數(shù)據(jù)作為函數(shù)調(diào)用的參數(shù)返回
- 收到結(jié)果后因為是 script 標(biāo)簽,所以瀏覽器會當(dāng)做是腳本運(yùn)行腌乡,從而達(dá)到跨域獲取數(shù)據(jù)的目的盟劫。
驗證過程:
- 通過
node index2.js
在終端中啟動服務(wù),監(jiān)聽端口 3000导饲,這樣服務(wù)端就建立起來了
- 我們通過端口號的不同來模擬跨域的場景捞高,打開另一個終端窗口,在頁面同目錄下輸入
http-server
通過端口8080來訪問剛才的頁面渣锦,相當(dāng)于開啟兩個監(jiān)聽不同端口的 http 服務(wù)器硝岗,通過頁面中的請求來模擬跨域的場景。打開瀏覽器袋毙,訪問http://127.0.0.1:8080
就可以看到從http://127.0.0.1:3000
獲取到的數(shù)據(jù)了型檀。
至此,通過 JSONP 跨域獲取數(shù)據(jù)已經(jīng)成功了听盖,但這種跨域方式存在一定的優(yōu)缺點(diǎn):
- 優(yōu)點(diǎn):
- 它不像XMLHttpRequest 對象實(shí)現(xiàn) Ajax 請求那樣受到同源策略的限制
- 兼容性好胀溺,在古老的瀏覽器也能很好的運(yùn)行
- 不需要 XMLHttpRequest 或 ActiveX 的支持裂七;并且在請求完畢后可以通過調(diào)用 callback 的方式回傳結(jié)果。
- 缺點(diǎn):
- 只支持 GET 請求仓坞。
- 歷史原因遺留的bug背零,注定被淘汰
- 只支持跨域 HTTP 請求這種情況,不能解決不同域的兩個頁面或 iframe 之間進(jìn)行數(shù)據(jù)通信的問題
- 容易遭受XSS攻擊无埃,因為我們拿到的是對方接口的數(shù)據(jù)作為js執(zhí)行徙瓶,如果得到的是一個很危險js,獲取了用戶信息和cookies嫉称,這時執(zhí)行了js就會出現(xiàn)安全問題侦镇。
2、CORS
CORS是一個W3C標(biāo)準(zhǔn)织阅,全稱:Cross-origin resource sharing壳繁,跨域資源共享±竺蓿跨域資源共享標(biāo)準(zhǔn)新增了一組 HTTP 首部字段闹炉,允許服務(wù)器聲明哪些源站有權(quán)限訪問哪些資源。允許瀏覽器向跨源服務(wù)器發(fā)出 XMLHttpRequest 請求江耀,克服了 ajax 只能同源使用的限制剩胁。
CORS 需要瀏覽器和服務(wù)器同時支持才可以生效,對于開發(fā)者來說祥国,CORS 通信與同源的 ajax 通信沒有差別昵观,代碼完全一樣。瀏覽器一旦發(fā)現(xiàn) ajax 請求跨源舌稀,就會自動添加一些附加的頭信息啊犬,有時還會多出一次附加的請求,但用戶不會有感覺壁查。
因此觉至,實(shí)現(xiàn) CORS 通信的關(guān)鍵是服務(wù)器。只要服務(wù)器實(shí)現(xiàn)了 CORS 接口睡腿,就可以跨源通信语御。
代碼如下
html部分
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>跨域</title>
<script src="index1.js"></script>
</head>
<body>
</body>
</html>
js部分,文件名為index1.js
var xhr = new XMLHttpRequest()//1
xhr.open('GET', 'http://127.0.0.1:3000', true)//2
xhr.onload = function () {
console.log(xhr.responseText)
}//4席怪,因為onload(callback)是異步的应闯,后執(zhí)行
xhr.send()//3
這似乎跟一次正常的異步 ajax 請求沒有什么區(qū)別,關(guān)鍵是在服務(wù)端收到請求后的處理:
后端邏輯挂捻,文件名為index2.js
var http = require('http')//獲取http模塊
http.createServer(function (req, res) {//創(chuàng)建服務(wù)器并監(jiān)聽127.0.0.1的3000端口
//設(shè)置響應(yīng)頭為200碉纺,即OK狀態(tài),一般也要提前約定好,不能亂寫,設(shè)置允許訪問的白名單
res.writeHead(200, {
'Access-Control-Allow-Origin': 'http://127.0.0.1:8080'
})
res.end('hello,dolby')
}).listen(3000, '127.0.0.1')
//打印這句話只是方便在終端中查看代碼是否生效
console.log('啟動服務(wù)骨田,監(jiān)聽 127.0.0.1:3000')
關(guān)鍵是在于設(shè)置響應(yīng)頭中的 Access-Control-Allow-Origin
耿导,該值要與請求頭中 Origin 一致才能生效,否則將跨域失敗态贤。
與JSONP一樣舱呻,再次開啟兩個 http 服務(wù)器進(jìn)程:
打開瀏覽器:
成功的關(guān)鍵在于 Access-Control-Allow-Origin
是否包含請求頁面的域名,如果不包含的話抵卫,瀏覽器將認(rèn)為這是一次失敗的異步請求狮荔,將會調(diào)用 xhr.onerror
中的函數(shù)。
- CORS 的優(yōu)點(diǎn):
- 使用簡單方便介粘,更為安全
- 支持 POST 請求方式
- 精確控制資源訪問權(quán)限
- 客戶端無需增加額外代碼
- 缺點(diǎn):
- CORS僅兼容 IE 10 以上
3、Server Proxy服務(wù)器代理
需要跨域的請求操作時發(fā)送請求給后端晚树,讓后端幫你代為請求姻采,然后將獲取的結(jié)果發(fā)送給你。
假設(shè)你的頁面需要獲取 CNode:Node.js專業(yè)中文社區(qū) 論壇上一些數(shù)據(jù)爵憎,如通過 https://cnodejs.org/api/v1/topics慨亲,因為不同域,所以你可以請求后端讓其代為轉(zhuǎn)發(fā)請求
const url = require('url');
const http = require('http');
const https = require('https');
const server = http.createServer((req, res) => {
const path = url.parse(req.url).path.slice(1);
if(path === 'topics') {
https.get('https://cnodejs.org/api/v1/topics', (resp) => {
let data = "";
resp.on('data', chunk => {
data += chunk;
});
resp.on('end', () => {
res.writeHead(200, {
'Content-Type': 'application/json; charset=utf-8'
});
res.end(data);
});
})
}
}).listen(3000, '127.0.0.1');
console.log('啟動服務(wù)宝鼓,監(jiān)聽 127.0.0.1:3000');
通過代碼你可以看出刑棵,當(dāng)你訪問http://127.0.0.1:3000
時,服務(wù)器收到請求愚铡,會代你發(fā)送請求https://cnodejs.org/api/v1/topics
蛉签,最后將獲取到的數(shù)據(jù)發(fā)送給瀏覽器。
同樣地開啟服務(wù):
打開瀏覽器訪問http://localhost:3000/topics
就可以看到:
跨域請求成功沥寥。
4種通過iframe跨域與其他頁面通信的方式
1碍舍、location.hash:
在 url 中,http://www.baidu.com#helloworld
的 #helloworld
就是 location.hash邑雅,改變 hash 值不會導(dǎo)致頁面刷新片橡,所以可以利用 hash 值來進(jìn)行數(shù)據(jù)的傳遞,當(dāng)然數(shù)據(jù)量是有限的淮野。
假設(shè) localhost:8080 下有文件 cs1.html 要和 localhost:8081 下的 cs2.html 傳遞消息捧书,cs1.html 首先創(chuàng)建一個隱藏的 iframe,iframe 的 src 指向 localhost:8081/cs2.html骤星,這時的 hash 值就可以做參數(shù)傳遞经瓷。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CS1</title>
</head>
<body>
<script>
// http://localhost:8080/cs1.html
let ifr = document.createElement('iframe');
ifr.style.display = 'none';
ifr.src = "http://localhost:8081/cs2.html#data";
document.body.appendChild(ifr);
function checkHash() {
try {
let data = location.hash ? location.hash.substring(1) : '';
console.log('獲得到的數(shù)據(jù)是:', data);
}catch(e) {
}
}
window.addEventListener('hashchange', function(e) {
console.log('獲得的數(shù)據(jù)是:', location.hash.substring(1));
});
</script>
</body>
</html>
cs2.html 收到消息后通過 parent.location.hash 值來修改 cs1.html 的 hash 值,從而達(dá)到數(shù)據(jù)傳遞妈踊。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CS2</title>
</head>
<body>
<script>
// http://locahost:8081/cs2.html
switch(location.hash) {
case "#data":
callback();
break;
}
function callback() {
const data = "some number: 1111"
try {
parent.location.hash = data;
}catch(e) {
// ie, chrome 下的安全機(jī)制無法修改 parent.location.hash
// 所以要利用一個中間的代理 iframe
var ifrproxy = document.createElement('iframe');
ifrproxy.style.display = 'none';
ifrproxy.src = 'http://localhost:8080/cs3.html#' + data; // 該文件在請求域名的域下
document.body.appendChild(ifrproxy);
}
}
</script>
</body>
</html>
由于兩個頁面不在同一個域下IE了嚎、Chrome不允許修改parent.location.hash的值,所以要借助于 localhost:8080 域名下的一個代理 iframe 的 cs3.html 頁面
<script>
parent.parent.location.hash = self.location.hash.substring(1);
</script>
之后老規(guī)矩,開啟兩個 http 服務(wù)器:
這里為了圖方便歪泳,將 cs1,2,3 都放在同個文件夾下萝勤,實(shí)際情況的話 cs1.html 和 cs3.html 要與 cs2.html 分別放在不同的服務(wù)器才對。
之后打開瀏覽器訪問 localhost:8080/cs1.html呐伞,注意不是 8081敌卓,就可以看到獲取到的數(shù)據(jù)了,此時頁面的 hash 值也已經(jīng)改變伶氢。
缺點(diǎn):
- 數(shù)據(jù)直接暴露在了 url 中
- 數(shù)據(jù)容量和類型都有限
2趟径、window.name:
window.name(一般在 js 代碼里出現(xiàn))的值不是一個普通的全局變量,而是當(dāng)前窗口的名字癣防,要注意的是每個 iframe 都有包裹它的 window蜗巧,而這個 window 是top window 的子窗口,而它自然也有 window.name 的屬性蕾盯,window.name 屬性的神奇之處在于 name 值在不同的頁面(甚至不同域名)加載后依舊存在(如果沒修改則值不會變化)幕屹,并且可以支持非常長的 name 值(2MB)。
舉個簡單的例子:你在某個頁面的控制臺輸入:
window.name = "Hello World";
window.location = "http://www.baidu.com";
頁面跳轉(zhuǎn)到了百度首頁级遭,但是 window.name 卻被保存了下來望拖,還是 Hello World,跨域解決方案似乎可以呼之欲出了:
首先創(chuàng)建 a.html 文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>a.html</title>
</head>
<body>
<script>
let data = '';
const ifr = document.createElement('iframe');
ifr.src = "http://localhost:8081/b.html";
ifr.style.display = 'none';
document.body.appendChild(ifr);
ifr.onload = function() {
ifr.onload = function() {
data = ifr.contentWindow.name;
console.log('收到數(shù)據(jù):', data);
}
ifr.src = "http://localhost:8080/c.html";
}
</script>
</body>
</html>
再創(chuàng)建 b.html 文件:
<script>
window.name = "你想要的數(shù)據(jù)!";
</script>
http://localhost:8080/a.html
在請求遠(yuǎn)端服務(wù)器http://localhost:8081/b.html
的數(shù)據(jù)挫鸽,我們可以在該頁面下新建一個 iframe说敏,該 iframe 的 src 屬性指向服務(wù)器地址(利用 iframe 標(biāo)簽的跨域能力),服務(wù)器文件 b.html 設(shè)置好 window.name 的值丢郊。
但是由于 a.html 頁面和該頁面 iframe 的 src 如果不同源的話盔沫,則無法操作 iframe 里的任何東西,所以就取不到 iframe 的 name 值蚂夕,所以我們需要在 b.html 加載完后重新?lián)Q個 src 去指向一個同源的 html 文件迅诬,或者設(shè)置成'about:blank;'
都行,這時候我只要在 a.html 相同目錄下新建一個 c.html 的空頁面即可婿牍。如果不重新指向 src 的話直接獲取的 window.name 的話會報錯:
老規(guī)矩侈贷,打開兩個 http 服務(wù)器:
打開瀏覽器就可以看到結(jié)果:
3、postMessage:
postMessage 是 HTML5 新增加的一項功能等脂,跨文檔消息傳輸(Cross Document Messaging)俏蛮,目前:Chrome 2.0+、Internet Explorer 8.0+, Firefox 3.0+, Opera 9.6+, 和 Safari 4.0+ 都支持這項功能上遥。
首先創(chuàng)建 a.html 文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>a.html</title>
</head>
<body>
<iframe src="http://localhost:8081/b.html" style='display: none;'></iframe>
<script>
window.onload = function() {
let targetOrigin = 'http://localhost:8081';
window.frames[0].postMessage('我要給你發(fā)消息了!', targetOrigin);
}
window.addEventListener('message', function(e) {
console.log('a.html 接收到的消息:', e.data);
});
</script>
</body>
</html>
創(chuàng)建一個 iframe搏屑,使用 iframe 的一個方法 postMessage 可以向http://localhost:8081/b.html
發(fā)送消息,然后監(jiān)聽 message粉楚,可以獲得其他文檔發(fā)來的消息辣恋。
同樣的 b.html 文件:
<script>
window.addEventListener('message', function(e) {
if(e.source != window.parent) {
return;
}
let data = e.data;
console.log('b.html 接收到的消息:', data);
parent.postMessage('我已經(jīng)接收到消息了!', e.origin); });
</script>
同樣的開啟 http 服務(wù)器亮垫,在一個終端下執(zhí)行http-server并在瀏覽器中打開http://127.0.0.1:8080/a.html,開啟另一個終端執(zhí)行http-server并在瀏覽器中打開http://127.0.0.1:8081/b.html伟骨,效果如下
4饮潦、document.domain降域:
對于主域相同而子域不同的情況下,可以通過設(shè)置 document.domain 的辦法來解決携狭,具體做法是可以在 http://www.example.com/a.html
和http://sub.example.com/b.html
兩個文件分別加上 document.domain = "a.com"
继蜡;然后通過 a.html 文件創(chuàng)建一個 iframe,去控制 iframe 的 window逛腿,從而進(jìn)行交互稀并,當(dāng)然這種方法只能解決主域相同而二級域名不同的情況,如果你異想天開的把 script.example.com 的 domain 設(shè)為 qq.com 顯然是沒用的单默,那么如何測試呢碘举?
測試的方式稍微復(fù)雜點(diǎn),需要安裝 nginx 做域名映射雕凹,如果你電腦沒有安裝 nginx殴俱,請先去安裝一下: nginx news
先創(chuàng)建一個 a.html 文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>a.html</title>
</head>
<body>
<script>
document.domain = 'example.com';
let ifr = document.createElement('iframe');
ifr.src = 'http://sub.example.com/b.html';
ifr.style.display = 'none';
document.body.append(ifr);
ifr.onload = function() {
let win = ifr.contentWindow;
alert(win.data);
}
</script>
</body>
</html>
再創(chuàng)建一個 b.html 文件:
<script>
document.domain = 'example.com';
window.data = '傳送的數(shù)據(jù):1111';
</script>
同樣的開啟 http 服務(wù)器,在瀏覽器中打開:
這時只是開啟了兩個 http 服務(wù)器枚抵,還需要通過 nginx 做域名映射,將Example Domain
映射到 localhost:8080
明场,sub.example.com
映射到 localhost:8081
上
打開操作系統(tǒng)下的 hosts 文件:mac 是位于 /etc/hosts
文件汽摹,并添加:
127.0.0.1 www.example.com
127.0.0.1 sub.example.com
這樣在瀏覽器打開這兩個網(wǎng)址后就會訪問本地的服務(wù)器。
之后打開 nginx 的配置文件:/usr/local/etc/nginx/nginx.conf
苦锨,并在 http 模塊里添加:
server {
listen 80;
server_name www.example.com;
location / {
proxy_pass http://127.0.0.1:8080/;
}
}
server {
listen 80;
server_name sub.example.com;
location / {
proxy_pass http://127.0.0.1:8081/;
}
}
上面代碼的意思是:如果訪問本地的域名是Example Domain
就由 localhost:8080 代理該請求逼泣。
所以我們這時候在打開瀏覽器訪問Example Domain
的時候其實(shí)訪問的就是本地服務(wù)器 localhost:8080。
參考資料: