JS跨域笔记

原文连接javascript

因项目本地开发时,调用API都是涉及到跨域的问题,而如今前端工程化,前端构建工具会集成跨域的功能,所以也没有深刻地区探究跨域的问题,而本身自己也对跨域问题还有一些模糊之处,所以决定写下这篇文章,督促本身了解的同时,也是作个记录,方便之后回顾。html

本文目录结构:

  1. 什么是跨域
  2. 现阶段跨域的解决方案及案例
  3. 最佳实践

1、什么是跨域

1. 跨域的两个误区
  1. 动态请求就会有跨域问题
  2. 跨域就是请求发不出去

对于误区1,跨域仅仅存在于浏览器端,不存在于其余环境;前端

对于误区2,只要网络没有问题,全部跨域的请求都是能正常发送出去,而且服务端也能收到请求并正常返回结果,只是因为跨域限制,被浏览器拦截了。java

这也是为何咱们用postman等代理工具模拟请求时,能够获取到返回信息;node

若是是非简单请求,(除GET,POST,HEAD以外,且http头信息不超出一下字段:Accept、Accept-LanguageContent-LanguageLast-Event-IDContent-Type(限于三个值:application/x-www-form-urlencodedmultipart/form-datatext/plain)),都会先发出预请求(preflight),预请求询问服务端该请求容许跨域否,接着服务端会返回只有headers不含body的信息,而后浏览器根据headers中的信息进行判断,如果容许跨域,则再次发送请求,不然抛出跨域限制的错误。程序员

2. 为何跨域仅仅限制读取远端的数据

若是限制写入端(也就是发送请求端),那么服务器的资源仅仅只能同源请求,没法作到资源共享。web

3. 浏览器如何识别一个请求是否跨域

同源策略限制了从同一个源加载的文档或脚本如何与来自另外一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。json

若是两个页面的协议(如:http,https)、域名或ip(如:binnera.com.cn)、端口(通常web网站都是默认80端口)都相同,则两个页面具备相同的源,而只要其中任意一个不一样,则浏览器则会将这两个源之间的请求视为跨域。前端工程化

咱们以http://www.binenar.com.cn为例,进行具体说明跨域

连接 结果 缘由
http://www.binenar.com.cn/blog 同协议同域名同端口(默认80端口)
http://www.binenar.com.cn/blog 同协议同域名同端口
http://www.binenar.com.cn:81/blog 同协议同域名不一样端口
https://www.binenar.com.cn 协议不一样
http://binenar.com.cn/blog 域名不一样(若是有作域名映射,那么两个域名能够指向同一个ip)
http://b.binenar.com.cn/blog 域名不一样
4. 浏览器跨域限制主要限制了什么
  1. 不一样的源没法读取对方的CookieLocalStorageIndexDB
  2. 没法获取DOM,BOM
  3. JS没法获取AJAX以及Fetch请求的结果。
5. 浏览器容许的跨域资源请求

浏览器容许嵌入跨域资源的请求

  • <script src="..."></script>标签嵌入跨域脚本;
  • <link rel="stylesheet" href="..."> 标签嵌入CSS,CSS的跨域须要一个设置正确的Content-Type 消息头;
  • <img src="...">嵌入图片;
  • <video><audio>嵌入多媒体资源;
  • @font-face 引入的字体;
  • <frame><iframe> 载入的任何资源,可经过设置X-Frame-Options消息头来阻止iframe嵌入资源。

2、现阶段跨域的解决方案及案例

1. 跨域资源共享(CORS)

若是是简单请求,请求发送出去时,浏览器会在请求头添加Origin字段:

Origin: http://binnear.com.cn
复制代码

告诉服务端该请求是来自那个源。

接着服务端接受到请求后,并在响应头加上以下字段:

Access-Control-Allow-Origin: http://binnear.com.cn
复制代码

表明服务端容许的域,浏览器收到后,便会容许这次请求,以上字段若被设置为*,则表示可接受任意的源访问。

若是是非简单请求:

const url = 'http://binnear.com.cn/data';
const xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();
复制代码

浏览器则会发送预请求,在请求头会添加如下字段:

OPTIONS /data HTTP/1.1
Origin: http://binnear.com.cn
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
复制代码

OPTIONS是预请求的识别字段,Access-Control-Request-Method列出请求方法,Access-Control-Request-Headers指定发送的额外的头信息。

