Introduzione alle mappe di origine JavaScript

Ryan Seddon

Hai mai desiderato di poter mantenere il codice lato client leggibile e, soprattutto, di cui è possibile eseguire il debug anche dopo averlo combinato e minimizzato, senza influire sulle prestazioni? Bene, ora puoi sfruttare la magia delle mappe di origine.

Le mappe di origine consentono di mappare un file combinato/minimizzato a uno stato non creato. Quando sviluppi per la produzione, minimizzando e combinando i file JavaScript, generi una mappa di origine che contiene informazioni sui tuoi 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 restituisce la posizione originale. Gli strumenti per sviluppatori (attualmente le build notturne di WebKit, Google Chrome o Firefox 23 e versioni successive) sono in grado di analizzare automaticamente la mappa sorgente e di farla 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. Se selezioni "Ottieni posizione originale", esegui una query sulla mappa di origine inserendo il numero di riga e di colonna generato e restituisce la posizione nel codice originale. Assicurati che la console sia aperta in modo da poter vedere l'output.

Esempio di come funziona la libreria delle mappe sorgente di Mozilla JavaScript.

Mondo reale

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

Come attivare 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 di sviluppo integrati.

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

Perché dovrei interessarmi 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 preannuncia roseo con discussioni su linguaggi compilato in JavaScript come CoffeeScript e persino con la possibilità di aggiungere il supporto per preprocessori CSS come SASS o LESS.

In futuro potremmo utilizzare 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/LESS e altri
  • Praticamente qualsiasi linguaggio compilato in JavaScript

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

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

Un altro esempio che ho creato 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 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 di generare una mappa sorgente più il codice ES3 equivalente.

Debug di Traceur ES6 utilizzando le mappe di origine.

Demo: Scrivere ES6, eseguirne il debug, visualizzare la mappatura del codice sorgente in azione

Come funziona la mappa di origine?

L'unico compilatore/miniatore JavaScript che al momento supporta la generazione di mappe di origine è il compilatore Closure. Ti spiegherò come si usa più avanti. Una volta combinato e minimizzato il codice JavaScript, sarà presente un file mappa di origine.

Attualmente, il compilatore Closure non aggiunge alla fine il commento speciale necessario per indicare agli strumenti di sviluppo 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 dei commenti era //@, ma a causa di alcuni problemi e dei commenti della 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 dei commenti. Questa modifica della sintassi influisce anche sull'URL di origine.

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

Metti Mi piace al commento per indicare all'utente 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 su una sola riga.

Esempio di WebKit DevTools per mostrare le mappe di origine e quelle di origine disattivate.

Il file della mappa di origine verrà scaricato solo se le mappe di origine sono attivate e i tuoi strumenti di sviluppo sono aperti. 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 di origine?

Dovrai utilizzare il compilatore Closure per minimizzare, concatenare 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 dei comandi importanti sono --create_source_map e --source_map_format. Questo è obbligatorio perché la versione predefinita è V2 e vogliamo lavorare solo con la versione V3.

L'anatomia di una mappa di origine

Per comprendere meglio una mappa di origine, prenderemo un piccolo esempio di file di 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 succose:

  • Numero di versione su cui si basa la mappa di origine
  • Il nome del file del codice generato (il tuo file di produzione minifed/combinato)
  • sourceRoot consente di anteporre le origini a una struttura di cartelle. Anche questa è una tecnica per risparmiare spazio
  • sorgenti contiene tutti i nomi di file che sono stati combinati
  • contiene tutti i nomi di variabili/metodi riportati nel codice.
  • Infine, la proprietà mappings è il luogo in cui avviene la magia utilizzando valori VLQ Base64. Il vero risparmio di spazio viene fatto qui.

VLQ in Base64 e mantenere ridotte le dimensioni della mappa di origine

In origine, le specifiche della mappa sorgente avevano un output molto dettagliato di tutte le mappature, perciò la dimensione della mappa di origine era circa 10 volte superiore al 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 finisce con una mappa sorgente di circa 300 kB.

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

VLQ (Variable Length Quantity) viene utilizzato insieme alla codifica del valore in un valore Base64. La proprietà mappings è una stringa molto grande. All'interno di questa stringa sono presenti punti e virgola (;) che rappresentano un numero di riga all'interno del file generato. All'interno di ogni riga sono presenti virgole (,) che rappresentano ogni segmento al suo interno. Ciascuno di questi segmenti è 1, 4 o 5 nei campi a lunghezza variabile. Alcuni potrebbero essere visualizzati più lunghi, ma contengono bit di continuazione. Ogni segmento si basa sul segmento precedente, il che aiuta 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ò avere una lunghezza variabile pari a 1, 4 o 5. Questo diagramma è considerato una lunghezza variabile di quattro con un bit di continuazione (g). Analizziamo questo segmento e ti mostreremo come funziona la mappa di origine rispetto alla località originale.

I valori mostrati sopra sono puramente valori decodificati in Base64; è necessaria un'ulteriore elaborazione per ottenere i valori reali. Solitamente, ciascun segmento prevede cinque cose:

  • Colonna generata
  • File originale in cui compare la notifica
  • 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 i segmenti in un altro segmento passeranno da quattro a cinque variabili. Il valore g nel diagramma dei segmenti riportato sopra è quello che viene chiamato bit di continuazione che consente un'ulteriore ottimizzazione nella fase di decodifica VLQ Base64. Un bit di continuazione consente di costruire sul valore di un segmento in modo da poter archiviare grandi numeri senza dover archiviare un numero elevato. Si tratta di una tecnica di risparmio di spazio molto intelligente che ha le sue radici nel formato MIDI.

