跨域

定义
js跨域是指通过js在不同的域之间进行数据传输或通信,比如用ajax向一个不同的域请求数据,或者通过js获取页面中不同域的框架中(iframe)的数据。只要协议、域名、端口有任何一个不同,都被当作是不同的域。


同源
: 域名,协议,端口均相同。

  • 协议:http、https
  • 域名:分为主域名和子域名
  • 端口:8080

补充小知识

例如: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页面

    • hash.html页面传送数据到getHash.html页面,由于两个页面不在同一个域下IE、Chrome不允许修改parent.location.hash的值,所以要借助于父窗口域名下的一个代理iframe

hash.html页面的关键代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
switch(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
2
//proxy.html的parent是hash.html,hash.html的parent是getHash.html
parent.parent.location.hash=self.location.hash.substring(1);

4.jsonp

  • 原理

    因为通过script标签引入的js是不受同源策略的限制的。JSONP包含两部分:回调函数和数据。回调函数是当响应到来时要放在当前页面被调用的函数。数据就是传入回调函数中的json数据,也就是回调函数的参数了。

  • 优缺点:

    • 优点:不受同源策略的影响;兼容性很好,在很古老的浏览器中都可以运行,并且请求完毕以后可以通过调用callback的方式回传结果。
    • 缺点:只支持GET请求而不支持POST等其它类型的HTTP请求;单向跨域

jsonp.html关键代码如下

1
2
3
var 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
      18
      var 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);
    }

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
    21
    var 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
    6
    app.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
    24
    var 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
    20
    app.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
    26
    var 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
    10
    var 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
13
io.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
    8
    var 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
    5
    var app=require('express')();
    app.get('/img',function (req,res) {
    res.send("我是一张图片");
    });
    app.listen(3000);

看这里!

代码详情请见:跨域