定义
js跨域是指通过js在不同的域之间进行数据传输或通信,比如用ajax向一个不同的域请求数据,或者通过js获取页面中不同域的框架中(iframe)的数据。只要协议、域名、端口有任何一个不同,都被当作是不同的域。
同源
: 域名,协议,端口均相同。
- 协议:http、https
- 域名:分为主域名和子域名
- 端口:8080
补充小知识
- 一级域名:也叫根域名,如:sojson.com、baidu.com、sina.com、sina.com.cn、sina.cn.net 等等
- 二级域名:指增加了一级,包括www。如:www.sojson.com、icp.sojson.com、zhidao.baidu.com、www.baidu.com 等等。
例如:http://www.abc.com:8080/scripts/jquery.js
协议:http:// ,
子域名:www ,
主域名:abc.com ,
端口号:8080 。
- 浏览器有一个同源策略,其限制之一是不能通过ajax的方法去请求不同源中的文档。第二个限制是浏览器中不同域的框架之间是不能进行js的交互操作的。不同的框架之间是可以获取window对象的,但却无法获取相应的属性和方法。
1
2例如:
有一个页面,它的地址是www.winnie.cn/a.html , 在这个页面里面有一个iframe,它的src是winnie.cn/b.html, 很显然,这个页面与它里面iframe框架的**主域名相同,二级域名不同(子域名不同)**,所以我们是无法通过在页面中书写js代码来获取iframe中的内容。
解决跨域的方法
1.document.domain+iframe(只有在主域相同的时候才能使用该方法)
限制
我们只能把document.domain设置成自身或更高一级的域名。
代码
documentDomain.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<body>
<p style="color:orange;font-size:24px;">我是需要访问iframe的页面</p>
<iframe id="iframe" src="http://winnie.cn/documentDomain.html" onload="test()" name="winnie.cn"></iframe>
<script>
/* document.domain="winnie.cn"; *///设置成主域
function test(){
var iframe = document.getElementById('iframe');
var win = iframe.contentWindow;
var doc = win.document;
var name= win.name;
alert(win);
alert(doc);
alert(name);
}
</script>
</body>iframe.html
1
2
3
4
5
6<body>
<p style="color:pink;font-size:20px;">我是iframe要显示的页面</p>
<script>
document.domain="winnie.cn";
</script>
</body>
2.window.name
原理
window对象有个name属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个window.name的,每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。
限制
window.name的值只能是字符串的形式,这个字符串的大小最大能允许2M左右甚至更大的一个容量,具体取决于不同的浏览器,但一般是够用了。
实现过程
windowName.html页面使用一个隐藏的iframe来充当一个中间人的角色,由iframe去获取windowNameData.html中window.name的数据,然后windowName.html再去获取iframe中的数据。
注意
windowName.html和iframe中的src不在同一个域,无法直接获取iframe中的值,因为浏览器的同源策略。所以在获取到windowNameData.html页面中的数据以后,还要将这个iframe的src设成和windowName.html同一个域才行。
windowName.html页面的关键代码如下1
2
3
4
5
6
7
8
9
10
11
12
13 function getData(){//iframe载入windowNameData.html后才执行该函数
let iframe=document.getElementById("proxy");
iframe.src="about:blank";//设置成与当前页面同源的任意一个页面,about:blank也行
iframe.onload=function(){//这时当前页面与iframe同源,可以互相访问
let data=iframe.contentWindow.name;
alert(data);
}
}
<body>
<iframe id="proxy" src="http://winnie.cn/windowNameData.html" style="display:none" onload="getData()"></iframe>
</body>
windowNameData.html的关键代码如下1
window.name="我是windowName页面想要获取的数据,所有可以转化成字符串来传递的数据都可以在这里使用,包括Json数据";
3.location.hash
原理
因为父窗口可以对iframe进行URL读写,iframe也可以读写父窗口的URL,URL有一部分被称为hash,就是#号及其后面的字符,它一般用于浏览器锚点定位,Server端并不关心这部分,应该说HTTP请求过程中不会携带hash,所以这部分的修改不会产生HTTP请求,但是会产生浏览器历史记录。此方法的原理就是改变URL的hash部分来进行双向通信。
限制
由于两个页面不在同一个域下IE、Chrome不允许修改parent.location.hash的值,所以要借助于父窗口域名下的一个代理iframe。
getHash.html传送数据到hash.html页面
- getHash.html下修改iframe的src为http://winnie.cn/hash.html#paramdo
- hash.html页面监听到url中hash部分的变化,触发相应的操作
- hash.html页面传送数据到getHash.html页面,由于两个页面不在同一个域下IE、Chrome不允许修改parent.location.hash的值,所以要借助于父窗口域名下的一个代理iframe
- hash.html下创建一个隐藏的iframe,此iframe的src是www.winnie.cn域下的,并挂上要传送hash数据,如src="http://www.winnie.cn/proxy.html#somedata"
- proxy.html页面中可以用parent.parent.location.hash修改getHash中的url
- getHash.html监听到url发生变化,触发相应的操作
hash.html页面的关键代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19switch(location.hash){//参数处理
case "#paramdo":
callBack();
break;
case "#paramset":
//do something
break;
}
function callBack(){
try{
parent.location.hash="somedata";
}catch(e){
//ie、chrome的安全机制无法修改parent.location.hash,所以要利用一个www.winnie.cn域下的代理iframe
let ifrproxy=document.createElement("iframe");
ifrproxy.style.display="none";
ifrproxy.src="http://www.winnie.cn/proxy.html#somedata";
document.body.appendChild(ifrproxy);
}
}
proxy.html页面的关键代码如下
1 | //proxy.html的parent是hash.html,hash.html的parent是getHash.html |
4.jsonp
原理
因为通过script标签引入的js是不受同源策略的限制的。JSONP包含两部分:回调函数和数据。回调函数是当响应到来时要放在当前页面被调用的函数。数据就是传入回调函数中的json数据,也就是回调函数的参数了。
优缺点:
- 优点:不受同源策略的影响;兼容性很好,在很古老的浏览器中都可以运行,并且请求完毕以后可以通过调用callback的方式回传结果。
- 缺点:只支持GET请求而不支持POST等其它类型的HTTP请求;单向跨域
jsonp.html关键代码如下1
2
3var script = document.createElement("script");
script.src = "https://api.douban.com/v2/book/search?q=javascript&count=1&callback=handleResponse";
document.body.insertBefore(script, document.body.firstChild);
5.CORS
基本思想
使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功还是失败。实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
实现过程:
简单请求,客户端代码同Ajax相似,但是open方法中的url是绝对路径
1
let request=createCORSRequest('GET',"http://winnie.cn/data.php",true);
服务器端的代码中要设置Access-Control-Allow-Origin,否则无法进行跨域
1
2
3
4<?php
header('Access-Control-Allow-Origin: http://www.winnie.cn');
echo 'Hello World!';//输出
?>非简单请求
客户端代码和简单请求类似,服务器端代码有所不同,并且会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)。
preflight.js关键代码如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18var http = require('http');
var server = http.createServer((req, res) => {
//之后设置了Access-Control-Allow-Origin,才会允许跨域
//要处理带凭证的请求,此Header不能使用*。
res.setHeader('Access-Control-Allow-Origin', 'http://www.winnie.cn');
res.setHeader('Access-Control-Allow-Methods', 'POST, DELETE, GET,PUT');
res.setHeader('Access-Control-Allow-Headers', 'x-token');
//设置预请求缓存1天,1天内再次请求,可以跳过预请求
//此功能需要客户端缓存支持,如果客户端禁用缓存,那么每次都会预请求
res.setHeader('Access-Control-Max-Age', 60 * 60 * 24);
//只有设置了该Header,才允许带凭证的请求。
res.setHeader('Access-Control-Allow-Credentials', true);
res.write('Nodejs');
res.end();
});
server.listen(8888, () => {
console.log('started.');
});
6.postMessage
使用方法
- otherWindow.postMessage(message, targetOrigin);
- otherWindow:指目标窗口,也就是接收消息的窗口
- message: 是要发送的消息,类型为 String、Object (IE8、9 不支持)
- targetOrigin: 是限定消息接收范围,不限制请使用 ‘*’
getMessage.html页面通过message事件监听并接收消息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19//获取不同源的window对象
document.domain="winnie.cn";
//通过message事件监听并接受消息
let handleMessage=function(event){
let data=event.data;//消息
let origin=event.origin;//消息的源地址
let source=event.source;//postMessage.html中的window对象
if(origin=="http://www.winnie.cn"){
alert(data);
alert(source);
}
}
if(typeof window.addEventListener != 'undefined'){
window.addEventListener("message",handleMessage,false);
}
else if(typeof window.attachEvent != 'undefined'){
//IE
window.attachEvent("onmessage",handleMessage);
}- otherWindow.postMessage(message, targetOrigin);
7.Comet
定义
一种服务器向页面推送数据的技术,能够让信息近乎实时地被推送到页面上。有两种实现方式:长轮询和流。
7.1.长轮询
定义
页面发送一个到服务器的请求,然后服务器一直保持连接打开,直到有数据可发送。发送完数据以后,浏览器关闭连接,随即又发起一个到服务器的新请求。轮询的优势是所有浏览器都支持,因为使用XHR对象和setTimeout()就能实现。代码
longpolling.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21var btn = document.querySelector("#btn"), counter = 1;
var poll = function() {
var xhr = new XMLHttpRequest();
xhr.open('GET', "http://localhost:3000/poll/"+counter, true);
xhr.onreadystatechange = function(){
if(xhr.readyState == 4) {
var res = document.querySelector("#result");
res.innerHTML += xhr.responseText + "<br>";
if(++counter <= 5){
poll();
} else {
counter = 1;
}
}
};
xhr.send(null);
};
btn.onclick = function(){
document.querySelector("#result").innerHTML = "";
poll();
};cometServer.js
1
2
3
4
5
6app.get("/poll/:id",function (req,res) {
res.append("Access-Control-Allow-Origin","http://www.winnie.cn");
setTimeout(function () {
res.send("第"+req.params.id+"次请求");
},2000)
});
7.2.HTTP流
定义
流不同于轮询,因为它在页面的整个生命周期内只使用一个HTTP连接。在Firefox、Safari、Opera、Chrome中,通过侦听readystatechange事件及检测readySate的值是否为3,就可以利用XHR对象实现HTTP流。随着不断从服务器接收数据,readySate的值会周期性地变为3,当readySate值为3时,responseText属性中就会保存接收到的所有数据。
代码
httpStream.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24var btn2 = document.querySelector("#btn");
btn2.onclick = function(){
var client = createClient("http://localhost:3000/stream", function(data){
document.querySelector("#results").innerHTML += "Received:"+data+"<br>";
}, function(data){
document.querySelector("#results").innerHTML += "Done!";
});
}
function createClient(url, process, finished){
var xhr = new XMLHttpRequest(), received = 0;//received用于记录已经处理了多少个字符
xhr.open('GET', url, true);//true表示异步
xhr.onreadystatechange = function(){
var result;
if(xhr.readyState == 3){
result = xhr.responseText.substring(received);
received += result.length;
process(result);//回调函数处理传入的新数据
} else if(xhr.readyState == 4) {
finished(xhr.responseText);
}
}
xhr.send(null);
return xhr;
}cometServer.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20app.get("/stream",function (req,res) {
res.append("Access-Control-Allow-Origin","http://www.winnie.cn");
res.append('Content-Type','text/html');
var count = 0;
setTimeout(function generateRandomNum() {
res.write(Math.random()+' ');
count++;
if(count<5){
/* 现在已经不推荐使用arguments.callee();
原因:访问 arguments 是个很昂贵的操作,因为它是个很大的对象,
每次递归调用时都需要重新创建。影响现代浏览器的性能,还会影响闭包。*/
/* setTimeout(arguments.callee,1000); */
setTimeout(generateRandomNum,1000);
}else{
/* 结束响应,告诉客户端所有消息已经发送。当所有要返回的内容发送完毕时,该函数必须被调用一次。
如何不调用该函数,客户端将永远处于等待状态。 */
res.end();
}
},1000);
});
8.SSE(Server-Sent Events,服务器发送事件)
定义
创建到服务器的单向连接,服务器可以通过这个连接发送任意数量的数据,服务器响应的MIME类型必须是text/event-stream.服务器发回的数据以字符串的形式保存在event.data中。响应的格式是纯文本,最简单的情况是每个数据项都带有前缀data:,只有在包含data:的数据行后面有空行时,才会触发message事件。
代码
SSE.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26var btn=document.querySelector("#btn");
var btn2=document.querySelector("#btn2");
var result=document.querySelector("#result");
var source;
btn.onclick=function () {
source=new EventSource("http://localhost:8088/sse");
//参数是false,事件处理程序以冒泡模式触发
source.addEventListener("open",function () {
result.innerHTML+="建立连接<br/>";
},false);
source.addEventListener("error",function () {
result.innerHTML+="连接无法建立<br/>";
},false);
source.addEventListener("connecttime",function (e) {
result.innerHTML+="连接建立时间:"+e.data+"<br/>";
},false);
source.addEventListener("message",function (e) {
result.innerHTML+="接受更新时间:"+e.data+"<br/>";
},false)
};
btn2.onclick=function () {
if(source){
source.close();
result.innerHTML+="关闭连接<br/>";
}
}SSE.js
1
2
3
4
5
6
7
8
9
10var app=require("express")();
app.get("/sse",function (req,res) {
res.append("Access-Control-Allow-Origin","http://www.winnie.cn");
res.append("Content-Type","text/event-stream");
res.write("event: connecttime\n");
var timer=setInterval(function () {
//\r使光标移动到本行行首,\n使光标移动到下一行行首
res.write(`data:${new Date()}\r\n\n`);
},2000);
});
9.Web Sockets
- 目标
在一个单独的持久连接上提供双工、双向通信。 实现
利用npm中socket.io的插件,socket.io 会自动重新连接代码
双向交流,模拟情景:客户端不停发送随机数,当随机数大于0.95时,服务端延时1s后向客户端发送警告以及警告次数。
WebSocket.html
注意:在head标签中用script标签引入node_modules/socket.io-client/dist/socket.io.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var btn=document.getElementById("btn");
btn.onclick=function () {
var socket = io.connect('http://localhost:8090');
let interval = setTimeInterval(()=>{
socket.emit('random', Math.random());
}, 500);
socket.on('warn', count=>{
console.log('warning count : '+count);
});
socket.on('disconnect', ()=>{
console.log('disconnected!');
clearInterval(interval);
});
};
websocket.js
1
2
3
4
5
6
7
8
9
10
11
12
13io.on('connection', socket=>{
socket.on('random', value=>{
console.log(value);
if(value>0.95){
if(typeof socket.warnign==='undefined'){
socket.warning = 0;// socket对象可用来存储状态和自定义数据
}
setTimeout(()=>{
socket.emit('warn', ++socket.warning);
}, 1000);
}
});
});
10.图像Ping
定义
与服务器进行简单、单向的跨域通信。请求的数据是通过查询字符串形式发送的,而响应可以是任何内容。用途
常用于跟踪用户点击页面或动态广告曝光次数
缺点
- 只能发送Get请求
- 无法访问服务器的响应文本
代码
imagePing.html
1
2
3
4
5
6
7
8var btn=document.getElementById('start-ping');
btn.onclick=function(){
var img=new Image();
img.onload=img.onerror=function(){
document.getElementById('result').innerHTML="finished";
}
img.src=`http://localhost:3000/img?r=${Math.random()}`;
}imagePing.js
1
2
3
4
5var app=require('express')();
app.get('/img',function (req,res) {
res.send("我是一张图片");
});
app.listen(3000);
看这里!
代码详情请见:跨域