Introdução aos mapas de origem JavaScript

Você já pensou em manter seu código do lado do cliente legível e, o mais importante, depurável, mesmo depois de combiná-lo e minificá-lo, sem afetar o desempenho? Agora você pode aproveitar a mágica dos mapas de origem.

Os mapas de origem são uma forma de mapear um arquivo combinado/minimizado de volta para um estado não construído. Ao compilar para produção, além de reduzir e combinar seus arquivos JavaScript, você gera um mapa de origem que contém informações sobre seus arquivos originais. Ao consultar um determinado número de linha e coluna no JavaScript gerado, você pode fazer uma pesquisa no mapa de origem que retorna o local original. As ferramentas para desenvolvedores (atualmente as versões noturnas do WebKit, Google Chrome ou Firefox 23+) podem analisar o mapa de origem automaticamente e fazer parecer que você está executando arquivos não reduzidos e não combinados.

A demonstração permite clicar com o botão direito do mouse em qualquer lugar da área de texto que contenha a origem gerada. Selecionar "Get original location" consultará o mapa de origem transmitindo o número de linha e coluna gerados e retornará a posição no código original. Verifique se o console está aberto para que você possa ver a saída.

Exemplo da biblioteca de mapas de origem do Mozilla JavaScript em ação.

Mundo real

Antes de visualizar a seguinte implementação dos mapas de origem no mundo real, certifique-se de ter ativado o recurso de mapas de origem no Chrome Canary ou no WebKit Nightly clicando no ícone de engrenagem das configurações no painel de ferramentas para desenvolvedores e marcando a opção "Ativar mapas de origem".

Como ativar mapas de origem nas ferramentas para desenvolvedores do WebKit.

O Firefox 23+ tem mapas de origem ativados por padrão nas ferramentas de desenvolvimento integradas.

Como ativar mapas de origem nas ferramentas para desenvolvedores do Firefox.

Por que devo me preocupar com mapas de origem?

No momento, o mapeamento de origem só está funcionando entre JavaScript descompactado/combinado e JavaScript compactado/não combinado, mas o futuro parece brilhar com conversas sobre linguagens compiladas para JavaScript, como o CoffeeScript, e até mesmo com a possibilidade de adicionar suporte a pré-processadores CSS, como SASS ou LESS.

No futuro, poderíamos usar facilmente quase qualquer linguagem como se ela tivesse suporte nativo no navegador com os mapas de origem:

  • CoffeeScript
  • ECMAScript 6 e além
  • SASS/LESS e outros
  • Qualquer linguagem que possa ser compilada em JavaScript

Dê uma olhada no screencast do CoffeeScript, sendo depurado em uma versão experimental do console do Firefox:

Recentemente, o Google Web Toolkit (GWT) adicionou suporte para mapas de origem. Ray Cromwell, da equipe do GWT, fez um screencast incrível mostrando o suporte ao mapa de origem em ação.

Outro exemplo que reuni usa a biblioteca Traceur do Google, que permite escrever ES6 (ECMAScript 6 ou Next) e compilá-lo em código compatível com ES3. O compilador Traceur também gera um mapa de origem. Confira esta demonstração das características e classes do ES6 usadas como se tivessem suporte nativo no navegador, graças ao mapa de origem.

A área de texto da demonstração também permite escrever ES6 que será compilado rapidamente e gera um mapa de origem mais o código ES3 equivalente.

Depuração do Traceur ES6 usando mapas de origem.

Demonstração: escrever o ES6, depurá-lo, ver o mapeamento de origem em ação

Como o mapa de origem funciona?

No momento, o único compilador/minimizador JavaScript compatível com a geração de mapas de origem é o Closure compilador. Explicarei como usá-la mais tarde. Depois de combinar e minimizar seu JavaScript, haverá um arquivo de mapa de origem junto com ele.

Atualmente, o closure Compiler não adiciona o comentário especial necessário para indicar às ferramentas de desenvolvimento dos navegadores que um mapa de origem está disponível:

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

