场景:跨域请求报错:
Failed to load http://localhost:3000/crossdomain/cors: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘null’ is therefore not allowed access.
只要说到跨域,就必须聊到 JSONP,JSONP 全称为:JSON with padding,可用于解决老版本浏览器的跨域数据访问问题。
由于 web 页面上调用 js 文件不受浏览器同源策略的影响,所以通过 script 标签可以进行跨域请求:
jsonp 之所以能够跨域的关键在于页面调用 JS 脚本是不受同源策略的影响,相当于向后端发起一条 http 请求,跟后端约定好函数名,后端拿到函数名,动态计算出返回结果并返回给前端执行 JS 脚本,相当于是一种 “动态 JS 脚本”
接下来我们通过一个实例来尝试:
后端逻辑:
// jsonp/server.js
const url = require("url");
require("http")
.createServer((req, res) => {
const data = {
x: 10
};
// 拿到回调函数名
const callback = url.parse(req.url, true).query.callback;
console.log(callback);
res.writeHead(200);
res.end(`${callback}(${JSON.stringify(data)})`);
})
.listen(3000, "127.0.0.1");
console.log("启动服务,监听 127.0.0.1:3000");
前端逻辑:
// jsonp/index.html
<script>
function jsonpCallback(data) {
alert('获得 X 数据:' + data.x);
}
</script>
<script src="http://127.0.0.1:3000?callback=jsonpCallback"></script>
然后在终端开启服务:
之所以能用脚本指令,是因为我在 package.json 里面设置好了脚本命令:
{
// 输入 yarn jsonp 等于 "node ./jsonp/server.js & http-server ./jsonp"
"scripts": {
"jsonp": "node ./jsonp/server.js & http-server ./jsonp",
"cors": "node ./cors/server.js & http-server ./cors",
"proxy": "node ./serverProxy/server.js",
"hash": "http-server ./hash/client/ -p 8080 & http-server ./hash/server/ -p 8081",
"name": "http-server ./name/client/ -p 8080 & http-server ./name/server/ -p 8081",
"postMessage": "http-server ./postMessage/client/ -p 8080 & http-server ./postMessage/server/ -p 8081",
"domain": "http-server ./domain/client/ -p 8080 & http-server ./domain/server/ -p 8081"
},
// ...
}
yarn jsonp
// 因为端口 3000 和 8080 分别属于不同域名下
// 在 localhost:3000 查看效果,即可收到后台返回的数据 10
至此,通过 JSONP 跨域获取数据已经成功了,但是通过这种方式也存在着一定的优缺点:
优点:
缺点:
动态请求就会有跨域的问题
跨域只存在于浏览器端,不存在于安卓/ios/Node.js/python/ java等其它环境
跨域就是请求发不出去了
跨域的请求是可以发送出去的,并且服务端可以接收到请求并返回结果,只是结果被浏览器给拦截了。
CORS 是一个 W3C 标准,全称是"跨域资源共享"(Cross-origin resource sharing)它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 ajax 只能同源使用的限制。
CORS 需要浏览器和服务器同时支持才可以生效,对于开发者来说,CORS 通信与同源的 ajax 通信没有差别,代码完全一样。浏览器一旦发现 ajax 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨域通信。
前端逻辑很简单,只要正常发起 ajax 请求即可:
// cors/index.html
<script>
const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://127.0.0.1:3000', true);
xhr.onreadystatechange = function() {
if(xhr.readyState === 4 && xhr.status === 200) {
alert(xhr.responseText);
}
}
xhr.send(null);
</script>
这似乎跟一次正常的异步 ajax 请求没有什么区别,关键是在服务端收到请求后的处理:
// cors/server.js
require("http")
.createServer((req, res) => {
res.writeHead(200, {
"Access-Control-Allow-Origin": "http://localhost:8080",
"Content-Type": "text/html;charset=utf-8"
});
res.end("这是你要的数据:1111");
})
.listen(3000, "127.0.0.1");
console.log("启动服务,监听 127.0.0.1:3000");
成功的关键在于 Access-Control-Allow-Origin 是否包含请求页面的域名,如果不包含的话,浏览器将认为这是一次失败的异步请求,将会调用 xhr.onerror 中的函数。
CORS 的优缺点:
CORS把请求分为两种,一种是简单请求,另一种是复杂请求(需要触发预检请求),这两者是相对的,怎样才算“不简单”?只要属于下面的其中一种就不是简单请求:
(1)使用了除GET/POST/HEAD之外的请求方式,如PUT/DELETE
(2)使用了除Content-Type/Accept等几个常用的http头
预检请求使用OPTIONS方式去检查当前请求是否安全,请求如下:
full | 204 | xhr |
---|---|---|
full | 200 | xhr |
服务端响应response headers如下:
Access-Control-Allow-Headers | DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,body | 允许的请求头 |
---|---|---|
Access-Control-Allow-Methods | GET, POST, OPTIONS, PUT, DELETE, PATCH | 允许的请求方式 |
Access-Control-Allow-Max-Age | 17280000 | 预检请求有效期:发送OPTIONS的时间间隔 |
如果在预检请求检测到当前请求不符合服务端设定的要求,则不会发出去了直接抛异常,这个时候就不用去发“复杂”的请求了。
为了支持CORS,nginx可以这么配:
location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Expose-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
}
服务器代理,顾名思义,当你需要有跨域的请求操作时发送请求给后端,让后端帮你代为请求,然后最后将获取的结果发送给你。
假设有这样的一个场景,你的页面需要获取 CNode:Node.js 专业中文社区 论坛上一些数据,如通过 https://cnodejs.org/api/v1/topics,当时因为不同域,所以你可以将请求后端,让其对该请求代为转发
代码如下:
// serverProxy/server.js
const url = require("url");
const http = require("http");
const https = require("https");
const server = http
.createServer((req, res) => {
const path = url.parse(req.url).path.slice(1);
if (path === "topics") {
https.get("https://cnodejs.org/api/v1/topics", resp => {
let data = "";
resp.on("data", chunk => {
data += chunk;
});
resp.on("end", () => {
res.writeHead(200, {
"Content-Type": "application/json; charset=utf-8"
});
res.end(data);
});
});
}
})
.listen(3000, "127.0.0.1");
console.log("启动服务,监听 127.0.0.1:3000");
通过代码你可以看出,当你访问 http://127.0.0.1:3000/topics 的时候,服务器收到请求,会代你发送请求 https://cnodejs.org/api/v1/topics 最后将获取到的数据发送给浏览器。
遗留思考问题: