跨域 什么是跨域 跨域是浏览器为了安全而报的错误,如果不同源去请求资源,那么就会报跨域的错误。
同源概念:协议,域名,端口号一致
报错类似如下
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
了
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" ); myScript.src = "http://localhost:4000/getAjax?name=1" ; document .querySelector ("head" ).appendChild (myScript); });
后端:接收值,返回值 我们知道,前端引入了标签 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); });
这是因为异步了,可以等待标签加载完后再接收,如下所示:
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 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)} )` ; });
这样就较好的解决异步问题了,也是 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
标签 appendChild
到 head
中去!
做着做着你就会遇到这个问题:如何处理这个成功回调参数?
因为你观察咱们的调用方式,是通过 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; let path = `${url} ?${o2u(data)} &${cbName} =${randomFunc} ` ; 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 成功回调函数的内容" ); }, }); });
完整代码如下:这里也有 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 ; } function jsonpFunc (url, data, cbName, cbFunc ) { let randomFunc = "myRandomFunciotn" + Math .random ().toString ().substr (2 ); window [randomFunc] = cbFunc; let path = `${url} ?${o2u(data)} &${cbName} =${randomFunc} ` ; 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); } } 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" );
写法类似如下图所示: 插叙:但是你这样设置,前端浏览器可能也报错,就像我下面列举的奇怪的错误中图片描述的,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
解决的 ,就会有这种预检请求
预检请求发送条件:简单的请求没有预检请求,比方说
请求方式是 get
、post
、head
, content-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
这个东西