服务端收到预请求后,检查请求的字段后,确认容许跨域,便作出响应,

Access-Control-Allow-Origin: http://binnear.com.cn
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
复制代码

响应头中会包含以上三个字段,正好对应咱们请求头中添加的字段,表示容许的域,容许的请求方法,以及额外的请求头。浏览器接受到后,知道此次请求已被许可,接着发送真正的请求。

2. JSONP跨域

相信每个接触过跨域的程序员都或多或少了解过JSONP跨域,它的原理也很简单,就是利用上面咱们提过的嵌入跨域资源请求的方法。

<script type='text/javascript'>
    function localFn(data) {
        console.log('这是获取到的远程数据':data)
    }
    const script = document.createElement('script');
    script.src = 'http://binnear.com.cn?callback=localFn';
    document.body.appendChild(script);
</script>
复制代码
  1. 首先咱们定义了一个全局函数localFn;
  2. 建立一个script标签,并将src属性指向咱们须要跨域请求的API;
  3. 将建立的script标签添加到页面

经过以上3步,咱们发送上面所示的API请求,而后服务端会返回一段可执行的JS代码:

localFn({remark: '我是远程数据对象里面的属性值'})
复制代码

由于咱们以前定义了全局的localFn,因此这段代码就会执行localFn这个函数,并将数据传递给形参data,在localFn内咱们就经过data获取到服务端的数据了。

注意点:crc中的localFn能够为任意名,callback这个key是由接口提供者所定义。

3. 基于iframe的跨域
  1. 经过window.name传输跨域资源
  2. 经过window.postMessage传输跨域资源

讲述以上方法以前咱们先本地配置一下本地跨域模拟环境

sever1.js配置以下:

const http = require('http');
const fs = require('fs');
const documentRoot = 'D:/code/sever1/';

const server = http.createServer(function (req, res) {
    const url = req.url;
    const file = documentRoot + url;
    fs.readFile(file, function (err, data) {
        if (err) {
            res.writeHeader(404, {
                'content-type': 'text/html;charset="utf-8"'
            });
            res.write('<h1>404错误</h1><p>你要找的页面不存在</p>');
            res.end();
        } else {
            res.write(data);
            res.end();
        }
    });
}).listen(8888);

console.log('服务器开启成功');
复制代码

sever2.js的配置与sever1.js大体相同,只不过咱们将文件路径更改成domain2的路径,监听的端口号改成了8889

const http = require('http');
const fs = require('fs');
const documentRoot = 'D:/code/sever2/';

const server = http.createServer(function (req, res) {
    const url = req.url;
    const file = documentRoot + url;
    fs.readFile(file, function (err, data) {
        if (err) {
            res.writeHeader(404, {
                'content-type': 'text/html;charset="utf-8"'
            });
            res.write('<h1>404错误</h1><p>你要找的页面不存在</p>');
            res.end();
        } else {
            res.write(data);
            res.end();
        }
    });

}).listen(8889);

console.log('服务器开启成功');
复制代码

domain1.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>domain1</title>
</head>
<body>
  <div>this is domain 1</div>
</body>
</html>
复制代码

domain2.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>domain1</title>
</head>
<body>
  <div>this is domain 2</div>
</body>
</html>
复制代码

接着打开两个命令行工具,分别进入sever1和sever2文件夹,执行如下命令:

node ./sever1.js
node ./sever2.js
复制代码

接着进入到浏览器中,输入以下连接

http://localhost:8888/domain1.html
http://localhost:8888/domain2.html
复制代码

页面输出this is domain 1this is domain 2则启动成功,到此咱们前期的准备已经完成,接下来咱们利用搭建好的环境来模拟window.name如何进行跨域传输数据。

window.name

在domain1中咱们添加如下代码:

<script>
	window.name = JSON.stringify({info: 'this is domain1\'s name'})
</script>
复制代码

咱们在domain1中,将一段json字符串赋值给了domain1中window的name属性。

接着咱们在domain2中添加以下代码

<script>
    const iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    iframe.src = 'http://localhost:8888/domain1.html';
    document.body.appendChild(iframe);

    iframe.onload = () => {
      console.log(iframe.contentWindow.name)
    }
</script>
复制代码

