JSONP方法解决跨域问题
什么是同源策略
源:协议、域名、端口号。
在数据响应回来时浏览器会检测请求数据的源与发送请求的HTML页面的源是否一致。
维基百科的解释如下图,详细讲解可参阅维基百科-同源策略
需要注意的一点是,直接在浏览器中打开访问链接,相当于就是直接访问,是能看到数据。但是,当从一个页面发送请求访问时,会判断是不是跨域,如果是跨域,请求其实也是发送出去了的,服务器也响应了,但是响应回来后被浏览器拦截了,因为浏览器的安全策略中有个同源策略。
JSONP(JSON+Padding)方法及其原理
JSONP方法:利用script标签不受同源策略限制的特性,在HTML页面中动态生成script标签,利用script标签的src属性访问服务器端,服务器端会返回一个函数的调用(fuName(返回的数据)),并将响应数据放在回调函数的参数里,HTML页面通过回调函数的参数获取响应数据。
以下提供两个我看过的讲解jsonp的视频:
YouTube-跨域讲解了jsonp方法在HTML页面以及服务器端的数据交互,
YouTube-jsonp讲解了jsonp方法访问某外部接口时的函数调用。
注:img、link和script标签不受同源策略限制,允许跨域加载。
我的讲述若有不明确的地方,也可参考博客,下图就是该博客的部分内容截图。
代码实现
jsonp方法代码实现
以城市接口wis.qq.com/city/like 为例,该接口所需参数见下图,该图来源博客
第一步,要直接访问一次接口来查看回调函数名是什么;
这个接口是在腾讯天气的地址搜索栏里调用的,所以我们在搜索框中调用一次该接口,查看网络中的请求头信息,发现回调函数名称是callback,如下图:
我们就可以确定访问的地址应是:wis.qq.com/city/like?s…
第二步,此次调用以香港为例。
function getData(data) {
let script = document.getElementById("jsonp")
script.parentNode.removeChild(script) //移除该新创的节点
console.log(data)
}
// 创建script节点
let script = document.createElement("script")
script.id = "jsonp"
script.src = "https://wis.qq.com/city/like?source=pc&city=香港&callback=getData"
// 将script节点添加到DOM树中,否则该节点不会生效
document.body.appendChild(script)
如下图,便是此次调用接口的请求头和响应数据。
jsonp函数封装
按照上面的办法,每次调用jsonp都要重复上述代码,很不方便,所以我们可以进一步地封装该方法。
按照上述思路,我们一共需要四个参数,访问url、访问参数、接口定义的回调函数参数名、回调函数参数体。
(注:不同接口的回调函数参数名不同,我见到的大部分接口回调函数参数名都是callback,也有例外,如百度网搜索框的回调函数参数名是cb,如下图
)
封装后的代码如下:
/*
url: 访问链接
params: 参数名/参数值组成的json格式的数据
cbName: 访问接口时规定的回调函数参数名
cbFunction: 回调函数具体函数体
*/
function jsonp(url, params, cbName, cbFunction) {
let strParams = ""
// 遍历json格式的参数,组合为符合格式的字符串
for (let key in params) {
// 经过最后一次遍历时最后一个参数末尾后会多一个&,后接回调函数参数
strParams += key + "=" + params[key] + "&"
}
// Math.random(): 返回 0 ~ 1 之间的随机数,降低同时调用多个jsonp 请求时回调函数名称的重复率
let fnName = cbName + Math.random().toString().replace('.', '')
// 创建script节点
let script = document.createElement("script")
script.src = url + "?" + strParams + cbName + "=" + fnName
// 将script节点添加到DOM树中,否则该节点不会生效
document.body.appendChild(script)
// 将回调函数加入全局作用域
window[fnName] = cbFunction
// 当这个 script 标签加载完毕时将其移出
script.onload = () => script.parentNode.removeChild(script)
}
调用方式如下:
jsonp("https://wis.qq.com/city/like", {
"source": "pc",
"city": "香港",
}, "callback",
(data) => console.log(data))
需注意的问题
- 回调函数一定要放在全局作用域,因为服务端接收到回调函数后会返回页面中的script中去找,如果不写在全局作用域中根本找不到;
- 不同接口的回调函数参数名不同,封装函数的话这个地方需要单独作为参数。