跨域

什么是跨域

跨域是浏览器为了安全而报的错误,如果不同源去请求资源,那么就会报跨域的错误。

同源概念:协议,域名,端口号一致

报错类似如下

Access to XMLHttpRequest at 'http://localhost:4000/getAjax' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

跨域是后台接口已经通了,但是浏览器拦截掉了,浏览器处于安全角度考虑,拦截掉了数据。

前端解决: JSONP

只支持 get 方式,不支持 post 方式

实现原理

html 中有些标签是没有同源限制的,比方说 scriptiframeimg 等,其中用到的就是 script

1
2
3
4
<script src="..."></script>
<img>
<link>
<iframe>

那么,我通过创建 script 标签,去访问跨域的资源,然后拿到资源就可以了~

步骤

前端:创建标签,拼接传递参数

前端:创建一个 script 标签,写上链接,可以看到参数跟 get 请求差不多,都是 queryString

1
2
3
4
5
6
let button = document.getElementById("btn");
button.addEventListener("click", () => {
let myScript = document.createElement("script"); // 创建游离的 script 标签
myScript.src = "http://localhost:4000/getAjax?name=1"; // 添加 src 属性
document.querySelector("head").appendChild(myScript); // 插入游离的标签到 head 标签中
});

后端:接收值,返回值

我们知道,前端引入了标签 script ,就会向后端发送个请求,去寻找资源。

后端通过 ctx.query 来获取前端传来的值

1
2
3
4
5
router.get("/getAjax", (ctx) => {
console.log("请求到了");
console.log(ctx.query);
ctx.body = "var a = 1";
});

然而这里会有个拐弯儿:后端返回的是 前端要执行的代码!!!

这里还会有一个问题,前端如何接收后端传来的东西呢,我如果直接打印 a,会 undefined

1
2
3
4
5
6
7
let button = document.getElementById("btn");
button.addEventListener("click", () => {
let myScript = document.createElement("script");
myScript.src = "http://localhost:4000/getAjax?name=1";
document.querySelector("head").appendChild(myScript);
console.log(a); // undefined
});

这是因为异步了,可以等待标签加载完后再接收,如下所示:

1
2
3
4
5
6
7
8
9
10
let button = document.getElementById("btn");
button.addEventListener("click", () => {
let myScript = document.createElement("script");
myScript.src = "http://localhost:4000/getAjax?name=1";
document.querySelector("head").appendChild(myScript);
myScript.onload = function () {
// 当标签加载完成后打印变量
console.log(a); // 1
};
});

但是这样处理不是很好

有一个方法:我可以跟后端商量好一个函数名称,后台返回函数执行命令,这样后台处理完之后发送一个执行函数的命令,我前端执行函数即可。

这里会有一点绕,慢点看

前端代码:

1
2
3
4
5
6
7
8
9
let button = document.getElementById("btn");
button.addEventListener("click", () => {
let myScript = document.createElement("script");
myScript.src = "http://localhost:4000/getAjax?name=1&cb=cbfunc"; // 这里我会传递一个函数名,等待后端来发送执行命令
document.querySelector("head").appendChild(myScript);
});
const cbfunc = (res) => {
console.log(res); // 这个函数名已经定义好了,就等着后端来调用了,在这里能够获得到后端传来的值
};

后端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
router.get("/getAjax", (ctx) => {
console.log("请求到了");
console.log(ctx.query);
let cb = ctx.query.cb; // 后端获取到前端传来的函数名
let data = {
// 后端要返给前端的参数
a: 1,
b: 2,
c: 3,
};
ctx.body = `${cb}(${JSON.stringify(data)})`; // 注意序列化
// 上面已经说了,这里返回前端能够执行的代码,所以这里就是一个前端函数执行的语句,类似这样:funcName()
// 也就是后端命令前端去执行它了
});

这样就较好的解决异步问题了,也是 jsonp 的雏形

封装 Ajax 代码

