目录
前言
一篇优秀的文章:JavaScript学习总结(二)——延迟对象、跨域、模板引擎、弹出层、AJAX示例 - 张果 - 博客园
一、跨域
1、跨域的产生
同源策略:同协议、同域名、同端口。
同源策略限制以下几种行为:
- Cookie、LocalStorage 和 IndexDB 无法读取。
- DOM 和 Js对象无法获得。
- AJAX 请求不能发送。
不遵守同源策略的通信就会产生跨域。
二、解决跨域
1、具备src的标签
原理:所有具有src
属性的HTML标签都是可以跨域的
在浏览器中,<script>
、<img>
、<iframe>
和<link>
这几个标签是可以加载跨域(非同源)的资源的,并且加载的方式其实相当于一次普通的GET请求,唯一不同的是,为了安全起见,浏览器不允许这种方式下对加载到的资源的读写操作,而只能使用标签本身应当具备的能力(比如脚本执行、样式应用等等)。
2、JSONP
JSONP(JSON with padding,填充式 JSON 或参数式 JSON)看起来与 JSON 差不多,只不过是被包含在函数调用中的 JSON。
callback({"name": "value"});
JSONP 由两部分组成:回调函数 和 数据。回调函数是当响应到来时应该在页面中调用的函数,回调函数的名字一般是在请求中指定的。而数据就是传入回调函数中的 JSON 数据。下面是一个典型的 JSONP 请求:
http://freetest.net/json/?callback=handleResponse
JSONP 的原理是:动态插入 <script> 标签,通过<script> 标签的 src 特性引入一个 js 文件,当这个 js 文件载入成功时会执行我们在 url 参数中指定的回调函数,并且会把我们需要的 json 数据作为参数传入。
function handleResponse(res){
console.log(res);
}
var script = document.createElement("script");
script.src = "http://freetest.net/json/?callback=handleResponse";
document.body.insertBefore(script, document.body.firstChild);
优点是:兼容性好,简单易用,支持 浏览器与服务器之间的双向通信。
缺点是:
- 只支持 GET 请求。
- JSONP 是从别的域加载代码执行,如果别的域不安全,很可能会在响应中夹带一些恶意的代码,而此时除了完全放弃使用 JSONP 调用之外,没有办法追究。
- 要确定 JSONP 是否失败很麻烦,虽然 HTML5 给 <script> 元素新增了一个 error 事件处理程序,但目前没有得到任何浏览器的支持。为此开发人员不得不使用计时器检测指定的时间内是否接收到了响应。就算这样也不尽人意,毕竟不是每个用户上网的速度和宽带都一样。
由于 JSONP 存在这样大的缺点,所以,现在已经没有使用 JSONP 的必要了,其他方法不能跨域的 JSONP 一样不能跨,其他方法要修改服务器端的 JSONP 一样要修改。
3、CORS
CORS(Cross-Origin Resource Sharing,跨域资源共享)定义了在必须访问跨域资源时,浏览器与服务器应该如何沟通。
CORS 的基本思想是:使用自定义的 HTTP 头部让浏览器与服务器进行沟通,从而决定请求或响应是否成功。
比如一个简单的使用 GET 或 POST 发送的 Ajax 请求,需要给它附加一个额外的 Origin 头部,其中包含请求页面的源信息(协议、域名和端口)。以便服务器根据这个头部信息来决定是否给予响应。下面是 Origin 头部的示例:
Origin: http://www.test.net
如果服务器认为请求可以接受,就在 Access-Control-Allow-Origin 头部回发相同的源信息(如果是公共资源,可以回发 “*”)。例如:
Access-Control-Allow-Origin:http://www.test.net
如果没有这个头部,或者有这个头部但源信息不匹配,浏览器就会驳回请求。
【传送门:Ajax 的详细知识与案例,请戳:js Ajax_weixin79893765432...的博客-CSDN博客_js,ajax】
除 IE 之外的浏览器以及苹果和安卓都支持 XHR(XMLHttpRequest)类型对 CORS 的实现。IE8 中引入了 XDR(XDomainRequest)类型,用 XDR 实现了类似 XHR 的功能。因为不同浏览器对 CORS 的支持不一样,所以就有了跨浏览器的 CORS:
function createCORSRequest(method, url){
var xhr = new XMLHttpRequest();
if("withCredentials" in xhr){// 检测是否支持 CORS
xhr.open(method, url, true);
}else if(typeof XDomainRequest != "undefined"){// 兼容 IE
xhr = new XDomainRequest();
xhr.open(method, url);
}else{
xhr = null;
}
return xhr;
}
var xhr = createCORSRequest("get", "http://www.test.net");
if(xhr){
xhr.onload = function(){
// 对 xhr.responseText 进行处理
}
xhr.send();
}
Firefox、Safari 和 Chrome 中的 XHR 对象与 IE 中的 XDR 对象类似,都提供了够用的接口,他们共同的属性和方法如下:
- responseText 属性:用于取得响应的内容。
- abort() 方法:用于停止正在的请求。
- send() 方法:用于发送请求。
- error 事件:用于替代 onreadystatechange 检测错误。
- load 事件:用于替代 onreadystatechange 检测成功。
以上成员都包含在 createCORSRequest() 函数返回的对象中,在所有浏览器中都能正常使用。
4、WebSocket 协议(★★★★★)
HTTP 协议有一个缺陷:通信只能由客户端发起。这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用“轮询”——每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。但是轮询的效率低,非常浪费资源。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。
Websocket 是 HTML5 新增的一种 “全双工通信协议”——客户端和服务端基于 TCP 握手连接成功后,两者之间就可以建立持久性的连接,实现双向数据传输。
(1)、WebSocket 协议的特点
- 建立在 TCP 协议之上,服务器端的实现比较容易。
- 与 HTTP 协议有着良好的兼容性。默认端口也是 80 和 443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
- 数据格式比较轻量,性能开销小,通信高效。
- 可以发送文本,也可以发送二进制数据。
- 没有同源限制,客户端可以与任意服务器通信。
- 协议标识符是 ws(如果加密,则为wss),服务器网址就是 URL。
ws://test.com
wss://test.com
只有支持 WebSocket 协议的服务器才能识别 ws 和 wss 标识符,才能正常工作。
(2)、WebSocket 协议客户端的 API
①、用 WebSocket 构造函数创建它的实例
WebSocket 对象作为一个构造函数,用于新建 WebSocket 实例,参数是一个 URL(注意:HTTP 协议的 URL 可以相对可以绝对,但是 WebSocket 协议的 URL 必须是绝对路径)。
var ws = new WebSocket('ws://localhost:8080');
由于 WebSocket 协议 不受同源策略对的限制,所以,可以通过它访问任何站点的链接,至于是否会跨域通信,则完全取决于服务器。
②、建立 WebSocket 链接
实例化 WebSocket 对象后,浏览器马上尝试创建连接,WebSocket 的实例上有一个 readyState 属性,该属性返回实例对象的当前状态,共有四种。
- WebSocket.CONNECTING:值为 0,表示正在连接。
- WebSocket.OPEN:值为 1,表示连接成功,可以通信了。
- WebSocket.CLOSING:值为 2,表示连接正在关闭。
- WebSocket.CLOSED:值为 3,表示连接已经关闭,或者打开连接失败。
switch (ws.readyState) {
case WebSocket.CONNECTING:
// do something
break;
case WebSocket.OPEN:
// do something
break;
case WebSocket.CLOSING:
// do something
break;
case WebSocket.CLOSED:
// do something
break;
default:
// this never happens
break;
}
③、关闭 WebSocket 链接
要关闭 WebSocket 链接,可以在任何时候调用 colse() 方法。调用了 close() 方法后,readyState 的值立即就会变成 2(正在关闭连接),而关闭连接之后就会变成 3。
ws.close();
④、发送数据
实例对象的 send() 方法用于向服务器发送数据。
var ws = new WebSocket("ws://www.test.com/server.php");
ws.send("Hello world");
因为 WebSocket 协议只能发送纯文本数据,所以对于复杂的数据结构,发送之前都必须通过 JSON.stringify() 方法序列化,例如:
var data = {
name: "Mary",
age: 18,
say: function(){
alert("hello");
}
}
ws.send(JSON.stringify(data));
⑤、接收数据
当服务器向客户端发来消息时,WebSocket 对象就会触发 message 事件,该事件把返回的数据保存在 event.data 属性中。
ws.onmessage = function(event){
var event = event || window.event;
var data = event.data;
// 处理数据
}
// 或者
ws.addEventListener("message", function(event) {
var event = event || window.event;
var data = event.data;
// 处理数据
});
实例对象的 message() 方法用于接收服务器返回的数据。
⑥、其他事件
实例对象还有其他三个事件,在连接生命周期的不同阶段触发。
- open 事件:在成功建立链接时触发。
- error 事件:在发生错误时触发,连接不能持续。
- close 事件:在连接关闭时触发。
在这三个事件中,只有 close 事件的 event 对象有额外的信息。该事件的event对象有三个额外的属性:
- wasClean 属性:是一个布尔值,表示连接是否已经明确地关闭。
- code 属性:服务器返回的数值状态码。
- reason 属性:是一个字符串,包含服务器发回的消息。
可以把以上信息显示给用户,也可以记录到日志中以便将来分析。
ws.onclose = function(event){
console.log(`确定清除吗?${event.wasClean},服务器返回的状态码是${event.code},关闭的原因是${event.reason}`);
}
进一步学习 WebSocket,请看 阮老师的 WebSocket 教程:WebSocket 教程 - 阮一峰的网络日志
关于 WebSocket 和socket 的区别请戳这里:WebSocket 和socket 的区别 - zhangniuniu - 博客园
5、postMessage(★★★★★)
postMessage() 方法是 HTML5 中新增的功能,用于跨文档消息传递(简称 XDM),
由于我在之前的文章写过,所以恕不赘述,详情请戳:js HTML5 中定义的 JavaScript 脚本_weixin79893765432...的博客-CSDN博客_js html5
6、location.hash
实现原理: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。
(1)、a.html:(http://www.domain1.com/a.html)
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 向b.html传hash值
setTimeout(function() {
iframe.src = iframe.src + '#user=admin';
}, 1000);
// 开放给同域c.html的回调方法
function onCallback(res) {
alert('data from c.html ---> ' + res);
}
</script>
(2)、b.html:(http://www.domain2.com/b.html)
<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 监听a.html传来的hash值,再传给c.html
window.onhashchange = function () {
iframe.src = iframe.src + location.hash;
};
</script>
(3)、c.html:(http://www.domain1.com/c.html)
<script>
// 监听b.html传来的hash值
window.onhashchange = function () {
// 再通过操作同域a.html的js回调,将结果传回
window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
};
</script>
7、document.domain
此方案仅限主域相同,子域不同的跨域应用场景。
实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。
(1)、父窗口:(http://www.domain.com/a.html)
<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
document.domain = 'domain.com';
var user = 'admin';
</script>
(2)、子窗口:(http://child.domain.com/b.html)
<script>
document.domain = 'domain.com';
// 获取父窗口中变量
alert('get js data from parent ---> ' + window.parent.user);
</script>
8、window.name
window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
(1)、a.html:(http://www.domain1.com/a.html)
var proxy = function(url, callback) {
var state = 0;
var iframe = document.createElement('iframe');
// 加载跨域页面
iframe.src = url;
// onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
iframe.onload = function() {
if (state === 1) {
// 第2次onload(同域proxy页)成功后,读取同域window.name中数据
callback(iframe.contentWindow.name);
destoryFrame();
} else if (state === 0) {
// 第1次onload(跨域页)成功后,切换到同域代理页面
iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
state = 1;
}
};
document.body.appendChild(iframe);
// 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
function destoryFrame() {
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
}
};
// 请求跨域b页面数据
proxy('http://www.domain2.com/b.html', function(data){
alert(data);
});
(2)、proxy.html:(http://www.domain1.com/proxy....
中间代理页,与a.html同域,内容为空即可。
(3)、b.html:(http://www.domain2.com/b.html)
<script>
window.name = 'This is domain2 data!';
</script>
总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。
三、其他
1、反向代理
反向代理的原理:受同源策略的限制,非同源的通信就会产生跨域,导致客户端直接与服务器通讯失败,于是另辟蹊径,可以在本地配置一个服务器(我自测采用的是Wampserver(PHP+Apache)),配置好反向代理后,直接访问本地服务器,本地服务器会去访问远程服务器,服务期间的通信没有跨域,所以就绕开了跨域,借助本地服务器成功返回了数据。
反向代理学习资源大全:js的反向代理 - 腾讯云开发者社区 - 腾讯云
- 反向代理的概念:反向代理为什么叫“反向”代理? - 腾讯云开发者社区-腾讯云
- Node.js 配置反向代理:使用 NodeJS 实现反向代理 - 腾讯云开发者社区-腾讯云
- Nginx 配置反向代理:Nginx的反向代理与负载均衡 - 腾讯云开发者社区-腾讯云
2、只针对Chrome的跨域解决办法
允许Chrome读写一个特定的文件夹。
(1)、windows 操作系统
①、打开任务管理器,确保里面没有chrome 进程
②、右键谷歌浏览器,"属性"中打开文件所在位置,进入浏览器安装文件夹
③、在文件位置栏中启动cmd命令行,输入chrome.exe,确定是否能启动浏览器。如果不能请检查自己浏览器文件的路径是否正确
④、 然后关掉chrome浏览器,通过在命令行输入chrome.exe --disable-web-security --user-data-dir 或 chrome.exe --args --disable-web-security --user-data-dir。
⑤、将指令“ --disable-web-security --user-data-dir” 或 “ --args --disable-web-security --user-data-dir”粘贴到这条下图的目标中,注意有空格,这两种方式都是一样的,直接粘贴这种其实就相当于系统打开谷歌时执行了“目标”里的指令而已。
(2)、MAC OS 操作系统
1⃣️、允许Chrome读写一个文件夹,比如:CCD。
新建一个文件夹,作为允许chrome读写的文件夹,查看其路径。
在terminal里运行以下命令:
open -n /Applications/Google\ Chrome.app/ --args --disable-web-security --user-data-dir=刚刚新建的文件夹的路径(我的是/Users/CCD)
命令执行后,弹出窗口,提示如下图:
点击启动即可开启一个新的网页,该网页已经可以读写CCD文件夹了。
2⃣️、解决302报错
再运行项目,可能会报错302(资源被重定向找不到了),怎么办呢?
在浏览器地址栏输入并访问下面的网址:
// chrome 浏览器的某些实验功能
chrome://flags/
ctrl+F 搜索:same-site-by-default-cookies,并将其设置为 disabled(禁用)。
最后别忘了点击 relaunch哦。
四、对跨域的进一步学习
请戳此链接:https://segmentfault.com/a/1190000011145364
五、特殊的跨域
首先抛出一个问题:在本地访问本机起的服务会产生跨域吗?
答案是会的。下面我就来还原一下这个问题。
比如:我的 IP 地址是 192.168.x.x,我的端口号是 8080,起的服务的端口号是 8181。
目标接口是:http://192.168.x.x:8080/getData。
我前端页面访问的 URL 是:http://192.168.x.x:8080/home。
所以,我做了代理(本案例用的是 vue2):
module.exports = defineConfig({
DevServer: {
port: 8080,
hot: true,
proxy: {
'/api': {
target: "http://192.168.x.x:8181",
ws: false,
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
})
代理后,我要访问的接口会变为:http://192.168.x.x:8181/api/getData。
这样做是正确的,没问题。但是,如果我前端页面访问的 URL 改为:http://localhost:8080/home,会发生什么?产生跨域了。至此,问题还原了。这是为什么呢?
这是因为 localhost 与 192.168.x.x 是不同的,localhost 不会被 DNS 解析为本机 IP 地址,那么显然这违反了 “同源策略” 的 “同域名” 的原则了,所以就会产生跨域了。