在我们想要获取其他网站数据的时候,浏览器的同源策略(Same origin policy)会禁止此项行为,但有时不得不实现这一操作,就会涉及跨域的问题。解决跨域也就成了前端必须掌握的技能,其中JSONP就是一种解决该问题的好方法。
一、JSONP跨域原理
由于
script
标签不受浏览器同源策略的影响,允许跨域引用资源。因此可以通过动态创建script标签,然后利用src属性进行跨域,这就是JSONP跨域的基本原理。
JSONP通过script标签的src属性发送请求,src请求地址与普通ajax请求地址的不同之处在于其后面会加一段类似“callback=a”的字符串,服务端接收到这段加了特殊后缀的url后就会用a方法包裹(浏览器所请求的)目标数据(返回给前端)。此时,前端并没有声明a方法,所以在script发送请求之前,应该在window上注册a方法,以在接收到后端数据时用此方法解析数据。
// 1. 定义一个回调函数a用来接收返回的数据
function a(data) {
// 处理数据的代码
console.log(data)
}
// 2. 动态创建一个script标签,并且告诉后端回调函数名叫a
let body = document.getElementsByTagName('body')[0]
let script = document.createElement('script')
script.src = 'https://wy310.cn/get_auth_name?callback=a'
body.appendChild(script)
// 3. 通过script.src请求'https://wy310.cn/get_auth_name?callback=a'
// 4. 后端识别该URL格式并处理该请求,然后返回a({"name": "大海"})给浏览器
// 5. 浏览器在接收到a({"name": "大海"})之后立即执行,也就是执行a方法获得后端返回的数据,完成一次跨域请求
二、封装promise型JSONP
实际开发中我们选择github上的第三方JSOP库(具体实现可以查阅index.js),先来看看API:
jsonp(url, opts, fn)
url
(String) url to fetchopts
(Object), optionalparam
(String) name of the query string parameter to specify the callback (defaults to callback)timeout
(Number) how long after a timeout error is emitted. 0 to disable (defaults to 60000)prefix
(String) prefix for the global callback functions that handle jsonp responses (defaults to __jp)name
(String) name of the global callback functions that handle jsonp responses (defaults to prefix + incremented counter)
fn
callback
调用jsonp(url, opts, fn)
,在回调函数fn中就可以拿到目标数据data。但现在采用ES6开发很少使用回调函数的形式,而是采用promise,下面看看怎么将其封装成promise风格:
1.安装jsonp
在vue项目中引入jsonp,项目根目录下执行命令:
cnpm i jsonp -S
2.promise封装
像jsonp这种经常使用的工具,应该单独抽象出来,便于以后在项目开发过程中调用。所以在src/common/js中新建jsonp.js
:
// 引入上一步从github安装的jsonp,
// 即“原始jsonp”(与下面自己封装的“jsonp”区分开)
import originJSONP from 'jsonp'
// param1:我们希望url仅仅是一个纯净的地址
// param2:后面的各种参数通过data传入,然后拼接在一起
// param3:option对应原始jsonp的第二个参数:opts
export default function jsonp (url, data, option) {
// 拼接url时判断是否已有问号
url += (url.indexOf('?') > -1) ? '&' : '?' + param(data)
return new Promise((resolve, reject) => {
originJSONP(url, option, (err, data) => {
// 如果没错误,就resolve(data)
if (!err) {
resolve(data)
} else {
reject(err)
}
})
})
}
// 将data(参数对象)封装到url里面
function param (data) {
let url = ''
for (let i in data) {
let value = data[i] !== undefined ? data[i] : ''
// url拼接参数,参数之间用&隔开
url += `&${i}=${encodeURIComponent(value)}`
}
// 如果url有data,将第一个"&"删掉
return url ? url.substring(1) : ''
}
3.测试
测试之前,提一个“配置别名”的知识点,build/webpack.base.conf.js
文件内的alias
意为别名,通过配置alias,可以在今后使用import x from ‘../../x’时省去计算层级的烦恼:
module.exports = {
...
resolve: {
alias: {
'@': resolve('src'),
'api': resolve('src/api'),
'common': resolve('src/common'),
'components': resolve('src/components')
}
},
...
}
// 配置别名common前
import jsonp from '../common/js/jsonp'
// 配置别名common后
import jsonp from 'common/js/jsonp'
尝试使用上面封装的jsonp获取腾讯网页版QQ音乐的推荐歌单数据,src/api/recommend.js
:
import jsonp from 'common/js/jsonp'
export function getRecommend () {
const url = 'https://c.y.qq.com/musichall/fcgi-bin/fcg_yqqhomepagerecommend.fcg'
const data = {
g_tk: 1928093487,
inCharset: 'utf-8',
outCharset: 'utf-8',
notice: 0,
format: 'jsonp',
platform: 'h5',
uin: 0,
needNewCode: 1
}
// 此'jsonpCallback'就是上文说的'a'
const options = { param: 'jsonpCallback' }
return jsonp(url, data, options)
}
在的created钩子里调用getRecommend(),将其中的slider数据渲染到轮播图组件中去,src/components/recommend/recommend.vue
:
import { getRecommend } from 'api/recommend'
export default {
data () {
return {
recommends: []
}
},
created () {
this._getRecommend()
},
methods: {
_getRecommend () {
// 调用promise风格的getRecommend()
getRecommend().then(res => {
if (res.code === 0) {
this.recommends = res.data.slider
}
})
}
}
}
成功获取推荐歌单数据: