Introduzione alle mappe di origine JavaScript

Ryan Seddon

Avete mai desiderato di poter 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 sorgente.

Le mappe di origine sono un modo per mappare un file combinato/minificato di nuovo a uno stato non creato. Quando crei una mappa per la produzione, oltre a minimizzare e combinare i file JavaScript, generi una mappa di origine che contiene informazioni sui file originali. Quando esegui una query su un determinato numero di riga e colonna nel codice JavaScript generato, puoi eseguire una ricerca nella mappa di origine che restituisca la posizione originale. Gli strumenti per sviluppatori (attualmente build notturne WebKit, Google Chrome o Firefox 23 e versioni successive) sono in grado di analizzare automaticamente la mappa di origine per far sembrare che tu stia eseguendo file non minimizzati e non combinati.

La demo ti consente di fare clic con il tasto destro del mouse in qualsiasi punto dell'area di testo contenente l'origine generata. Seleziona "Ottieni la posizione originale" per eseguire una query sulla mappa di origine passando il numero di riga e di colonna generati, quindi restituire la posizione nel codice originale. Assicurati che la console sia aperta in modo da poter vedere l'output.

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

Mondo reale

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

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

In Firefox 23 e versioni successive le mappe del codice sorgente sono abilitate per impostazione predefinita negli strumenti per sviluppatori integrati.

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

Perché dovrei preoccuparmi delle mappe di origine?

Al momento, la mappatura del codice sorgente funziona solo tra JavaScript non compresso/combinato e JavaScript compresso/non combinato, ma il futuro si prospetta promettente grazie alla conversazione sui linguaggi compilati-to-JavaScript come CoffeeScript e persino alla possibilità di aggiungere il supporto per preprocessori CSS come SASS o LESS.

In futuro potremmo usare facilmente quasi tutte le lingue come se fossero supportate in modo nativo nel browser con le mappe di origine:

  • CoffeeScript
  • ECMAScript 6 e versioni successive
  • SASS/MENO e altro
  • Praticamente qualsiasi linguaggio che esegue la compilazione in JavaScript

Dai un'occhiata a questo screencast di CoffeeScript di cui viene eseguito il debug in una build sperimentale della console Firefox:

In Google Web Toolkit (GWT) è stato recentemente aggiunto il supporto per Source Maps. Ray Cromwell del team GWT ha realizzato uno screencast fantastico che mostra il supporto della mappa di origine in azione.

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

L'area di testo nella demo permette anche di scrivere ES6 che verrà compilato al volo e genererà una mappa sorgente più il codice ES3 equivalente.

Debug di Traceur ES6 tramite le mappe di origine.

Demo: scrivi ES6, esegui il debug, visualizza la mappatura di origine in azione

Come funziona la mappa di origine?

L'unico compilatore/minificatore JavaScript che al momento supporta la generazione di mappe di origine è il compilatore Closure. Ti spiegherò come utilizzarlo più avanti. Dopo aver combinato e minimizzato il codice JavaScript, verrà creato un file della mappa di origine.

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

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

Ciò consente agli strumenti per sviluppatori di mappare le chiamate alla loro posizione nei file sorgente originali. In precedenza il pragma dei commenti era //@, ma a causa di alcuni problemi e nei commenti della compilazione condizionale di IE, è stata presa la decisione di modificarlo in //#. Attualmente Chrome Canary, WebKit Nightly e Firefox 24 e versioni successive supportano il nuovo pragma dei commenti. Questa modifica della sintassi interessa anche sourceURL.

Se non ti piace l'idea del commento strano, in alternativa puoi impostare un'intestazione speciale sul file JavaScript compilato:

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

Come per il commento, questo indicherà al consumatore della mappa di origine dove cercare la mappa di origine associata a un file JavaScript. Questa intestazione risolve anche il problema di fare riferimento alle mappe di origine nelle lingue che non supportano i commenti di una sola riga.

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

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

Come faccio a generare una mappa di origine?

Dovrai utilizzare il compitore di chiusura per minimizzare, concattare e generare una mappa di origine per i tuoi 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. Questa operazione è necessaria perché la versione predefinita è la V2 e vogliamo lavorare solo con la V3.

L'anatomia di una mappa di origine

Per comprendere meglio una mappa di origine, prenderemo un piccolo esempio di file della mappa di origine che verrebbe generato dal compilatore Closure e approfondiremo il funzionamento della sezione "mappature". L'esempio seguente è una leggera variazione rispetto all'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 oggetto letterale contenente molte informazioni interessanti:

  • Numero di versione su cui si basa la mappa di origine
  • Il nome file del codice generato (il tuo file di produzione minifed/combinato)
  • sourceRoot consente di anteporre alle origini una struttura di cartelle, anch'essa una tecnica per risparmiare spazio.
  • source contiene tutti i nomi di file combinati
  • contiene tutti i nomi di variabili/metodi visualizzati nel codice.
  • Infine, la proprietà mapping è dove avviene l'uso magico dei valori VLQ Base64. Il vero risparmio di spazio è fatto qui.

Base64 VLQ e limitazione della mappa di origine

In origine, la specifica della mappa di origine aveva un output molto dettagliato di tutte le mappature e faceva che la mappa di origine fosse circa 10 volte la dimensione del codice generato. La versione due lo ha ridotto di circa il 50% e la versione tre lo ha ridotto di nuovo di un altro 50%, quindi per un file da 133 kB si ottiene una mappa sorgente di ~300 kB.

In che modo hanno ridotto le dimensioni mantenendo al contempo le mappature complesse?

VLQ (Variable Length Quantity) viene utilizzato insieme alla codifica del valore in un valore Base64. La proprietà mapping è una stringa molto grande. In questa stringa sono presenti punti e virgola (;) che rappresentano un numero di riga nel file generato. All'interno di ogni riga sono presenti virgole (,) che rappresentano ogni segmento all'interno di quella riga. Ciascuno di questi segmenti è 1, 4 o 5 in campi di lunghezza variabile. Alcuni potrebbero apparire più lunghi, ma questi contengono bit di continuazione. Ogni segmento si basa sul precedente, il che consente di 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 già detto, ciascun segmento può essere lungo 1, 4 o 5 pezzi. Questo diagramma è considerato una lunghezza variabile di quattro con un bit di continuazione (g). Scomponiamo questo tratto e ti mostreremo come la mappa di origine genera la posizione originale.

I valori mostrati sopra sono puramente i valori decodificati Base64, è necessario elaborare un po' di più per ottenere i valori veri. Solitamente ogni segmento prevede cinque cose:

  • Colonna generata
  • File originale in cui compare
  • 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, quindi tutti i segmenti passeranno da quattro a cinque variabili di lunghezza. Il valore g nel diagramma di segmento sopra è quello che viene chiamato un bit di continuazione, che consente un'ulteriore ottimizzazione nella fase di decodifica VLQ Base64. Un bit di continuazione ti consente di basarti su un valore di segmento in modo da poter memorizzare grandi numeri senza dover archiviare un grande numero, una tecnica molto intelligente di risparmio di spazio che ha le sue radici nel formato midi.

Il diagramma riportato sopra AAgBC, una volta elaborato ulteriormente, restituirà 0, 0, 32, 16, 1. 32 è il bit di continuazione che aiuta a creare il seguente valore pari a 16. B decodificato esclusivamente in Base64 è 1. Quindi i valori importanti utilizzati sono 0, 0, 16, 1. Questo ci permette di sapere che la riga 1 (le righe vengono mantenute dal punto e virgola) la colonna 0 del file generato mappa al file 0 (la matrice dei file 0 è foo.js), la riga 16 alla colonna 1.

Per mostrare come vengono decodificati i segmenti, farò riferimento alla Source Map JavaScript Library di Mozilla. Puoi anche consultare il codice di mappatura sorgente degli strumenti per sviluppatori WebKit, anch'essi scritti in JavaScript.

Per capire correttamente come otteniamo il valore 16 da B, è necessario avere una conoscenza di base degli operatori bit per bit e di come funziona la specifica per la mappatura del codice sorgente. La cifra precedente, g, viene contrassegnata come bit di continuazione confrontando la cifra (32) con la cifra VLQ_CONTINUATION_BIT (binario 100000 o 32) mediante l'operatore AND (&) a livello di bit.

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

Questo restituisce 1 in ogni posizione dei bit in cui sono presenti entrambi. Di conseguenza, un valore decodificato in Base64 di 33 & 32 restituirà 32, poiché condivide solo la posizione a 32 bit, come puoi vedere nel diagramma riportato sopra. In questo modo il valore di spostamento di bit viene aumentato di 5 per ogni bit di continuazione precedente. Nel caso precedente è spostato solo di 5 una volta, quindi lo spostamento a sinistra di 1 (B) di 5.

1 <<../ 5 // 32

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

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

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

Ecco fatto: ecco come si fa a trasformare 1 in 16. Questo processo può sembrare troppo complicato, ma una volta che i numeri iniziano a crescere, diventa più logico.

Potenziali problemi XSSI

La specifica menziona i problemi di inclusione degli script tra siti che potrebbero derivare dall'utilizzo di una mappa di origine. Per ovviare a questo problema, si consiglia di anteporre ")]}" alla prima riga della mappa di origine per invalidare deliberatamente JavaScript e generare un errore di sintassi. Gli strumenti per sviluppatori di WebKit sono già in grado di risolvere questo problema.

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 questo caso, vengono rimossi tutti i caratteri che portano alla prima entità di nuova riga (\n).

sourceURL e displayName in azione: funzioni di valutazione e anonime

Sebbene non facciano parte della specifica della mappa di origine, le due convenzioni che seguono consentono di semplificare molto lo sviluppo quando utilizzi funzioni di valutazione e anonime.

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

Demo: Guarda il codice visualizzato da eval() come script tramite sourceURL

//# sourceURL=sqrt.coffee
Come appare il commento speciale sourceURL negli strumenti per sviluppatori

L'altro strumento di supporto ti consente di assegnare un nome alle funzioni anonime utilizzando la proprietà displayName disponibile nel contesto attuale della funzione anonima. Profila la demo seguente 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.

Durante la profilazione del codice all'interno degli strumenti per sviluppatori, viene visualizzata la proprietà displayName anziché qualcosa del tipo (anonymous). Tuttavia, displayName è quasi completamente morto in acqua e non verrà inserito in Chrome. Tuttavia, non abbiamo perso alcuna speranza ed è stata suggerita una proposta molto migliore chiamata debugName.

Al momento della scrittura, il nome di valutazione è disponibile solo nei browser Firefox e WebKit. La proprietà displayName si trova solo nelle notturne WebKit.

Mettiamoci in moto insieme

Al momento è in corso una discussione molto lunga sull'aggiunta a CoffeeScript del supporto delle mappe di origine. Controlla il problema e aggiungi il tuo supporto per aggiungere la generazione della mappa di origine al compilatore CoffeeScript. Sarà una grande vittoria per CoffeeScript e i suoi fedeli follower.

UglifyJS presenta anche un problema della mappa di origine che dovresti dare un'occhiata.

Molti tools generano mappe sorgente, tra cui il compilatore Coffeescript. Lo considero un punto controverso ora.

Più strumenti abbiamo a disposizione in grado di generare mappe di origine, meglio sarà, quindi chiedi o aggiungi il supporto delle mappe di origine al tuo progetto open source preferito.

Non è perfetto

Una cosa che le mappe di origine non sono adatte al momento sono le espressioni di visualizzazione. Il problema è che il tentativo di ispezionare il nome di un argomento o di una variabile nel contesto di esecuzione attuale non restituirà nulla perché in realtà non esiste. Ciò richiederebbe una sorta di mappatura inversa per cercare il nome reale dell'argomento/variabile che vuoi controllare rispetto al nome effettivo dell'argomento/della variabile nel codice JavaScript compilato.

Questo ovviamente è un problema risolvibile e con maggiore attenzione alle mappe di origine possiamo iniziare a vedere alcune funzionalità straordinarie e una migliore stabilità.

Problemi

Recentemente, in jQuery 1.9 è stato aggiunto il supporto per le mappe di origine quando vengono pubblicate dalle reti CDN ufficiali. Indicava inoltre un bug particolare quando i commenti della compilazione condizionale di IE (//@cc_on) venivano utilizzati prima del caricamento di jQuery. Da allora è stato eseguito un commit per mitigare questo problema racchiudendo sourceMappingURL in un commento su più righe. La lezione da imparare non è l'uso dei commenti condizionali.

Questo problema è stato risolto con la modifica della sintassi in //#.

Strumenti e risorse

Ecco alcune risorse e strumenti aggiuntivi che dovresti consultare:

Le mappe di origine sono un'utilità molto potente nel set di strumenti di uno sviluppatore. È super utile poter mantenere la tua app web snella ma facilmente eseguibile il debug. Si tratta anche di uno strumento di apprendimento molto efficace per i nuovi sviluppatori, che consente loro di vedere in che modo gli sviluppatori esperti strutturano e scrivono le loro app senza dover guadare il codice minimizzato illeggibile.

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