Introducción a los mapas de origen de JavaScript

¿Alguna vez deseaste poder hacer que tu código del cliente sea legible y, sobre todo, depurable, incluso después de combinarlo y reducirlo, sin afectar el rendimiento? Bueno, ahora puedes aprovechar la magia de los mapas de origen.

Los mapas de origen son una forma de volver a asignar un archivo combinado o reducido a un estado no compilado. Cuando realizas compilaciones para producción, además de reducir y combinar los archivos JavaScript, generas un mapa de origen que contiene información sobre los archivos originales. Cuando consultas un determinado número de línea y columna en tu JavaScript generado, puedes realizar una búsqueda en el mapa de origen que devuelve la ubicación original. Las herramientas para desarrolladores (actualmente, las compilaciones nocturnas de WebKit, Google Chrome o Firefox 23 y versiones posteriores) pueden analizar el mapa de origen automáticamente y hacer que parezca que estuvieras ejecutando archivos sin combinar y sin reducir.

La demostración te permite hacer clic con el botón derecho en cualquier parte del área de texto que contiene la fuente generada. Si seleccionas "Obtener ubicación original", se realizará una consulta en el mapa de origen pasando la línea y el número de columna generados, y se mostrará la posición en el código original. Asegúrate de que tu consola esté abierta para poder ver el resultado.

Ejemplo de la biblioteca de mapas de orígenes de Mozilla JavaScript en acción.

Caso real

Antes de ver la siguiente implementación real de mapas de origen, asegúrate de haber habilitado la función de mapas de origen en Chrome Canary o WebKit todas las noches. Para ello, haz clic en el ícono de configuración en el panel de herramientas para desarrolladores y marca la opción "Habilitar mapas de origen".

Cómo habilitar los mapas de fuentes en las herramientas para desarrolladores de WebKit

Firefox 23 y versiones posteriores tienen habilitados los mapas de fuentes de forma predeterminada en las herramientas para desarrolladores integradas.

Cómo habilitar los mapas de orígenes en las herramientas para desarrolladores de Firefox

¿Por qué deberían importarme los mapas de origen?

En este momento, la asignación de orígenes solo funciona entre JavaScript sin comprimir/combinar y JavaScript comprimido/descombinado, pero el futuro se ve brillante con las charlas de lenguajes compilados para JavaScript, como CoffeeScript, e incluso la posibilidad de agregar compatibilidad con preprocesadores de CSS, como SASS o LESS.

En el futuro, podríamos usar fácilmente casi cualquier lenguaje como si fuera compatible de forma nativa en el navegador con los mapas de origen:

  • CoffeeScript
  • ECMAScript 6 y versiones posteriores
  • SASS/LESS y otros
  • Casi cualquier lenguaje que se compile en JavaScript

Observa esta presentación en pantalla de la depuración de CoffeeScript en una compilación experimental de la consola de Firefox:

Recientemente, Google Web Toolkit (GWT) agregó compatibilidad con mapas de origen. Ray Cromwell del equipo de GWT realizó una increíble presentación en pantalla en la que se muestra la asistencia del mapa de origen en acción.

Otro ejemplo que reuní utiliza la biblioteca Traceur de Google, que te permite escribir ES6 (ECMAScript 6 o Next) y compilarlo en código compatible con ES3. El compilador Traceur también genera un mapa de orígenes. Mira esta demostración de las características y clases de ES6 que se usan como si fueran compatibles de forma nativa en el navegador, gracias al mapa de origen.

El área de texto de la demostración también te permite escribir ES6, que se compilará sobre la marcha y generará un mapa de origen junto con el código ES3 equivalente.

Depuración de Traceur ES6 con mapas de orígenes

Demostración: Escribe ES6, depura, visualiza la asignación de origen en acción

¿Cómo funciona el mapa de orígenes?

El único compilador y minificador de JavaScript que admite, por el momento, la generación de mapas de origen es el compilador Closure. (Más adelante te explicaré cómo utilizarlo). Una vez que hayas combinado y reducido tu código JavaScript, habrá un archivo de mapa de origen junto a él.

