Introdução aos mapas de origem JavaScript

Ryan Seddon

Você já pensou em manter seu código do lado do cliente legível e, o mais importante, depurável, mesmo depois de ele ter sido combinado e minimizado, sem afetar o desempenho? Agora você pode fazer isso com a magia dos mapas de origem.

Os mapas de origem são uma maneira de mapear um arquivo combinado/minificado de volta a um estado não criado. Ao criar apps para produção, além de minificar e combinar arquivos JavaScript, você gera um mapa de origem com informações sobre os 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 de desenvolvedor (atualmente, WebKit Nightly builds, Google Chrome ou Firefox 23+) podem analisar o mapa de origem automaticamente e fazer com que pareça que você está executando arquivos não reduzidos e não combinados.

A demonstração permite que você clique com o botão direito do mouse em qualquer lugar da área de texto que contenha a origem gerada. Selecione "Get original location" (Obter localização original) para consultar o mapa de origem transmitindo o número da linha e da 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 real do Source Maps, ative o recurso de mapas de origem no Chrome Canary ou no WebKit Nightly clicando no cog de configurações no painel de ferramentas do desenvolvedor e marcando a opção "Enable source maps" (Ativar mapas de origem).

Como ativar mapas de origem nas ferramentas do desenvolvedor do WebKit.

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

Como ativar os mapas de origem nas ferramentas de desenvolvedor do Firefox.

Por que os mapas de origem são importantes?

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

No futuro, poderíamos facilmente usar quase qualquer linguagem, como se ela fosse nativamente compatível no navegador com os mapas de origem:

  • CoffeeScript
  • ECMAScript 6 e além
  • SASS/LESS e outros
  • Praticamente qualquer linguagem que compila para JavaScript

Veja este screencast do CoffeeScript, sendo depurado em uma versão experimental do console do Firefox:

O Google Web Toolkit (GWT) adicionou recentemente o 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 eu reuni usa a biblioteca Traceur do Google, que permite escrever ES6 (ECMAScript 6 ou Next) e compilá-lo em um 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 sendo usadas como se fossem compatíveis de forma nativa no navegador, graças ao mapa de origem.

A área de texto da demonstração também permite escrever um ES6, que será compilado em tempo real e gerará um mapa de origem e o código ES3 equivalente.

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

Demonstração: como escrever ES6, depurar e visualizar o mapeamento de origem em ação

Como o mapa de origem funciona?

No momento, o único compilador/minimizador JavaScript que tem suporte para a geração de mapa de origem é o closure compilador. Vou explicar como usá-la mais tarde. Depois de combinar e minimizar seu JavaScript, haverá um arquivo de mapa de origem junto com ele.

Atualmente, o compilador closure não adiciona o comentário especial no final, que é necessário para indicar às ferramentas de desenvolvimento do navegador que um mapa de origem está disponível:

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

Isso permite que as ferramentas de desenvolvedor mapeiem chamadas de volta para o 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, a decisão de alterá-lo para //# foi tomada. No momento, o Chrome Canary, o WebKit Nightly e o Firefox 24+ são compatíveis com o pragma do novo comentário. Esta alteração de sintaxe também afeta sourceURL.

Se não gostar da ideia desse comentário estranho, você também pode definir um cabeçalho especial no seu arquivo JavaScript compilado:

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

Assim como o comentário, isso 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 oferecem suporte a 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 suas ferramentas de desenvolvedor estiverem abertas. Também é necessário fazer upload dos arquivos originais para que as ferramentas de desenvolvimento possam consultá-los 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 pegar um pequeno exemplo de um arquivo de mapa de origem que seria gerado pelo closure compilador e nos aprofundarmos em como a seção "mapeamentos" funciona. O exemplo a seguir é uma pequena variação do exemplo da especificação V3.

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

Acima, é possível ver que um mapa de origem é um literal de objeto que contém muitas informações úteis:

  • Número da versão em que 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 antes das origens, o que também economiza espaço.
  • origens contém todos os nomes de arquivos que foram combinados
  • nomes contém todos os nomes de variáveis/métodos que aparecem em todo o código.
  • Por fim, a propriedade dos mapeamentos é onde a mágica acontece usando valores Base64 VLQ. A economia real de espaço é feita aqui.

VLQ Base64 e como manter o mapa de origem pequeno

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

Como o tamanho foi reduzido, mantendo os mapeamentos complexos?

VLQ (quantidade de comprimento variável) é usado com a codificação do valor em um valor Base64. A propriedade de mapeamentos é uma string muito grande. Nessa string, há pontos e vírgulas (;) que representam um número de linha no arquivo gerado. Dentro de cada linha há vírgulas (,) que representam cada segmento dentro dessa linha. Cada um desses segmentos tem campos de tamanho variável 1, 4 ou 5. Alguns podem parecer mais longos, mas 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 o comprimento variável de 1, 4 ou 5. Este 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 no local original.

Os valores mostrados acima são puramente os valores decodificados em Base64. Há mais processamento para se obter os 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. Assim, os segmentos alternam entre quatro e cinco tamanhos variáveis. O valor g no diagrama de segmento acima é o que chamamos de bit de continuação, o que permite uma otimização adicional no estágio de decodificação Base64 VLQ. Um bit de continuação permite construir um valor de segmento para que você possa armazenar números grandes sem ter que armazenar um número grande, uma técnica muito inteligente de economia de espaço que tem suas raízes no formato midi.

Depois de processado, o diagrama acima AAgBC retornaria 0, 0, 32, 16, 1. Ou seja, 32 é o bit de continuação que ajuda a criar o valor 16 a seguir. B puramente decodificado em Base64 é 1. Portanto, os valores importantes usados são 0, 0, 16, 1. Isso nos permite saber que a linha 1 (as linhas são contadas pelos pontos e vírgulas) e 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, faremos referência à biblioteca JavaScript do mapa de origem do Mozilla. Você também pode consultar o código de mapeamento-fonte das ferramentas de desenvolvimento 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 comparando o dígito (32) e o VLQ_CONTINUATION_BIT (binário 100000 ou 32) usando o operador bit a bit AND (&).

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

Isso retorna um 1 em cada posição de bit em que ambos aparecem. Portanto, um valor decodificado em Base64 de 33 & 32 retornaria 32, já que eles só compartilham a localização de 32 bits, como você pode conferir 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 em 5 só ocorre uma vez, portanto, deslocando 1 (B) à esquerda em 5.

1 <<../ 5 // 32

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

Em seguida, esse valor é convertido de um valor assinado VLQ ao deslocar o número (32) um ponto para a direita.

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

É assim que se transforma 1 em 16. Esse processo pode parecer complicado demais, mas faz mais sentido quando os números começam a aumentar.

Possíveis problemas de XSSI

A especificação menciona problemas de inclusão de scripts entre sites que podem surgir do consumo de um mapa de origem. Para atenuar isso, recomendamos incluir ")]}" na primeira linha do mapa de origem para invalidar o JavaScript deliberadamente e gerar um erro de sintaxe. As ferramentas de desenvolvimento do WebKit já podem 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 à primeira entidade de nova linha (\n).

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

Embora não façam parte das especificações do mapa de origem, as duas convenções a seguir permitem tornar o desenvolvimento muito mais fácil ao trabalhar com evals e funções anônimas.

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

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

//# sourceURL=sqrt.coffee
Como aparece 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 ver 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 caracterizar o perfil do código nas ferramentas de desenvolvimento, a propriedade displayName será exibida, em vez de algo como (anonymous). No entanto, o displayName está praticamente abandonado e não será lançado no Chrome. Mas nem toda a esperança está perdida, e uma proposta muito melhor foi sugerida, chamada debugName.

Até agora, a nomenclatura de eval estava disponível apenas nos navegadores Firefox e WebKit. A propriedade displayName está disponível apenas no WebKit noturno.

Vamos nos unir.

Atualmente, há uma longa discussão sobre a adição de suporte ao mapa de origem ao CoffeeScript. Confira o problema e inclua seu suporte para adicionar a geração do mapa de origem ao compilador do CoffeeScript. Isso será uma grande vitória para o CoffeeScript e seus devotos seguidores.

O UglifyJS também tem um problema de mapa de origem (link em inglês).

Muitas tools geram mapas de origem, incluindo o compilador coffeescript. Eu considero isso um ponto de discussão agora.

Quanto mais ferramentas disponíveis para gerar mapas de origem, melhor seremos. Portanto, vá em frente e peça ou adicione suporte a 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 a tentativa de inspecionar um argumento ou nome de variável no contexto de execução atual não vai retornar nada, porque ele não existe. 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 no 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.

Issues

Recentemente, o jQuery 1.9 adicionou suporte a mapas de origem quando veiculados fora de CDNs oficiais. Ele também apontou um bug específico quando comentários de compilação condicional do IE (//@cc_on) eram usados antes do carregamento do jQuery. Desde então, houve uma confirmação para mitigar isso envolvendo o sourceMappingURL em um comentário de várias linhas. A lição a ser aprendida não use comentário condicional.

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

Ferramentas e recursos

Aqui estão mais alguns recursos e ferramentas que você deve conferir:

Os mapas de origem são um utilitário muito poderoso em um conjunto de ferramentas para desenvolvedores. É muito útil conseguir manter seu app da Web enxuto, mas facilmente depurável. Também é uma ferramenta de aprendizado muito poderosa para desenvolvedores iniciantes verem como os desenvolvedores experientes estruturam e programam seus apps sem ter que vasculhar códigos minimizados ilegíveis.

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