Aguarde...

14 de setembro de 2024

Apresentando @svg-use

Apresentando @svg-use

Esta postagem apresenta o @svg-use, um conjunto de ferramentas e plugins agrupadores para carregar ergonomicamente arquivos SVG como componentes, por meio do mecanismo do SVG <use href>.

Primeiro, olhamos para o cenário atual para incorporar ícones SVG em frontends JS, e especialmente componentes React. Na maior parte, isso envolve inlining SVG como código JS (também chamado de SVG-in-JS). Analisamos o espaço do problema para encontrar os problemas que o inlining resolve, e pesamos seus custos e benefícios.

Depois disso, introduzimos <img href>e SVG’s <use href>, e onde eles se encaixam no espaço do problema. Apresentamos a @svg-usecadeia de ferramentas como uma forma de torná-la <use href>mais ergonômica, a fim de torná-la competitiva com inlining.

Por fim, olhamos para o futuro e investigamos como os futuros padrões da web podem facilitar esses padrões.

O problema central

Uma técnica comum no ecossistema JS (e especialmente React) é converter ícones SVG em componentes, para que eles possam ser importados pelo código JS. Uma biblioteca comum para essa tarefa é svgr, por Greg Bergé e colaboradores. Ela fornece plugins bundler para facilitar a conversão de SVG para JSX. Vamos chamar essa abordagem de SVG-in-JS , para fins de comparação.

Para dar um exemplo concreto, SVG-in-JS resulta em um arquivo como este. Observe como os caminhos e formas SVG são todos inline:

const SvgComponent = (props) => (
  <svg width="1em" height="1em" viewBox="0 0 48 1" {...props}>
    <path d="M0 0h48v1H0z" fill="currentColor" fillRule="evenodd" />
    {/* and possibly other paths / shapes here */}
  </svg>
);

export default SvgComponent;

A abordagem SVG-in-JS é contrastada com a referência ao SVG como um ativo e seu uso em img[src]ou em svg > use[href]. Antes de discutirmos essas abordagens, vamos ver quais problemas o SVG-in-JS resolve.

O primeiro problema é o tema 🎨. Ao incluir o SVG inline, é possível usar atributos HTML regulares e seletores CSS, e herdar propriedades personalizadas facilmente. Na maioria das vezes, isso é feito para herdar currentColor, mas existem outras propriedades personalizadas e esquemas de tema mais personalizados.

import SvgComponent from './icon.svg';

const MyComponent = () => {
  return <SvgComponent style="color: blue;" />;
};

Outro problema é a entrega e a portabilidade 📦. Ao elevar o SVG para o reino do JS, ele pode ser carregado por meio de módulos ES, assim como qualquer outro código JS. Isso não é um problema particularmente grande para aplicativos, que normalmente usam empacotadores capazes de referenciar ativos no gráfico do módulo. No entanto, quando se trata de bibliotecas compartilháveis , isso fornece um mecanismo de entrega que funciona em qualquer lugar que o JS seja suportado, sem configuração em nome do usuário. Isso tem a ressalva de que você precisa de um tempo de execução JSX, como React, mas, novamente, as bibliotecas de ícones são frequentemente publicadas no contexto de uma determinada estrutura.

Em geral, referenciar ativos (como imagens ou mesmo CSS) de JS não é padronizado atualmente. Portanto, é difícil enviar bibliotecas reutilizáveis ​​que dependem de ativos, pelo menos de uma forma geral, ou seja, uma que não assuma um bundler específico.

Embora isso tenha sido verdade historicamente, e ainda não haja padronização para carregamento de ativos, há uma técnica que funciona com (a maioria) dos bundlers atuais e todos os navegadores da web atuais. Isso é chamado de “novo esquema de URL” (artigo web.dev por Ingvar Stepanyan):

// This is resolved relative to the JS file, instead of the document
// bundlers resolve this as well
const svgUrl = new URL('./path/to/svg.svg', import.meta.url);

A técnica acima resolve apenas o problema de entrega 📦 e não resolve o problema de tema 🎨.

A abordagem SVG-in-JS é, portanto, atraente, porque resolve problemas reais de uma forma relativamente simples. No entanto, também vem com algumas desvantagens.

Desvantagens do SVG-in-JS

Ao incorporar SVGs em JS, estamos incorrendo em uma série de custos de tempo de execução.

Resumidamente:

  • O código de cada componente é analisado várias vezes: primeiro como JS, depois como HTML quando inserido no documento. Isso acontece para cada instância do componente.
  • Cada ícone SVG é duplicado no DOM para cada instância, aumentando o tamanho do DOM.
  • O tamanho do ícone SVG aumenta o tamanho do pacote JS. Alguns ícones SVG comuns podem ser grandes, por exemplo, bandeiras de países com designs intrincados. Em uma nota semelhante, também é fácil acidentalmente embutir SVGs grandes.

Todos os pontos acima levam a um atraso em métricas de interatividade significativas. Este artigo de Jacob ‘Kurt’ Groß mergulha nas diferentes desvantagens do SVG-in-JS, bem como em diferentes alternativas. Recomendo fortemente a leitura desse artigo para entender as compensações.

Acredito que os custos de tempo de execução são grandes o suficiente para muitos casos comuns, o que justifica uma alternativa.

Uma primeira alternativa: <img src>

Uma abordagem simples seria usar um <img src>e avaliar como ele se compara aos nossos critérios.

Podemos usar imagens da seguinte forma:

// Current bundlers resolve the URL to a file (or can be configured to do so)
import iconUrl from './path/to/icon.svg';

// Both bundlers and browsers can resolve this, as above
const anotherIconUrl = new URL('./path/to/svg.svg', import.meta.url);

const MyComponent = () => (
  <>
    <img src={iconUrl} alt="" />
    <img src={anotherIconUrl} loading="lazy" alt="" />
  </>
);

Isso resolve o problema de entrega 📦 efetivamente, tanto para aplicativos quanto para bibliotecas compartilháveis ​​(por meio do novo esquema de URL). A imagem em si não existe no pacote JS, resolvendo efetivamente as considerações de desempenho.

imgelemento é um dos elementos mais robustos e fornece um monte de flexibilidade para ajustar a entrega ao usuário final, como loadingfetchPriority. O imgelemento também pode carregar recursos de origem cruzada. Considero o imgelemento o benchmark, quando se trata de entrega.

Infelizmente, o imgelemento não aborda o problema de tema 🎨 de forma alguma. Não há herança de CSS para falar, então quaisquer cores que estejam presentes no SVG original são aquelas que serão mostradas ao usuário final.

Portanto, o imgelemento é útil para SVGs multicoloridos (como bandeiras de países, que vejo embutidas com bastante frequência :/), mas não é muito útil para ícones de interface do usuário, que devem ser flexíveis e temáticos.

(Isso pode mudar no futuro; falaremos mais sobre isso depois.)

<use href> Mecanismo SVG

O SVG fornece o <use>elemento, que pode referenciar SVGs externos da mesma origem por meio do hrefatributo.

Desconsiderando JS e React por um momento, se fôssemos referenciar um SVG em useHTML (o “código do usuário”), ficaria assim:

<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
  <use href="https://example.com/icon.svg#someId" style="color: blue;"></use>
</svg>

Para fazer o trabalho acima, precisamos de algumas peças móveis:

  1. (opcional) a viewBox, para permitir o dimensionamento intrínseco do externo svg.
  2. urlpara referenciar o SVG externo por.
  3. para idreferenciar o SVG externo (embora o SVG 2 permita referenciar sem um id, essa parte não parece ser suportada em navegadores).
  4. um sistema de tema, para nos permitir personalizar o SVG referenciado. Isso pode ser feito com currentColor(para ícones monocromáticos) ou via propriedades personalizadas CSS, que podem ser herdadas pelo SVG referenciado.

O SVG em si (o “asset”) se parece com isso. Crucialmente, ele não está embutido no pacote JS, mas existe por si só, similar ao img[src]exemplo acima:

<svg
  xmlns="http://www.w3.org/2000/svg"
  viewBox="0 0 24 24"
  id="use-href-target"
  stroke="currentColor"
  fill="none"
  stroke-width="2"
>
  <line x1="5" y1="12" x2="19" y2="12" />
  <polyline points="12 5 19 12 12 19" />
</svg>

A tese central é que a configuração acima é desejável em termos de suas características de tempo de execução (mais sobre isso em breve), mas é mais tediosa de configurar do que SVG-in-JS, devido à falta de uma cadeia de ferramentas dedicada.

As @svg-usebibliotecas consideram a estrutura acima (o “código do usuário” e o “ativo”) como um alvo de compilação e fornecem uma cadeia de ferramentas para vincular os dois ao seu código JS. Antes de falarmos sobre a implementação, vamos avaliar esta solução em relação ao espaço do problema.

Prós e contras de<use href>

Ao referenciar um ativo externo, estamos evitando os custos de análise dupla e tamanho do pacote JS; precisamos apenas enviar uma URL e alguns metadados para o pacote JS, bem como um componente wrapper (por conveniência). Também estamos reduzindo o tamanho do DOM, já que um único useé menor do que a maioria dos ícones e envolve menos elementos.

O tema 🎨 é obtido por meio de uma transformação no SVG e pode ser tão simples quanto passar para baixo currentColor. Propriedades personalizadas CSS e currentColorsão herdadas conforme o esperado. A transformação de tema é feita estaticamente e não tem custo de tempo de execução.

A portabilidade 📦 nessa abordagem é boa, porque você pode usar os SVGs resultantes diretamente, e não apenas no React. Você pode até mesmo escrevê-los <use href>manualmente, se quiser, ou criar seu próprio componente wrapper, no framework de sua escolha.

A entrega de bibliotecas compartilhadas pode ser confiável, usando o new URLesquema como um alvo de transformação. O repositório svg-use tem um exemplo relevante.

Uma desvantagem, em termos de entrega, é a falta de Cross-Origin Resource Sharing (CORS) para <use href> referências SVG. Este é um problema real, que só pode ser resolvido de forma confiável no nível de especificação. No entanto, o SVG-in-JS é frequentemente usado para casos de uso de SVG locais e de biblioteca compartilhada, que são hospedados na mesma origem, então a falta de CORS não é um problema para substituí-los.

Caso você use uma Content Delivery Network (CDN) para seus ativos de aplicativo (incluindo JS e SVG), você precisa de algum mecanismo para reescrever as URLs no momento da construção ou no tempo de execução, para apontá-las para um proxy de mesma origem. Isso pode ser mais ou menos eficiente, dependendo da implementação, e seu limite para aceitar essa troca pode ser diferente do meu. Os @svg-use/reactcomponentes padrão habilitam essa funcionalidade.

Em termos de estratégias de carregamento, usenão é tão flexível quanto img. Por exemplo, não há fetchPriorityou loading especificado. Isso está quase no mesmo nível do inlining, que é ansioso (e síncrono) por padrão, e é tornado preguiçoso por meio de import()chamadas JS e mecanismos relacionados. Se você tem ícones que precisam ser exibidos de forma síncrona, então o inlining pode ser a melhor abordagem.

Dito isso, há casos em que embutir os SVGs ou usar img[src]é a abordagem melhor ou mais simples, dependendo dos seus padrões de carregamento. Não afirmo que o <use href>padrão resolve todos os cenários, mas mesmo que leve a metade dos SVGs não serem mais embutidos em JS “por padrão”, será um bom passo.

A solução central em@svg-use

Voltando ao JS e ao React, assumindo alguma configuração do bundler ( webpackrollupvite ), os desenvolvedores consumiriam a <use href>estrutura assim (assumindo alguma configuração do bundler):

import { Component as ArrowIcon } from './arrow.svg?svgUse';

const MyComponent = () => {
  return (
    <button>
      <ArrowIcon color="currentColor" role="img" aria-label="Continue" />
    </button>
  );
};

arrow.svgSVG é transformado e compatível <use href>automaticamente.

Os desenvolvedores também podem usar bibliotecas reutilizáveis, sem precisar configurar seu bundler. As bibliotecas usam as @svg-useferramentas como transformações estáticas, emitindo o new URLpadrão para o hrefunder the hood.

import { ArrowIcon } from 'my-shared-library';

const MyComponent = () => (
  <button>
    <ArrowIcon color="currentColor" role="img" aria-label="Continue" />
  </button>
);

Em profundidade

Quando você escreve isto:

import {
  Component as ArrowIcon,
  // not necessary to import; shown for demonstration
  href,
  id,
  viewBox,
} from './arrow.svg?svgUse';

const MyComponent = () => {
  return (
    <button>
      <ArrowIcon color="currentColor" role="img" aria-label="Continue" />
    </button>
  );
};

Com uma fonte arrow.svgde:

<svg
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  stroke="#111"
  fill="none"
  stroke-width="2"
>
  <line x1="5" y1="12" x2="19" y2="12" />
  <polyline points="12 5 19 12 12 19" />
</svg>

Um plugin específico do bundler inicia a seguinte cadeia:

  1. O plugin resolve ./arrow.svgem relação ao arquivo e invoca @svg-use/core
    1. Análises de núcleo ./arrow.svg, para garantir que ele atenda às invariantes
    2. O núcleo extrai o idviewBoxdo elemento SVG de nível superior
    3. O Core executa uma transformação de tema personalizável para transformar os preenchimentos e traços do elemento SVG em propriedades personalizadas CSS configuráveis ​​(ou currentColor)
    4. O Core retorna o conteúdo SVG transformado e as informações extraídas
  2. O plugin emite o SVG transformado como um ativo (usando a lógica do empacotador) e resolve sua possível URL.
  3. O plugin passa a URL para o Core, para criar um módulo JS. É isso que o código do userland finalmente vê.
  4. O módulo exporta as propriedades extraídas e as passa para uma “fábrica de componentes”, por conveniência.
  5. O plugin passa o módulo JS para o empacotador.

O ativo SVG transformado ( /assets/arrow-1234.svg) é este:

<svg
  xmlns="http://www.w3.org/2000/svg"
  viewBox="0 0 24 24"
  id="use-href-target"
  stroke="var(--svg-use-href-primary, currentColor)"
  fill="none"
  stroke-width="2"
>
  <line x1="5" y1="12" x2="19" y2="12" />
  <polyline points="12 5 19 12 12 19" />
</svg>

O módulo JS ad-hoc é o equivalente a isto:

import { createThemedExternalSvg } from '@svg-use/react';

export const id = 'use-href-target';
export const href = new URL('/assets/arrow-1234.svg', import.meta.url).href;
export const viewBox = '0 0 24 24';

/* createThemedExternalSvg is a component factory function */
export const Component = createThemedExternalSvg({ href, id, viewBox });

Essa abordagem combina conveniência (usar Componentdiretamente) com composição (como usar hrefidpara construir seus próprios componentes wrapper, usar hrefpara pré-carregamento etc.). Consulte o repositório svg-use para mais detalhes sobre personalizações, como a transformação de tema.

Os utilitários principais são componíveis e fáceis de estender. Além disso, a segurança de tipo e a conveniência do usuário são essenciais; isso deve ser tão (ou quase tão) conveniente quanto SVG-in-JS.

O futuro

Criei @svg-use porque vi uma lacuna na ferramenta para adotar o <svg use>padrão, em um contexto JS contemporâneo. No entanto, meu sonho é que os padrões da web peguem algumas das arestas, para que essa biblioteca eventualmente se torne obsoleta (ou simplesmente boa de se ter) .

Aqui estão algumas propostas das quais tenho conhecimento, que podem mudar o cálculo em torno desta biblioteca. Estou ordenando-as de “bom ter” para “existencial”.

Atributos de importação JS

JavaScript suporta atributos de importação, que permitem passar metadados sobre como um módulo deve ser carregado.

No exemplo anterior, você pode ter notado um ?svgUsesufixo de parâmetro de consulta irritante na importação. Isso é usado para desambiguar o carregamento de SVGs em bundlers, entre aqueles que usam @svg-usee outros.

Atributos de importação tornariam um pouco mais fácil escrever importações:

import { Component as Arrow } from './arrow.svg?svgUse&theme=none';
import { Component as Arrow2 } from './arrow-2.svg' with { type: 'svgUse', theme: 'none' };

Atributos de importação existem em navegadores atuais, exceto Firefox. Isso não é um grande problema, porque os bundlers podem transpilá-los e apenas passar os metadados para os plugins.

No entanto, estou evitando-os porque o TypeScript ainda não expõe declarações de tipo com base em atributos de importação. Assim, para obter segurança de tipo, ainda usamos a abordagem baseada em string, no nome do módulo.

Referências de ativos JS

new URLpadrão é legal para referenciar ativos relativos, mas funciona principalmente com base em convenção. Os Bundlers tratam isso como uma sintaxe especial para resolver ativos, mas eu adoraria uma construção sintática mais concreta, para evitar ambiguidades. Por exemplo, esbuild não suporta o novo padrão de URL, com algumas preocupações em torno da padronização.

Houve (há?) uma proposta para referências de ativos em JavaScript, mas parece ter parado. Além disso, parece ter sido escrita em uma época em que atributos de importação não existiam, mas posso estar errado.

Há algum outro trabalho sendo feito em torno de importações, como importações de fase de origem. Embora isso não ajude com ativos por si só, tenho um pressentimento de que o interesse renovado em mecanismos de carregamento de ES pode levar a uma proposta para ativos no futuro.

Referências com import.meta.resolve

No tópico de marcadores sintáticos mais claros para resolver caminhos relativos, há a import.meta.resolvefunção interna.

Esta função resolve um especificador de módulo para uma URL, com a URL do módulo atual como base. A URL resolvida não precisa resolver para um módulo real, o que significa que podemos facilmente referenciar ativos não-JS.

Em outras palavras, essas duas invocações seriam equivalentes:

const assetPath = new URL('./arrow.svg', import.meta.url).href;
const assetPath2 = import.meta.resolve('./arrow.svg');

Há algumas outras sutilezas quando se trata de resolver especificadores de módulos bare, mas isso não faz diferença para nosso caso de uso. O suporte do navegador para import.meta.resolve não é tão amplo quanto new URL, mas ainda é amplamente suportado em navegadores atuais.

Meu pressentimento é que isso import.meta.resolvepoderia substituir new URLcomo o alvo de construção para svg-useum futuro próximo, porque parece um sinal mais claro de uma dependência de importação. Estou ansioso para ver o que os empacotadores e ambientes não-navegadores farão com isso.

Uso de crossorigin SVG

Se o SVG suportasse o uso de crossorigin, então enviar bibliotecas de ícones compartilhados em CDNs seria muito mais simples, sem ter que fazer proxy/reescrever as URLs (e evitar quebrar coisas acidentalmente). Isso parece ter parado, mas posso estar errado.

SVG 2: referenciando SVGs sem um fragmento de id

De acordo com SVG 2, você poderia fazer isso:

<svg>
  <use href="arrow.svg"></use>
</svg>

…e faça referência ao SVG mais alto. Isso parece especificado, mas não implementado.

Isso simplificaria a necessidade de um ide sua extração estaticamente.

Parâmetros CSS vinculados

Se os Parâmetros Vinculados CSS forem aceitos, poderemos passar propriedades personalizadas para SVGs dentro de img[src]. (Estou omitindo a sintaxe, porque ela está em fluxo).

Isso evita a configuração com svg > use[href], em favor do imgelemento muito mais simples. Como mencionado anteriormente, o imgelemento é realmente robusto em termos de carregamento e entrega. Ele já pode ser carregado cross-origin, então isso possivelmente evita os problemas de CDN. Como um SVG imgé exibido como um todo, isso também evita o idproblema.

Esta é a proposta mais promissora . Se ela cair nos navegadores, então esta biblioteca será útil principalmente para duas coisas: tematização (pegar um ad-hoc svge trazê-lo para um sistema) e extrair a proporção de aspecto intrínseca (o viewBox, essencialmente). Os wrappers ajustarão seus internos, mas provavelmente não precisarão mudar sua interface externa (viva!)

Encerrando

Abordamos muita coisa neste post!

Como resumo, nós olhamos para os desafios de entregar ativos em JS, especificamente a entrega de ícones SVG. Nós olhamos para técnicas SVG-in-JS, e pesamos seus prós e contras, em termos de entrega, tematização, ergonomia e desempenho de tempo de execução. Nós então olhamos para as alternativas de <img href><use href>, com uma lente similar.

Por fim, introduzimos @svg-use, uma cadeia de ferramentas para tornar o <use href> padrão mais ergonômico e consistente. Também analisamos desenvolvimentos futuros de especificações da web, que poderiam mudar o cálculo em torno desses padrões.

Postado em BlogTags:
Escreva um comentário