Actualmente, el compilador Closure no agrega el comentario especial al final que se requiere para indicar a las herramientas de desarrollo de navegadores que hay un mapa de origen disponible:

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

Esto permite que las herramientas para desarrolladores asignen llamadas a su ubicación en los archivos fuente originales. Anteriormente, la regla de comentario era //@, pero debido a algunos problemas con eso y los comentarios de compilación condicional de IE, se tomó la decisión de cambiarlo a //#. Actualmente, Chrome Canary, WebKit Nightly y Firefox 24+ son compatibles con la nueva directiva pragma de comentarios. Este cambio de sintaxis también afecta sourceURL.

Si no te gusta la idea del comentario extraño, como alternativa, puedes establecer un encabezado especial en tu archivo JavaScript compilado:

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

Como en el comentario, esto le indicará al consumidor del mapa de origen dónde buscar el mapa de origen asociado con un archivo JavaScript. Este encabezado también soluciona el problema de hacer referencia a mapas de origen en lenguajes que no admiten comentarios en una sola línea.

Ejemplo de mapas de origen activados y desactivados de WebKit Devtools

El archivo del mapa de fuentes solo se descargará si tienes habilitados los mapas de orígenes y las herramientas para desarrolladores están abiertas. También tendrás que cargar los archivos originales, de modo que las herramientas para desarrolladores puedan hacer referencia a ellos y mostrarlos cuando sea necesario.

¿Cómo genero un mapa de fuentes?

Deberás usar el compilador de cierre para reducir, concatenar y generar un mapa de fuentes para tus archivos JavaScript. El comando es el siguiente:

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

Las dos marcas de comando importantes son --create_source_map y --source_map_format. Esto es necesario porque la versión predeterminada es V2 y solo queremos trabajar con V3.

Anatomía de un mapa de orígenes

Para comprender mejor un mapa de origen, tomaremos un pequeño ejemplo de un archivo de mapa de origen que generaría el compilador Closure y profundizaremos en el funcionamiento de la sección "mapeos". El siguiente ejemplo es una leve variación del ejemplo de especificaciones de V3.

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

Arriba puedes ver que un mapa de origen es un literal de objeto que contiene mucha información interesante:

  • Número de versión en la que se basa el mapa de origen
  • El nombre del archivo del código generado (tu archivo de producción reducida/combinada)
  • sourceRoot te permite anteponer una estructura de carpetas a las fuentes; esta también es una técnica para ahorrar espacio
  • source contiene todos los nombres de archivos que se combinaron
  • names contiene todos los nombres de variables o métodos que aparecen en todo tu código.
  • Por último, la propiedad mappings es donde ocurre la magia con los valores de VLQ Base64. Aquí se logra ahorrar espacio de verdad.

VLQ Base64 y cómo mantener el mapa de origen pequeño

Originalmente, la especificación del mapa de origen tenía un resultado muy detallado de todas las asignaciones y daba como resultado que el mapa de origen tuviera alrededor de 10 veces el tamaño del código generado. La versión dos redujo esa cantidad en aproximadamente un 50%, y la versión tres lo volvió a reducir en otro 50%. Por lo tanto, para un archivo de 133 KB, se obtiene un mapa de origen de aproximadamente 300 KB.

Entonces, ¿cómo redujeron el tamaño y, al mismo tiempo, mantuvieron las asignaciones complejas?

Se usa VLQ (cantidad de longitud variable) junto con la codificación del valor en un valor Base64. La propiedad de asignaciones es una cadena muy grande. Dentro de esta cadena, hay punto y coma (;) que representa un número de línea dentro del archivo generado. Dentro de cada línea, hay comas (,) que representan cada segmento dentro de esa línea. Cada uno de estos segmentos puede ser 1, 4 o 5 en campos de longitud variable. Algunas pueden parecer más largas, pero contienen bits de continuación. Cada segmento se basa en el anterior, lo que ayuda a reducir el tamaño del archivo, ya que cada bit está relacionado con sus segmentos anteriores.

Desglose de un segmento dentro del archivo JSON de mapa de origen.

Como se mencionó anteriormente, cada segmento puede tener 1, 4 o 5 de longitud variable. Este diagrama se considera una longitud variable de cuatro con un bit de continuación (g). Desglosaremos este segmento y te mostraremos cómo el mapa de origen determina la ubicación original.

Los valores que se muestran arriba son solo valores decodificados en Base64. Hay un poco de procesamiento más para obtener sus valores verdaderos. En cada segmento, normalmente, se obtienen cinco respuestas:

  • Columna generada
  • Archivo original en el que apareció este
  • Número de línea original
  • Columna original
  • Y, si está disponible, el nombre original

No todos los segmentos tienen un nombre, un nombre de método o argumento, por lo que en todo el segmento cambiará entre cuatro y cinco longitudes variables. El valor g del diagrama del segmento anterior es lo que se denomina un bit de continuación, lo que permite una mayor optimización en la etapa de decodificación de VLQ Base64. Un bit de continuación te permite desarrollar un valor de segmento para que puedas almacenar números grandes sin tener que almacenar uno grande, una técnica muy inteligente de ahorro de espacio que tiene sus raíces en el formato MIDI.

Una vez procesado, el diagrama anterior AAgBC mostraría 0, 0, 32, 16, 1; 32 es el bit de continuación que ayuda a compilar el siguiente valor de 16. B puramente decodificado en Base64 es 1. Por lo tanto, los valores importantes que se usan son 0, 0, 16, 1. Esto nos permite saber que la línea 1 (las líneas se mantienen contadas por punto y dos puntos) la columna 0 del archivo generado se asigna al archivo 0 (el array de archivos 0 es foo.js), la línea 16 en la columna 1.

Para mostrar cómo se decodifican los segmentos, haré referencia a la biblioteca JavaScript del mapa de fuentes de Mozilla. También puedes ver el código fuente de asignación de las herramientas para desarrolladores de WebKit, también escrito en JavaScript.

Para entender correctamente cómo obtenemos el valor 16 de B, necesitamos tener conocimientos básicos de los operadores a nivel de bits y cómo funciona la especificación para la asignación de origen. El dígito anterior, g, se marca como un bit de continuación cuando se compara el dígito (32) y VLQ_CONTINUATION_BIT (binario 100000 o 32) mediante el operador AND (&) a nivel de bits.

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

Esto devuelve un 1 en cada posición de bits donde ambos aparecen. Por lo tanto, un valor decodificado en Base64 de 33 & 32 mostraría 32, ya que solo comparten la ubicación de 32 bits, como puedes ver en el diagrama anterior. Luego, esto aumenta el valor de cambio del bit en 5 para cada bit de continuación anterior. En el caso anterior, solo se movió 5 una vez, por lo que cambia 1 (B) a la izquierda por 5.

1 <<../ 5 // 32

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

Ese valor se convierte a partir de un valor con firma de VLQ desplazando el número (32) a la derecha un espacio.

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

Y así es como se convierte en 1 en 16. Este proceso puede parecer demasiado complicado, pero tiene más sentido una vez que las cifras empiezan a aumentar.

Posibles problemas de XSSI

La especificación menciona problemas de inclusión de secuencias de comandos entre sitios que podrían surgir debido al consumo de un mapa de fuentes. Para mitigar este problema, te recomendamos que antepongas ")]}" a la primera línea de tu mapa de origen a fin de invalidar JavaScript de manera intencional y que se genere un error de sintaxis. Las herramientas para desarrolladores de WebKit ya pueden controlar esto.

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

Como se muestra más arriba, los tres primeros caracteres se dividen para verificar si coinciden con el error de sintaxis en la especificación y, de ser así, se quitan todos los caracteres que conducen a la primera entidad de línea nueva (\n).

sourceURL y displayName en acción: funciones de evaluación y anónimas

