No quieres construir tu propio minificador

Imagen

Esta publicación se origina en mi blog personal, en http://www.mullie.eu/dont-build-your-own-minifier/

Es probable que todos los desarrolladores hayan considerado al menos escribir su propio marco o CMS. Hasta que empieces a darte cuenta de cuánto trabajo es y cuántos de tus problemas ya han sido resueltos por otra persona. Luego, tira la toalla y comienza a usar (y con suerte, a contribuir) a proyectos de código abierto existentes que se adapten a sus necesidades. Escribir un minificador es muy parecido.

Mientras trabajábamos en un CMS que habíamos comenzado, queríamos servir nuestro CSS y JavaScript minificado, automáticamente. Lanzamos algunas expresiones regulares a esos archivos estáticos. Con el tiempo, se volvió más complejo, se convirtió en un proyecto propio.

Minificar

Estado de construcción
Cobertura de código
Calidad del código
Ultima versión
Descargas totales
Licencia

Como puede ver (¡mire los botones brillantes!), Este minificador basado en PHP todavía existe. Activo, incluso: solo recientemente le he dado algunas actualizaciones importantes.

Puede probarlo en línea en http://www.minifier.org .

Caracteristicas

CSS

  • Comentarios de tiras
  • Tiras de espacios en blanco
  • Importa @importarchivos CSS -ed
  • Incluye peque√Īos archivos est√°ticos en el archivo minificado (codificado en base64)
  • Acorta los c√≥digos de color hexadecimales

JS

  • Comentarios de tiras
  • Tiras de espacios en blanco

Lecciones aprendidas

No te atraí a esta publicación para presumir de las características, ¡así que hablemos de algunas de las dificultades!

CSS

El minificador de CSS fue el más fácil de construir. CSS no tiene una lógica compleja, es bastante sencillo.

Hasta que encontramos caminos relativos rompi√©ndose …

Una de las características del minificador de CSS es que incluirá todo el contenido de los @importarchivos CSS -ed en el archivo principal (para guardar solicitudes de varios archivos). Si el @importarchivo CSS padre & -ed estuviera en directorios diferentes, las rutas relativas en el @importarchivo -ed serían incorrectas:

/css/parent.css

@import 'subdir/child.css';

/css/subdir/child.css

body: {
background
: url('../../images/my-fancy-background.gif');
}

Si simplemente reemplazamos la @importlínea en parent.css con el contenido de subdir / child.css , vería que la ruta de la imagen de fondo ahora sería incorrecta. Todavía haría referencia a ../../images/my-fancy-background.gif , pero ahora usaría la ubicación de parent.css (que está en un directorio superior) para resolver esa ruta en contra.

Esto no solo era un problema potencial para combinar importaciones, sino que también sería un problema cuando el directorio de destino en el que escribirás los archivos CSS minificados es diferente al de tu archivo fuente. Si eres como yo, querrás mantenerlos separados, por lo que esto también podría ser un problema.

De todos modos, ese problema se ha abordado. El resto del minificador de CSS es relativamente sencillo, aunque algunas de las expresiones regulares son bastante complejas, principalmente debido a diferencias en la sintaxis al hacer referencia a otros archivos:

@import file.css;
@import 'file.css';
@import "file.css";
@import url(file.css);
@import url('file.css');
@import url("file.css");

JS

JavaScript fue una historia completamente diferente. Comencemos diciendo que todavía no estoy 100% satisfecho con ese minificador. JavaScript es un lenguaje complejo y, para optimizar correctamente el código JavaScript, debe poder interpretarlo correctamente. Entonces puede deshacerse correctamente del código redundante. Desafortunadamente, no construí un intérprete de JavaScript (¡ahora eso habría sido un proyecto paralelo!)

De hecho, preferiría alejarme de la implementación actual basada en expresiones regulares (principalmente porque es intensiva / lenta), pero no creo que esté trabajando en eso pronto. La velocidad de minimización solo será lenta en archivos realmente grandes e, incluso entonces, solo se minificará una vez (después de eso, cada nuevo usuario debería obtener la versión ya minificada).

