Introdução aos mapas de origem JavaScript

Você já quis manter o código do lado do cliente legível e, mais importante, depurável mesmo depois de combiná-lo e minificá-lo, 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 para um estado não criado. Ao desenvolver para produção, além de minificar e combinar arquivos JavaScript, você gera um mapa de origem que deté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 a localização original. As ferramentas para desenvolvedores (atualmente, builds noturnos do WebKit, Google Chrome ou Firefox 23+ e versões mais recentes) podem analisar o mapa de origem automaticamente e fazer com que ele pareça estar executando arquivos não unificados e não combinados.

A demonstração permite que você clique com o botão direito em qualquer lugar do campo de texto que contém a origem gerada. Selecionar "Pegar localização original" vai consultar o mapa de origem transmitindo a linha e o número da coluna gerados e retornar a posição no código original. Verifique se o console está aberto para que você possa conferir a saída.

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

Mundo real

Antes de conferir a implementação real dos mapas de origem, verifique se você ativou o recurso de mapas de origem no Chrome Canary ou no WebKit nightly. Para isso, clique na engrenagem de configurações no painel de ferramentas de desenvolvimento e marque a opção "Ativar mapas de origem".

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

O Firefox 23 e versões mais recentes têm mapas de origem ativados por padrão nas ferramentas de desenvolvimento integradas.

Como ativar mapas de origem nas ferramentas de desenvolvimento do Firefox.

Por que os mapas de origem são importantes?

No momento, o mapeamento de origem só funciona entre JavaScript não compactado/combinado e JavaScript compactado/não combinado, mas o futuro parece promissor 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, poderemos usar quase qualquer idioma como se ele tivesse suporte nativo no navegador com mapas de origem:

  • CoffeeScript
  • ECMAScript 6 e versões mais recentes
  • SASS/LESS e outros
  • Praticamente qualquer linguagem que seja compilada em JavaScript

Confira este screencast de CoffeeScript sendo depurado em um build experimental do console do Firefox:

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

Outro exemplo que criei usa a biblioteca Traceur do Google, que permite escrever ES6 (ECMAScript 6 ou Next) e fazer a compilação para um código compatível com ES3. O compilador do Traceur também gera um mapa de origem. Confira esta demonstração de como as classes e os atributos do ES6 são usados como se tivessem suporte nativo no navegador, graças ao mapa de origem.

O textarea na demonstração também permite que você escreva 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: escrever ES6, depurar e conferir o mapeamento de origem em ação

Como funciona o mapa de origem?

No momento, o único compilador/minificador JavaScript que oferece suporte à geração de mapas de origem é o Closure Compiler. Vou explicar como usar mais tarde. Depois de combinar e minificar o JavaScript, um arquivo de mapa de origem será criado.

No momento, o Closure Compiler não adiciona o comentário especial no final que é necessário para indicar às ferramentas de desenvolvimento de navegadores que um mapa de origem está disponível:

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

Isso permite que as ferramentas de desenvolvedor mapeiem as chamadas de volta para o local nos arquivos de origem originais. Anteriormente, o pragma de comentário era //@, mas devido a alguns problemas com ele e comentários de compilação condicional do IE, foi tomada a decisão de mudar para //#. Atualmente, o Chrome Canary, o WebKit Nightly e o Firefox 24 e versões mais recentes oferecem suporte ao novo pragma de comentário. Essa mudança na sintaxe também afeta o sourceURL.

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

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

Como o comentário, isso vai 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 não compatíveis com comentários de uma linha.

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

O arquivo do mapa de origem só será transferido se você tiver os mapas de origem ativados e as ferramentas de desenvolvimento abertas. Você também vai precisar fazer upload dos 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ê vai precisar usar o Closure Compiler para minificar, 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 flags 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.

A anatomia de um mapa de origem

Para entender melhor um mapa de origem, vamos usar um exemplo de arquivo de mapa de origem que seria gerado pelo compilador Closure e entrar em mais detalhes sobre 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, você pode conferir que um mapa de origem é um literal de objeto que contém muitas informações interessantes:

  • Número da versão em que o mapa de origem é baseado
  • O nome do arquivo do código gerado (seu arquivo de produção minifed/combinado)
  • O sourceRoot permite que você adicione uma estrutura de pastas às origens. Isso também é uma técnica de economia de espaço.
  • sources contém todos os nomes de arquivos que foram combinados
  • names contém todos os nomes de variáveis/métodos que aparecem no código.
  • Por fim, a propriedade de mapeamentos é onde a mágica acontece usando valores VLQ de Base64. A economia de espaço real é feita aqui.

Base64 VLQ e manter o mapa de origem pequeno

Originalmente, a especificação do mapa de origem tinha uma saída muito detalhada de todos os mapeamentos e resultou em um mapa de origem cerca de 10 vezes maior que o código gerado. A versão 2 reduziu isso em cerca de 50%, e a versão 3 reduziu em mais 50%. Portanto, para um arquivo de 133 kB, você terá um mapa de origem de aproximadamente 300 kB.

Como eles reduziram o tamanho e mantiveram 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. Dentro dessa string, há pontos e vírgulas (;) que representam um número de linha no arquivo gerado. Em cada linha, há vírgulas (;) que representam cada segmento nela. Cada um desses segmentos é 1, 4 ou 5 em campos de comprimento variável. 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.

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

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

Os valores mostrados acima são puramente os valores decodificados em Base64. É necessário mais processamento para conseguir os valores reais. Cada segmento geralmente trabalha com cinco coisas:

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

Nem todos os segmentos têm um nome, nome de método ou argumento, então os segmentos mudam entre quatro e cinco comprimentos de variável. O valor g no diagrama de segmento acima é o que chamamos de bit de continuação, que permite uma otimização adicional na etapa de decodificação do VLQ Base64. Um bit de continuação permite que você construa um valor de segmento para armazenar números grandes sem precisar de um número grande, uma técnica muito inteligente de economia de espaço que tem origem no formato MIDI.

O diagrama acima AAgBC, depois de processado, retornaria 0, 0, 32, 16, 1. O 32 é o bit de continuação que ajuda a criar o valor seguinte de 16. B decodificado puramente 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 contadas pelos pontos e vírgulas) da 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 fazer referência à biblioteca JavaScript do mapa de origem da Mozilla. Você também pode conferir o código de mapeamento de origem das ferramentas de desenvolvimento do WebKit, que também foi escrito em JavaScript.

Para entender como chegamos ao valor 16 de B, precisamos ter um entendimento básico dos operadores de bit e 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 AND bitwise (&).

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 compartilham apenas o local de 32 bits, conforme mostrado no diagrama acima. Isso aumenta o valor de deslocamento do bit em 5 para cada bit de continuação anterior. No caso acima, ele só foi deslocado em 5 uma vez, ou seja, deslocar 1 (B) para a esquerda em 5.

1 <<../ 5 // 32

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

Esse valor é convertido de um valor assinado VLQ deslocando o número (32) para a direita em um ponto.

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

Pronto. É assim que você transforma 1 em 16. Pode parecer um processo complicado, mas, quando os números começam a aumentar, faz mais sentido.

Possíveis problemas com XSSI

A especificação menciona problemas de inclusão de script entre sites que podem surgir do consumo de um mapa de origem. Para evitar isso, recomendamos que você adicione ")]}" à primeira linha do mapa de origem para invalidar intencionalmente o JavaScript e gerar um erro de sintaxe. As ferramentas de desenvolvimento do WebKit já podem fazer isso.

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

Como mostrado acima, os três primeiros caracteres são divididos para verificar se correspondem ao erro de sintaxe na especificação. Se forem, todos os caracteres que levam à primeira entidade de nova linha (&n) são removidos.

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

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

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

Demo: mostrar o código eval() como um script usando sourceURL

//# sourceURL=sqrt.coffee
Como é o comentário especial &quot;sourceURL&quot; nas ferramentas de desenvolvimento

O outro auxiliar permite que você nomeie 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 um perfil do código nas ferramentas de desenvolvimento, a propriedade displayName será mostrada em vez de algo como (anonymous). No entanto, o displayName está praticamente morto e não vai ser usado no Chrome. Mas ainda há esperança, e uma proposta muito melhor foi sugerida, chamada debugName.

No momento da escrita, a nomenclatura de avaliação está disponível apenas nos navegadores Firefox e WebKit. A propriedade displayName está disponível apenas em versões noturnas do WebKit.

Vamos nos unir

Atualmente, há uma discussão muito longa sobre a adição do suporte a mapas de origem ao CoffeeScript. Confira o problema e adicione seu suporte para que a geração de mapas de origem seja adicionada ao compilador do CoffeeScript. Essa será uma grande vitória para o CoffeeScript e seus seguidores dedicados.

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

Muitas ferramentas geram mapas de origem, incluindo o compilador CoffeeScript. Agora considero isso uma questão discutível.

Quanto mais ferramentas disponíveis para gerar mapas de origem, melhor. Então, peça ou adicione suporte a mapas de origem ao seu projeto de código aberto favorito.

Não é perfeito

No momento, os mapas de origem não atendem às expressões de monitoramento. O problema é que tentar 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.

Esse é um problema que pode ser resolvido, e com mais atenção aos mapas de origem, podemos começar a notar alguns recursos incríveis e uma melhor estabilidade.

Problemas

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

Isso foi resolvido com a mudança da sintaxe para //#.

Ferramentas e recursos

Confira alguns outros recursos e ferramentas que você pode usar:

Os mapas de origem são um utilitário muito eficiente no conjunto de ferramentas de um desenvolvedor. É muito útil manter o app da Web enxuto, mas fácil de depurar. Ela também é uma ferramenta de aprendizado muito útil para desenvolvedores mais novos, que podem conferir como desenvolvedores experientes estruturam e programam apps sem precisar lidar com códigos minificados ilegíveis.

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