这个是上一章的内容了,这里就是封装了一下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
function ajax(options) {
let opts = Object.assign(
{
method: "get",
url: "",
headers: {
"content-type": "application/x-www-form-urlencoded",
},
jsonp: "cb",
data: "",
success: function () {},
},
options
);

let xhr = new XMLHttpRequest();
if (options.method == "get") {
let data = o2u(opts.data);
options.url = options.url + "?" + data;
}
xhr.open(options.method, options.url, true);
for (let key in opts.headers) {
xhr.setRequestHeader(key, opts.headers[key]);
}
let sendData;
switch (opts.headers["content-type"]) {
case "application/x-www-form-urlencoded":
sendData = o2u(opts.data);
break;
case "application/json":
sendData = JSON.stringify(opts.data);
break;
}
xhr.onload = function () {
let resData;
if (xhr.getResponseHeader("content-type").includes("xml")) {
resData = xhr.responseXML;
} else {
resData = JSON.parse(xhr.responseText);
}
options.success(resData);
};
if (options.method == "get") {
xhr.send();
} else {
xhr.send(sendData);
}
}

function o2u(obj) {
let keys = Object.keys(obj);
let values = Object.values(obj);
return keys
.map((v, k) => {
return `${v}=${values[k]}`;
})
.join("&");
}

在封装的 Ajax 中添加 JSONP

我们在使用自己封装的 ajax 的时候,调用的形式大概是这个样子的

1
2
3
4
5
6
7
8
9
10
11
12
ajax({
url: "http://localhost:4000/getAjax",
data: {
name: "你好",
age: 10,
},
dataType: "jsonp", // 这里说明是跨域访问
success(res) {
// 这是成功回调函数,稍后要处理它,是个小难点
console.log(res);
},
});

需求

我们希望在原有的 ajax 上添加跨域的功能

思路

  • 首先判断一下是不是跨域请求
  • 如果是的话,就去创建个 script 标签,
  • 设置它的 src 属性:传递的参数,回调函数的名称等
  • 把这个 script 标签 appendChildhead 中去!

做着做着你就会遇到这个问题:如何处理这个成功回调参数?

因为你观察咱们的调用方式,是通过 success() 处理返回值的,它你需要重新设置个名字的,不然直接拼接的话就成这个样子了

1
http://localhost:4000/getAjax?name=你好&age=10&cb=success(){}

这样肯定不行的啊。

解决的思路就是随机一个函数名,然后在 window 对象上定义一下函数,然后把 success() 语句 赋值给这个函数

1
2
3
4
5
6
7
8
9
function jsonpFunc(url, data, cbName, cbFunc) {
let randomFunc = "myRandomFunciotn" + Math.random().toString().substr(2); // 这样做防止函数重名
window[randomFunc] = cbFunc; // window 注册此函数
let path = `${url}?${o2u(data)}&${cbName}=${randomFunc}`;
// console.log(path);
let myScript = document.createElement("script");
myScript.src = path;
document.querySelector("head").appendChild(myScript);
}

解释的代码如下:
在这里插入图片描述
我们来进行证明:

  • 首先我来创建个跨域的 ajax 请求,成功回调函数体里写点东西哈
1
2
3
4
5
6
7
8
9
10
11
12
13
document.querySelector("button").addEventListener("click", function () {
ajax({
url: "http://localhost:4000/getAjax",
data: {
name: "hello",
age: 10,
},
dataType: "jsonp",
success(res) {
console.log("函数体:我是 success 成功回调函数的内容");
},
});
});
  • 接着点击按钮,发送请求
    在这里插入图片描述

  • 如何去验证呢? 去 window 对象下找函数去!!!我们打印 window 对象请添加图片描述

  • ok!验证成功

完整代码如下:这里也有 gitee 代码仓库 https://gitee.com/lovely_ruby/DailyPractice/tree/main/front/07/JSONP,可以克隆到本地

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
<script>
document.querySelector("button").addEventListener("click", function () {
ajax({
url: "http://localhost:4000/getAjax",
data: {
name: "你好",
age: 10,
},
dataType: "jsonp",
success(res) {
console.log(res);
},
});
});

function ajax(options) {
let opts = Object.assign(
{
method: "get",
url: "",
headers: {
"content-type": "application/x-www-form-urlencoded",
},
jsonp: "cb",
data: "",
success: function () {},
},
options
);

if (opts.dataType === "jsonp") {
// 做跨域的处理
jsonpFunc(opts.url, opts.data, opts.jsonp, opts.success);
return; // 这里记得return截断
}

function jsonpFunc(url, data, cbName, cbFunc) {
let randomFunc =
"myRandomFunciotn" + Math.random().toString().substr(2); // substr 截取
window[randomFunc] = cbFunc; // window 注册此函数
let path = `${url}?${o2u(data)}&${cbName}=${randomFunc}`;
// console.log(path);
let myScript = document.createElement("script");
myScript.src = path;
document.querySelector("head").appendChild(myScript);
}

let xhr = new XMLHttpRequest();
if (options.method == "get") {
let data = o2u(opts.data);
options.url = options.url + "?" + data;
}
xhr.open(options.method, options.url, true);
for (let key in opts.headers) {
xhr.setRequestHeader(key, opts.headers[key]);
}
let sendData;
switch (opts.headers["content-type"]) {
case "application/x-www-form-urlencoded":
sendData = o2u(opts.data);
break;
case "application/json":
sendData = JSON.stringify(opts.data);
break;
}
xhr.onload = function () {
let resData;
if (xhr.getResponseHeader("content-type").includes("xml")) {
resData = xhr.responseXML;
} else {
resData = JSON.parse(xhr.responseText);
}
options.success(resData);
};
if (options.method == "get") {
xhr.send();
} else {
xhr.send(sendData);
}
}

// 把对象传换成 queryString,就是 get 请求后面的 ? &
function o2u(obj) {
let keys = Object.keys(obj);
let values = Object.values(obj);
return keys
.map((v, k) => {
return `${v}=${values[k]}`;
})
.join("&");
}
</script>

练习:JSONP 请求 百度模糊搜索 接口

写在另一篇文章里了,点击这里跳转

瀑布流加载数据的的实现思路

检测滚动条是否到达底部,如果到达底部了就去加载文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
document.onscroll = function () {
let windowHeight = document.documentElement.clientHeight; // 用户窗口的大小
let contentHeight = document.documentElement.offsetHeight; // 页面总共的大小

let scrollHeight = contentHeight - windowHeight; // 计算出滚动条的高度
let scrollTop = document.documentElement.scrollTop; // 获取当前滚动条的高度

console.log("windowHeight:>>", windowHeight);
console.log("contentHeight:>>", contentHeight);
console.log("scrollTop:>>", scrollTop);

if (scrollTop > scrollHeight - 10) {
// 加载下一页数据的逻辑
}
};

后端解决:CORS 跨域资源共享

既然浏览器是为了安全考虑的,那么后端就可以在返回头中告诉浏览器安全即可。

解决的一个思路是:后端在返还头中给个标识,告诉浏览器不要去拦截

设置响应头

1
2
3
4
ctx.set("Access-Control-Allow-Origin", "*");
// 允许所有源,任何人请求这个接口都可以,会在返回头中写这么一段,告诉浏览器安全了,但是不建议全都通过,不建议这样写

ctx.set("Access-Control-Allow-Origin", "http://localhost:3000"); // 只是允许本地 3000 访问,建议这样写,指定域名

写法类似如下图所示:
在这里插入图片描述
插叙:但是你这样设置,前端浏览器可能也报错,就像我下面列举的奇怪的错误中图片描述的,CORS 并不好使,这可能是因为预检请求的原因,解决的办法就是下面描述的,加上 options 的请求

如果设置了规则为通配符*的话,你会发现 cookie 没了
一是因为用了通配符,用了通配符的话就不让携带 cookie
二是要设置允许携带凭证,withCredentials ,(cookie 其实也算是一种凭证)。

在这里插入图片描述

