文章目录
跨域
什么是跨域
跨域是浏览器为了安全而报的错误,如果不同源去请求资源,那么就会报跨域的错误。
同源概念:协议,域名,端口号一致
报错类似如下
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
中有些标签是没有同源限制的,比方说 script
、iframe
、img
等,其中用到的就是 script
了
<script src="..."></script>
<img>
<link>
<iframe>
那么,我通过创建 script 标签,去访问跨域的资源,然后拿到资源就可以了~
步骤
前端:创建标签,拼接传递参数
前端:创建一个 script
标签,写上链接,可以看到参数跟 get
请求差不多,都是 queryString
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
来获取前端传来的值
router.get('/getAjax', ctx => {
console.log('请求到了');
console.log(ctx.query);
ctx.body = 'var a = 1'
})
然而这里会有个拐弯儿:后端返回的是 前端要执行的代码!!!
这里还会有一个问题,前端如何接收后端传来的东西呢,我如果直接打印 a,会 undefined
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
});
这是因为异步了,可以等待标签加载完后再接收,如下所示:
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
}
});
但是这样处理不是很好
有一个方法:我可以跟后端商量好一个函数名称,后台返回函数执行命令,这样后台处理完之后发送一个执行函数的命令,我前端执行函数即可。
这里会有一点绕,慢点看
前端代码:
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); // 这个函数名已经定义好了,就等着后端来调用了,在这里能够获得到后端传来的值
};
后端:
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 代码
这个是上一章的内容了,这里就是封装了一下,
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 的时候,调用的形式大概是这个样子的
ajax({
url: "http://localhost:4000/getAjax",
data: {
name: "你好",
age: 10,
},
dataType: "jsonp", // 这里说明是跨域访问
success(res) { // 这是成功回调函数,稍后要处理它,是个小难点
console.log(res);
},
});
需求
我们希望在原有的 ajax
上添加跨域的功能
思路
- 首先判断一下是不是跨域请求
- 如果是的话,就去创建个
script
标签, - 设置它的
src
属性:传递的参数,回调函数的名称等 - 把这个
script
标签appendChild
到head
中去!
做着做着你就会遇到这个问题:如何处理这个成功回调参数?
因为你观察咱们的调用方式,是通过 success()
处理返回值的,它你需要重新设置个名字的,不然直接拼接的话就成这个样子了
http://localhost:4000/getAjax?name=你好&age=10&cb=success(){}
这样肯定不行的啊。
解决的思路就是随机一个函数名,然后在 window 对象上定义一下函数,然后把 success() 语句
赋值给这个函数
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 请求,成功回调函数体里写点东西哈
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,可以克隆到本地
<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 请求 百度模糊搜索 接口
瀑布流加载数据的的实现思路
检测滚动条是否到达底部,如果到达底部了就去加载文件
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 跨域资源共享
既然浏览器是为了安全考虑的,那么后端就可以在返回头中告诉浏览器安全即可。
解决的一个思路是:后端在返还头中给个标识,告诉浏览器不要去拦截
设置响应头
ctx.set("Access-Control-Allow-Origin","*");
// 允许所有源,任何人请求这个接口都可以,会在返回头中写这么一段,告诉浏览器安全了,但是不建议全都通过,不建议这样写
ctx.set("Access-Control-Allow-Origin","http://localhost:3000"); // 只是允许本地 3000 访问,建议这样写,指定域名
写法类似如下图所示:
插叙:但是你这样设置,前端浏览器可能也报错,就像我下面列举的奇怪的错误中图片描述的,CORS
并不好使,这可能是因为预检请求的原因,解决的办法就是下面描述的,加上 options 的请求
如果设置了规则为通配符
*
的话,你会发现cookie
没了
一是因为用了通配符,用了通配符的话就不让携带cookie
了
二是要设置允许携带凭证,withCredentials
,(cookie
其实也算是一种凭证)。
设置请求头的样子如下图所示:
koa2-cors 依赖
安装这个依赖并配置
const cors = require("koa2-cors");
app.use(
cors({
origin: "*",
allowMethods: ["GET"],
})
);
一个例子的截图如下
预检请求
介绍
复杂请求的时候,请求真正接口前先去探探路,看看服务器让不让访问,类似敢死队?
浏览器的同源策略,就是出于安全考虑,浏览器会限制从脚本发起的跨域 HTTP
请求(比如异步请求GET, POST, PUT, DELETE, OPTIONS
等等),所以浏览器会向所请求的服务器发起两次请求
- 第一次是浏览器使用
OPTIONS
方法发起一个预检请求 - 第二次才是真正的异步请求
第一次的预检请求获知服务器是否允许该跨域请求:如果允许,才发起第二次真实的请求;如果不允许,则拦截第二次请求。
Access-Control-Max-Age
用来指定本次预检请求的有效期,单位为秒,,在此期间不用发出另一条预检请求
预检请求发生的条件
如果你是解决跨域,并且是通过 CORS
解决的,就会有这种预检请求
预检请求发送条件:简单的请求没有预检请求,比方说
- 请求方式是
get
、post
、head
,content-type
只有如下几条时text/plain
multipart/form-data
application/x-www-form-urlencoded
会被认为是简单请求
除了简单请求,其他的情况都算是复杂请求,需要发送预检请求
处理预检请求
随便给他们返还点东西即可,但是也要设置跨域,还有请求头
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 = {}
})
预检请求的有效期
我们会发现每次请求都需要走两次接口,性能不太好,所以成功一次的话可以把这个缓存记录下来,下次就只请求一次了
// 在预检请求中设置返回头的信息
ctx.set('Access-Control-Max-Age', 10) // 单位是秒
测试如下:
Chrome 没有 Options 请求的问题(已解决,因为自己傻掉了,忘记关掉过滤)
以下是 Chrome
老版本的截图
最新的 Chrome 浏览器没有这个报错(猜测),而是如下这样子(目前:2021年7月)
所以换家浏览器, 用火狐来测试。
不对,是我傻掉了,我把这个过滤掉了,放开过滤就好了!!!!
后端解决:后端代理
思路
既然跨域是浏览器的安全策略,那么就让后端去访问跨域的链接,后端和后端之间的访问就没有跨域这一说了,绕过了浏览器!
后端服务器去请求跨域的资源后,再返还给前端
主要的是知道数据流是怎么走的,知道思路!!!
奇怪的错误
问题:后台设置
CORS
不好使 ?? 浏览器还报奇怪的错误
中文路径的问题,把路径里的中文改成英文的就好了,淦!!!
我试了试,又报哪个错误了,然后把这句话加上之后,就好了,但是你如果注释掉,他虽然是好使的,但是等一会儿就不好使了,报跨域的错误了
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
这个东西