1. 同源策略(Same origin Policy)
瀏覽器處于安全方面的考慮律姨,只允許與本域下的接口交互雕旨。不同源的客戶端腳本在沒有明確授權(quán)的情況下喳魏,不能讀寫對(duì)方的資源羡藐。
本域指的是:
- 同協(xié)議:如都是 http 或者 https;
- 同域名:如都是 http://baidu.com/a 和 http://baidu.com/b;
- 同端口:如都是80端口亥啦。
如:
不同源的例子:
- http://baidu.com/a.js 和 https://baidu.com/a.php(協(xié)議不同)
- http://baidu.com/a.js 和 http://bbs.baidu.com/a.php(域名不同炭剪,域名必須完全相同才可以)
- http://baidu.com/a.js 和 http://baidu.com:8080/a.php(端口不同,第一個(gè)是80)
需要注意的是:對(duì)于當(dāng)前頁面來說翔脱,頁面存放 JS 文件的域不重要奴拦,重要的是加載該 JS 頁面所在什么域(即:當(dāng)前代碼的執(zhí)行環(huán)境)。
報(bào)錯(cuò)范例:
server届吁,瀏覽器訪問 http://locallhost:8080/index.html
server.js
var http = require('http')
var fs = require('fs')
var path = require('path')
var url = require('url')
http.createServer(function(req, res){
var pathObj = url.parse(req.url, true)
switch (pathObj.pathname) {
case '/getWeather':
res.end(JSON.stringify({beijing:'sunny'}))
break;
default:
fs.readFile(path.join(__dirname, pathObj.pathname), function(e, data){
if(e) {
res.writeHead(404, 'not found')
res.end('<h1>404 Not Found</h1>')
}else {
res.end(data)
}
})
}
}).listen(8080)
index.html
<h1>饑人谷</h1>
<script>
var xhr = new XMLHttpRequest()
xhr.open('GET', 'http://localhost:8080/getWeather', true)
xhr.send()
xhr.onload = function(){
console.log(xhr.responseText)
}
</script>
猜測出現(xiàn)報(bào)錯(cuò)的原因(報(bào)錯(cuò):No 'Access-Control-Allow-origin' header ...):
- 請求未發(fā)出去错妖,直接被瀏覽器攔截掉了;
- 請求發(fā)出去了疚沐,但服務(wù)器發(fā)現(xiàn)不一樣暂氯,這是服務(wù)器未響應(yīng);
- 請求發(fā)出了亮蛔,服務(wù)器也響應(yīng)了痴施,但是數(shù)據(jù)返回來之后瀏覽器發(fā)現(xiàn)不對(duì),然后攔截掉了究流。
可能是這些原因?qū)е挛覀兊臄?shù)據(jù)得不到辣吃,做個(gè)測試看到底是什么原因:
// server.js
case '/getWeather':
console.log('get weather') // 查看它是否出現(xiàn)
res.end(JSON.stringify({beijing:'sunny'}))
break;
' get weather ' 這句話出現(xiàn)了,表示請求發(fā)出去了梯嗽,服務(wù)器也響應(yīng)了齿尽,但瀏覽器發(fā)現(xiàn)不對(duì)攔截掉了。
同源策略是瀏覽器的安全機(jī)制灯节。
2. 跨域的幾種方法
1. JSONP
HTML 中 script 標(biāo)簽可以加載其他域下的js循头,比如我們經(jīng)常引入一個(gè)其他域下線上cdn的jQuery。那如何利用這個(gè)特性實(shí)現(xiàn)從其他域下獲取數(shù)據(jù)呢炎疆?
可以先這樣試試:
<script src="http://api.jirengu.com/weather.php"></script>
這時(shí)候會(huì)向天氣接口發(fā)送請求獲取數(shù)據(jù)卡骂,獲取數(shù)據(jù)后做為 js 來執(zhí)行。 但這里有個(gè)問題形入, 數(shù)據(jù)是 JSON 格式的數(shù)據(jù)全跨,直接作為 JS 運(yùn)行的話我如何去得到這個(gè)數(shù)據(jù)來操作呢?
這樣試試:
<script src="http://api.jirengu.com/weather.php?callback=showData"></script>
這個(gè)請求到達(dá)后端后亿遂,后端會(huì)去解析 callback 這個(gè)參數(shù)獲取到字符串showData浓若,在發(fā)送數(shù)據(jù)做如下處理:
之前后端返回?cái)?shù)據(jù): {"city": "hangzhou", "weather": "晴天"} 現(xiàn)在后端返回?cái)?shù)據(jù): showData({"city": "hangzhou", "weather": "晴天"}) 前端script標(biāo)簽在加載數(shù)據(jù)后會(huì)把 「showData({“city”: “hangzhou”, “weather”: “晴天”})」做為 js 來執(zhí)行,這實(shí)際上就是調(diào)用showData這個(gè)函數(shù)蛇数,同時(shí)參數(shù)是 {“city”: “hangzhou”, “weather”: “晴天”}挪钓。 用戶只需要在加載提前在頁面定義好showData這個(gè)全局函數(shù),在函數(shù)內(nèi)部處理參數(shù)即可:
<script>
function showData(ret){
console.log(ret);
}
</script>
<script src="http://api.jirengu.com/weather.php?callback=showData"></script>
這就是 JSONP(JSON with padding)耳舅,總結(jié):
JSONP 是通過 script 標(biāo)簽加載數(shù)據(jù)的方式去獲取數(shù)據(jù)當(dāng)做 JS 代碼來執(zhí)行 提前在頁面上聲明一個(gè)函數(shù)碌上,函數(shù)名通過接口傳參的方式傳給后臺(tái),后臺(tái)解析到函數(shù)名后在原始數(shù)據(jù)上「包裹」這個(gè)函數(shù)名,發(fā)送給前端馏予。換句話說天梧,JSONP 需要對(duì)應(yīng)接口的后端的配合才能實(shí)現(xiàn)。
JSONP 就是返回一個(gè) JSON 然后給你加了一些東西霞丧。
server.js
var http = require('http')
var fs = require('fs')
var path = require('path')
var url = require('url')
http.createServer(function(req, res){
var pathObj = url.parse(req.url, true)
switch(pathObj.pathname) {
case '/getNews':
var news = [
"第11日前瞻:中國沖擊4金 博爾特再戰(zhàn)200米羽球",
"正直播柴飚/洪煒出戰(zhàn) 男雙力爭會(huì)師決賽",
"女排將死磕巴西呢岗!郎平安排男陪練模仿對(duì)方核心"
]
res.setHeader('Content-Type', 'text/json; charset=utf-8')
if(pathObj.query.callback){
res.end(pathObj.query.callback + '(' +JSON.stringify(news) + ')')
}else {
res.end(JSON.stringify(news))
}
break;
default:
fs.readFile(path.join(__dirname, pathObj.pathname), function(e, data){
if(e){
res.writeHead(404, 'not found')
res.end('<h1>404 Not Found</h1>')
}else {
res.end(data)
}
})
}
}).listen(8080)
服務(wù)端代碼注解(server.js):
- 當(dāng)收到參數(shù)
/getNews
時(shí),返回一個(gè)數(shù)組var news
蚯妇。 -
res.setHeader
是為了讓我直接去訪問這個(gè)請求時(shí)不出現(xiàn)亂碼敷燎,這個(gè)是可要可不要的暂筝。 - 關(guān)鍵在于:
if(pathObj.query.callback){
res.end(pathObj.query.callback + '(' +JSON.stringify(news) + ')')
}else {
res.end(JSON.stringify(news))
}
break;
...
})
-
path.query
如果它有 callback 這個(gè)參數(shù)箩言,那就callback
加上(
加上數(shù)組再加上)
-
else {res.end(JSON.stringify(news))}
如果沒有 callback 這個(gè)參數(shù)就直接發(fā)數(shù)據(jù)。
后端要做的實(shí)際以前就是直接發(fā)送數(shù)據(jù)焕襟,現(xiàn)在加了一個(gè)判斷陨收,相當(dāng)于這個(gè)接口就支持跨域了,支持 JSONP 了鸵赖。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JSONP</title>
</head>
<body>
<div class="container">
<ul class="news">
</ul>
<button class="show">show news</button>
</div>
<script>
$('.show').addEventListen('click', function(){
var script = document.createElement('script');
script.src = 'http://127.0.0.1:8080/getNews?callback=appendHtml';
document.head.appendChild(script);
document.head.removeChild(script);
})
function appendHtml(news) {
var html = '';
for(var i = 0; i < news.length; i++){
html += '<li>' +news[i] + '</li>';
}
console.log(html);
$('.news').innerHTML = html;
}
function $(selector){
return document.querySelector(selector);
}
</script>
</body>
</html>
靜態(tài)頁面代碼注解(index.html):
功能作用:當(dāng)點(diǎn)擊按鈕時(shí)务漩,向后臺(tái)發(fā)個(gè)請求獲取數(shù)據(jù),獲取到數(shù)據(jù)之后它褪,再把數(shù)據(jù)拼裝 DOM 放到頁面上饵骨。
- 當(dāng)點(diǎn)擊按鈕時(shí),先去創(chuàng)建一個(gè) script 標(biāo)簽
var script = document.createElement('script')
- 然后給這個(gè) script 設(shè)置 src 的標(biāo)簽
script.src = 'http://127.0.0.1:8080/getNews?callback=appendHtml'
src 里面有個(gè)getNews
茫打,有個(gè)callback=appendHtml
居触。 - 而我們?nèi)窒旅嬗幸粋€(gè) appendHtml這樣一個(gè)方法
function appendHtml(news){}
- 然后再把 script 標(biāo)簽放到 head 上
document.head.appendChild(script)
- 放上后為了讓 HTML 看起來干凈一點(diǎn),再把 script 標(biāo)簽移除掉
只要把 script 標(biāo)簽放上去老赤,它就會(huì)向這個(gè)東西發(fā)請求轮洋,獲取到之后就會(huì)去執(zhí)行,不管后面是否去移除 script 標(biāo)簽抬旺。
- 當(dāng)創(chuàng)建標(biāo)簽弊予、設(shè)置 src 之后,這是瀏覽器會(huì)向這個(gè) src 里的東西發(fā)送請求开财。發(fā)送請求得到的是
appendHtml(數(shù)組)
這個(gè)數(shù)據(jù)汉柒,這個(gè)數(shù)據(jù)到來之后,由于我們把 script 標(biāo)簽放到頁面上责鳍,這個(gè)數(shù)據(jù)就會(huì)當(dāng)成 JS 去執(zhí)行碾褂。執(zhí)行它時(shí),因?yàn)轫撁嫔弦呀?jīng)有了 appendHtml 這個(gè)函數(shù)薇搁,所以就會(huì)執(zhí)行這個(gè)函數(shù)斋扰,把appendHtml(數(shù)組)
作為參數(shù)傳遞進(jìn)去。
打開終端,輸入 node server.js 传货,瀏覽器打開 http://localhost:8080/index.html
3. CORS
CORS 全稱是跨域資源共享(Cross-Origin Resource Sharing)屎鳍,是一種 ajax 跨域請求資源的方式,支持現(xiàn)代瀏覽器问裕,IE 支持10以上逮壁。實(shí)現(xiàn)方式很簡單:當(dāng)你使用 XMLHttpRequest 發(fā)送請求時(shí),瀏覽器發(fā)現(xiàn)該請求不符合同源策略粮宛,會(huì)給該請求加一個(gè)請求頭:Origin窥淆。后臺(tái)進(jìn)行一系列處理,如果確定接受請求則在返回結(jié)果中加入一個(gè)響應(yīng)頭:Access-Control-Allow-Origin巍杈;瀏覽器判斷該響應(yīng)頭中是否包含 Origin 的值忧饭,如果有,則瀏覽器會(huì)處理響應(yīng)筷畦,我們就可以拿到響應(yīng)數(shù)據(jù)词裤;如果不包含,瀏覽器直接駁回鳖宾,這時(shí)我們無法拿到響應(yīng)數(shù)據(jù)吼砂。所以 CORS 的表象是讓你覺得它與同源的 ajax 請求沒啥區(qū)別,代碼完全一樣鼎文。
server.js
var http = require('http')
var fs = require('fs')
var path = require('path')
var url = require('url')
http.createServer(function(req, res){
var pathObj = url.parse(req.url, true)
switch (pathObj.pathname) {
case '/getNews':
var news = [
"第11日前瞻:中國沖擊4金 博爾特再戰(zhàn)200米羽球",
"正直播柴飚/洪煒出戰(zhàn) 男雙力爭會(huì)師決賽",
"女排將死磕巴西渔肩!郎平安排男陪練模仿對(duì)方核心"
]
res.setHeader('Access-Control-Allow-Origin','http://localhost:8080')
//res.setHeader('Access-Control-Allow-Origin','*')
res.end(JSON.stringify(news))
break;
default:
fs.readFile(path.join(__dirname, pathObj.pathname), function(e, data){
if(e){
res.writeHead(404, 'not found')
res.end('<h1>404 Not Found</h1>')
}else{
res.end(data)
}
})
}
}).listen(8080)
服務(wù)端代碼注解(server.js):
還是一樣的,只是在返回?cái)?shù)據(jù)時(shí)有一個(gè) setHeader 設(shè)置響應(yīng)頭:
res.setHeader('Access-Control-Allow-Origin','http://localhost:8080')
加上了 'Access-Control-Allow-Origin'(訪問控制)拇惋,允許的域是 'http://localhost:8080' (注意協(xié)議周偎、域名、端口)這個(gè)蚤假。
換句話說栏饮,加了這句話,瀏覽器看到這句話之后就表示:如果我的域和它允許的是一樣的磷仰,就可以去響應(yīng)袍嬉,繼續(xù)去處理返回的數(shù)據(jù)。
CROS 就是讓 ajax 本身支持跨域請求灶平。
res.setHeader('Access-Control-Allow-Origin','*')
星號(hào)指:任何人都可以使用我的數(shù)據(jù)伺通,瀏覽器允許任何人獲取數(shù)據(jù)。
對(duì)于老的瀏覽器逢享,比如 IE10 以下的罐监,就可以使用 JSONP 的方式
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JSONP</title>
</head>
<body>
<div class="container">
<ul class="news">
</ul>
<button class="show">show news</button>
</div>
<script>
$('.show').addEventListen('click', function(){
var xhr = new XMLHttpRequest()
xhr.open('GET', 'http://127.0.0.1:8080/getNews', true)
xhr.send()
xhr.onload = function(){
appendHtml(JSON.parse(xhr.responseText))
}
})
function appendHtml(news) {
var html = '';
for(var i = 0; i < news.length; i++){
html += '<li>' +news[i] + '</li>';
}
$('.news').innerHTML = html;
}
function $(selector){
return document.querySelector(selector);
}
</script>
</body>
</html>
靜態(tài)頁面代碼注解(index.html):
前端其實(shí)就是發(fā)生了 ajax 請求。
啟動(dòng)終端瞒爬,執(zhí)行 node server.js 弓柱,瀏覽器打開 http://localhost:8080/index.html沟堡,查看效果和網(wǎng)絡(luò)請求。
4. 降域
JSONP 和 CORS 本質(zhì)上是像一個(gè)不同域下的接口去獲取數(shù)據(jù)矢空,有時(shí)會(huì)涉及到另一種跨域(跨域是因?yàn)闉g覽器會(huì)阻止)航罗,不同域下的 iframe 的一個(gè)操作。
使用 iframe 可以顯示屁药,但無法使用 JS 粥血,獲取都不能獲取到。
范例:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>a.html</title>
<style>
.ct{
width: 910px;
margin: auto;
}
.main{
float: left;
width: 450px;
height: 300px;
border: 1px solid #ccc;
}
.main input{
margin: 20px;
width: 200px;
}
.iframe{
float: right;
}
iframe{
width: 450px;
height: 300px;
border: 1px dashed #ccc;
}
</style>
</head>
<body>
<div class="ct">
<h1>使用降域?qū)崿F(xiàn)跨域</h1>
<div class="main">
<input type="text" placeholder="http://a.jrg.com:8080/a.html">
</div>
<iframe src="http://b.jrg.com:8080/b.html" frameborder="0" ></iframe>
</div>
<script>
//URL: http://a.jrg.com:8080/a.html
document.querySelector('.main input').addEventListener('input', function(){
console.log(this.value);
window.frames[0].document.querySelector('input').value = this.value;
})
document.domain = "jrg.com"
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>b.html</title>
<style>
html,body{
margin: 0;
}
input{
margin: 20px;
width: 200px;
}
</style>
</head>
<body>
<input id="input" type="text" placeholder="http://b.jrg.com:8080/b.html">
<script>
// URL: http://b.jrg.com:8080/b.html
document.querySelector('#input').addEventListener('input', function(){
window.parent.document.querySelector('input').value = this.value;
})
document.domain = 'jrg.com';
</script>
</body>
</html>
document.domain = "jrg.com"
讓當(dāng)前域?qū)崿F(xiàn)降域酿箭。
有 "http://a.jrg.com:8080/a.html"
和 "http://b.jrg.com:8080/b.html"
(iframe)這兩個(gè)地址复亏,他們倆 jrg.com:8080...
這后面的部分是一樣的, 兩個(gè)域名不一樣的不能進(jìn)行降域缭嫡。
5. postMessage
沒有主域的限制缔御。有兩個(gè)域名,域名不同械巡。
范例:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>a.html</title>
<style>
.ct{
width: 910px;
margin: auto;
}
.main{
float: left;
width: 450px;
height: 300px;
border: 1px solid #ccc;
}
.main input{
margin: 20px;
width: 200px;
}
.iframe{
float: right;
}
iframe{
width: 450px;
height: 300px;
border: 1px dashed #ccc;
}
</style>
</head>
<body>
<div class="ct">
<h1>使用postMessage實(shí)現(xiàn)跨域</h1>
<div class="main">
<input type="text" placeholder="http://a.jrg.com:8080/a.html">
</div>
<iframe src="http://localhost:8080/b.html" frameborder="0" ></iframe>
</div>
<script>
//URL: http://a.jrg.com:8080/a.html
$('.main input').addEventListener('input', function(){
console.log(this.value);
window.frames[0].postMessage(this.value,'*');
})
window.addEventListener('message',function(e) {
$('.main input').value = e.data
console.log(e.data);
});
function $(id){
return document.querySelector(id);
}
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>b.html</title>
<style>
html,body{
margin: 0;
}
input{
margin: 20px;
width: 200px;
}
</style>
</head>
<body>
<input id="input" type="text" placeholder="http://b.jrg.com:8080/b.html">
<script>
// URL: http://b.jrg.com:8080/b.html
$('#input').addEventListener('input', function(){
window.parent.postMessage(this.value, '*');
})
window.addEventListener('message',function(e) {
$('#input').value = e.data
console.log(e.data);
});
function $(id){
return document.querySelector(id);
}
</script>
</body>
</html>
postMessage 原理:
當(dāng)我們需要和 iframe 里的東西進(jìn)行操作時(shí)刹淌,我們會(huì)向當(dāng)前這個(gè) iframe 去 postMessage 一個(gè)東西。
因?yàn)槲乙僮骼锩娴臇|西讥耗,但我并·不能直接去操作,而是發(fā)送一個(gè)消息疹启,只要你愿意處理這個(gè)消息古程,那你就可以再給我一個(gè)響應(yīng),而不是像以前一樣喊崖,我可以直接獲取到你的 DOM挣磨,可以直接去修改你的 JS。
范例中就是:我像 window.frames[0] 直接 post 這么一個(gè) Message(this.value, '*')
(這個(gè)星號(hào)表示在任何域下都可以去接受)荤懂。
對(duì)于 b(我本身這個(gè)頁面)茁裙,我可以監(jiān)聽 message 這么一個(gè)事件。當(dāng)聽到 message 這一個(gè)事件時(shí)节仿,那我就知道有人嵌套了我晤锥,給我發(fā)了一個(gè)消息。那我就可以獲取到這個(gè)數(shù)據(jù)廊宪,獲取到這個(gè)數(shù)據(jù)之后矾瘾,我就去做對(duì)應(yīng)的事情。這樣的話我就可以定義出一些接口了箭启。
我告訴你壕翩,假設(shè)你嵌套我的頁面之后,如果你想去操作我的東西傅寡,那你直接操作不了放妈。你可以去發(fā)這個(gè)消息北救,而我內(nèi)部對(duì)這個(gè)消息有一種實(shí)現(xiàn)。這樣的話我們之間就可以互通了芜抒。