设置请求头的样子如下图所示:
在这里插入图片描述

koa2-cors 依赖

安装这个依赖并配置

1
2
3
4
5
6
7
const cors = require("koa2-cors");
app.use(
cors({
origin: "*",
allowMethods: ["GET"],
})
);

一个例子的截图如下
在这里插入图片描述


预检请求

介绍

复杂请求的时候,请求真正接口前先去探探路,看看服务器让不让访问,类似敢死队?

浏览器的同源策略,就是出于安全考虑,浏览器会限制从脚本发起的跨域 HTTP 请求(比如异步请求GET, POST, PUT, DELETE, OPTIONS等等),所以浏览器会向所请求的服务器发起两次请求

  • 第一次是浏览器使用OPTIONS方法发起一个预检请求
  • 第二次才是真正的异步请求

第一次的预检请求获知服务器是否允许该跨域请求:如果允许,才发起第二次真实的请求;如果不允许,则拦截第二次请求。
Access-Control-Max-Age用来指定本次预检请求的有效期,单位为秒,,在此期间不用发出另一条预检请求

预检请求发生的条件

如果你是解决跨域,并且是通过 CORS 解决的,就会有这种预检请求

预检请求发送条件:简单的请求没有预检请求,比方说

  • 请求方式是 getpostheadcontent-type 只有如下几条时
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded

会被认为是简单请求

除了简单请求,其他的情况都算是复杂请求,需要发送预检请求

处理预检请求

随便给他们返还点东西即可,但是也要设置跨域,还有请求头

1
2
3
4
5
6
7
8
9
router.options("/*", (ctx) => {
console.log("走了options");
ctx.set("Access-Control-Allow-Origin", "http://localhost:3000"); // 预检请求也需要允许跨域
ctx.set(
"Access-Control-Allow-Headers",
"Content-Type, Content-Length, Authorization, Accept, X-Requested-With , mytest"
);
ctx.body = {};
});

预检请求的有效期

我们会发现每次请求都需要走两次接口,性能不太好,所以成功一次的话可以把这个缓存记录下来,下次就只请求一次了

1
2
// 在预检请求中设置返回头的信息
ctx.set("Access-Control-Max-Age", 10); // 单位是秒

在这里插入图片描述

测试如下:
请添加图片描述

Chrome 没有 Options 请求的问题(已解决,因为自己傻掉了,忘记关掉过滤)

以下是 Chrome 老版本的截图
在这里插入图片描述
最新的 Chrome 浏览器没有这个报错(猜测),而是如下这样子(目前:2021 年 7 月)
在这里插入图片描述
所以换家浏览器, 用火狐来测试。

在这里插入图片描述
不对,是我傻掉了,我把这个过滤掉了,放开过滤就好了!!!

在这里插入图片描述


后端解决:后端代理

思路

既然跨域是浏览器的安全策略,那么就让后端去访问跨域的链接,后端和后端之间的访问就没有跨域这一说了,绕过了浏览器!

后端服务器去请求跨域的资源后,再返还给前端

主要的是知道数据流是怎么走的,知道思路!!!

在这里插入图片描述


奇怪的错误

问题:后台设置 CORS 不好使 ?? 浏览器还报奇怪的错误

中文路径的问题,把路径里的中文改成英文的就好了,淦!!!
在这里插入图片描述
在这里插入图片描述
我试了试,又报哪个错误了,然后把这句话加上之后,就好了,但是你如果注释掉,他虽然是好使的,但是等一会儿就不好使了,报跨域的错误了

1
2
3
4
5
6
7
8
9
router.options("/*", (ctx) => {
ctx.set("Access-Control-Allow-Origin", "http://localhost:3000");
ctx.set(
"Access-Control-Allow-Headers",
"Content-Type, Content-Length, Authorization, Accept, X-Requested-With , mytest"
);
ctx.set("Access-Control-Max-Age", 600);
ctx.body = "";
});

时效性是因为我设置了 Access-Control-Max-Age",600 这个东西