浏览器的同源策略会导致跨域,这里同源策略又分为以下两种 DOM 同源策略:
即不能通过 ajax 的方法去请求不同源中的文档、浏览器中不同域的框架之间是不能进行js的交互操作的。
只要协议、域名、端口有任何一个不同,都被当作是不同的域,之间的请求就是跨域操作。
还有一点比较重要,为了安全,限制跨域是浏览器的行为,而不是 JS / 服务端的行为。你也可以自己开发一个浏览器,或者拿开源代码改,使得自己开发的浏览器能够跨域。
为了便于客户端使用数据,逐渐形成了一种非正式传输协议,人们把它称作 JSONP。该协议的一个要点就是通过创建一个 script 标签,将 src 设置为目标请求,插入到 dom 中,服务器接受该请求并返回数据,数据通常被包裹在回调钩子中。
直接打开 index.html 文件浏览器端会报错。就是同源限制造成的。
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
</body>
<script>
function request(type, url, data) {
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
console.log(xhr.responseText)
} else {
console.log(xhr.status)
}
}
}
xhr.open(type, url, true)
xhr.send(data)
}
request('get', 'http://localhost:3000/user', null) // 网页默认端口 80 or 8000
</script>
</html>
JSONP 是 JSON with padding(填充式 JSON 或参数式 JSON)的简写,是应用 JSON 的一种方式。
JSONP 的实现原理很简单,利用 <script> 标签没有同源限制的特点,也就是 <script> 的src链接可以访问不同源的。不受同源限制的还有 <img>、<iframe>、<link>,对应这两个标签实现的跨域方法也有,比如图片 ping 等。(图片 ping:可以访问任何 url,一般用来进行点击追踪,做页面分析常用的方法。缺点:不能访问响应文本,只能监听是否响应)
下面为允许跨域资源嵌入的示例,即一些不受同源策略影响的标签示例:
我们访问服务端,一般是获取存JSON数据,而JSONP则返回的是,包含函数的数据,将我们需要的 JSON 数据作为函数的参数。
即在客户端一般通过 <script> 标签的 src 访问带有 callback 查询参数的请求,来获取返回带有函数的数据,然后执行它,即可拿到这份数据(服务端注入的数据)完成跨域访问。
// 服务器返回的内容: 以字符串的形式返回
callback({"name": "xuthus"};
由于使用 script 标签的 src 属性,因此只支持get方法。
JSONP的请求过程:
通过这种方式,即可实现跨域获取数据。
function createScript(url) {
let script = document.createElement('script');
script.setAttribute('src', url);
script.setAttribute('type', 'text/javascript');
script.async = true; // 或者直接插入文档底部 document.body.appendChild(script);
return script;
}
// NumberObject.toString(radix) radix数字转化的基数:2~36之间的整数,36表示36进制,能将26个字母全部运用上
// /[^a-z]+/g :[^a-z]表示除a~z之间的字符 +:{1,}表示出现的所有字符
// 字符串的方法slice和substring都表示切割从起始位置到结束位置(不包括)之间的字符 substr()第一个参数表示切割的起始位置 第二个参数表示切割的长度
function gernerateCbName(prefix, num) {
return prefix + Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, num);
}
// 我们将所有的callback都设置成了一个全局变量,这样的原因是因为我们需要在数据请求完成之后调用这个方法,因此不得不设置为一个全局变量。
// 但是当我们有多个请求,并且每个请求的处理都是不一样的时候,这个变量将会被覆盖。这是不行的,因此我们应该为每一次请求设置一个唯一且不会冲突的变量
// 页面中若有大量这样的请求之后,window中会含有大量的全局变量,而且还有大量的script标签,这显然不是我们想要的,所以我们要在请求完成之后删除变量和script标签。
function jsonp({ url, params, timerout = 0}) {
let timer = null;
let cbName = gernerateCbName('cb');
let arr = [];
params = { ...params, cbName }
for(let key in params) {
arr.push(`${key}=${params[key]}`)
}
let script = createScript(`${url}?${arr.join('&')}`);
document.getElementsByTagName('head')[0].appendChild(script);
// 错误处理(例如资源加载失败)
script.onerror = function() {
reject(new Error(`fetch ${url} failed`));
window[cbName] = null;
timer && clearTimeout(timer);
document.getElementsByTagName('head')[0].removeChild(script);
}
return new Promise((resolve, reject) => {
window[cbName] = function(data) {
resolve(data);
window[cbName] = null;
timer && clearTimeout(timer);
document.getElementsByTagName('head')[0].removeChild(script);
}
// 超时处理
if(timerout != 0) {
timer = setTimeout(() => {
reject(new Error('TimeOut'));
timer && clearTimeout(timer);
// window[cbName] = null;
// document.getElementsByTagName('head')[0].removeChild(script);
}, timerout);
}
});
}
// 使用
jsonp({
url: 'http://localhost:3000',
params: {
name: 'xql'
},
timerout: 1
}).then((data) => {
console.log(data);
}).catch((err) => {
console.log(err);
});
测试:node server.js 打开index.js 可以看到跨域成功
// server.js
let express = require('express');
let app = express();
app.get('/', function(req, res){
let { cbName } = req.query; // cbName是和前端约定好的字段名称,即服务端和客户端这个字段保持一致
console.log(cbName);
res.end(`${cbName}('hello')`); // `${cbName}(数据 or JSON数据)`
});
app.listen(3000);
Cross-Origin Resource Sharing 跨源资源共享
浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
可以把CORS分为:简单请求、复杂请求。
流程:
Access-Control-Allow-Origin: http://api.bob.com: 该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。
Access-Control-Allow-Credentials: true:该字段可选。它的值是一个布尔值且只有 true 这一个值,表示是否允许发送Cookie,同时开发者必须在AJAX请求中打开withCredentials: true属性才生效。默认情况下,Cookie不包括在CORS请求之中。
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。
"预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。除了Origin字段,"预检"请求的头信息包括两个特殊字段。
Access-Control-Request-Method:该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,例如PUT。Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,例如X-Custom-Header。
服务器收到"预检"请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。
如果浏览器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。
一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。
优点
缺点
使用方式
如果两个网页不同源,就无法拿到对方的DOM。典型的例子是 iframe 窗口和window.open方法打开的窗口,它们与父窗口无法通信。
比如,父窗口运行下面的命令,如果iframe窗口不是同源,就会报错。
// 父窗口想获取子窗口的DOM,因为跨源导致报错。
document.getElementById("myIFrame").contentWindow.document
// Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.
反之亦然,子窗口获取主窗口的DOM也会报错。
window.parent.document.body
// 报错
如果两个窗口一级域名相同,只是二级域名不同,那么设置document.domain属性,就可以规避同源政策,拿到DOM。
对于完全不同源的网站,目前有三种方法,可以解决跨域窗口的通信问题。
有一个页面,它的地址是 http://www.example.com/a.html , 在这个页面里面有一个 iframe,它的src是 http://example.com/b.html, 很显然,这个页面与它里面的iframe框架是不同域的,所以我们是无法通过在页面中书写js代码来获取iframe中的东西的。
<iframe id = "iframe" src="http://example.com/b.html" onload="test()"></iframe>
<script type="text/javascript">
// function test(){
// var iframe = document.getElementById('ifame');
// var win = document.contentWindow;// undefined 可以获取到iframe里的window对象,但该window对象的属性和方法几乎是不可用的
// var doc = win.document;//这里获取不到iframe里的document对象
// var name = win.name;//这里同样获取不到window对象的name属性
// console.log(win)
// }
document.domain = 'example.com';//设置成主域
function test(){
alert(document.getElementById('iframe').contentWindow);//contentWindow 可取得子窗口的 window 对象
}
</script>
<script type="text/javascript">
document.domain = 'example.com';
//在iframe载入这个页面也设置document.domain,使之与主页面的document.domain相同
</script>
原理:location.hash方式跨域,是子框架能够修改父框架src的hash值,子框架通过修改父框架 src 的 hash 值来传递信息。且更改hash值,页面不会刷新。但是传递的数据的字节数是有限的。
步骤:动态插入一个iframe,将iframe的src属性指向服务端地址。这时top window和包裹这个iframe的子窗口是不能通信的(同源策略),所以改变子窗口的路径就行了,将数据当做改变后的路径的hash值加在路径上,然后就能通信了(和window.name跨域几乎相同),将数据加在index页面地址的hash值上。index页面监听地址的hash值变化(html5有hashchange事件,用setInterval不断轮询判断兼容ie6/7),然后做出判断,处理数据。
假设域名a.com下的文件cs1.html要和jianshu.com域名下的cs2.html传递信息。
1、cs1.html首先创建自动创建一个隐藏的iframe,iframe的src指向jianshu.com域名下的cs2.html页面。
2、cs2.html响应请求后再将通过修改cs1.html的hash值来传递数据。
3、同时在cs1.html上加一个定时器,隔一段时间来判断 location.hash的值有没有变化,一旦有变化则获取获取hash值。
注:由于两个页面不在同一个域下IE、Chrome不允许修改parent.location.hash的值,所以要借助于a.com域名下的一个代理iframe。
//a页面的代码
<script type="text/javascript">
iframe = document.createElement('iframe');
iframe.style.display = 'none';
var state = 0;
iframe.onload = function() {
if(state === 1) {
var data = window.location.hash;
console.log(data);
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
} else if(state === 0) {
state = 1;
iframe.contentWindow.location = 'http://localhost: 4000/b.html';
}
};
document.body.appendChild(iframe);
</script>
//b页面代码
<script type="text/javascript">
parent.location.hash = "world";
</script>
优点:1.可以解决域名完全不同的跨域。2.可以实现双向通讯。
缺点:location.hash会直接暴露在URL里,并且在一些浏览器里会产生历史记录,数据安全性不高也影响用户体验。另外由于URL大小的限制,支持传递的数据量也不大。有些浏览器不支持 onhashchange 事件,需要轮询来获知URL的变化。
原理:window.name(一般在js代码里出现)的值不是一个普通的全局变量,而是当前窗口的名字,这里要注意的是每个iframe都有包裹它的window,而这个window是top window的子窗口,每个 iframe 都有 window.name 的属性。window.name属性的神奇之处在于一个 iframe 的 name 值在不同的页面(甚至不同域名,即改变了这个iframe的 src 默认情况下,name依然保持不变)加载后依旧存在(如果没修改则值不会变化),并且可以支持非常长的 name 值(2MB)。
window.name = data;//父窗口先打开一个子窗口,载入一个不同源的网页,该网页将信息写入。
location = 'http://parent.url.com/xxx.html';//接着,子窗口跳回一个与主窗口同域的网址。
var data = document.getElementById('myFrame').contentWindow.name。//然后,主窗口就可以读取子窗口的window.name了。
步骤:在iframe载入过程中,迅速重置iframe.src的指向,使之与index.html同源,那么index页面就能去获取到这个 iframe 的name值了(把子框架的 src 改变和父框架同源,而子框架的 name 依然保持不变)!
所以,iframe子框架需要一直不停地刷新,每次触发onload事件后,重置src,相当于重新载入页面,又触发onload事件,于是就不停地刷新了(但是需要的数据还是能输出的);
// index.html
<script type="text/javascript">
iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = 'http://localhost: 4000/b.html';
document.body.appendChild(iframe);
var state = 0;
iframe.onload = function() {
if(state === 1) {
var data = JSON.parse(iframe.contentWindow.name);
console.log(data);
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
} else if(state === 0) {
state = 1;
iframe.contentWindow.location = 'http://localhost: 8000/proxy.html'; // 父框架的域
}
};
</script>
// http://localhost: 4000/b.html文件
<script type="text/javascript">
window.name = "hello";
</script>
优点:window.name容量很大,可以放置非常长的字符串;(2MB)
缺点:必须监听子窗口window.name属性的变化,影响网页性能。
信息传递除了客户端与服务器之前的传递,还存在以下几个问题:
上面两种方法都属于破解,再加上iframe用的比较少了,所以这些方法也就有点过时了。window.postMessage是一个HTML5的api,允许两个窗口之间进行跨域发送消息,不论这两个窗口是否同源。这个应该就是以后解决dom跨域通用方法了。
// 语法:data:将要发送到其他 window 的数据; targetOrigin:指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI
otherWindow.postMessage(data, targetOrigin, [transfer]);
父窗口和子窗口都可以通过message事件,监听对方的消息。message事件的事件对象event,提供以下三个属性:
// a.html
<iframe src="http://localhost:4000/b.html" id="frame" onload="load()"></iframe>
<script>
function load(params){
let frame = document.getElementById('frame');
//获取iframe中的窗口,给iframe里嵌入的window发消息
frame.contentWindow.postMessage('hello','http://localhost:4000')
// 接收b.html回过来的消息
window.onmessage = function(e){
console.log(e.data)
}
}
</script>
// b.html
<script>
//监听a.html发来的消息
window.onmessage = function(e){
console.log(e.data)
//给发送源回消息
e.source.postMessage('nice to meet you',e.origin)
}
</script>
/*
* A窗口的域名是<http://example.com:8080>,以下是A窗口的script标签下的代码:
*/
var popup = window.open(...popup details...);
// 如果弹出框没有被阻止且加载完成
// 这行语句没有发送信息出去,即使假设当前页面没有改变location(因为targetOrigin设置不对)
popup.postMessage("The user is 'bob' and the password is 'secret'",
"https://secure.example.net");
// 假设当前页面没有改变location,这条语句会成功添加message到发送队列中去(targetOrigin设置对了)
popup.postMessage("hello there!", "http://example.org");
function receiveMessage(event)
{
// 我们能相信信息的发送者吗? (也许这个发送者和我们最初打开的不是同一个页面).
if (event.origin !== "http://example.org")
return;
// event.source 是我们通过window.open打开的弹出页面 popup
// event.data 是 popup发送给当前页面的消息 "hi there yourself! the secret response is: rheeeeet!"
}
window.addEventListener("message", receiveMessage, false);
/*
* 弹出页 popup 域名是<http://example.org>,以下是script标签中的代码:
*/
//当A页面postMessage被调用后,这个function被addEventListenner调用
function receiveMessage(event)
{
// 我们能信任信息来源吗?
if (event.origin !== "http://example.com:8080")
return;
// event.source 就当前弹出页的来源页面
// event.data 是 "hello there!"
// 假设你已经验证了所受到信息的origin (任何时候你都应该这样做), 一个很方便的方式就是把enent.source
// 作为回信的对象,并且把event.origin作为targetOrigin
event.source.postMessage("hi there yourself! the secret response " +
"is: rheeeeet!",
event.origin);
}
window.addEventListener("message", receiveMessage, false);
优点:不需要后端介入就可以非常简单的的做到跨域,一个函数外加两个参数(请求url,发送数据)就可以搞定;移动端兼容性好;
缺点:
文章开头说了,浏览器的同源策略会导致,LocalStorage有同源限制。
解决办法:通过window.postMessage,读写其他窗口的 LocalStorage 也成为了可能。