保存后,咱们进入http://localhost:8889/domain2.html这个页面,而后刷新,打开控制台:

咦,竟然被跨域限制了,不是说好window.name传输跨域资源的吗?怎么仍是被限制了呢?

不要急,猜测下,咱们是否是忽略了什么事情,而后翻阅资源后,发现当前文件的所在的源与iframe的src指向的源不一样,那么就没法操做iframe中的任何东西,天然window.name也就没法读取了。

原来是iframe的跨域限制了,那么问题来了,既然这样,window.name那不就是没有办法跨域传输数据了吗?

对于上面问题,window.name自身提供了的一个神奇功能,给了咱们跨域传输的可能:那就是window.name的值在不一样页面或域下,加载后依然存在。

结合这个功能咱们再来优化一下咱们在domain2.html中新加的代码:

<script>
    const iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    iframe.src = 'http://localhost:8888/domain1.html';
    document.body.appendChild(iframe);

    iframe.onload = () => {
      iframe.src = 'about:blank' // 新增代码
      console.log(iframe.contentWindow.name)
    }
</script>
复制代码

咱们新增了一行代码,就是在iframe加载完后,立马将scr指向domain2的源,这个时候iframe就与domain2的源一致了,咱们就能读取到了iframe下的window.name属性了,并且由于window.name的神奇的功能,它的值依然是咱们在domain1中设置的值。很好,成功彷佛在向咱们招手了,保存文件,浏览器打开domain2的连接,刷新。

window.name的数据咱们确实获取到了,可是控制却在不停地输出日志,仔细思考一下,发现是iframescr从新指向后,便触发了onload,致使进入死循环,再次优化,同时避免404的error,代码在次优化以下:

<script>
    const iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    iframe.src = 'http://localhost:8888/domain1.html';
    document.body.appendChild(iframe);
    let state = 0;
    iframe.onload = () => {
      if (state === 0) {
        state = 1
        iframe.src = ''
      }
      if (state === 1) {
        console.log(iframe.contentWindow.name)
        document.body.removeChild(iframe);
      }
    }
</script>
复制代码

保存后在次刷新页面,咱们终于如愿以偿地获得了咱们但愿的结果:

没有了死循环,数据正常获取,只是惟一不舒服的地方就是仍然有跨域限制的报错,怎么消除这个error,就留给爱探索的你了~~

小记:window.name能够携带的信息限制为2M。

window.postMessager

咱们依旧用sever1和sever2文件夹的内容,删除关于window.name的相关代码,接着咱们在domain1.html中添加以下代码:

<script>
    window.addEventListener('message', function (e) {
      console.log('data from domain1 ---> ' + e.data);
      const data = { info: 'this is domain2' }
      window.parent.postMessage(JSON.stringify(data), 'http://localhost:8889');
    }, false);
</script>
复制代码

domain2.html中添加以下代码

<script>
    const iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    iframe.src = 'http://localhost:8888/domain1.html';
    document.body.appendChild(iframe);
    iframe.onload = function () {
      const data = { info: 'this is domain 1' };
      iframe.contentWindow.postMessage(JSON.stringify(data), 'http://localhost:8888/');
    };
    window.addEventListener('message', function (e) {
      console.log('data from domain2 ---> ' + e.data);
    }, false);
</script>
复制代码

domain2.html中,咱们经过iframedomain1.html引入进来,在iframe加载完成后,咱们经过iframe中的postMessage方法将data数据发送给domain1.html所在的域,同时监听domain2.html中的message事件

接着咱们在domain1.html中监听domain1.html中的message事件,获取到domain2.html传送过来的data,同时将零一份data经过domain2.html中的postMessage方法发送给domain2.html所在的域。

保存后,刷新domain2.html所在的页面,在控制台中咱们能够看到以下信息。

这样咱们就完成了在domain2.html中取domain1.html中的数据,并能够作到二者之间的交互。

4. 服务端代理

前文咱们有说过,跨域仅仅浏览器端限制了咱们读取远程的数据,因此利用这一点,咱们能够将跨域资源由服务端代理后,再将资源返回给咱们,

5. WebSocket协议跨域

WebSocket protocol是HTML5一种新的协议,它实现了浏览器端与服务端的双工通讯,同时容许跨域通信,这里不作叙述,有兴趣的能够去了解一下~

相关文章
相关标签/搜索