Isso permite que as ferramentas do desenvolvedor mapeiem chamadas de volta ao local nos arquivos de origem originais. Anteriormente, o pragma do comentário era //@, mas devido a alguns problemas com ele e os comentários de compilação condicional do IE, tomamos a decisão de mudá-lo para //#. Atualmente, o Chrome Canary, o WebKit Nightly e o Firefox 24+ são compatíveis com o novo pragma de comentários. Essa mudança de sintaxe também afeta sourceURL.

Se não gostar da ideia do comentário estranho, você pode definir um cabeçalho especial em seu arquivo JavaScript compilado:

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

Assim como o comentário, ele informará ao consumidor do mapa de origem onde procurar o mapa de origem associado a um arquivo JavaScript. Esse cabeçalho também resolve o problema de referenciar mapas de origem em linguagens que não aceitam comentários de linha única.

Exemplo de WebKit Devtools de mapas de origem ativados e desativados.

O download do arquivo de mapa de origem só será feito se os mapas de origem estiverem ativados e as ferramentas de desenvolvimento estiverem abertas. Você também precisará fazer o upload dos seus arquivos originais para que as ferramentas de desenvolvimento possam fazer referência a eles e exibi-los quando necessário.

Como gerar um mapa de origem?

Você precisará usar o Closure compilador para reduzir, concatenar e gerar um mapa de origem para seus arquivos JavaScript. O comando é o seguinte:

java -jar compiler.jar \
--js script.js \
--create_source_map ./script-min.js.map \
--source_map_format=V3 \
--js_output_file script-min.js

As duas sinalizações de comando importantes são --create_source_map e --source_map_format. Isso é necessário porque a versão padrão é a V2, e só queremos trabalhar com a V3.

Anatomia de um mapa de origem

Para entender melhor um mapa de origem, vamos usar um pequeno exemplo de um arquivo de mapa de origem que seria gerado pelo closure Compiler e nos aprofundar em como a seção "mappings" funciona. O exemplo a seguir é uma ligeira variação em relação ao exemplo de especificação V3.

{
    version : 3,
    file: "out.js",
    sourceRoot : "",
    sources: ["foo.js", "bar.js"],
    names: ["src", "maps", "are", "fun"],
    mappings: "AAgBC,SAAQ,CAAEA"
}

Observe acima que o mapa de origem é um literal de objeto que contém muitas informações úteis:

  • Número da versão na qual o mapa de origem se baseia
  • O nome do arquivo do código gerado (seu arquivo de produção minimizado/combinado)
  • O sourceRoot permite incluir uma estrutura de pastas no início das origens. Essa também é uma técnica para economizar espaço
  • source contém todos os nomes de arquivo que foram combinados
  • Os nomes contêm todos os nomes de variáveis/métodos que aparecem em todo o código.
  • Por fim, a propriedade de mapeamentos é onde a mágica acontece usando valores Base64 VLQ. A real economia de espaço é feita aqui.

Base64 VLQ e como manter pequeno o mapa de origem

Originalmente, a especificação do mapa de origem tinha uma saída muito detalhada de todos os mapeamentos, o que resultou em um mapa de origem cerca de 10 vezes maior que o código gerado. A versão dois reduziu esse valor em cerca de 50% e a versão três reduziu novamente em mais 50%, portanto, para um arquivo de 133 KB você acaba com um mapa de origem de aproximadamente 300 KB.

Então, como é possível reduzir o tamanho e ainda manter os mapeamentos complexos?

VLQ (Variable Length Quantity) é usado com a codificação do valor em um valor Base64. A propriedade de mapeamentos é uma string muito grande. Dentro dessa string existem pontos e vírgulas (;) que representam um número de linha dentro do arquivo gerado. Dentro de cada linha existem vírgulas (,) que representam cada segmento dentro dela. Cada um desses segmentos tem campos de comprimento variável de 1, 4 ou 5. Alguns podem parecer maiores, mas estes contêm bits de continuação. Cada segmento é baseado no anterior, o que ajuda a reduzir o tamanho do arquivo, já que cada bit é relativo aos segmentos anteriores.

