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
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
@import
Arquivos 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 @import
arquivos CSS -ed no arquivo principal (para salvar solicitações de vários arquivos). Se o @import
arquivo CSS pai e -ed estivesse em diretórios diferentes, os caminhos relativos no @import
arquivo -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 @import
linha 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:
- Meu minificador : minificador JS e CSS baseado em PHP
- UglifyJS ou UglifyJS2 : minificador JS baseado em node.js
- JShrink : minificador JS baseado em PHP
- clean-css : minificador CSS baseado em node.js