Front End

11 jul, 2011

JSON e JSONP – Parte 2

Publicidade

No primeiro artigo, falamos do JSON. Neste, abordaremos o JSONP.

JSONP

Vimos que podemos usar JSON
para transportar dados entre servidor e cliente, e que isso pode ser feito com relativa
segurança. Mas e para buscar dados em outros domínios? Sei que o Twitter tem
uma API poderosa para obter dados históricos de tweets, mas sou limitado pela
política de mesma origem. Ou seja, a menos que meu cliente esteja no domínio
twitter.com, o uso de um XHR get comum me retornará nada mais do que um erro
HTTP.

A forma padrão de
contornar isso é usar o Cross
Origin Resource Sharing (CORS)
, que agora é implementado pela maioria
dos browsers modernos.  No entanto, muitos desenvolvedores acham essa abordagem pesada
e um tanto pedante
.

O JSONP (documentado pela
primeira vez por Bob Ippolito em 2005) é uma alternativa simples e
efetiva que usa a capacidade das  tags script de obter conteúdo de qualquer
servidor.

Ele funciona assim: uma tag script tem um atributo src que pode ser configurado para qualquer
resource path, como uma URL, e não precisa retornar um arquivo JavaScript.
Assim, posso facilmente criar um fluxo JSON dos dados que alimentam meu
twitter  para meu cliente.

1	var scriptTag = document.createElement('SCRIPT');
2 scriptTag.src = "http://www.twitter.com/status/user_timeline/angustweets.json?count=5";
3
4 document.getElementsByTagName('HEAD')[0].appendChild(scriptTag);

Essas são boas notícias,
exceto pelo fato de não terem absolutamente nenhum efeito em minha página web,
a não ser enchê-la com um monte de JSON
indisponível. Para poder usar dados de tag script, precisamos interagir com
nosso JavaScript existente. É aí que a parte P (ou “padding” – acolchoado) do
JSON se apresenta. Se pudermos fazer o servidor envolver (wrap) sua resposta em
uma de nossas próprias funções, podemos fazer com que ele seja útil.

OK, aí vai:

01	var logIt = function(data) {
02 //print last tweet text
03 window.console && console.log(data[0].text);
04 }
05
06 var scriptTag = document.createElement('SCRIPT');
07 scriptTag.src = "http://www.twitter.com/status/user_timeline/angustweets.json?count=5&callback=logIt";
08
09 document.getElementsByTagName('HEAD')[0].appendChild(scriptTag);
10 /* console will log:
11 @marijnjh actually I like his paren-free proposal (but replacing global w/ modules seems iffy) JS needs to re-assert simplicity as an asset */

Uau, como fiz isso? Bem, não
sem muita ajuda do twitter, que juntamente com muitas outras APIs agora suportam requisições JSONP. Note o parâmetro de solicitação
extra: callback=loglt. Isso informa o servidor (twitter) para envolver sua resposta
em minha função (loglt). 

O JSONP parece bem bacana.
Por que toda essa confusão?

OK, finalmente estamos
prontos e em condição de checar a discussão JSMentors.com a que me referi no artigo anterior.
Peter Van der Zee, Kyle Simpson (conhecido como Getify) e outros estão
compreensivelmente preocupados com a segurança do JSONP.

Por quê? Porque sempre que
chamamos o JSONP, invocamos qualquer código que o servidor coloque em nossas
mãos, sem questionamentos, e sem volta. É como ir de olhos vendados a um
restaurante e pedir que coloquem comida na sua boca. Em alguns lugares você
pode confiar, em outros, não.

Peter recomenda tirar a função padding da resposta e implementá-la manualmente somente depois que a
resposta tiver sido confirmada como JSON puro.

A ideia é basicamente sólida,
mas ele acrescenta alguns detalhes de implementação. Ele também lamenta a
exigência atual do fornecimento de uma variável global. A proposta do Kyle é
similar: ele também defende uma verificação pós-resposta, baseada no mime type da tag script – ele sugere a introdução de um novo mime type específico para o JSONP (por exemplo: “application/json-p”), que
dispararia tal validação. 

