Introduzione alle mappe di origine JavaScript

Ryan Seddon

Hai mai desiderato mantenere il codice lato client leggibile e, soprattutto, eseguibile il debug anche dopo averlo combinato e minimizzato, senza influire sulle prestazioni? Ora puoi farlo grazie alla magia delle mappe di origine.

Le mappe di origine consentono di mappare un file combinato/minimizzato a uno stato non compilato. Quando esegui la compilazione per la produzione, oltre a ridurre al minimo e combinare i file JavaScript, generi una mappa di origine che contiene informazioni sui file originali. Quando esegui una query su una determinata riga e su un determinato numero di colonna nel codice JavaScript generato, puoi eseguire una ricerca nella mappa di origine che restituisce la posizione originale. Gli strumenti per sviluppatori (attualmente le build notturne di WebKit, Google Chrome o Firefox 23 e versioni successive) possono analizzare automaticamente la mappa sorgente e dare l'impressione che tu stia eseguendo file non minimizzati e non combinati.

La demo ti consente di fare clic con il tasto destro del mouse in un punto qualsiasi dell'area di testo contenente il codice sorgente generato. Se selezioni "Richiedi posizione originale", verrà eseguita una query sulla mappa di origine passando il numero di riga e di colonna generati e verrà restituita la posizione nel codice originale. Assicurati che la console sia aperta per poter vedere l'output.

Esempio della libreria di mappe di origine JavaScript di Mozilla in azione.

Mondo reale

Prima di visualizzare la seguente implementazione pratica delle mappe sorgente, assicurati di aver attivato la funzionalità delle mappe sorgente in Chrome Canary o WebKit nightly facendo clic sull'icona a forma di ingranaggio delle impostazioni nel riquadro degli strumenti per gli sviluppatori e selezionando l'opzione "Attiva mappe sorgente".

Come attivare le mappe di origine negli strumenti per sviluppatori WebKit.

In Firefox 23 e versioni successive, le mappe sorgente sono attivate per impostazione predefinita negli strumenti per sviluppatori integrati.

Come attivare le mappe di origine negli strumenti per sviluppatori di Firefox.

Perché dovrei preoccuparmi delle mappe di origine?

Al momento la mappatura delle origini funziona solo tra JavaScript non compresso/combinato e JavaScript compresso/non combinato, ma il futuro è roseo con i talk sui linguaggi compilati in JavaScript come CoffeeScript e persino la possibilità di aggiungere il supporto per i pre-processori CSS come SASS o LESS.

In futuro potremmo utilizzare facilmente quasi tutte le lingue come se fossero supportate nativamente nel browser con le mappe di origine:

  • CoffeeScript
  • ECMAScript 6 e versioni successive
  • SASS/LESS e altri
  • Praticamente qualsiasi linguaggio che viene compilato in JavaScript

Dai un'occhiata a questo screencast del debug di CoffeeScript in una build sperimentale della console di Firefox:

Di recente, Google Web Toolkit (GWT) ha aggiunto il supporto per le mappe di origine. Ray Cromwell del team GWT ha realizzato uno splendido screencast che mostra il supporto delle mappe di origine in azione.

Un altro esempio che ho creato utilizza la libreria Traceur di Google, che ti consente di scrivere ES6 (ECMAScript 6 o Next) e di compilarlo in codice compatibile con ES3. Il compilatore Traceur genera anche una mappa delle origini. Dai un'occhiata a questa demo di tratti e classi ES6 utilizzati come se fossero supportati nativamente nel browser, grazie alla mappa di origine.

La textarea nella demo ti consente anche di scrivere codice ES6 che verrà compilato dinamicamente e genererà una mappa sorgente oltre al codice ES3 equivalente.

Debug di Traceur ES6 utilizzando le mappe di origine.

Demo: scrivi ES6, esegui il debug e visualizza la mappatura delle origini in azione

Come funziona la mappa delle origini?

Al momento, l'unico compilatore/minimizzatore JavaScript che supporta la generazione di mappe di origine è il compilatore Closure. (Ti spiegherò come utilizzarlo più avanti). Una volta combinato e minimizzato il codice JavaScript, verrà creato un file mappa di origine.

Al momento, il compilatore Closure non aggiunge il commento speciale alla fine necessario per indicare agli strumenti per sviluppatori dei browser che è disponibile una mappa sorgente:

//# sourceMappingURL=/path/to/file.js.map

In questo modo, gli strumenti per sviluppatori possono mappare le chiamate alla loro posizione nei file di origine originali. In precedenza, il pragma del commento era //@, ma a causa di alcuni problemi con questo e con i commenti di compilazione condizionale di IE, è stata presa la decisione di cambiarlo in //#. Al momento, Chrome Canary, WebKit Nightly e Firefox 24 e versioni successive supportano il nuovo pragma comment. Questa modifica alla sintassi interessa anche sourceURL.

Se non ti piace l'idea di inserire un commento strano, puoi impostare un'intestazione speciale nel file JavaScript compilato:

X-SourceMap: /path/to/file.js.map

Come per il commento, questo indica all'utente che utilizza la mappa di origine dove cercare la mappa di origine associata a un file JavaScript. Questa intestazione risolve anche il problema del riferimento alle mappe di origine in lingue che non supportano i commenti a riga singola.

Esempio di WebKit Devtools con le mappe di origine attive e disattivate.

Il file della mappa di origine verrà scaricato solo se hai attivato le mappe di origine e hai aperto gli strumenti per gli sviluppatori. Dovrai anche caricare i file originali in modo che gli strumenti per sviluppatori possano farvi riferimento e visualizzarli quando necessario.

Come faccio a generare una mappa delle origini?

Dovrai utilizzare il compilatore Closure per ridurre al minimo, concatenare e generare una mappa di origine per i file JavaScript. Il comando è il seguente:

java -jar compiler.jar \
--js script.js \
--create_source_map ./script-min.js.map \
--source_map_format=V3 \
--js_output_file script-min.js

I due flag di comando importanti sono --create_source_map e --source_map_format. Questo è necessario perché la versione predefinita è la 2 e vogliamo lavorare solo con la 3.

L'anatomia di una mappa di origine

Per comprendere meglio una mappa sorgente, esamineremo un piccolo esempio di file di mappa sorgente che verrebbe generato dal compilatore Closure e analizzeremo più in dettaglio il funzionamento della sezione "mapping". L'esempio seguente è una leggera variazione dell'esempio della specifica V3.

{
    version : 3,
    file: "out.js",
    sourceRoot : "",
    sources: ["foo.js", "bar.js"],
    names: ["src", "maps", "are", "fun"],
    mappings: "AAgBC,SAAQ,CAAEA"
}

Sopra puoi vedere che una mappa di origine è un letterale oggetto contenente molte informazioni interessanti:

  • Numero di versione su cui si basa la mappa di origine
  • Il nome del file del codice generato (il file di produzione minifed/combinato)
  • sourceRoot ti consente di anteporre alle origini una struttura di cartelle. Questa è anche una tecnica per risparmiare spazio
  • sources contiene tutti i nomi dei file combinati
  • names contiene tutti i nomi di variabili/metodi che compaiono nel codice.
  • Infine, la proprietà mappings è dove avviene la magia utilizzando i valori VLQ base64. È qui che si ottiene il vero risparmio di spazio.

VLQ Base64 e mantenimento della mappa di origine di piccole dimensioni

In origine, la specifica della mappa di origine aveva un output molto dettagliato di tutte le mappature e la mappa di origine era circa 10 volte più grande del codice generato. La seconda versione ha ridotto questo valore di circa il 50% e la terza versione di un altro 50%, quindi per un file di 133 kB si ottiene una mappa di origine di circa 300 kB.

Come hanno fatto a ridurre le dimensioni mantenendo le mappature complesse?

Viene utilizzata la quantità con lunghezza variabile VLQ (Variable Length Quantity) insieme alla codifica del valore in un valore Base64. La proprietà mappings è una stringa molto grande. All'interno di questa stringa sono presenti i punti e virgola (;) che rappresentano un numero di riga all'interno del file generato. All'interno di ogni riga sono presenti delle virgole (,) che rappresentano ogni segmento all'interno della riga. Ciascuno di questi segmenti è 1, 4 o 5 nei campi di lunghezza variabile. Alcuni potrebbero sembrare più lunghi, ma contengono bit di continuazione. Ogni segmento si basa sul precedente, il che contribuisce a ridurre le dimensioni del file poiché ogni bit è relativo ai segmenti precedenti.

Suddivisione di un segmento all'interno del file JSON della mappa di origine.

Come accennato in precedenza, ogni segmento può essere di lunghezza variabile 1, 4 o 5. Questo diagramma è considerato di lunghezza variabile di quattro con un bit di continuazione (g). Analizzeremo questo segmento e ti mostreremo come la mappa di origine calcola la posizione originale.

I valori mostrati sopra sono puramente i valori decodificati in Base64, ma è necessaria un'ulteriore elaborazione per ottenere i valori effettivi. In genere ogni segmento calcola cinque elementi:

  • Colonna generata
  • File originale in cui è apparso
  • Numero di riga originale
  • Colonna originale
  • E, se disponibile, il nome originale

Non tutti i segmenti hanno un nome, un nome di metodo o un argomento, pertanto i segmenti in tutto il codice passeranno da quattro a cinque di lunghezza variabile. Il valore g nel diagramma del segmento riportato sopra è chiamato bit di continuazione e consente un'ulteriore ottimizzazione nella fase di decodifica VLQ Base64. Un bit di continuazione ti consente di creare un valore di segmento in modo da poter memorizzare numeri grandi senza doverli memorizzare, una tecnica molto intelligente per risparmiare spazio che ha le sue radici nel formato MIDI.

Il diagramma AAgBC riportato sopra, dopo un ulteriore trattamento, restituirà 0, 0, 32, 16, 1, dove 32 è il bit di continuazione che aiuta a creare il valore successivo di 16. B decodificato in modo puramente Base64 è 1. Pertanto, i valori importanti utilizzati sono 0, 0, 16, 1. Questo ci fa sapere che la riga 1 (le righe vengono conteggiate dai punti e virgola) colonna 0 del file generato corrisponde al file 0 (array di file 0 è foo.js), riga 16 colonna 1.

Per mostrare come vengono decodificati i segmenti, farò riferimento alla libreria JavaScript Source Map di Mozilla. Puoi anche esaminare il codice di mappatura delle origini degli strumenti per sviluppatori WebKit, anch'esso scritto in JavaScript.

Per comprendere correttamente come otteniamo il valore 16 da B, dobbiamo avere una conoscenza di base degli operatori bitwise e del funzionamento delle specifiche per la mappatura delle origini. Il numero precedente, g, viene contrassegnato come bit di continuazione confrontando il numero (32) e VLQ_CONTINUATION_BIT (100000 o 32 in binario) utilizzando l'operatore AND (&) a livello di bit.

32 & 32 = 32
// or
100000
|
|
V
100000

Restituisce un 1 in ogni posizione del bit in cui entrambi sono presenti. Pertanto, un valore decodificato Base64 di 33 & 32 restituirà 32 perché condividono solo la posizione a 32 bit, come puoi vedere nel diagramma sopra. Di conseguenza, il valore di spostamento del bit aumenta di 5 per ogni bit di continuazione precedente. Nel caso riportato sopra, viene spostato di 5 solo una volta, quindi viene spostato di 5 a sinistra 1 (B).

1 <<../ 5 // 32

// Shift the bit by 5 spots
______
|    |
V    V
100001 = 100000 = 32

Questo valore viene poi convertito da un valore VLQ con segno spostando il numero (32) di un posto a destra.

32 >> 1 // 16
//or
100000
|
 |
 V
010000 = 16

Ecco qua: è così che si trasforma 1 in 16. Questa procedura potrebbe sembrare troppo complicata, ma quando i numeri iniziano ad aumentare, ha più senso.

Potenziali problemi XSSI

La specifica menziona i problemi di inclusione di script tra siti che potrebbero verificarsi con l'utilizzo di una mappa di origine. Per attenuare il problema, ti consigliamo di anteporre ")]}" alla prima riga della mappa di origine per invalidare deliberatamente il codice JavaScript in modo da generare un errore di sintassi. Gli strumenti per sviluppatori WebKit sono già in grado di gestire questa situazione.

if (response.slice(0, 3) === ")]}") {
    response = response.substring(response.indexOf('\n'));
}

Come mostrato sopra, i primi tre caratteri vengono suddivisi per verificare se corrispondono all'errore di sintassi nella specifica e, in caso affermativo, vengono rimossi tutti i caratteri che precedono la prima entità di nuova riga (\n).

sourceURL e displayName in azione: funzioni Eval e anonime

Sebbene non facciano parte della specifica della mappa di origine, le due convenzioni riportate di seguito ti consentono di semplificare notevolmente lo sviluppo quando lavori con eval e funzioni anonime.

Il primo helper è molto simile alla proprietà //# sourceMappingURL ed è effettivamente menzionato nella specifica della mappa di origine V3. Se includi il seguente commento speciale nel codice, che verrà valutato, puoi assegnare un nome alle valutazioni in modo che vengano visualizzate come nomi più logici negli strumenti di sviluppo. Dai un'occhiata a una semplice demo che utilizza il compilatore CoffeeScript:

Demo: visualizza il codice di eval() come script tramite sourceURL

//# sourceURL=sqrt.coffee
Aspetto del commento speciale sourceURL negli strumenti per sviluppatori

L'altro helper consente di assegnare un nome alle funzioni anonime utilizzando la proprietà displayName disponibile nel contesto corrente della funzione anonima. Crea il profilo della seguente demo per vedere la proprietà displayName in azione.

btns[0].addEventListener("click", function(e) {
    var fn = function() {
        console.log("You clicked button number: 1");
    };

    fn.displayName = "Anonymous function of button 1";

    return fn();
}, false);
Visualizzazione della proprietà displayName in azione.

Quando esegui il profiling del codice negli strumenti per gli sviluppatori, viene visualizzata la proprietà displayName anziché qualcosa di simile a (anonymous). Tuttavia, displayName non è più supportato e non verrà incluso in Chrome. Tuttavia, non è ancora tutto perduto ed è stata suggerita una proposta molto migliore chiamata debugName.

Al momento della stesura di questo articolo, la denominazione eval è disponibile solo nei browser Firefox e WebKit. La proprietà displayName è disponibile solo nelle build notturne di WebKit.

Uniamoci

Al momento è in corso un'ampia discussione sull'aggiunta del supporto delle mappe di origine a CoffeeScript. Dai un'occhiata al problema e fornisci il tuo supporto per l'aggiunta della generazione di mappe di origine al compilatore CoffeeScript. Sarà una grande vittoria per CoffeeScript e i suoi fedeli seguaci.

UglifyJS presenta anche un problema con la mappa di origine che dovresti esaminare.

Molti strumenti generano mappe di origine, incluso il compilatore CoffeeScript. Ora considero questo un punto controverso.

Più strumenti abbiamo a disposizione per generare mappe sorgente, meglio è, quindi non esitare a chiedere o ad aggiungere il supporto delle mappe sorgente al tuo progetto open source preferito.

Non è perfetto

Al momento le mappe di origine non supportano le espressioni di monitoraggio. Il problema è che il tentativo di ispezionare un nome di argomento o variabile all'interno del contesto di esecuzione corrente non restituirà nulla perché non esiste realmente. Ciò richiederebbe una sorta di mappatura inversa per cercare il nome reale dell'argomento/della variabile che vuoi ispezionare rispetto al nome effettivo dell'argomento/della variabile nel codice JavaScript compilato.

Ovviamente, si tratta di un problema risolvibile e, prestando maggiore attenzione alle mappe di origine, possiamo iniziare a vedere alcune funzionalità straordinarie e una maggiore stabilità.

Problemi

Di recente, jQuery 1.9 ha aggiunto il supporto delle mappe di origine quando vengono pubblicate da CDN ufficiali. Ha anche segnalato un bug particolare quando i commenti di compilazione condizionale di IE (//@cc_on) vengono utilizzati prima del caricamento di jQuery. Da allora è stato eseguito un commit per ovviare al problema inserendo sourceMappingURL in un commento su più righe. Lezione da imparare: non utilizzare il commento condizionale.

Il problema è stato risolto modificando la sintassi in //#.

Strumenti e risorse

Ecco alcune risorse e strumenti aggiuntivi che dovresti consultare:

Le mappe sorgente sono un'utilità molto potente nel set di strumenti di uno sviluppatore. È molto utile poter mantenere la tua app web snella, ma facilmente debbugabile. È anche uno strumento di apprendimento molto efficace per gli sviluppatori più recenti, che possono vedere come gli sviluppatori esperti strutturano e scrivono le loro app senza dover consultare codice compresso illeggibile.

Che cosa aspetti? Inizia subito a generare mappe di origine per tutti i progetti.