Detalhamento de um segmento no arquivo JSON do mapa de origem.

Como mencionado acima, cada segmento pode ter comprimento variável de 1, 4 ou 5. Esse diagrama é considerado um comprimento variável de quatro com um bit de continuação (g). Detalharemos esse segmento e mostraremos como o mapa de origem funciona fora do local original.

Os valores mostrados acima são puramente os valores decodificados em Base64. Ocorre um pouco mais de processamento para chegar aos valores reais. Cada segmento geralmente inclui cinco itens:

  • Coluna gerada
  • Arquivo original em que apareceu
  • Número da linha original
  • Coluna original
  • E, se disponível, o nome original

Nem todo segmento tem um nome, nome de método ou argumento, de modo que os segmentos ao longo vão alternar entre quatro e cinco comprimentos variáveis. O valor g no diagrama de segmento acima é o que é chamado de bit de continuação, que permite mais otimização no estágio de decodificação Base64 VLQ. Um bit de continuação permite desenvolver um valor de segmento para que você possa armazenar números grandes sem precisar armazenar um número grande. Essa é uma técnica muito inteligente para economizar espaço que tem suas raízes no formato midi.

Uma vez processado, o diagrama acima AAgBC retornaria 0, 0, 32, 16, 1 - sendo 32 o bit de continuação que ajuda a criar o seguinte valor, 16. B puramente decodificada em Base64 é 1. Portanto, os valores importantes usados são 0, 0, 16, 1. Isso nos informa que a linha 1 (as linhas são mantidas na contagem por ponto e vírgula) a coluna 0 do arquivo gerado é mapeada para o arquivo 0 (a matriz de arquivos 0 é foo.js), linha 16 na coluna 1.

Para mostrar como os segmentos são decodificados, vou usar a biblioteca JavaScript de mapa de origem do Mozilla. Você também pode consultar o código de mapeamento-fonte das ferramentas para desenvolvedores do WebKit, também escrito em JavaScript.

Para entender corretamente como obtemos o valor 16 de B, precisamos ter uma compreensão básica dos operadores bit a bit e de como a especificação funciona para o mapeamento de origem. O dígito anterior, g, é sinalizado como um bit de continuação ao comparar o dígito (32) e o VLQ_CONTINUATION_BIT (binário 100000 ou 32) usando o operador E (&) bit a bit.

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

Isso retorna um 1 em cada posição de bit em que ambos aparecem. Assim, um valor decodificado em Base64 de 33 & 32 retornaria 32, já que eles compartilham apenas a localização de 32 bits, como você pode ver no diagrama acima. Isso aumenta o valor de deslocamento do bit em 5 para cada bit de continuação anterior. No caso acima, a mudança só ocorre uma vez em 5, portanto, desloca-se 1 (B) à esquerda por 5.

1 <<../ 5 // 32

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

Esse valor é então convertido de um valor assinado por VLQ deslocando-se para a direita o número (32) uma posição.

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

Então, aí está: é assim que você transforma 1 em 16. Esse pode parecer um processo excessivamente complicado, mas, quando os números começam a ficar maiores, ele faz mais sentido.

Possíveis problemas de XSSI

A especificação menciona problemas de inclusão de script em vários sites que podem surgir no consumo de um mapa de origem. Para atenuar isso, é recomendável incluir ")]}" no início da primeira linha do seu mapa de origem para invalidar o JavaScript deliberadamente. Assim, um erro de sintaxe será gerado. As ferramentas de desenvolvimento do WebKit já conseguem lidar com isso.

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

Como mostrado acima, os três primeiros caracteres são cortados para verificar se correspondem ao erro de sintaxe na especificação e, em caso afirmativo, removem todos os caracteres que levam à entidade da primeira linha nova (\n).

sourceURL e displayName em ação: funções de avaliação e anônimas

Embora não façam parte da especificação do mapa de origem, as duas convenções a seguir facilitam muito o desenvolvimento ao trabalhar com evals e funções anônimas.

O primeiro auxiliar é muito semelhante à propriedade //# sourceMappingURL e, na verdade, é mencionado na especificação V3 do mapa de origem. Ao incluir o comentário especial a seguir no código, que será avaliado, é possível nomear os evals para que apareçam como nomes mais lógicos nas ferramentas de desenvolvimento. Confira uma demonstração simples usando o compilador do CoffeeScript:

Demonstração: veja o código eval() exibido como um script via sourceURL

//# sourceURL=sqrt.coffee
Como é o comentário especial sourceURL nas ferramentas para desenvolvedores

O outro auxiliar permite nomear funções anônimas usando a propriedade displayName disponível no contexto atual da função anônima. Crie o perfil da demonstração a seguir para conferir a propriedade displayName em ação.

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);
Mostrando a propriedade displayName em ação.

Ao criar o perfil do seu código nas ferramentas para desenvolvedores, a propriedade displayName será mostrada em vez de algo como (anonymous). No entanto, o displayName está praticamente morto e não será incorporado ao Chrome. Mas não perdemos a esperança, e uma proposta muito melhor foi sugerida, chamada debugName.

No momento em que este artigo foi escrito, a nomeação de eval estava disponível apenas nos navegadores Firefox e WebKit. A propriedade displayName só é usada no WebKit Nightly.

Vamos unir forças

Atualmente, há uma longa discussão sobre a adição do suporte a mapas de origem ao CoffeeScript. Confira o problema e adicione sua ajuda para adicionar a geração do mapa de origem ao compilador do CoffeeScript. Essa será uma grande vantagem para o CoffeeScript e seus devotos seguidores.

O UglifyJS também tem um problema no mapa de origem que você precisa analisar.

Muitas tools geram mapas de origem, incluindo o compilador hourscript. Considero isso um ponto discutivel.

Quanto mais ferramentas disponíveis para gerar mapas de origem, melhor seremos. Portanto, vá em frente e solicite ou adicione suporte para mapas de origem ao seu projeto de código aberto favorito.

Não é perfeito

Uma coisa que os mapas de origem não atendem no momento são as expressões de observação. O problema é que tentar inspecionar um argumento ou nome de variável dentro do contexto de execução atual não retornará nada, já que ele não existe de verdade. Isso exigiria algum tipo de mapeamento reverso para procurar o nome real do argumento/variável que você quer inspecionar em comparação com o nome real do argumento/variável em seu JavaScript compilado.

É claro que esse é um problema solucionável e, com mais atenção nos mapas de origem, podemos começar a ver alguns recursos incríveis e maior estabilidade.

Problemas

Recentemente, o jQuery 1.9 adicionou suporte a mapas de origem quando veiculados fora de CDNs oficiais. Ele também apontava um bug peculiar quando os comentários de compilação condicional do IE (//@cc_on) eram usados antes do carregamento do jQuery. Desde então, houve um commit (link em inglês) para reduzir isso ao unir o sourceMappingURL em um comentário de várias linhas. Não use comentários condicionais para a lição a ser aprendida.

Desde então, isso foi resolvido com a mudança da sintaxe para //#.

Ferramentas e recursos

Confira outros recursos e ferramentas que você deve conferir:

Os mapas de origem são um utilitário muito poderoso no conjunto de ferramentas de um desenvolvedor. Isso é muito útil para manter seu app da Web leve, mas facilmente depurável. Ele também é uma ferramenta de aprendizado muito eficiente para ajudar desenvolvedores iniciantes a entender como desenvolvedores experientes estruturam e criam apps sem ter que lidar com códigos minificados ilegíveis.

O que você está esperando? Comece a gerar mapas de origem para todos os projetos agora mesmo.