Minha solução JSONP

Concordo com o
espírito dos argumentos tanto do Kyle quanto do Peter. Aqui vai um framework
JSONP leve que considera algumas de suas preocupações. A função evalJSONP é um
callback wrapper que usa uma closure para ligar o custom callback aos dados de
resposta. O custom callback pode ser de qualquer alcance e, como no exemplo a
seguir, pode ser uma função anônima criada dinamicamente. O wrapper evalJSONP
assegura que o callback somente será invocado se a resposta JSON for válida.

01	var jsonp = {
02 callbackCounter: 0,
03
04 fetch: function(url, callback) {
05 var fn = 'JSONPCallback_' + this.callbackCounter++;
06 window[fn] = this.evalJSONP(callback);
07 url = url.replace('=JSONPCallback', '=' + fn);
08
09 var scriptTag = document.createElement('SCRIPT');
10 scriptTag.src = url;
11 document.getElementsByTagName('HEAD')[0].appendChild(scriptTag);
12 },
13
14 evalJSONP: function(callback) {
15 return function(data) {
16 var validJSON = false;
17 if (typeof data == "string") {
18 try {validJSON = JSON.parse(data);} catch (e) {
19 /*invalid JSON*/}
20 } else {
21 validJSON = JSON.parse(JSON.stringify(data));
22 window.console && console.warn(
23 'response data was not a JSON string');
24 }
25 if (validJSON) {
26 callback(validJSON);
27 } else {
28 throw("JSONP call returned invalid or empty JSON");
29 }
30 }
31 }
32 }

(Atualização: com a sugestão
de Brian Grinstead 
e Jose Antonio Perez , eu acertei o utilitário para
suportar carregamento concorrente de scripts).

Eis aqui alguns
exemplos de uso…

01	//The U.S. President's latest tweet...
02 var obamaTweets = "http://www.twitter.com/status/user_timeline/BARACKOBAMA.json?count=5&callback=JSONPCallback";
03 jsonp.fetch(obamaTweets, function(data) {console.log(data[0].text)});
04
05 /* console logs:
06 From the Obama family to yours, have a very happy Thanksgiving. http://OFA.BO/W2KMjJ
07 */
08
09 //The latest reddit...
10 var reddits = "http://www.reddit.com/.json?limit=1&jsonp=JSONPCallback";
11 jsonp.fetch(reddits , function(data) {console.log(data.data.children[0].data.title)});
12
13 /* console logs:
14 You may remember my kitten Swarley wearing a tie. Well, he's all grown up now, but he's still all business. (imgur.com)
15 */

Note que sites como o
twitter.com na verdade retornam JSON sem aspas, o que faz a tag script carregar um objeto JavaScript. Em tais
casos, é o método JSON.stringify
que na verdade faz a validação, removendo qualquer atributo não compatível com o JSON. Depois disso, eles com certeza passam no teste
JSON.parse. E isso é
lamentável, porque mesmo que eu possa limpar o objeto de qualquer dado não
JSON, nunca terei certeza se o servidor estava tentando me enviar conteúdo
malicioso (sinteticamente, um método horroroso para comparar o objeto original
recebido  com a versão stringfied e
parsed) – o melhor que posso fazer é colocar uma advertência no console.

Esclarecendo, isso é um pouco
mais seguro, mas não é seguro. Se o provedor do servidor simplesmente escolher
ignorar sua solicitação para envolver (wrap) a resposta dele em sua função,
então você estará vulnerável, mas, do contrário, o que apresentei deve fazer do
uso do JSONP algo muito tranquilo.

Também listado aqui. Espero que seja útil.

Leitura complementar

?

Texto original disponível em http://javascriptweblog.wordpress.com/2010/11/29/json-and-jsonp/