Introducción a los mapas de origen de JavaScript

¿Alguna vez quisiste poder mantener tu código del cliente legible y, lo que es más importante, depurable incluso después de combinarlo y reducirlo, sin afectar el rendimiento? Ahora puedes hacerlo gracias a la magia de los mapas de fuentes.

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 tus archivos JavaScript, se genera un mapa de fuentes que contiene información sobre los archivos originales. Cuando consultas un número determinado de línea y columna en el 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 fuentes automáticamente y hacer que parezca que estás ejecutando archivos sin minificar y sin combinar.

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

Ejemplo de la biblioteca de mapa de fuentes de JavaScript de Mozilla en acción.

Caso real

Antes de ver la siguiente implementación real de los mapas de origen, asegúrate de haber habilitado la función de mapas de origen en Chrome Canary o WebKit todas las noches haciendo clic en el engranaje de configuración en el panel de herramientas para desarrolladores y marcando la casilla "Habilitar mapas de origen" de 12 a 1 con la nueva opción de compresión.

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

Firefox 23 y las versiones posteriores tienen mapas de orígenes habilitados de forma predeterminada en las herramientas integradas para desarrolladores.

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

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

En este momento, la asignación de fuentes solo funciona entre JavaScript sin comprimir o combinado y JavaScript comprimido o sin combinar, pero el futuro se ve prometedor con conversaciones sobre lenguajes compilados con 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 fuese compatible de forma nativa con el navegador con mapas de origen:

  • CoffeeScript
  • ECMAScript 6 y versiones posteriores
  • SASS/LESS y otras opciones
  • Prácticamente cualquier lenguaje que se compile en JavaScript

Echa un vistazo a 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 mostraba la compatibilidad con el mapa de fuentes en acción.

En otro ejemplo que armé, se usa 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 origen. Mira esta demostración de rasgos y clases de ES6 que se usan como compatibles de forma nativa en el navegador gracias al mapa de fuentes.

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 y el código ES3 equivalente.

Depuración de Traceur ES6 con mapas de fuentes

Demostración: Escribir ES6, depurarlo y ver la asignación de fuentes en acción

¿Cómo funciona el mapa de fuentes?

El único compilador/minificador de JavaScript que admite, por el momento, la generación de mapas de origen es el compilador Closure. (Explicaremos cómo utilizarlo más adelante). Una vez que hayas combinado y reducido tu JavaScript, habrá un archivo de mapa de origen.

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

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

De esta manera, las herramientas para desarrolladores pueden asignar llamadas a su ubicación en los archivos fuente originales. Anteriormente, la pragma de comentario era //@, pero, debido a algunos problemas con ella y los comentarios de la compilación condicional de IE, se tomó la decisión de cambiarla a //#. Actualmente, Chrome Canary, WebKit Nightly y Firefox 24 (o versiones posteriores) admiten la nueva directiva pragma de comentarios. Este cambio en la sintaxis también afecta a sourceURL.

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

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

Al igual que el comentario, esto indicará a tu consumidor del mapa de fuentes 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 de una sola línea.

Ejemplo de Herramientas para desarrolladores de WebKit de mapas de fuentes activados y mapas de fuentes desactivados.

El archivo del mapa de origen solo se descargará si los mapas de origen están habilitados y tus herramientas para desarrolladores están abiertas. También deberás subir tus archivos originales para 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, concatar 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 importantes del comando son --create_source_map y --source_map_format. Esto es necesario, ya que la versión predeterminada es V2 y solo queremos trabajar con V3.

Anatomía de un mapa de fuentes

Para comprender mejor un mapa de origen, tomaremos un pequeño ejemplo de un archivo de mapa de origen que generaría el compilador de Closure y analizaremos en mayor detalle cómo se comportan las "asignaciones" cómo funciona. El siguiente ejemplo es una ligera variación del ejemplo con las 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 el que se basa el mapa de fuentes
  • El nombre de archivo del código generado (tu archivo de producción minifed/combinado)
  • sourceRoot te permite anteponer a las fuentes con una estructura de carpetas; esta también es una técnica para ahorrar espacio.
  • source contiene todos los nombres de archivo que se combinaron
  • name contiene todos los nombres de variables o métodos que aparecen en todo el código.
  • Por último, la propiedad de asignaciones es donde ocurre la magia mediante los valores de VLQ Base64. Aquí se realiza el ahorro de espacio real.

VLQ Base64 y mantener el mapa de origen pequeño

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

Entonces, ¿cómo redujo el tamaño sin dejar de mantener los mapeos complejos?