Ahora en las partes desagradables.

Cadenas, comentarios y expresiones regulares

Imagina que quieres eliminar todos los comentarios de una sola l√≠nea del c√≥digo fuente de JavaScript: parece simple, ¬Ņverdad? Todo lo que necesitamos es algo como:

$content = preg_replace('|//.*$|m', '', $content);

¬°Derecho! Sin embargo, ¬Ņy si este fuera nuestro contenido?

alert("Here's a string that happens to have 2 // inside of it");

O quiz√°s:

var a=/abc\/def\//.test("abc");

Nuestro código fuente se habría reducido a cualquiera de estos, lo que lo habría roto:

alert("Here's a string that happens to have 2
var a=/abc\/def\

Es importante conocer el contexto en el que est√° operando:

  • No se debe cambiar nada en una cadena: est√°n destinados a tener todos los caracteres que tienen
  • Lo mismo para las expresiones regulares (que pueden confundirse f√°cilmente con comentarios).

Esto significa revisar el código fuente carácter a carácter, para ver exactamente cuándo comienza un comentario (que podemos eliminar por completo) o una cadena o expresión regular (que debe conservarse por completo).

ASI

Otro rompepelotas: inserción automática de punto y coma . JavaScript no requiere que las declaraciones terminen con un punto y coma. Si no encuentra un punto y coma y lo que está en la siguiente línea no tiene sentido en la misma declaración, se recuperará automáticamente como si hubiera un punto y coma que termina la línea anterior.

Cuando se minimiza el código fuente, se trata de deshacerse de la mayor cantidad posible de código redundante, incluidas las nuevas líneas. Sin embargo, debido a ASI, no podemos eliminar las nuevas líneas de manera confiable: si se omitió el punto y coma, unir ambas líneas puede hacer que el código deje de tener sentido. P.ej:

var a = 1,
b
= 2
var a = 1
var b = 2

Si tuviéramos que eliminar las nuevas líneas para ambos, obtendríamos:

var a = 1,b = 2
var a = 1var b = 2

Ahora ese √ļltimo no se ve bien, ¬Ņverdad?

He solucionado este problema en particular mediante:

  • eliminando nuevas l√≠neas alrededor de la mayor√≠a de los operadores
  • reemplazar l√≠neas nuevas por espacios para algunas palabras clave
  • eliminando los espacios restantes cuando en cualquier lado hay un valor / no variable / …

Esto nos lleva a la mayor parte del camino con respecto a la eliminaci√≥n de nuevas l√≠neas, pero todav√≠a hay algunas que a√ļn no se pueden eliminar de manera confiable sin interpretar correctamente el c√≥digo. Considerar:

function test()
{
return 'test';
}

y:

var string = test()
alert
(string)

Uno de los caracteres despu√©s del cual no eliminar√© las l√≠neas nuevas es el par√©ntesis de cierre, porque se puede usar en m√ļltiples contextos diferentes. En nuestro primer ejemplo, estar√≠amos bien:

function test(){return 'test'}

En nuestro segundo ejemplo, sin embargo, no tanto. Esto, nuevamente, causaría un error:

var string=test()alert(string)

No estoy particularmente en una caza de brujas contra las nuevas líneas: son solo 1 carácter, como un punto y coma. Pero sí, todavía sobreviven algunos que podrían omitirse por completo. Digamos que seguiré ignorando esto por ahora.

Una ventaja de ASI para minificar es que podemos omitir el √ļltimo punto y coma (si lo hay) del c√≥digo fuente, y el √ļltimo punto y coma justo antes de cerrar un bloque (justo antes del }car√°cter). ASI entrar√° en acci√≥n aqu√≠, y podemos estar seguros de que no entrar√° en conflicto con una nueva declaraci√≥n a partir de ahora.

Contribuir

En lugar de construir su propio minificador, es posible que desee considerar usar y contribuir a las alternativas existentes. Cualquier proyecto aceptar√° felizmente tu ayuda, as√≠ que aqu√≠ tienes una peque√Īa lista de minificadores: