jsonp (JSON with Padding) 是一种解决浏览器跨域请求的一种方案,它需要前端代码和后台的共同努力才能实现。
本文着重介绍它的原理和实现。
# 同源策略 ↵
要理解 jsonp 的出现,先得清楚同源策略,浏览器对同源的定义是:
在 http 请求的 url 里,如果协议,端口(如果指定了一个)和域名对于两个页面是相同的,则两个页面具有相同的源
浏览器指定了许多策略来对待不同源的 http 请求,这些策略约束了 ajax 或者 img 标签的行为。
如果没有同源策略,基于 Session 存储会话的 Web 系统可能会很危险:
- 某个用户登录了银行 但是没退出
- 然后他去了别的网站,刚好那个网站含有破坏性 JavaScript 代码
- 虽然 JS 不能访问到其他源的 cookie ,但是可以发起 XHR 请求,这些请求里面会带上 Session
- 如果攻击者分析过银行的系统设计,他就有能力获得用户的交易记录,甚至发起转账请求等等
而有了同源策略,攻击者的 XHR 将会被浏览器 ban 掉,不会发起,保证了安全。
而有时候,同源策略太严格了,不适合开发,尤其是有多个域名和子域名的时候就更明显了。
# 跨域资源共享 ↵
这个是服务端的解决方案,借助
Access-Control-Allow-*
一系列字段让浏览器解除策略影响。00app.use('*', function(req, res, next){ 01 res.header("Access-Control-Allow-Origin", "*"); 02 res.header("Access-Control-Allow-Headers", "X-Requested-With"); 03 res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS"); 04 05 next(); 06})
# jsonp ↵
jsonp 的原理非常精巧,它利用了 JavaScript 可以直接解读 JSON 的特性,还有 Script 标签 src 属性可以跨域请求脚本的历史遗留问题,它的过程如下:
假设某接口URL为
http://xxx.com/api/ping?xx=abc&d=asd
且 GET 它会得到 JSON 字符串:
00{"hello": "world"}
在同源策略的情况下,我们无法利用 ajax 在其他源下请求这个 URL
因此,出现了如下标签的写法:
00<script src="http://xxx.com/api/ping?xx=abc&d=asd"></script>
很显然,这样会执行那个 JSON 字符串,也就是说,成功捕获到了这个 JSON 字符串对应的对象。
如果后台稍作修饰,让这个接口返回这样的
JSON
:00alert({"hello": "world"});
那么以下标签,将会 alert 一个对象出来:
00<script src="http://xxx.com/api/ping?xx=abc&d=asd"></script>
其中 alert 可以算是 ajax 中的回调函数,该过程可以称呼为 alert 捕获了 http 请求结果。
这便是 jsonp 的原理。
# 实现 ↵
在此实现一个简单的 jsonp 封装
00// 把对象转化成查询字符串 01var queryStringify = o => { 02 return Object.keys(o).map(key => { 03 // let val = encodeURIComponent(o[key]); 04 let val = o[key]; 05 return [ key, val ] 06 }).map(temp => { 07 let [key, val] = temp; 08 09 return `${key}=${val}`; 10 }).join('&') 11} 12 13// 计数器和函数名的命名空间 14let JSONP_COUNTER = 0; 15let CB_NAME = 'GW_JSONP'; 16 17var getFuncName = () => CB_NAME + JSONP_COUNTER ++; 18 19function jsonp(url, query){ 20 // 每次调用的名字都不一样 21 let funcName = getFuncName(); 22 // 用来告诉后台修饰返回的 json 需要的捕获函数名 23 query.callback = funcName; 24 25 // 计算出 script 需要的 url 26 let fullurl = url + '?' + queryStringify(query) 27 28 console.log('URL', fullurl); 29 30 return new Promise((res, rej) => { 31 // 先设置回调 (即 alert 这样的捕获函数) 32 window[funcName] = res; 33 34 // DOM 创建一个 src 为 fullurl 的 script 并插入到最后面 35 var body = document.getElementsByTagName('body')[0]; 36 var script = document.createElement('script'); 37 script.setAttribute('src', fullurl); 38 body.appendChild(script); 39 }); 40}
# 例子 ↵
利用上面封装的 jsonp 去做一次跨域请求:
目标 API http://m.kugou.com/app/i/getSongInfo.php , 该接口由酷狗提供,支持 jsonp 跨域。
它需要的参数有:
- cmd, 一般是 ‘playInfo’
- hash, 歌曲的 hash , 可以通过酷狗提供的其他接口提供,这里使用常量
a97e9c9ebdee85bc52147de6825f3da0&
进行获取 - format 表示返回的结果格式,这里填 ‘jsonp’
- callback 表示用于修饰json用的捕获函数名,这个字段由我们封装的函数内部维护,无须操作
00let target_url = `http://m.kugou.com/app/i/getSongInfo.php`; 01let query = { 02 cmd: 'playInfo', 03 hash: 'a97e9c9ebdee85bc52147de6825f3da0&', 04 format: 'jsonp' 05} 06 07jsonp(target_url, query).then(console.log);
结果如图:

jsonp 调用结果
当然也可以正常播放和查看专辑封面的

埃若漫鹅老师
还有就是这链接可能会失效 233
# 使用过程中的种种 ↵
其实原理我很早看过,但没有亲手写一个,感觉现在才理解到精华,此外,今晚在用的时候发现 jsonp 还是有些问题:
它只支持 GET, 这是 script 的原因, 而且也无法在 https 下请求 http 资源, 不过我找到了一个替代的方案去解决这个问题,即利用 https 代理请求来完成 http 资源的获取。
关于代理,我选择了 JsonBird, 当然不是打广告,可是它确实比较好使,开源免费,如果有所顾忌,可以挂在自己服务器上运行。