VLQ (Cantidad de longitud variable) se usa junto con la codificación del valor en un valor Base64. La propiedad de mapeos es una string 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 tiene 1, 4 o 5 en campos de longitud variable. Algunos pueden parecer más largos, 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 se relaciona con sus segmentos anteriores.

Es el desglose de un segmento en el archivo JSON del mapa de origen.

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

Los valores que se muestran arriba son puramente valores decodificados en Base64. Se requiere más procesamiento para obtener sus valores verdaderos. Por lo general, cada segmento se encarga de cinco cosas:

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

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

El diagrama anterior AAgBC, una vez que se procesó, mostraría 0, 0, 32, 16, 1, es decir, 32 sería 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 el punto y coma), 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 de mapas de origen de Mozilla. También puedes consultar el código de asignación fuente de las herramientas para desarrolladores de WebKit, que también está escrito en JavaScript.

Para comprender 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 fuentes. El dígito anterior, g, se marca como bit de continuación comparando el dígito (32) y el VLQ_CONTINUATION_BIT (binario 100000 o 32) mediante el operador AND (&).

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

Esto devuelve un 1 en cada posición de bit donde aparece ambos. 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 se puede ver en el diagrama anterior. Luego, esto aumenta el valor de desplazamiento del bit a 5 para cada bit de continuación anterior. En el caso anterior, solo se desplaza 5 una vez, por lo que desplaza 1 (B) por 5 a la izquierda.

1 <<../ 5 // 32

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

Luego, ese valor se convierte a partir de un valor con signo VLQ desplazando el número (32) a la derecha un punto.

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

Ya está. Así es como conviertes 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 del consumo de un mapa de fuentes. Para mitigar este problema, te recomendamos que antepongas ")]}" la primera línea del mapa de fuentes invalidar JavaScript de forma deliberada y que se produzca un error de sintaxis. Las herramientas para desarrolladores de WebKit ya pueden encargarse de esto.

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

Como se muestra 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 la primera línea (\n).

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

Si bien no forman parte de las especificaciones del mapa de origen, las siguientes dos convenciones 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 V3 del mapa de fuentes. Si incluyes el siguiente comentario especial en tu código, que se evaluará, puedes nombrar las evaluaciones para que aparezcan como nombres más lógicos en tus herramientas para desarrolladores. Mira una demostración sencilla con el compilador CoffeeScript:

Demostración: Observa el código de eval() que se muestra 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 asistente 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);
Mostrar 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 podrá acceder a Chrome. Sin embargo, no se perdió la esperanza y se sugirió una propuesta mucho mejor llamada debugName.

Al momento de escribir el nombre "eval", solo está disponible en los navegadores Firefox y WebKit. La propiedad displayName solo está disponible en modos nocturnos de WebKit.

Unámonos

Actualmente, hay un extenso debate sobre la compatibilidad con mapas de origen que se agrega a CoffeeScript. Revisa el problema y agrega tu compatibilidad para que la generación de mapas de origen se agregue al compilador CoffeeScript. Esto será un gran logro para CoffeeScript y sus devotos seguidores.

UglifyJS también tiene un error en el mapa de fuentes que también debes analizar.

Muchas herramientas generan mapas de origen, incluido el compilador Coffeescript. Considero que esto ahora es discutible.

Mientras más herramientas disponibles para nosotros puedan generar mapas de origen, mejor estaremos, así que pregunta o agrega asistencia de mapas de código fuente a tu proyecto de código abierto favorito.

No es perfecta

Algo que no se pueden abordar en los mapas de origen en este momento son las expresiones supervisadas. El problema es que, si intentas inspeccionar un argumento o el nombre de una variable dentro del contexto de ejecución actual, no se mostrará 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, es un problema que se puede resolver y, con más atención en los mapas de origen, podemos comenzar a ver algunas funciones asombrosas y una mejor estabilidad.

Problemas

Recientemente, jQuery 1.9 agregó compatibilidad con mapas de origen cuando se entrega desde CDN oficiales. También señalaba un error específico cuando se usaban comentarios de compilación condicional de IE (//@cc_on) antes de que se cargara jQuery. Desde entonces, ha habido una confirmación para mitigar esto uniendo sourceMappingURL en un comentario de varias líneas. La lección que se debe aprender no utiliza el comentario condicional.

Desde entonces, se solucionó 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 en el conjunto de herramientas de un desarrollador. Es muy útil poder mantener tu app web limpia, pero fácilmente depurable. También es una herramienta de aprendizaje muy poderosa para que los desarrolladores más nuevos puedan ver cómo los desarrolladores experimentados estructuran y escriben sus apps sin tener que explorar código reducido ilegible.

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