Il diagramma sopra AAgBC, una volta elaborato ulteriormente, restituirà 0, 0, 32, 16, 1; il 32 è il bit di continuazione che aiuta a creare il seguente valore di 16. B decodificato puramente in Base64 è 1. Quindi i valori importanti utilizzati sono 0, 0, 16, 1. Questo poi ci permette di sapere che la riga 1 (le righe sono mantenute il conteggio con il punto e virgola) la colonna 0 del file generato è mappata al file 0 (l'array di file 0 è foo.js), la riga 16 nella colonna 1.

Per mostrare come vengono decodificati i segmenti, farò riferimento alla libreria JavaScript della mappa sorgente di Mozilla. Puoi anche dare un'occhiata al codice di mappatura sorgente degli strumenti per sviluppatori di WebKit, anch'esso scritto in JavaScript.

Per capire correttamente come otteniamo il valore 16 da B, dobbiamo avere una conoscenza di base degli operatori a livello di bit e di come le specifiche funzionano per la mappatura del codice sorgente. La cifra precedente, g, viene contrassegnata come bit di continuazione confrontando la cifra (32) e VLQ_CONTINUATION_BIT (binario 100.000 o 32) utilizzando l'operatore AND (&) a livello di bit.

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

Restituisce 1 in ogni posizione di bit in cui è visualizzato entrambi. Quindi un valore 33 & 32 decodificato in Base64 restituirà 32 poiché condividono solo la posizione a 32 bit, come puoi vedere nel diagramma sopra. Questo aumenta quindi il valore di spostamento dei bit di 5 per ogni bit di continuazione precedente. Nel caso sopra, è spostato di 5 solo una volta, quindi a sinistra lo spostamento di 1 (B) di 5.

1 <<../ 5 // 32

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

Tale valore viene quindi convertito da un valore relativo VLQ spostando il numero (32) di uno spot verso destra.

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

Ecco fatto: è così che passi da 1 a 16. Può sembrare un processo eccessivamente complicato, ma quando i numeri iniziano ad aumentare diventa più logico.

Potenziali problemi XSSI

La specifica menziona i problemi di inclusione di script tra siti che potrebbero derivare dall'utilizzo di una mappa di origine. Per mitigare il problema, ti consigliamo 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 gestire 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 nelle specifiche e in questo caso vengono rimossi tutti i caratteri che portano alla prima nuova entità di riga (\n).

sourceURL e displayName in azione: funzioni di valutazione e anonime

Sebbene non facciano parte delle specifiche della mappa di origine, le due convenzioni seguenti consentono di semplificare lo sviluppo quando si lavora con valutazioni e funzioni anonime.

Il primo helper è molto simile alla proprietà //# sourceMappingURL ed è effettivamente menzionato nelle specifiche della mappa sorgente V3. Includendo il seguente commento speciale nel tuo codice, che verrà valutato, puoi assegnare un nome agli eval in modo che appaiano come nomi più logici nei tuoi strumenti di sviluppo. Guarda una semplice demo utilizzando il compilatore CoffeeScript:

Demo: vedi il codice di eval() sotto forma di script tramite URL di origine

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

L'altro helper consente di assegnare un nome a funzioni anonime utilizzando la proprietà displayName disponibile nel contesto corrente della funzione anonima. Profila la 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.

Durante la profilazione del codice all'interno degli strumenti per sviluppatori, verrà mostrata la proprietà displayName anziché con una proprietà simile a (anonymous). Tuttavia, displayName è praticamente inattivo e non sarà disponibile in Chrome. Ma ogni speranza non è persa: è stata suggerita una proposta molto migliore chiamata debugName.

Al momento della scrittura, la denominazione valutativa è disponibile solo nei browser Firefox e WebKit. La proprietà displayName si trova solo nei set notturni di WebKit.

Rafforziamoci insieme

Al momento si discute molto a lungo dell'aggiunta del supporto delle mappe di origine a CoffeeScript. Controlla il problema e aggiungi il tuo supporto per aggiungere la generazione delle mappe di origine al compilatore CoffeeScript. Questa sarà una grande vittoria per CoffeeScript e per i suoi fedeli follower.

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

Molti tools generano mappe di origine, incluso il compilatore Coffeescript. Ora lo considero una questione controversa.

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

Non è perfetto

Un aspetto che le mappe di origine non sono utili 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 corrente non restituirà nulla perché in realtà non esiste. Ciò richiederebbe un qualche tipo di mappatura inversa per cercare il nome reale dell'argomento/della variabile che vuoi esaminare rispetto al nome effettivo dell'argomento/della variabile nel codice JavaScript compilato.

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

Problemi

Di recente, jQuery 1.9 ha aggiunto il supporto per le mappe di origine quando vengono pubblicate al di fuori di CDN ufficiali. Segnala inoltre un bug peculiare quando vengono utilizzati i commenti della compilazione condizionale di IE (//@cc_on) prima del caricamento di jQuery. A quel punto, esiste un commit per mitigare questo problema inserendo sourceMappingURL in un commento su più righe. Una lezione da imparare non utilizzare i commenti condizionali.

Da allora il problema è stato risolto con la modifica della sintassi in //#.

Strumenti e risorse

Di seguito sono riportati altri strumenti e risorse che dovresti consultare:

Le mappe di origine sono un'utilità molto potente nel set di strumenti di uno sviluppatore. È utilissimo poter mantenere la tua app web snella ma facilmente di cui è possibile eseguire il debug. Si tratta anche di uno strumento di apprendimento molto potente per i nuovi sviluppatori che vogliono vedere come gli sviluppatori esperti strutturano e scrivono le proprie app senza dover cercare codice minimizzato illeggibile.

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