Si bien no forman parte de las especificaciones del mapa de origen, las dos convenciones siguientes te permiten facilitar mucho el desarrollo cuando trabajas con evaluaciones y funciones anónimas.

El primer asistente es muy similar a la propiedad //# sourceMappingURL y, en realidad, se menciona en la especificación de la versión 3 del mapa de origen. Si incluyes el siguiente comentario especial en tu código, que se evaluará, podrás nombrar evaluaciones para que aparezcan como nombres más lógicos en tus herramientas para desarrolladores. Echa un vistazo a una demostración simple con el compilador CoffeeScript:

Demostración: Observa el código eval() como una secuencia de comandos a través de sourceURL

//# sourceURL=sqrt.coffee
Cómo se ve el comentario especial de sourceURL en las herramientas para desarrolladores

El otro te permite nombrar funciones anónimas con la propiedad displayName disponible en el contexto actual de la función anónima. Genera un perfil de la siguiente demostración para ver la propiedad displayName en acción.

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);
Se muestra la propiedad displayName en acción.

Cuando generes perfiles de tu código en las herramientas para desarrolladores, se mostrará la propiedad displayName en lugar de algo como (anonymous). Sin embargo, displayName está prácticamente muerto en el agua y no llegará a Chrome. Sin embargo, no se pierde toda la esperanza, y se sugirió una propuesta mucho mejor llamada debugName.

Al momento de escribir, los nombres de los evaluadores solo están disponibles en los navegadores Firefox y WebKit. La propiedad displayName solo está disponible en las noches de WebKit.

Juntémonos

En la actualidad, hay un debate muy extenso sobre la compatibilidad con el mapa de origen que se agrega a CoffeeScript. Revisa el problema y agrega tu compatibilidad para agregar la generación de mapa de origen al compilador CoffeeScript. Esta será una gran victoria para CoffeeScript y sus devotos seguidores.

UglifyJS también tiene un problema de mapa de fuentes que deberías consultar.

Hay muchas tools que generan mapas de origen, incluido el compilador de Coffeescript. Considero que este es un punto discutible ahora.

Cuantas más herramientas tengamos disponibles para generar mapas de código fuente, mejor será. Así que sigue adelante y solicita la compatibilidad con mapas de código fuente para tu proyecto favorito de código abierto.

No es perfecto

Algo que los mapas de origen no ofrecen en este momento son las expresiones de supervisión. El problema es que intentar inspeccionar un argumento o nombre de variable dentro del contexto de ejecución actual no devolverá nada, ya que en realidad no existe. Esto requeriría algún tipo de asignación inversa para buscar el nombre real del argumento o la variable que deseas inspeccionar en comparación con el nombre real del argumento o la variable en tu JavaScript compilado.

Por supuesto, este es un problema que se puede solucionar y, con más atención en los mapas de origen, podemos comenzar a ver algunas funciones increíbles y una mayor estabilidad.

Issues

Recientemente, jQuery 1.9 agregó compatibilidad con los mapas de origen cuando se entregan fuera de CDN oficiales. También señaló un error específico cuando se usan comentarios de compilación condicionales de IE (//@cc_on) antes de que se cargue jQuery. Desde entonces, hubo una confirmación para mitigar este problema uniendo la sourceMappingURL en un comentario de varias líneas. La lección que se debe aprender no debe usar comentarios condicionales.

Esto se abordó con el cambio de la sintaxis a //#.

Herramientas y recursos

Estos son algunos recursos y herramientas adicionales que deberías consultar:

Los mapas de origen son una utilidad muy poderosa del conjunto de herramientas de un desarrollador. Es muy útil poder mantener tu app web limpia, pero que se pueda depurar fácilmente. También es una herramienta de aprendizaje muy potente para que los desarrolladores más nuevos puedan ver cómo los desarrolladores experimentados estructuran y escriben sus apps sin tener que leer código reducido ilegible.

¿Qué esperas? Comienza a generar mapas de origen para todos los proyectos ahora mismo.