跨域是前端开发平常工做中常常会面对的一个问题。平常工做中咱们都会使用像webpack-dev-server构建咱们的开发环境的接口代理、亦或是使用Charles等接口代理工具。上线后能够经过运维同窗配合nginx或是cors等方案来解决。javascript
在JavaScript中,有一个很重要的安全性限制,被称为“Same-Origin Policy”(同源策略)。这一策略对于JavaScript代码可以访问的页面内容作了很重要的限制,即JavaScript只能访问与包含它的文档在同一域下的内容。跨域是指浏览器不能执行其余网站的脚本。MDN上的解释(浏览器的同源策略限制了从同一个源加载的文档或脚本如何与另外一个源的资料进行交互,这是一个用于隔离潜在恶意文件的重要机制)。简而言之就是浏览器对脚本实施的安全机制。html
有两个页面的协议、端口(若是有指定)和主机都相同,则两个页面具备相同的源,即为同源。若协议/端口/主机 有一项不一样,则为说明二者非同源。前端
Url | 调用 | Url | 结果 |
---|---|---|---|
www.peanutyu.site/home | www.peanutyu.site/api/* | 调用成功,非跨域 | |
www.peanutyu.site/home | www.peanut.site/api/* | 调用失败,主域名不一样 | |
www.peanutyu.site/home | www.peanutyu.site/api/* | 调用失败,协议不一样 | |
www.peanutyu.site/home | blog.peanutyu.site/api/* | 调用失败,子域名不一样 | |
www.peanutyu.site/home | www.peanutyu.site:8080/api/* | 调用失败,端口号不一样 |
在HTML标签里,一些标签好比script、img、iframe这些获取资源的标签是没有跨域限制的,JSONP就是咱们去动态的建立一个Script标签再去请求一个带参网址来实现跨域通讯。因为script标签加载资源的方式是GET请求,因此JSONP只能发送GET请求。java
const xxService = require('../../service/xxService');
exports = module.exports = new class {
constructor() {}
jsonp () {
let [cb, username ] = [];
if (ctx.query) {
({ cb, username } = ctx.query);
}
const data = await xxService.xxMethods(username);
// cb参数是先后端约定的方法名字,后端返回一个直接执行的方法给前端,前端获取这个方法后立马执行,而且把返回的数据放在方法的参数里。
ctx.body = `${cb}(${JSON.stringify(data)})`;
}
}
复制代码
const script = document.createElement('script');
const body = document.body;
script.src = 'http://127.0.0.1:3000/api/jsonp?cb=callbackJsonp&username=peanut';
body.appendChild(script);
function callbackJsonp(res) {
const div = document.createElement('div');
div.innerText = JSON.stringify(res);
body.appendChild(div);
body.removeChild(script);
}
复制代码
$.ajax({
url: 'http://blog.peanutyu.site/api/*',
type: 'GET',
dateType: 'jsonp', // 设置请求方式为jsonp
jsonpCallback: 'callbackJsonp',
data: {
'username': 'peanut',
},
});
function callbackJsonp(res) {
console.log(res);
}
复制代码
这种跨域方式要求主域名相同。好比www.peanut.site、blog.peanut.site、 a.peanutyu.site这三者主域名都是peanutyu.site。主域名不一样就不能使用这种跨域方式。webpack
浏览器不一样域的页面之间是不能够经过JS来进行交互操做的。可是不一样的页面,是可以获取到彼此的window对象的。可是,咱们只能获取到一个几乎无用的window对象。好比一个页面它的地址为http://www.peanutyu.site/a.html,在这一个页面里有一个iframe,它的src为http://peanutyu.site/b.html,这个页面和它内部的iframe是不一样域的,因此咱们是没法经过在页面中书写js代码来获取iframe中的东西的。咱们只须要把http://www.peanutyu.site/a.html和http://peanutyu.site/b.html都设置成相同的域名便可。ios
但须要注意的是document.domain的设置是有限制的,咱们只能把document.domain设置成自身或更高一层的父域,而且主域必须相同。blog.peanutyu.site中某个文档能够设置document.domain为blog.peanutyu.site或者peanutyu.site中的任何一个,可是不能设置为a.blog.peanutyu.site。由于这是当前域的子域,也不可设置为baidu.com,由于主域不一样。nginx
假设咱们要在http://www.peanutyu.site/a.html的页面里访问http://peanutyu.site里面的数据web
在http://www.peanutyu.site/a.html设置document.domainajax
<!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>A页面</title>
</head>
<body>
<iframe id="iframe" src="http://peanutyu.site/b.html" style="display:none;"></iframe>
<script> $(function () { try { document.domain = "peanutyu.site"; //这里将document.domain设置成同样 } catch (e) { } $("#iframe").load(function () { var iframe = $("#iframe").contentDocument.$; iframe.get("http://peanutyu.site/api", function (data) { console.log(data); }); }); }); </script>
</body>
</html>
复制代码
在http://peanutyu.site/b.html也须要设置document.domain。json
<!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>B页面</title>
</head>
<body>
<script> $(function () { try { document.domain = "peanutyu.site"; //这里将document.domain设置成同样 } catch (e) { } }); </script>
</body>
</html>
复制代码
这里须要注意,在A页面内须要等待加载完B页面以后才能够获取到B页面中的
对象咱们即可以直接发送ajax请求,不过这种跨域方式只能够在主域相同的时候使用。
当iframe页面跳转到其余地址时,其window.name值保持不变而且能够支持存储很是长的name(2MB)。可是浏览器规定浏览器跨域iframe禁止互相调用或者传递值。可是调用iframe时window.name却不变,咱们正好可使用这个特性来互相传值,固然跨域下是不允许读取iframe的window.name的值。
由于规定若是index.html页面和该页面里的iframe的src若是不一样源,就没法操做iframe内部的任何内容,因此也获取不到iframe的window.name属性了。不过既然要同源,咱们能够准备一个和主页面http://www.peanut.com/a.html相同域下的代理空页面http://www.peanut.com/proxy.html来指定src。
假设咱们有一个页面http://peanutyu.site/a.html须要从http://peanut.site/data.html内获取到数据
data页面代码
window.name = '我是data页面的数据';
复制代码
a页面代码
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
let state = 0;
iframe.onload = function() {
if (state === 1) {
const data = iframe.contentWindow.name;
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
} else {
state = 1;
iframe.contentWindow.location = 'http://peanutyu.site/proxy.html';
}
}
iframe.src = 'http://peanut.site/data.html';
document.body.appendChild(iframe);
复制代码
在iframe载入的过程当中,迅速重置iframe的location等同于从新载入页面,便会从新调用iframe的onload方法这时咱们的会走到条件为state === 1的内部,获取iframe的window.name的值,因为调用iframe时window.name不变,因此咱们便取到了不一样域内window.name的值。
CORS须要浏览器和服务器同时支持。目前,全部浏览器都支持该功能,IE浏览器不能低于IE10。 整个CORS通讯过程,都是浏览器自动完成,不须要用户参与。对于开发者来讲,CORS通讯与同源的AJAX通讯没有差异,代码彻底同样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感受。 所以,实现CORS通讯的关键是服务器。只要服务器实现了CORS接口,就能够跨源通讯。
浏览器将CORS请求分红两类:简单请求(simple request)和非简单请求(not-so-simple request)。只要同时知足如下两大条件,就属于简单请求。 (1) 请求方法是下面三种方法之一:
(2) HTTP的头信息不超出如下几种字段:
凡是不一样时知足上面两个条件,就属于非简单请求。浏览器对这两种请求的处理,是不同的。
对于简单请求,浏览器直接发出CORS请求。具体来讲,就是在头信息之中,增长一个Origin字段。下面是一个例子,浏览器发现此次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。
GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
复制代码
上面的头信息中,Origin字段用来讲明,本次请求来自哪一个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否赞成此次请求。
若是Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误没法经过状态码识别,由于HTTP回应的状态码有多是200。
若是Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
复制代码
上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。
该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。
该字段可选。它的值是一个布尔值,表示是否容许发送Cookie。默认状况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie能够包含在请求中,一块儿发给服务器。这个值也只能设为true,若是服务器不要浏览器发送Cookie,删除该字段便可。
该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。若是想拿到其余字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')能够返回FooBar字段的值。
上面说到,CORS请求默认不发送Cookie和HTTP认证信息。若是要把Cookie发到服务器,一方面要服务器赞成,指定Access-Control-Allow-Credentials字段。
Access-Control-Allow-Credentials: true
复制代码
另外一方面,开发者必须在AJAX请求中打开withCredentials属性。
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
复制代码
不然,即便服务器赞成发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。可是,若是省略withCredentials设置,有的浏览器仍是会一块儿发送Cookie。这时,能够显式关闭withCredentials。
xhr.withCredentials = false;
复制代码
须要注意的是,若是要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其余域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也没法读取服务器域名下的Cookie。
非简单请求是那种对服务器有特殊要求的请求,好比请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。 非简单请求的CORS请求,会在正式通讯以前,增长一次HTTP查询请求,称为"预检"请求(preflight)。 浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可使用哪些HTTP动词和头信息字段。只有获得确定答复,浏览器才会发出正式的XMLHttpRequest请求,不然就报错。 下面是一段浏览器的JavaScript脚本。
var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();
复制代码
上面代码中,HTTP请求的方法是PUT,而且发送一个自定义头信息X-Custom-Header。 浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,要求服务器确承认以这样请求。下面是这个"预检"请求的HTTP头信息。
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
复制代码
"预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪一个源。除了Origin字段,"预检"请求的头信息包括两个特殊字段。
该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。
该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header。
服务器收到"预检"请求之后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段之后,确认容许跨源请求,就能够作出回应。
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
复制代码
上面的HTTP回应中,关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com能够请求数据。该字段也能够设为星号,表示赞成任意跨源请求。
Access-Control-Allow-Origin: *
复制代码
若是浏览器否认了"预检"请求,会返回一个正常的HTTP回应,可是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不一样意预检请求,所以触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出以下的报错信息。
XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
复制代码
服务器回应的其余CORS相关字段以下。
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
复制代码
该字段必需,它的值是逗号分隔的一个字符串,代表服务器支持的全部跨域请求的方法。注意,返回的是全部支持的方法,而不单是浏览器请求的那个方法。这是为了不屡次"预检"请求。
若是浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,代表服务器支持的全部头信息字段,不限于浏览器在"预检"中请求的字段。
该字段与简单请求时的含义相同。
该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即容许缓存该条回应1728000秒(即20天),在此期间,不用发出另外一条预检请求。
一旦服务器经过了"预检"请求,之后每次浏览器正常的CORS请求,就都跟简单请求同样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。
下面是"预检"请求以后,浏览器的正常CORS请求。
PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
复制代码
上面头信息的Origin字段是浏览器自动添加的。下面是服务器正常的回应。
Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8
复制代码
上面头信息中,Access-Control-Allow-Origin字段是每次回应都一定包含的。
CORS与JSONP的使用目的相同,可是比JSONP更强大。 JSONP只支持GET请求,CORS支持全部类型的HTTP请求。JSONP的优点在于支持老式浏览器,以及能够向不支持CORS的网站请求数据。
WebSocket 是 HTML5 开始提供的一种在单个 TCP 链接上进行全双工通信的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,容许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只须要完成一次握手,二者之间就直接能够建立持久性的链接,并进行双向数据传输。
原生WebSocket API使用起来不太方便,咱们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
前端代码
<div>
<input type="text" id="inputText">
</div>
<script src="example.com/socket.io.js"></script>
<script> var socket = io('http://www.peanutyu.site'); // 链接成功处理 socket.on('connect', function() { // 监听服务端消息 socket.on('message', function(msg) { console.log('data from server: ---> ' + msg); }); // 监听服务端关闭 socket.on('disconnect', function() { console.log('Server socket has closed.'); }); }); document.getElementById('inputText').onblur = function() { socket.send(this.value); }; </script>
复制代码
Node Server
var http = require('http');
var socket = require('socket.io');
// 启http服务
var server = http.createServer(function(req, res) {
res.writeHead(200, {
'Content-type': 'text/html'
});
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
// 监听socket链接
socket.listen(server).on('connection', function(client) {
// 接收信息
client.on('message', function(msg) {
client.send('hello:' + msg);
console.log('data from client: ---> ' + msg);
});
// 断开处理
client.on('disconnect', function() {
console.log('Client socket has closed.');
});
});
复制代码
HTML5 window.postMessage是一个安全的、基于事件的消息API。
在须要发送消息的源窗口调用postMessage方法就能够向外发送消息。其中源窗口能够是如下的几种状况。
发送消息
win.postMessage(msg, targetOrigin);
复制代码
postMessage接受两个参数
接收消息
window.addEventListener('message', function receiveMessage(event) {
if (event.origin === 'http://www.peanut.site') {
console.log(event.data); // 传递的数据
}
}, false);
复制代码
event的属性有
Nginx配置
server{
# 监听9999端口
listen 9999;
# 域名是localhost
server_name localhost;
#凡是localhost:9999/api这个样子的,都转发到真正的服务端地址http://localhost:9871
location ^~ /api {
proxy_pass http://localhost:9871;
}
}
复制代码
请求的时候直接用回前端这边的域名http://localhost:9999,这就不会跨域,而后Nginx监听到凡是localhost:9099/api这个样子的,都转发到真正的服务端地址http://localhost:9871
axios.get('http://localhost:9999/api/iframePost', params).then(result => console.log(result)).catch(() => {});
复制代码
Nginx转发的方式彷佛很方便!但这种使用也是看场景的,若是后端接口是一个公共的API,好比一些公共服务获取天气什么的,前端调用的时候总不能让运维去配置一下Nginx,若是兼容性没问题(IE 10或者以上),CROS才是更通用的作法吧。
目前中后台比较经常使用的接口处理方式。Spa页面经过服务器根路由或者/index渲染由前端来控制路由跳转。剩下/api路径下开发咱们的接口请求。
后台配置
// 页面路由
router.get('/index', async function(ctx, next) {
// 打包JS时间戳
let timeT = moment().valueOf();
// 配置基本版本号
let buildPath = config.assetsServerName;
try {
let env = process.env.NODE_ENV;
// 从Redis内获取的JS版本号
let configInfo = await RedisService.getServerConfigInfoByEnv(env);
if(configInfo) {
let info = JSON.parse(configInfo);
if(info && info['build']) {
buildPath = info['build'].url;
}
}
// 渲染SPA页面
await ctx.render('index', {assetsPath: buildPath, tag: timeT});
} catch(e) {
// 报错渲染配置版本号SPA页面
await ctx.render('index', {assetsPath: buildPath, tag: timeT});
}
})
// 接口路由
router.get('/api/list', xxController.methods); // 获取列表方法、 具体逻辑处理经过controller完成
复制代码
前端代码
_reqData() {
axios.get('/api/list', {}).then(result => {console.log(result)}).catch(() => {});
}
componentWillMount() {
this._reqData();
}
复制代码