准备
首先明白一件事:
跨域是浏览器的限制
也就是说不是你真的不能访问,而是浏览器出于他自己的种种担心拦截了你的访问。
至于浏览器在担心什么(什么是跨域,为什么会有跨域),参考:百度
其实cors的原理就是,告诉浏览器:"你别担心啦,这个请求我ok的,让他访问吧"。
而jsonp的原理是,使用浏览器不担心的方式去请求:script标签中的src属性(其他带有src属性的标签也可以,比如img)。
先来模拟出跨域的场景,在本地启动一个最简单的node服务,返回查询参数。
// 新建一个server.js文件,当然前提要安装node
const http = require('http');
const querystring = require('querystring');
const server = http.createServer((req, res) => {
const query = querystring.parse(req.url.split('?')[1]);
const queryStr = JSON.stringify(query);
res.writeHead(200, {
"Content-type": "text/plain; charset=utf-8"
});
res.end(queryStr);
});
server.listen(9999);
console.log('server run at 9999');
启动服务
node server.js

先在浏览器里直接访问一下 http://127.0.0.1:9999/?name=无用书生&age=25 看到返回没有问题

然后就在当前页面(你现在正在阅读文章的掘金页面)f12打开控制台,在console里创建一个ajax请求
var xhr = new XMLHttpRequest();
xhr.open('get', 'http://127.0.0.1:9999/?name=无用书生&age=25');
xhr.send();
执行以后就出现跨域报错了

CORS
首先来看一下cors的跨域原理,其实报错里就写的很明白,我们访问的资源没有设置对掘金这个访问源的头。
在cors的规则中,请求分为简单请求和非简单请求,我们上面发送的就是一个简单请求。
关于简单请求和非简单请求,参考:CORS跨域原理解析
对于简单请求
只要在响应头中(response header)指明允许哪些访问源访问就可以了。
在server.js中给响应头添加 Access-Control-Allow-Origin
const http = require('http');
const querystring = require('querystring');
const server = http.createServer((req, res) => {
const query = querystring.parse(req.url.split('?')[1]);
const queryStr = JSON.stringify(query);
res.writeHead(200, {
"Content-type": "text/plain; charset=utf-8",
"Access-Control-Allow-Origin": "*" // * 代表允许所有的源访问
});
res.end(queryStr);
});
server.listen(9999);
console.log('server run at 9999');
重启node服务,再试一次

报错没有了,打开network可以看到 Access-Control-Allow-Origin
已经生效,数据也成功获取到


对于非简单请求
将上面请求的方法从get改成put,再次请求 (put方法就属于非简单请求)
var xhr = new XMLHttpRequest();
xhr.open('put', 'http://127.0.0.1:9999/?name=无用书生&age=25');
xhr.send();
可以看到跨域报错又出现了

刚才设置的响应头依然存在,却不起作用了。
另外这里可以看到我们本来发送的是put请求,请求方法那里写的却是options。原因就是对于非简单请求浏览器会先发送一次预检,预检通过才会发送真正的请求,这个options就是预检请求,因为没有通过,所以也就没有发送真正的请求

其实报错中也写的很明白,我们访问的资源没有设置允许对PUT这个方法的访问
在server.js中给响应头添加 Access-Control-Allow-Methods
,设置允许put方法的请求
const http = require('http');
const querystring = require('querystring');
const server = http.createServer((req, res) => {
const query = querystring.parse(req.url.split('?')[1]);
const queryStr = JSON.stringify(query);
res.writeHead(200, {
"Content-type": "text/plain; charset=utf-8",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT",
});
res.end(queryStr);
});
server.listen(9999);
console.log('server run at 9999');
重启服务以后再次请求,可以看到跨域报错就消失了

还可以看到依然先进行了一次预检请求,这次预检请求通过了,继续发送了put请求


非简单请求还对请求头的信息有所限制,原理还是一样的,通过Access-Control-Allow-Headers
在返回头中设置允许的访问头就ok了,比如
var xhr = new XMLHttpRequest();
xhr.open('put', 'http://127.0.0.1:9999/?name=无用书生&age=25');
xhr.setRequestHeader("X-Corx-Test", "aabbcc");
xhr.send();
设置允许 X-Corx-Test这个请求头
const http = require('http');
const querystring = require('querystring');
const server = http.createServer((req, res) => {
const query = querystring.parse(req.url.split('?')[1]);
const queryStr = JSON.stringify(query);
res.writeHead(200, {
"Content-type": "text/plain; charset=utf-8",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT",
"Access-Control-Allow-Headers": "X-Corx-Test"
});
res.end(queryStr);
});
server.listen(9999);
console.log('server run at 9999');
JSONP
由于掘金做了csp处理,无法测试jsonp,用百度进行演示。
什么是csp,参考:阮一峰博客
去掉node服务中对cors的配置
const http = require('http');
const querystring = require('querystring');
const server = http.createServer((req, res) => {
const query = querystring.parse(req.url.split('?')[1]);
const queryStr = JSON.stringify(query);
res.writeHead(200, {
"Content-type": "text/plain; charset=utf-8"
});
res.end(queryStr);
});
server.listen(9999);
console.log('server run at 9999');
在百度首页f12打开控制台,这时候如果再使用ajax请求我们的服务又会报跨域的错误
上面说过jsonp的原理就是使用script标签的src不受浏览器跨域限制的原理
在console中创建一个jsonp请求
var script = document.createElement('script');
script.src = 'http://127.0.0.1:9999/?name=无用书生&age=25';
document.head.appendChild(script);
执行以后看到这时候就没有报错了


这时候请求虽然成功了,还没拿到返回的数据
获取数据的方法就是在前端定义一个接收数据的函数,然后后端返回的js中执行这个函数,并把要返回的数据作为参数传入
比如在前端定义一个叫做 getData
的函数
var script = document.createElement('script');
script.src = 'http://127.0.0.1:9999/?name=无用书生&age=25';
document.head.appendChild(script);
function getData(res) {
console.log(res);
}
在后端返回的内容中调用这个函数,把数据传进去
const http = require('http');
const querystring = require('querystring');
const server = http.createServer((req, res) => {
const query = querystring.parse(req.url.split('?')[1]);
const queryStr = JSON.stringify(query);
res.writeHead(200, {
"Content-type": "text/plain; charset=utf-8",
});
let jsonpStr = `getData(${queryStr})`;
res.end(jsonpStr);
});
server.listen(9999);
console.log('server run at 9999');
重启服务以后,执行前端代码,数据就可以取到了

这个函数名字要前后端约定一致,另外获取完数据以后最好移除一下script标签
var script = document.createElement('script');
script.src = 'http://127.0.0.1:9999/?name=无用书生&age=25';
document.head.appendChild(script);
function getData(res) {
console.log(res);
document.head.removeChild(script);
}