Você não quer construir seu próprio minificador

Cenário

Esta postagem foi originada do meu blog pessoal, em http://www.mullie.eu/dont-build-your-own-minifier/

Cada desenvolvedor provavelmente pelo menos considerou escrever sua própria estrutura ou CMS. Até você começar a perceber o quanto isso dá trabalho e quanto dos seus problemas já foram resolvidos por outra pessoa. Então você joga a toalha e começa a usar (e, com sorte, a contribuir) para projetos de código aberto existentes que atendam às suas necessidades. Escrever um minificador é muito parecido.

Enquanto trabalhamos em um CMS que tínhamos começado, queríamos servir nosso CSS e JavaScript minimizado, automaticamente. Jogamos algumas expressões regulares nesses arquivos estáticos. Eventualmente, tornou-se mais complexo, tornou-se um projeto próprio.

Minify

Status da versão
Cobertura de código
Qualidade do código
Última versão
Total de downloads
Licença

Como você pode ver (veja os botões brilhantes!), Esse minificador baseado em PHP ainda está por aí. Ativo, até: Só recentemente fiz algumas atualizações importantes.

Você pode experimentá-lo online em http://www.minifier.org .

Características

CSS

  • Retira comentários
  • Tiras de espaço em branco
  • @importArquivos CSS importados
  • Inclui pequenos arquivos estáticos no arquivo reduzido (codificado em base64)
  • Encurta códigos de cores hexadecimais

JS

  • Retira comentários
  • Tiras de espaço em branco

Lições aprendidas

Não te atraí para este post para se gabar dos recursos, então vamos falar sobre algumas das dificuldades!

CSS

O minificador CSS foi o mais fácil de construir. CSS não tem lógica complexa, é bastante simples.

Até que encontramos caminhos relativos quebrando …

Um dos recursos do minificador CSS é que ele incluirá todo o conteúdo dos @importarquivos CSS -ed no arquivo principal (para salvar solicitações de vários arquivos). Se o @importarquivo CSS pai e -ed estivesse em diretórios diferentes, os caminhos relativos no @importarquivo -ed estariam incorretos:

/css/parent.css

@import 'subdir/child.css';

/css/subdir/child.css

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

Se apenas substituirmos a @importlinha em parent.css pelo conteúdo de subdir / child.css , você verá que o caminho para a imagem de fundo agora estaria incorreto. Ele ainda faria referência a ../../images/my-fancy-background.gif , mas agora usaria a localização de parent.css (que está em um diretório superior) para resolver esse caminho.

Isso não era apenas um problema potencial para combinar importações, mas também provaria ser um problema quando o diretório de destino no qual você gravará os arquivos CSS minificados for diferente do seu arquivo de origem. Se você for como eu, gostaria de mantê-los separados, então isso também pode ser um problema.

De qualquer forma, esse problema foi resolvido. O resto do minificador CSS é relativamente simples, embora algumas das expressões regulares sejam bastante complexas, principalmente devido a diferenças na sintaxe ao fazer referência a outros arquivos:

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

JS

JavaScript era uma outra história. Vamos começar dizendo que ainda não estou 100% satisfeito com esse minificador. JavaScript é uma linguagem complexa e, para otimizar adequadamente o código JavaScript, você teria que ser capaz de interpretá-lo corretamente. Então, você pode se livrar adequadamente do código redundante. Infelizmente, não construí um interpretador de JavaScript (agora, isso teria sido um projeto paralelo!)

Na verdade, eu preferiria deixar de usar a implementação atual baseada em regex (principalmente porque é intensiva / lenta), mas não acho que vou trabalhar nisso tão cedo. A velocidade de redução só será lenta em arquivos realmente grandes e, mesmo assim, você só reduzirá uma vez (depois disso, cada novo usuário deve obter a versão já reduzida.)

Agora nas partes desagradáveis.

Strings, comentários e expressões regulares

Imagine que você deseja remover todos os comentários de uma linha do código-fonte JavaScript: Parece simples, certo? Tudo o que precisamos é algo como:

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

Direito! No entanto, e se esse fosse o nosso conteúdo?

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

Ou talvez:

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

Nosso código-fonte teria sido reduzido a qualquer um destes, o que o quebraria:

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

É importante saber o contexto em que você está operando:

  • Nada em uma string deve ser alterado: eles devem ter todos os caracteres que possuem
  • O mesmo para expressões regulares (que podem ser facilmente confundidas com comentários!)

Isso significa percorrer o código-fonte caractere por caractere, para ver exatamente quando um comentário (que podemos remover completamente) ou string ou expressão regular (que deve ser preservada inteiramente) começa.

ASI

Outro destruidor de bolas: inserção automática de ponto e vírgula . JavaScript não exige que as instruções sejam encerradas com um ponto e vírgula. Se não encontrar um ponto e vírgula e o que quer que esteja na próxima linha não fizer sentido na mesma instrução, ele se recuperará automaticamente como se houvesse um ponto e vírgula terminando a linha anterior.

Ao minimizar o código-fonte, tudo se resume a se livrar do máximo de código redundante possível, incluindo novas linhas. Por causa do ASI, no entanto, não podemos remover novas linhas de maneira confiável: se o ponto-e-vírgula for omitido, juntar as duas linhas pode fazer com que o código deixe de fazer sentido. Por exemplo:

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

Se removêssemos as novas linhas para ambos, obteríamos:

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

Agora, esse último não parece bom, não é?

Eu resolvi esse problema específico:

  • eliminando novas linhas em torno da maioria dos operadores
  • substituindo novas linhas por espaços para algumas palavras-chave
  • removendo espaços restantes quando em qualquer lado é um / valor / não variável / …

Isso nos leva a maior parte do caminho com relação à eliminação de novas linhas, mas ainda há alguns que ainda não podem ser removidos de forma confiável sem a interpretação adequada do código. Considerar:

function test()
{
return 'test';
}

e:

var string = test()
alert
(string)

Um dos caracteres depois do qual não estou removendo novas linhas é o parêntese de fechamento, porque ele pode ser usado em vários contextos diferentes. Em nosso primeiro exemplo, estaríamos bem:

function test(){return 'test'}

Em nosso segundo exemplo, porém, nem tanto. Novamente, isso causaria um erro:

var string=test()alert(string)

Não estou particularmente em uma caça às bruxas contra novas linhas: eles têm apenas 1 caractere, como um ponto e vírgula. Mas sim, alguns ainda sobrevivem que poderiam ser totalmente omitidos. Vamos apenas dizer que vou continuar ignorando isso por enquanto.

Uma vantagem do ASI para minimizar é que podemos omitir o último (se houver) ponto-e-vírgula do código-fonte e o último ponto-e-vírgula logo antes de fechar um bloco (logo antes do }caractere). A ASI entrará em ação aqui e podemos ter certeza de que não entrará em conflito com uma nova declaração a partir da próxima!

Contribuir

Em vez de construir seu próprio minificador, você pode considerar o uso e a contribuição de alternativas existentes. Qualquer projeto aceitará sua ajuda de bom grado, então aqui está uma pequena lista de minificadores: