说到跨域我们需要先了一些概念
同源策略定义:
同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。而浏览器也是建立在这样的同源策略基础上的它是浏览器最核心最主要的功能之一。
同源定义:
两个url地址如果 协议、主机、端口号三个都完全相同就属于同源,就可以正常的进行数据的交互
给定一个地址:http://www.localhost:8080/abc/index.html,下列列出与之同源和非同源的例子
地址 | 是否同源 | 理由 |
---|---|---|
https://www.localhost:8080 | 非同源 | 协议不一样,一个http,一个https |
http://www.localhost:8081 | 非同源 | 端口号不一样,一个8080,一个8081 |
http://localhost:8080 | 非同源 | 主机不相同(需要精准匹配) |
http://www.abc.localhost:8080 | 非同源 | 主机名不同 |
http://www.localhost:8080/bck/a.html | 同源 | 只有具体路径不相同 |
http://www.localhost:8080/x/y/b.html | 同源 | 协议,主机,端口号都相同 |
而我们在构建web应用程序的时候也要遵循这样的约定,但是有时候为了实现跨域请求和获取一些数据就需要使用跨域请求的方式去实现。
解决方案:
JSONP
jsonp是最先提出的跨域请求解决方案,这里我们需要知道一些基础知识,在html中能够跨域请求资源的有:script、link、img、video、audio、frame、iframe、embed等有src属性的标签。这些标签的请求方式都是GET方式,其中能够请求服务器拿到服务器返回的数据只有script标签
jsonp就是通过此特性实现跨域和服务器交互,具体实现逻辑
最简单并且能够理解的例子:
服务器端:这里使用的nodejs的express搭建的服务器
// 引入express
let express = require('express');
// 创建一个服务器示例
let app = express();
// 这里是get请求的路由地址
app.get('/user', (req, res)=>{
//然后拿到地址的get参数
let callBack = req.query.callBack;
// 这里是需要发送给客户端的数据,这里传出去的是对象字符串形式
let data = JSON.stringify({
name: 'qwguo',
age: 34,
sex: 'man'
});
// 然后执行end方法结束连接,并且传给客户端一串字符串
// 其实这里传过去的是
/*
callBack({
name: 'qwguo',
age: 34,
sex: 'man'
});
*/
// 上边的形式
res.end(`${callBack}(${data})`);
});
// 然后服务器监听在3000端口上
let server = app.listen(3000, (res)=>{
var host = server.address().address;
var port = server.address().port;
// console.log(host);
console.log("应用实例,访问地址为 http://%s:%s", host, port);
})
客户端:
<script>
//首先我们在这里定义一个全局变量方法,用于异步请求的回调函数
// 并且该方法接收一个参数作为形参,是请求异步后端返回的数据
let callBack = function(res){
console.log(res);
}
</script>
<!--这里借助script可以跨域请求的特性,请求一个服务器接口地址
并且后边跟一个参数,名为:callBack
参数值是:callBack,这样就能请求到服务器的地址,
并且会把请求的结果当做一个js文件进行执行
-->
<script src="http://localhost:3000/user?callBack=callBack"></script>
通过上边的详细介绍,我们可以看出jsonp的跨域方式就是利用script标签会把请src请求的结果当做一个js文件进行执行的特性,然后由后端进行配合返回一个要执行的js方法,请求把参数放到该方法中作为参数。
封装JSONP方法
/**
* url: 请求的服务器地址,
* cb: 回调函数的名字
*/
function jsonp(url, cb){
var scriptDom = document.createElement('script');
var cbs = 'callBack=' + cb;
if(url.indexOf('?')===-1){
cbs = '?' + cbs;
}
scriptDom.src = url + cbs;
document.querySelector('head').appendChild(scriptDom);
setTimeout(function(){
scriptDom.remove();
});
}
//使用:
let btn = document.querySelector('#jsonp');
// 定义一个回调函数,并且需要时全局的
let callBack = function(res){
// 拿到服务器返回的数据进行后续操作
console.log(res);
}
// 然后给按钮绑定单击事件,执行调用jsonp方法
btn.addEventListener('click', function(){
jsonp('http://localhost:3000/user', 'callBack');
})
这里需要注意的是,json只能是get请求,并且需要定义个全局变量函数,用来回调方法的执行。
服务器端进行配置
只需要在服务器端进行相应头的设置即可,这里还是用nodejs进行设置
服务器端配置
// 检测请求所有的路径都进入此方法
app.all("*", function (req, res, next) {
// 给服务器相应头部设置'Accress-Control-Allow-Origin'为‘*’
res.header("Access-Control-Allow-Origin", "*");
// 然后在进行后续操作
next();
});
// 请求地址,可以是post,get等
app.get('/user2', (req, res)=>{
// 返回客户端数据
let data = JSON.stringify({
name: 'qwguo',
age: 34,
sex: 'man'
});
// 执行end进行结束连接并返回数据
res.end(data);
});
客户端请求
// 获取按钮元素
let serveCross = document.querySelector('#serveCross');
// 给按钮绑定事件
serveCross.addEventListener('click', function(){
// 实例化异步请求方法
var xhr = new XMLHttpRequest();
// 请求服务器接口地址
xhr.open('GET', 'http://localhost:3000/user2', true);
// 给异步请求对象绑定方法,进行状态监听
xhr.onreadystatechange = function(){
// 当状态变为4,并且状态码为200表示请求成功,
if(xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
// 通过responseText拿到服务器返回的数据
console.log(JSON.parse(xhr.responseText));
}
};
// 发送执行
xhr.send();
});
从上边的例子可以看出,只需要在服务器设置一个响应头的允许地址源,这里设置的是 *号,表示允许所有的地址,这种方式虽然可以,但是有安全隐患,最好的方法是设置允许的源地址例如:
app.all("*", function (req, res, next) {
//设置允许指定的源地址
res.header("Access-Control-Allow-Origin", "http://localhost:5500");
next();
});
设置document.domain实现
这里主要是用在非同源的iframe和当前页面进行通讯,给当前页面和iframe设置同一个document.domain来实现非同源通讯
服务器文件
app.all("*", function (req, res, next) {
// 这里设置了允许跨域请求的域名
res.header("Access-Control-Allow-Origin", "http://localhost:8080");
res.header("Content-Type", "application/json;charset=utf-8");
next();
});
// 请求接口
app.post("/user", (req, res) => {
let data = JSON.stringify({
name: "qwguo",
age: 34,
sex: "man",
});
// 把数据返回给客户端
res.end(data);
});
localhost:5500域名文件
<!--a.html,访问地址localhost:5500-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>a.html</title>
</head>
<body>
<input type="text" name="msg" id="msg" />
<button id="domainBtn">和iframe通讯</button>
<!-- 这里引入非同源的html文件 -->
<iframe src="http://localhost:8080/b.html" id="myIframe"></iframe>
<script>
// 请求一个iframe中的异步后回调该方法获取数据
function getData(j){
console.log(j);
}
// 这里设置domain为`localhost`
document.domain = "localhost";
window.onload = function () {
// 获取上边iframe窗口对象
var myIframe = document.getElementById("myIframe").contentWindow;
// 然后获取页面的按钮
var btn = document.getElementById("domainBtn");
// 给按钮绑定事件
btn.addEventListener("click", function (e) {
// 阻止事件默认行为
e.preventDefault();
// 获取要传递给localhost:8080域名的数据
var val = document.getElementById("msg").value;
// myIframe.setInner(val);
// 然后调用iframe中的一个方法传递数据
myIframe.getAjax(val);
});
};
</script>
</body>
</html>
localhost:8080域名文件
<!--b.html,访问地址localhost:8080-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>b.html</title>
</head>
<body>
<div>b.html</div>
<script>
// 这里设置域为localhost
document.domain = 'localhost';
// 请求异步接口的方法
function getAjax(val) {
// 实例化XMLHttpRequest
var xhr = new XMLHttpRequest();
// 请求异步数据
xhr.open('POST', 'http://localhost:3000/user', true);
// 设置请求时发送的数据类型
xhr.setRequestHeader('content-type', 'application/x-www-form-urlencoded');
// 监听xhr的状态变化
xhr.onreadystatechange = function () {
// 当readyState状态为4的时候表示异步请求结束
if (xhr.readyState === 4 && xhr.status === 200) {
// 拿到异步返回的数据
let getData = xhr.responseText
// 这里可以调用父级窗口的方法把接口返回的数据交给父级窗口
// 也就是上边的localhost:5500的域名文件
window.parent.getData(getData);
}
}
// 发送异步请求,并且把参数传递过去
xhr.send(JSON.stringify({ val: val }));
}
</script>
</body>
</html>
说明
通过上边的案例可以看到,当我们的localhost:5500域名a.html文件想要直接调用localhost:3000端口的接口数据是没有办法访问的应为有请求域名限制。
然后我们可以借助在当前页面创建一个iframe是localhost:8080的b.html文件,因为iframe 的src本身支持跨域加载文件
加载完毕后如果我们需要a.html和b.html进行数据通信,是无法进行的,这个时候我们就需要把两个文件的domain设置成相同的域domain=‘localhost’。
这个时候两个域名的文件就可以相互进行交互,调用某些方法。然后由于服务器端已经给8080的文件加了白名单,所以如果a.html中的文件需要请求3000的端数据可以借助该iframe作为中间的桥梁。
postMessage
这个方法是h5新增的支持跨域通信的方案,它可以安全地实现跨源通信,这个方法是通过两个窗口可以得到他对应的窗口的引用关系,比如说使用open方法进行打开的页面,打开的新页面就有一个parent属性表示父级窗口,然后通过给窗口对象增加postMessage进行交互。
代码流程
a.html
<!--a.html,访问地址localhost:5500-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>a.html</title>
</head>
<body>
<!-- 这里引入非同源的html文件 -->
<iframe src="http://localhost:8080/b.html" id="myIframe"></iframe>
<input type="text" name="msg" id="msg" />
<button id="domainBtn">和iframe通讯</button>
<span id="openWin">打开窗口</span>
<button id="newBtn">新交互</button>
<script>
// 这里绑定message方法用来监听子窗口当前窗口发消息
window.addEventListener('message', function (e) {
if (e.origin !== "http://localhost:8080") { // 验证消息来源地址
return;
}
console.log(e.data);
});
window.onload = function () {
// 获取上边iframe窗口对象
var myIframe = document.getElementById("myIframe").contentWindow;
// 然后获取页面的按钮
var btn = document.getElementById("domainBtn");
// 给按钮绑定事件
btn.addEventListener("click", function (e) {
// 获取要传递给localhost:8080域名的数据
var val = document.getElementById("msg").value;
// myIframe.postMessage(val, "http://localhost:8080");
myIframe.postMessage(val, "*");
});
/**
* 打开新窗口交互
*/
var openWin = document.querySelector('#openWin');
var newBtn = document.querySelector('#newBtn');
// 定义变量用来存储打开的窗口对象
var newWin = null;
openWin.addEventListener('click', function(){
// 把打开的窗口对象复制给变量
newWin = window.open('http://localhost:8080/c.html');
});
newBtn.addEventListener('click', function(){
// 获取要传递给localhost:8080域名的数据
var val = document.getElementById("msg").value;
// myIframe.postMessage(val, "http://localhost:8080");
newWin.postMessage(val, "*");
});
};
</script>
</body>
</html>
b.html
<!--b.html,访问地址localhost:8080-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>postMessage b.html</title>
</head>
<body>
<div>postMessage b.html</div>
<script>
// console.log(window.parent);
// 监听 message 事件
window.addEventListener('message', function (e) {
if (e.origin !== "http://localhost:5500") { // 验证消息来源地址
return;
}
getAjax(e.data);
});
// 请求异步接口的方法
function getAjax(val) {
// console.log(val)
// 实例化XMLHttpRequest
var xhr = new XMLHttpRequest();
// 请求异步数据
xhr.open('POST', 'http://localhost:3000/user2', true);
// 设置请求时发送的数据类型
xhr.setRequestHeader('content-type', 'application/x-www-form-urlencoded');
// 监听xhr的状态变化
xhr.onreadystatechange = function () {
// 当readyState状态为4的时候表示异步请求结束
if (xhr.readyState === 4 && xhr.status === 200) {
// 拿到异步返回的数据
let getData = xhr.responseText;
parent.postMessage(getData, "*");
}
}
// 发送异步请求,并且把参数传递过去
xhr.send(JSON.stringify({ val: val }));
}
</script>
</body>
</html>
c.html
<!--b.html,访问地址localhost:8080-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>postMessage c.html</title>
</head>
<body>
<div>postMessage c.html</div>
<div id="newDiv"></div>
<script>
// console.log(window.parent);
// 监听 message 事件
window.addEventListener('message', function (e) {
// 验证消息来源地址
if (e.origin !== "http://localhost:5500") {
return;
}
// 把发过来的数据写到页面
document.querySelector('#newDiv').innerHTML = e.data;
});
</script>
</body>
</html>
从上边案例可以看出postMessage类似于发布订阅这模型,在主页面广播需要传给关注它的页面数据,同时需要接收广播的页面需要绑定一个message用来接收父级页面传过来的数据。
如果子页面要给父页面传数据,父页面也需要绑定一个message方法进行接收。
这个功能更适合网站有二级域名的,当在主域名打开一个二级域名的页面,这个时候就可以通过这种方式进行数据交互。