¿Alguna vez deseaste que tu código del cliente fuera 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 origen.
Los mapas de origen son una forma de asignar un archivo combinado o reducido a un estado sin compilar. Cuando compilas para producción, además de minificar y combinar los archivos JavaScript, se genera un mapa de origen que contiene información sobre los archivos originales. Cuando consultas una línea y un número de columna determinados en el código JavaScript generado, puedes realizar una búsqueda en el mapa de origen que muestra la ubicación original. Las herramientas para desarrolladores (actualmente, 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 estás ejecutando archivos no reducidos ni combinados.
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 consultará el mapa de origen pasando el número de línea y columna generados, y se mostrará la posición en el código original. Asegúrate de que la consola esté abierta para que puedas ver el resultado.
Mundo 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 Nightly. Para ello, haz clic en el engranaje de configuración en el panel de herramientas para desarrolladores y marca la opción "Habilitar mapas de origen".
Firefox 23 y versiones posteriores tienen habilitados los mapas de origen de forma predeterminada en las herramientas para desarrolladores integradas.
¿Por qué debería importarme los mapas de origen?
En este momento, la asignación de origen solo funciona entre JavaScript sin comprimir o combinado y JavaScript comprimido o no combinado, pero el futuro se ve prometedor con conversaciones sobre lenguajes compilados en JavaScript, como CoffeeScript, y hasta la posibilidad de agregar compatibilidad con preprocesadores de CSS, como SASS o LESS.
En el futuro, podríamos usar fácilmente casi cualquier idioma como si fuera compatible de forma nativa en el navegador con mapas de origen:
- CoffeeScript
- ECMAScript 6 y versiones posteriores
- SASS/LESS y otros
- Casi cualquier lenguaje que se compile en JavaScript
Mira esta presentación en pantalla de CoffeeScript que se depura en una compilación experimental de la consola de Firefox:
Recientemente, Google Web Toolkit (GWT) agregó compatibilidad con los mapas de origen. Ray Cromwell, del equipo de GWT, realizó una presentación en pantalla increíble en la que se muestra la compatibilidad con los mapas de origen en acción.
Otro ejemplo que armé 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. Consulta esta demostración de los atributos y las clases de ES6 que se usan como si se admitieran 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 más el código ES3 equivalente.
Demostración: escribe ES6, depura y observa la asignación de origen en acción
¿Cómo funciona el mapa de origen?
Por el momento, el único compilador o minificador de JavaScript que admite la generación de mapas de origen es Closure Compiler. (Te explicaré cómo usarlo más adelante). Una vez que hayas combinado y reducido tu código JavaScript, junto a él existirá un archivo de mapa de origen.
Actualmente, el compilador de Closure no agrega el comentario especial al final que se requiere para indicar a las herramientas para desarrolladores de un navegador que hay un mapa de origen disponible:
//# sourceMappingURL=/path/to/file.js.map
Esto permite que las herramientas para desarrolladores asignen las llamadas a su ubicación en los archivos de origen originales. Anteriormente, el comentario pragma era //@
, pero debido a algunos problemas con ese elemento 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 y versiones posteriores admiten la nueva pragma de comentarios. Este cambio de 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 le indicará al consumidor de mapas 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.
El archivo del mapa de origen solo se descargará si tienes habilitados los mapas de origen y tienes abiertas las herramientas para desarrolladores. 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 origen?
Deberás usar el compilador Closure para minificar, concatenar y generar un mapa de origen 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 obligatorio, ya que la versión predeterminada es la V2 y solo queremos trabajar con la V3.
Anatomía de un mapa de origen
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 con más detalle cómo funciona la sección "mappings". El siguiente ejemplo es una ligera variación del ejemplo de la especificación de la versión 3.
{
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 objeto literal que contiene mucha información interesante:
- Es el número de versión en el que se basa el mapa de origen.
- Es el nombre del archivo del código generado (tu archivo de producción reducido o combinado).
- sourceRoot te permite anteponer las fuentes con una estructura de carpetas, lo que también es una técnica para ahorrar espacio.
- sources contiene todos los nombres de archivo que se combinaron.
- names contiene todos los nombres de variables o métodos que aparecen en tu código.
- Por último, la propiedad de asignaciones es donde ocurre la magia con los valores de VLQ de Base64. Aquí es donde se ahorra espacio.
VLQ de Base64 y mantén 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 el mapa de origen tenía alrededor de 10 veces el tamaño del código generado. La versión dos lo redujo en alrededor de un 50% y la versión tres lo redujo nuevamente en otro 50%, por lo que, para un archivo de 133 KB, terminas con un mapa de origen de alrededor de 300 KB.
Entonces, ¿cómo se redujo el tamaño y se mantuvieron las asignaciones complejas?
VLQ (cantidad de longitud variable) se usa junto con la codificación del valor en un valor Base64. La propiedad mappings es una cadena muy grande. Dentro de esta cadena, hay puntos y coma (;) que representan 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 es de 1, 4 o 5 en campos de longitud variable. Algunos pueden parecer más largos, pero contienen bits de Continuation. Cada segmento se basa en el anterior, lo que ayuda a reducir el tamaño del archivo, ya que cada bit es relativo a sus segmentos anteriores.
Como se mencionó anteriormente, cada segmento puede ser de 1, 4 o 5 de longitud variable. Este diagrama se considera una longitud variable de cuatro con un bit de Continuation (g). Analizaremos este segmento y te mostraremos cómo el mapa fuente calcula la ubicación original.
Los valores que se muestran arriba son solo los valores decodificados en Base64. Hay más procesamiento para obtener sus valores reales. Por lo general, cada segmento tiene cinco elementos:
- 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, un nombre de método o un argumento, por lo que los segmentos cambiarán entre cuatro y cinco longitudes variables. El valor g en el diagrama de segmentos anterior es lo que se denomina un bit de Continuation, que permite una mayor optimización en la etapa de decodificación de VLQ de Base64. Un bit de Continuation te permite basarte en un valor de segmento para almacenar números grandes sin tener que almacenar un número grande, una técnica muy inteligente para ahorrar espacio que tiene sus raíces en el formato MIDI.
El diagrama AAgBC
anterior, una vez procesado, mostraría 0, 0, 32, 16, 1, siendo el 32 el bit de Continuation que ayuda a crear el siguiente valor de 16. B decodificado en Base64 es 1. Por lo tanto, los valores importantes que se usan son 0, 0, 16 y 1. Esto nos permite saber que la línea 1 (las líneas se cuentan con los puntos y comas) de la columna 0 del archivo generado se asigna al archivo 0 (el array de archivos 0 es foo.js), línea 16 en la columna 1.
Para mostrar cómo se decodifican los segmentos, haré referencia a la biblioteca de JavaScript de Source Map de Mozilla. También puedes consultar el código de asignación de origen 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, debemos tener un conocimiento básico de los operadores 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 comparando el dígito (32) y VLQ_CONTINUATION_BIT (binario 100000 o 32) con el operador Y (&) de bits.
32 & 32 = 32
// or
100000
|
|
V
100000
Esto muestra un 1 en cada posición de bit en la que ambos lo muestran. 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. Esto aumenta el valor de desplazamiento de bits en 5 para cada bit de Continuation anterior. En el caso anterior, solo se desplaza 5 veces, por lo que se desplaza 1 (B) a la izquierda en 5.
1 <<../ 5 // 32
// Shift the bit by 5 spots
______
| |
V V
100001 = 100000 = 32
Luego, ese valor se convierte de un valor firmado de VLQ desplazando el número (32) un lugar hacia la derecha.
32 >> 1 // 16
//or
100000
|
|
V
010000 = 16
Así es como conviertes 1 en 16. Esto puede parecer un proceso demasiado complicado, pero una vez que las cifras comienzan a aumentar, tiene más sentido.
Posibles problemas de XSSI
La especificación menciona problemas de inclusión de secuencias de comandos entre sitios que podrían surgir por el consumo de un mapa de origen. Para mitigar esto, te recomendamos que agregues ")]}
" al principio de la primera línea de tu mapa de origen para invalidar JavaScript de forma deliberada y que se genere un error de sintaxis. Las herramientas para desarrolladores de WebKit ya pueden manejar esto.
if (response.slice(0, 3) === ")]}") {
response = response.substring(response.indexOf('\n'));
}
Como se muestra arriba, los primeros tres caracteres se cortan para comprobar si coinciden con el error de sintaxis en la especificación y, de ser así, se quitan todos los caracteres que preceden a la primera entidad de línea nueva (\n).
sourceURL
y displayName
en acción: Eval y funciones anónimas
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 elemento auxiliar se parece mucho a la propiedad //# sourceMappingURL
y se menciona en las especificaciones de Source Map V3. Si incluyes el siguiente comentario especial en tu código, que se evaluará, puedes nombrar evaluaciones para que aparezcan con nombres más lógicos en tus herramientas de desarrollo. Consulta una demostración simple con el compilador de CoffeeScript:
Demostración: Cómo ver el código de eval()
como una secuencia de comandos a través de sourceURL
//# sourceURL=sqrt.coffee
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);
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 inactivo y no se incluirá en Chrome. Pero no todo está perdido, ya que se sugirió una propuesta mucho mejor llamada debugName.
En el momento de escribir este artículo, la asignación de nombres de eval solo está disponible en los navegadores Firefox y WebKit. La propiedad displayName
solo está disponible en las compilaciones nocturnas de WebKit.
Unámonos
Actualmente, hay un debate muy extenso sobre la compatibilidad con los mapas de origen que se agregará a CoffeeScript. Revisa el problema y agrega tu compatibilidad para que se agregue la generación de mapas de origen al compilador de CoffeeScript. Esto será una gran victoria para CoffeeScript y sus seguidores.
UglifyJS también tiene un problema con el mapa de origen que deberías revisar.
Muchas herramientas generan mapas de origen, incluido el compilador de CoffeeScript. Ahora considero que este es un punto discutible.
Cuanto más herramientas tengamos disponibles que puedan generar mapas de origen, mejor, así que no dudes en solicitar o agregar compatibilidad con mapas de origen a tu proyecto de código abierto favorito.
No es perfecto
Por el momento, los mapas de origen no admiten expresiones de vigilancia. El problema es que intentar inspeccionar un argumento o un nombre de variable dentro del contexto de ejecución actual no mostrará nada, ya que no existe realmente. 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 código JavaScript compilado.
Por supuesto, este es un problema que se puede resolver y, con más atención en los mapas de origen, podemos comenzar a ver algunas funciones increíbles y una mejor estabilidad.
Problemas
Recientemente, jQuery 1.9 agregó compatibilidad con los mapas de origen cuando se entregaban desde CDN oficiales. También señaló un error peculiar cuando se usan comentarios de compilación condicional de IE (//@cc_on) antes de que se cargue jQuery. Desde entonces, se realizó un commit para mitigar esto uniendo sourceMappingURL en un comentario de varias líneas. Lección que se debe aprender: no uses comentarios condicionales.
Esto se solucionó con el cambio de la sintaxis a //#
.
Herramientas y recursos
Estos son algunos recursos y herramientas adicionales que deberías consultar:
- Nick Fitzgerald tiene una bifurcación de UglifyJS con compatibilidad con mapas de origen.
- Paul Irish tiene una demo práctica en la que muestra los mapas de origen.
- Consulta el conjunto de cambios de WebKit de cuándo se soltó
- El conjunto de cambios también incluyó una prueba de diseño que inició todo este artículo.
- Mozilla tiene un error que debes seguir sobre el estado de los mapas de origen en la consola integrada.
- Conrad Irwin escribió una gema de mapa de origen muy útil para todos los usuarios de Ruby.
- Más información sobre nombres de evaluación y la propiedad displayName
- Puedes consultar la fuente de Closure Compilers para crear mapas de origen.
- Hay algunas capturas de pantalla y se habla de la compatibilidad con los mapas de origen de GWT.
Los mapas de origen son una utilidad muy potente en el conjunto de herramientas de un desarrollador. Es muy útil poder mantener tu app web liviana, pero fácil de depurar. También es una herramienta de aprendizaje muy potente para que los desarrolladores más nuevos vean cómo los desarrolladores experimentados estructuran y escriben sus apps sin tener que navegar por código reducido ilegible.
¿Qué esperas? Comienza a generar mapas de origen para todos los proyectos ahora.