什么是“ 前端 ”“ 跨域 ” ?
常见跨域方式有这么几种:jsonp、cors,iframe+domain跨域、以及nginx反向代理,还有就是postMessage。
相比之“基于”前端的其余几种方法,iframe+domain和cors的方式不太常用 —— 没错,cors一般来讲是后端设置,但是完全可以让前端“一力以担之”。
jsonp方式解决跨域问题
jsonp是打破第一重限制,(因为)用了XMLHttpRequest就跨域,那不用这种方式了,我们来看一段jquery的带jsonp的ajax请求:
$.ajax({
type : "GET",
url : "http://api.map.baidu.com/geocoder/v2/",
data:"address=河南",
dataType:"jsonp",
jsonp:"callback", //回调函数名默认是callback,可以自定义回调函数名字,#但是必须和后台保持一致#
jsonpCallback:"showLocation", //数据返回成功之后,回调函数的名字是随机生成的,如jquery0122526({....}) 在这里自己指定一个(会被替换掉——这一行可以不写)
success : function(data){
alert("成功");
},
error : function(data){
alert("失败");
}
});
使用这种类型的话,会创建一个查询字符串参数 callback=? ,这个参数会加在请求的URL后面。服务器端应当在JSON数据前加上回调函数名,以便完成一个有效的JSONP请求 。
上面代码看似用了ajax请求,其实内部完全不是那么回事,多了jsonp和jsonpCallback选项,它内部将代码翻译并把页面上的dom操作成这样:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script type='text/javascript'>
// 后端返回直接执行的方法,相当于执行这个方法,由于后端把返回的数据放在方法的参数里,所以这里能拿到res。
window.showLocation = function (res) {
console.log(res)
//执行ajax回调
}
</script>
<script src='http://api.map.baidu.com/geocoder/v2/?address=河南&callback=showLocation' type='text/javascript'></script>
</body>
</html>
这个时候,html页面的script src标签回去访问api.map.baidu.com的服务端,由于script,img这种标签是不受浏览器xmlhttprequest限制的,可以随意访问,这个时候对应的后端代码取得address等参数,然后根据双方约定好的callback参数,返回一个被包装后的json。
但是他有局限性——需要后端配合:上面说“从服务器获取结果并让浏览器执行callback”,也就是说“必须再服务器端返回给前端一个名为callback值的函数调用”,比如这样:
// 客户端 —— 封装了jsonp函数
function jsonp(url, callback) {
// 把传递的回调函数挂载到全局上
let uniqueName = `jsonp${new Date().getTime()}`;
// 目的让 返回的callback执行且删除创建的标签
window[uniqueName] = data => {
// 从服务器获取结果并让浏览器执行callback
document.body.removeChild(script);
delete window[uniqueName];
callback && callback(data);
}
// 处理URL
url += `${url.includes('?')} ? '&' : '?}callback=${uniqueName}'`;
// 发送请求
let script = document.createElement('script');
script.src = url;
document.body.appendChild(script);
}
// 执行第二个参数 callback,获取数据
jsonp('http://127.0.0.1:1001/list?userName="lsh"', (result) => {
console.log(result);
})
// 服务器端 —— Api请求数据
app.get('/list', (req, res) => {
// 此时的callback 为传递过来的函数名字 (uniqueName)
let { callback } = req.query;
// 准备返回的数据(字符串)
let res = { code: 0, data: [10,20] };
let str = `${callback}($(JSON.stringify(res)))`;
// 返回给客户端数据
res.send(str);
})
cors解决跨域
前面说了,这种方式一般是后端设置的:在后台添加允许跨域:“跨域资源共享”(Cross-origin resource sharing)。它允许浏览器向跨源(协议 + 域名 + 端口)服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
前面也说了,这种方式完全可以前端独自完成:
CORS——跨域资源共享
我们可以通过添加 共享站 :www.corsproxy.com(我们也叫作“请求中转站”)
这主要是通过设置 Access-Control-Allow-Origin 来进行的。
使用方法: 在“共享站”后面加上url即可!
比如我们上面请求的:http://api.map.baidu.com/geocoder/v2/
现在我们这样来写:http://www.corsproxy.com/ api.map.baidu.com/geocoder/v2/
postMessage实现跨域通信
这常被用在比如“聊天机器人”上(至少笔者是这么干的…)
window.postMessage()方法可以安全地实现跨源通信。通常,对于两个不同页面的脚本,只有当执行它们的页面位于具有相同协议、端口号以及主机(即两个页面的模数Document.domain设置为相同的值)时,这两个脚本才能互相通信。
window.postMessage()方法提供了一种受控机制来规避此限制,只要正确的使用,就很安全。
本质上说,postMessage()是基于消息事件机制来实现跨域通信,它隶属于消息窗体本身,比如window以及window内嵌的frame的window,基本使用形式如下(通常被用在“发送方”页面中):
someWindow.postMessage(message,targetOrigin,[transfer]);
- someWindow:窗口的一个引用(一般是新窗口),比如 iframe的contentWindow属性、执行window.open返回的窗口对象,后者是命名过或数值索引的window.frames
- message:将要发送到其他window的数据。——不受格式限制,无需自己序列化
- targetOrigin:通过窗口的origin属性指定哪些窗口能接收到消息事件,此值可以是字符串“*”(表示无限制)
- transfer:(可选)是一串和message同时传递的Transferable对象
我们可以通过如下方式监听message(这通常被用在“接收方”页面中):
window.addEventListener("message", receiveMessage, false);
function receiveMessage(event){
let origin = event.origin || event.originalEvent.origin;
if (origin !== "http://aaa:8080")
return;
// ...
console.log(event.data)
}
// 派发消息的页面
winB.postMessage(_({text: '休息休息'}), origin)
其中,event中有几个核心属性需要注意 如下:
- data:从其他window中传递过来的对象
- origin:调用postMessage时消息发送方窗口的origin。这个字符串由“协议、😕/、域名、:端口号”拼接而成
- source:对发送消息的窗口对象的引用
跨域实践:聊天机器人
此demo只展示核心部分,使用postMessage完成。
我们分别有两个HTML:a.html和b.html,然后用node分别代理两个不同页面,设置不同端口:
//依赖一个http模块,相当于java中的import
// a.js
var http = require('http');
var fs = require('fs');
var { resolve } = require('path');
//创建一个服务器对象
server = http.createServer(function (req, res) {
//设置请求成功时响应头部的MIME为纯文本
res.writeHeader(200, {"Content-Type": "text/html"});
//向客户端(页面)输出字符
let data = fs.readFileSync(resolve(__dirname, './a.html'))
res.end(data);
});
//让服务器监听本地8000端口开始运行
server.listen(8000,'127.0.0.1');
console.log('http://127.0.0.1:8000')
// b.js
// ...
server.listen(8001,'127.0.0.1');
我们将a.html代理在8000端口下,将b.html代理在8001端口下。
搭建页面层级:这里将b页面以iframe的形式嵌入到a页面:
(a页面)
<body>
<div class="wrap">
<iframe src="http://127.0.0.1:8001" frameborder="0" id="b"></iframe>
<div class="control">
<input type="text" placeholder="请输入内容" id="ipt">
<span id="send">发送</span>
</div>
</div>
<script>
window.onload = function() {
let origin = 'http://127.0.0.1:8001';
let _ = (data) => JSON.stringify(data); //对象函数 & es6箭头表达式
let winB = document.querySelector('#b').contentWindow;
let sendBtn = document.querySelector('#send');
sendBtn.addEventListener('click', (e) => {
let text = document.querySelector('#ipt');
winB.postMessage(_({text: text.value}), origin)
text.value = '';
}, false)
winB.postMessage(_({text: ''}), origin)
}
</script>
</body>
通过iframe的contentWindow来拿到b页面窗体的引用,然后在发送按钮的点击事件中触发postMessage将数据发送给B。
(b页面)
<body>
<div class="content">
<h4>XMM只能陪聊机器人</h4>
<div class="content-inner"></div>
</div>
<script>
// 语料库-略
const pool = [];
window.addEventListener("message", receiveMessage, false);
let content = document.querySelector('.content-inner');
let initContentH = content.scrollHeight;
let _ = (data) => JSON.stringify(data);
function createChat(type, mes) {
let dialog = document.createElement('div');
dialog.className = type === 0 ? 'dialog robot' : 'dialog user';
let content = type === 0 ? `
<span class="tx">${type === 0 ? 'lab' : 'user'}</span>
<span class="mes">${mes}</span>
` : `
<span class="mes">${mes}</span>
<span class="tx">${type === 0 ? 'lab' : 'user'}</span>
`;
dialog.innerHTML = content;
return dialog
}
function scrollTop(el, h) {
if(el.scrollHeight !== h) {
el.scrollTop = h + 100;
}
}
function receiveMessage(event){
// 兼容其他浏览器
let origin = event.origin || event.originalEvent.origin;
if(origin === 'http://127.0.0.1:8000') {
let data = JSON.parse(event.data);
if(data && !data.text) {
mes = { text: '你好,我是机器人Lab,请问有什么可以帮到您的吗?' };
event.source.postMessage(_(mes), event.origin)
content.appendChild(createChat(0, mes.text))
}else {
content.appendChild(createChat(1, data.text))
scrollTop(content, initContentH)
setTimeout(() => {
content.appendChild(createChat(0, '正在解决'))
scrollTop(content, initContentH)
}, 2000);
}
}
}
</script>
</body>
改域
对于主域名相同,子域名不同的情况,可以通过修改 document.domain
的值来进行跨域。如果将其设置为其当前域的父域,则这个较短的父域将用于后续源检查。。
这时只要把 这两个页面的 document.domain
都设成相同的域名,那么父子页面之间就可以进行跨域通信了,同时还可以共享 cookie!
但要注意的是,只能把 document.domain 设置成更高级的父域才有效果