Construindo componentes da Web interoperáveis ​​​​que funcionam até mesmo com React PlatoBlockchain Data Intelligence. Pesquisa vertical. Ai.

Construindo Componentes Web Interoperáveis ​​Que Funcionam Até Com React

Aqueles de nós que são desenvolvedores web há mais de alguns anos provavelmente escreveram código usando mais de uma estrutura JavaScript. Com todas as opções disponíveis – React, Svelte, Vue, Angular, Solid – é quase inevitável. Uma das coisas mais frustrantes com as quais temos que lidar ao trabalhar em frameworks é recriar todos aqueles componentes de UI de baixo nível: botões, guias, menus suspensos, etc. , digamos React, mas precisaremos reescrevê-los se quisermos construir algo em Svelte. Ou Vue. Ou Sólido. E assim por diante.

Não seria melhor se pudéssemos definir esses componentes de UI de baixo nível uma vez, de maneira independente de framework, e então reutilizá-los entre frameworks? Claro que sim! E nós podemos; componentes da web são o caminho. Esta postagem mostrará como.

No momento, a história do SSR para componentes da web está um pouco ausente. Shadow DOM declarativo (DSD) é como um componente da web é renderizado no lado do servidor, mas, no momento em que este livro foi escrito, ele não estava integrado às suas estruturas de aplicativos favoritas, como Next, Remix ou SvelteKit. Se isso for um requisito para você, verifique o status mais recente do DSD. Mas caso contrário, se o SSR não for algo que você esteja usando, continue lendo.

Primeiro, algum contexto

Web Components são essencialmente elementos HTML que você mesmo define, como <yummy-pizza> ou o que quer que seja, desde o início. Eles são abordados aqui em CSS-Tricks (incluindo uma extensa série de Caleb Williams e um de John Rhea), mas examinaremos brevemente o processo. Essencialmente, você define uma classe JavaScript, herda-a de HTMLElemente, em seguida, defina quaisquer propriedades, atributos e estilos que o componente da Web possua e, é claro, a marcação que ele renderizará aos usuários.

Ser capaz de definir elementos HTML personalizados que não estão vinculados a nenhum componente específico é interessante. Mas esta liberdade é também uma limitação. Existir independentemente de qualquer estrutura JavaScript significa que você não pode realmente interagir com essas estruturas JavaScript. Pense em um componente React que busca alguns dados e depois renderiza alguns de outros Componente React, passando os dados. Isso realmente não funcionaria como um componente web, já que um componente web não sabe como renderizar um componente React.

Os componentes da Web se destacam particularmente como componentes foliares. Componentes de folha são a última coisa a ser renderizada em uma árvore de componentes. Estes são os componentes que recebem alguns adereços e renderizam alguns UI. Esses são não os componentes localizados no meio de sua árvore de componentes, transmitindo dados, definindo contexto, etc. UI terá a mesma aparência, não importa qual estrutura JavaScript esteja alimentando o restante do aplicativo.

O componente web que estamos construindo

Em vez de construir algo chato (e comum), como um botão, vamos construir algo um pouco diferente. No meu última postagem analisamos o uso de visualizações de imagens borradas para evitar o refluxo do conteúdo e fornecer uma interface de usuário decente para os usuários enquanto nossas imagens são carregadas. Vimos a codificação base64 de versões borradas e degradadas de nossas imagens e mostramos isso em nossa IU enquanto a imagem real era carregada. Também analisamos a geração de visualizações incrivelmente compactas e desfocadas usando uma ferramenta chamada Desfoque.

Essa postagem mostrou como gerar essas visualizações e usá-las em um projeto React. Esta postagem mostrará como usar essas visualizações de um componente da web para que possam ser usadas por qualquer Estrutura JavaScript.

Mas precisamos caminhar antes de podermos executar, então primeiro examinaremos algo trivial e bobo para ver exatamente como os componentes da web funcionam.

Tudo nesta postagem irá construir componentes web vanilla sem qualquer ferramenta. Isso significa que o código terá um pouco de clichê, mas deverá ser relativamente fácil de seguir. Ferramentas como Lit or stencil são projetados para construir componentes da web e podem ser usados ​​para remover grande parte desse padrão. Recomendo que você dê uma olhada! Mas para este post vou preferir um pouco mais de clichê em troca de não ter que apresentar e ensinar outra dependência.

Um componente de contador simples

Vamos construir o clássico “Hello World” dos componentes JavaScript: um contador. Iremos renderizar um valor e um botão que incrementa esse valor. Simples e enfadonho, mas nos permitirá examinar o componente web mais simples possível.

Para construir um componente web, o primeiro passo é criar uma classe JavaScript, que herda de HTMLElement:

class Counter extends HTMLElement {}

A última etapa é registrar o componente web, mas somente se ainda não o tivermos registrado:

if (!customElements.get("counter-wc")) { customElements.define("counter-wc", Counter);
}

E, claro, renderize:

<counter-wc></counter-wc>

E tudo o que está no meio é fazer com que o componente da web faça o que quisermos. Um método de ciclo de vida comum é connectedCallback, que é acionado quando nosso componente web é adicionado ao DOM. Poderíamos usar esse método para renderizar qualquer conteúdo que quiséssemos. Lembre-se, esta é uma classe JS herdada de HTMLElement, o que significa que nosso this value é o próprio elemento do componente web, com todos os métodos normais de manipulação do DOM que você já conhece e adora.

Na forma mais simples, poderíamos fazer isso:

class Counter extends HTMLElement { connectedCallback() { this.innerHTML = "<div style='color: green'>Hey</div>"; }
} if (!customElements.get("counter-wc")) { customElements.define("counter-wc", Counter);
}

…o que funcionará perfeitamente.

A palavra "ei" em verde.
Construindo Componentes Web Interoperáveis ​​Que Funcionam Até Com React

Adicionando conteúdo real

Vamos adicionar algum conteúdo útil e interativo. precisamos de <span> para manter o valor do número atual e um <button> para incrementar o contador. Por enquanto, criaremos esse conteúdo em nosso construtor e anexá-lo-emos quando o componente web estiver realmente no DOM:

constructor() { super(); const container = document.createElement('div'); this.valSpan = document.createElement('span'); const increment = document.createElement('button'); increment.innerText = 'Increment'; increment.addEventListener('click', () => { this.#value = this.#currentValue + 1; }); container.appendChild(this.valSpan); container.appendChild(document.createElement('br')); container.appendChild(increment); this.container = container;
} connectedCallback() { this.appendChild(this.container); this.update();
}

Se você está realmente enojado com a criação manual do DOM, lembre-se de que você pode definir innerHTMLou até mesmo criar um elemento de modelo uma vez como uma propriedade estática de sua classe de componente da web, cloná-lo e inserir o conteúdo para novas instâncias de componentes da web. Provavelmente há outras opções nas quais não estou pensando, ou você sempre pode usar uma estrutura de componentes da web como Lit or stencil. Mas para esta postagem, continuaremos a mantê-la simples.

Continuando, precisamos de uma propriedade de classe JavaScript configurável chamada value

#currentValue = 0; set #value(val) { this.#currentValue = val; this.update();
}

É apenas uma propriedade de classe padrão com um setter, junto com uma segunda propriedade para armazenar o valor. Uma reviravolta divertida é que estou usando a sintaxe de propriedade de classe JavaScript privada para esses valores. Isso significa que ninguém fora do nosso componente web poderá tocar nesses valores. Este é JavaScript padrão que é compatível com todos os navegadores modernos, então não tenha medo de usá-lo.

Ou fique à vontade para ligar _value se você preferir. E, por último, o nosso update método:

update() { this.valSpan.innerText = this.#currentValue;
}

Funciona!

O componente da web do contador.
Construindo Componentes Web Interoperáveis ​​Que Funcionam Até Com React

Obviamente, este não é um código que você deseja manter em escala. Aqui está um completo exemplo de trabalho se você quiser ver mais de perto. Como eu disse, ferramentas como Lit e Stencil foram projetadas para tornar isso mais simples.

Adicionando mais algumas funcionalidades

Esta postagem não é um mergulho profundo nos componentes da web. Não cobriremos todas as APIs e ciclos de vida; não vamos nem cobrir raízes de sombra ou slots. Há um conteúdo infinito sobre esses tópicos. Meu objetivo aqui é fornecer uma introdução decente o suficiente para despertar algum interesse, juntamente com algumas orientações úteis sobre como realmente utilização componentes da web com as estruturas JavaScript populares que você já conhece e adora.

Para isso, vamos aprimorar um pouco nosso componente counter web. Vamos fazer com que aceite um color atributo, para controlar a cor do valor exibido. E vamos também aceitar um increment propriedade, para que os consumidores deste componente da web possam incrementá-lo em 2, 3, 4 por vez. E para impulsionar essas mudanças de estado, vamos usar nosso novo contador em uma sandbox Svelte - chegaremos ao React daqui a pouco.

Começaremos com o mesmo componente web de antes e adicionaremos um atributo color. Para configurar nosso componente web para aceitar e responder a um atributo, adicionamos um static observedAttributes propriedade que retorna os atributos que nosso componente web escuta.

static observedAttributes = ["color"];

Com isso implementado, podemos adicionar um attributeChangedCallback método de ciclo de vida, que será executado sempre que qualquer um dos atributos listados em observedAttributes estão definidos ou atualizados.

attributeChangedCallback(name, oldValue, newValue) { if (name === "color") { this.update(); }
}

Agora atualizamos nosso update método para realmente usá-lo:

update() { this.valSpan.innerText = this._currentValue; this.valSpan.style.color = this.getAttribute("color") || "black";
}

Por último, vamos adicionar nosso increment propriedade:

increment = 1;

Simples e humilde.

Usando o componente contador no Svelte

Vamos usar o que acabamos de fazer. Iremos para nosso componente de aplicativo Svelte e adicionaremos algo assim:

<script> let color = "red";
</script> <style> main { text-align: center; }
</style> <main> <select bind:value={color}> <option value="red">Red</option> <option value="green">Green</option> <option value="blue">Blue</option> </select> <counter-wc color={color}></counter-wc>
</main>

E funciona! Nosso contador é renderizado, incrementado e o menu suspenso atualiza a cor. Como você pode ver, renderizamos o atributo color em nosso modelo Svelte e, quando o valor muda, Svelte cuida do trabalho braçal de chamar setAttribute em nossa instância de componente web subjacente. Não há nada de especial aqui: é a mesma coisa que já faz com os atributos de qualquer Elemento HTML.

As coisas ficam um pouco interessantes com o increment suporte. Isso é não um atributo em nosso componente web; é um suporte na classe do componente web. Isso significa que ele precisa ser definido na instância do componente web. Tenha paciência comigo, pois as coisas ficarão muito mais simples daqui a pouco.

Primeiro, adicionaremos algumas variáveis ​​ao nosso componente Svelte:

let increment = 1;
let wcInstance;

Nossa potência de um componente de contador permitirá que você aumente em 1 ou 2:

<button on:click={() => increment = 1}>Increment 1</button>
<button on:click={() => increment = 2}>Increment 2</button>

Mas, em teoria, precisamos obter a instância real do nosso componente web. Esta é a mesma coisa que sempre fazemos sempre que adicionamos um ref com Reagir. Com Svelte, é simples bind:this directiva:

<counter-wc bind:this={wcInstance} color={color}></counter-wc>

Agora, em nosso modelo Svelte, ouvimos alterações na variável de incremento do nosso componente e definimos a propriedade do componente web subjacente.

$: { if (wcInstance) { wcInstance.increment = increment; }
}

Você pode testar nesta demonstração ao vivo.

Obviamente não queremos fazer isso para cada componente ou suporte da web que precisamos gerenciar. Não seria bom se pudéssemos definir increment diretamente em nosso componente web, na marcação, como normalmente fazemos para adereços de componente, e temos, você sabe, apenas trabalhe? Em outras palavras, seria bom se pudéssemos excluir todos os usos de wcInstance e use este código mais simples:

<counter-wc increment={increment} color={color}></counter-wc>

Acontece que podemos. Este código funciona; Svelte cuida de todo esse trabalho braçal para nós. Confira nesta demonstração. Este é um comportamento padrão para praticamente todos os frameworks JavaScript.

Então, por que mostrei a maneira manual de definir a propriedade do componente web? Duas razões: é útil entender como essas coisas funcionam e, há pouco, eu disse que isso funciona para “praticamente” todos os frameworks JavaScript. Mas há uma estrutura que, irritantemente, não suporta a configuração de props de componentes web como acabamos de ver.

React é uma fera diferente

Construindo componentes da Web interoperáveis ​​​​que funcionam até mesmo com React PlatoBlockchain Data Intelligence. Pesquisa vertical. Ai.
Construindo Componentes Web Interoperáveis ​​Que Funcionam Até Com React

Reagir. A estrutura JavaScript mais popular do planeta não oferece suporte à interoperabilidade básica com componentes da web. Este é um problema bem conhecido e exclusivo do React. Curiosamente, isso foi corrigido no branch experimental do React, mas por algum motivo não foi incorporado à versão 18. Dito isso, ainda podemos acompanhar o progresso disso. E você pode tentar isso sozinho com um demonstração ao vivo.

A solução, claro, é usar um ref, pegue a instância do componente web e defina manualmente increment quando esse valor muda. Se parece com isso:

import React, { useState, useRef, useEffect } from 'react';
import './counter-wc'; export default function App() { const [increment, setIncrement] = useState(1); const [color, setColor] = useState('red'); const wcRef = useRef(null); useEffect(() => { wcRef.current.increment = increment; }, [increment]); return ( <div> <div className="increment-container"> <button onClick={() => setIncrement(1)}>Increment by 1</button> <button onClick={() => setIncrement(2)}>Increment by 2</button> </div> <select value={color} onChange={(e) => setColor(e.target.value)}> <option value="red">Red</option> <option value="green">Green</option> <option value="blue">Blue</option> </select> <counter-wc ref={wcRef} increment={increment} color={color}></counter-wc> </div> );
}

Conforme discutimos, codificar isso manualmente para cada propriedade de componente da web simplesmente não é escalonável. Mas nem tudo está perdido porque temos algumas opções.

Opção 1: use atributos em todos os lugares

Temos atributos. Se você clicou na demonstração do React acima, o increment prop não estava funcionando, mas a cor mudou corretamente. Não podemos codificar tudo com atributos? Infelizmente não. Os valores dos atributos só podem ser strings. Isso é bom o suficiente aqui, e poderíamos ir um pouco longe com essa abordagem. Números como increment pode ser convertido de e para strings. Poderíamos até mesmo stringificar/analisar objetos JSON. Mas eventualmente precisaremos passar uma função para um componente web e, nesse ponto, ficaremos sem opções.

Opção 2: embrulhe

Há um velho ditado que diz que você pode resolver qualquer problema na ciência da computação adicionando um nível de indireção (exceto o problema de muitos níveis de indireção). O código para definir esses adereços é bastante previsível e simples. E se o escondermos em uma biblioteca? As pessoas inteligentes por trás do Lit tem uma solução. Esta biblioteca cria um novo componente React para você depois que você fornece um componente da web e lista as propriedades necessárias. Embora inteligente, não sou fã dessa abordagem.

Em vez de ter um mapeamento individual de componentes da web para componentes React criados manualmente, o que eu prefiro é apenas um Componente React que passamos ao nosso componente web nome da tag para (counter-wc no nosso caso) — junto com todos os atributos e propriedades — e para que este componente renderize nosso componente web, adicione o refe descubra o que é um adereço e o que é um atributo. Essa é a solução ideal na minha opinião. Não conheço nenhuma biblioteca que faça isso, mas deve ser simples de criar. Vamos tentar!

Este é o uso estamos procurando:

<WcWrapper wcTag="counter-wc" increment={increment} color={color} />

wcTag é o nome da tag do componente web; o resto são as propriedades e atributos que queremos transmitir.

Esta é a aparência da minha implementação:

import React, { createElement, useRef, useLayoutEffect, memo } from 'react'; const _WcWrapper = (props) => { const { wcTag, children, ...restProps } = props; const wcRef = useRef(null); useLayoutEffect(() => { const wc = wcRef.current; for (const [key, value] of Object.entries(restProps)) { if (key in wc) { if (wc[key] !== value) { wc[key] = value; } } else { if (wc.getAttribute(key) !== value) { wc.setAttribute(key, value); } } } }); return createElement(wcTag, { ref: wcRef });
}; export const WcWrapper = memo(_WcWrapper);

A linha mais interessante está no final:

return createElement(wcTag, { ref: wcRef });

É assim que criamos um elemento no React com um nome dinâmico. Na verdade, é para isso que o React normalmente transpila o JSX. Todos os nossos divs são convertidos para createElement("div") chamadas. Normalmente não precisamos chamar essa API diretamente, mas ela estará disponível quando precisarmos.

Além disso, queremos executar um efeito de layout e percorrer cada adereço que passamos para nosso componente. Percorremos todos eles e verificamos se é uma propriedade com um in check that verifica o objeto de instância do componente da web, bem como sua cadeia de protótipo, que capturará quaisquer getters/setters que acabem no protótipo da classe. Se tal propriedade não existir, será considerado um atributo. Em ambos os casos, só o definimos se o valor realmente mudou.

Se você está se perguntando por que usamos useLayoutEffect em vez de useEffect, é porque queremos executar essas atualizações imediatamente antes que nosso conteúdo seja renderizado. Além disso, observe que não temos nenhum array de dependência para nosso useLayoutEffect; isso significa que queremos executar esta atualização em cada renderização. Isso pode ser arriscado, pois o React tende a renderizar novamente muito. Eu melhorei isso envolvendo tudo em React.memo. Esta é essencialmente a versão moderna do React.PureComponent, o que significa que o componente só será renderizado novamente se algum de seus adereços reais tiver sido alterado - e verifica se isso aconteceu por meio de uma simples verificação de igualdade.

O único risco aqui é que, se você estiver passando um objeto que está sofrendo mutação diretamente, sem reatribuir, não verá as atualizações. Mas isso é altamente desencorajado, especialmente na comunidade React, então não me preocuparia com isso.

Antes de prosseguir, gostaria de salientar uma última coisa. Você pode não estar satisfeito com a aparência do uso. Novamente, este componente é usado assim:

<WcWrapper wcTag="counter-wc" increment={increment} color={color} />

Especificamente, você pode não gostar de passar o nome da tag do componente web para o <WcWrapper> componente e preferir em vez disso o @lit-labs/react pacote acima, que cria um novo componente React individual para cada componente da web. Isso é totalmente justo e eu encorajo você a usar o que for mais confortável para você. Mas para mim, uma vantagem desta abordagem é que é fácil excluir. Se por algum milagre o React mesclar o manuseio adequado de componentes da web de seu ramo experimental em main amanhã, você poderá alterar o código acima deste:

<WcWrapper wcTag="counter-wc" increment={increment} color={color} />

…para isso:

<counter-wc ref={wcRef} increment={increment} color={color} />

Você provavelmente poderia até escrever um único codemod para fazer isso em qualquer lugar e depois excluir <WcWrapper> completamente. Na verdade, risque isso: uma pesquisa global e substituição por um RegEx provavelmente funcionaria.

A implementação

Eu sei, parece que foi preciso uma jornada para chegar aqui. Se você se lembra, nosso objetivo original era pegar o código de visualização da imagem que vimos no meu última postageme mova-o para um componente da web para que possa ser usado em qualquer estrutura JavaScript. A falta de interoperabilidade adequada do React adicionou muitos detalhes ao mix. Mas agora que temos uma ideia decente sobre como criar um componente web e usá-lo, a implementação será quase anticlimática.

Deixarei todo o componente da web aqui e destacarei algumas das partes interessantes. Se você quiser vê-lo em ação, aqui está um demonstração de trabalho. Ele alternará entre meus três livros favoritos nas minhas três linguagens de programação favoritas. O URL de cada livro será único a cada vez, para que você possa ver a visualização, embora provavelmente queira limitar as coisas na guia Rede do DevTools para realmente ver o que está acontecendo.

Ver código inteiro
class BookCover extends HTMLElement { static observedAttributes = ['url']; attributeChangedCallback(name, oldValue, newValue) { if (name === 'url') { this.createMainImage(newValue); } } set preview(val) { this.previewEl = this.createPreview(val); this.render(); } createPreview(val) { if (typeof val === 'string') { return base64Preview(val); } else { return blurHashPreview(val); } } createMainImage(url) { this.loaded = false; const img = document.createElement('img'); img.alt = 'Book cover'; img.addEventListener('load', () =&gt; { if (img === this.imageEl) { this.loaded = true; this.render(); } }); img.src = url; this.imageEl = img; } connectedCallback() { this.render(); } render() { const elementMaybe = this.loaded ? this.imageEl : this.previewEl; syncSingleChild(this, elementMaybe); }
}

Primeiro, registramos o atributo que nos interessa e reagimos quando ele muda:

static observedAttributes = ['url']; attributeChangedCallback(name, oldValue, newValue) { if (name === 'url') { this.createMainImage(newValue); }
}

Isso faz com que nosso componente de imagem seja criado, que será exibido apenas quando carregado:

createMainImage(url) { this.loaded = false; const img = document.createElement('img'); img.alt = 'Book cover'; img.addEventListener('load', () => { if (img === this.imageEl) { this.loaded = true; this.render(); } }); img.src = url; this.imageEl = img;
}

Em seguida, temos nossa propriedade de visualização, que pode ser nossa string de visualização base64 ou nosso blurhash pacote:

set preview(val) { this.previewEl = this.createPreview(val); this.render();
} createPreview(val) { if (typeof val === 'string') { return base64Preview(val); } else { return blurHashPreview(val); }
}

Isso adia para qualquer função auxiliar de que precisamos:

function base64Preview(val) { const img = document.createElement('img'); img.src = val; return img;
} function blurHashPreview(preview) { const canvasEl = document.createElement('canvas'); const { w: width, h: height } = preview; canvasEl.width = width; canvasEl.height = height; const pixels = decode(preview.blurhash, width, height); const ctx = canvasEl.getContext('2d'); const imageData = ctx.createImageData(width, height); imageData.data.set(pixels); ctx.putImageData(imageData, 0, 0); return canvasEl;
}

E, por último, o nosso render método:

connectedCallback() { this.render();
} render() { const elementMaybe = this.loaded ? this.imageEl : this.previewEl; syncSingleChild(this, elementMaybe);
}

E alguns métodos auxiliares para unir tudo:

export function syncSingleChild(container, child) { const currentChild = container.firstElementChild; if (currentChild !== child) { clearContainer(container); if (child) { container.appendChild(child); } }
} export function clearContainer(el) { let child; while ((child = el.firstElementChild)) { el.removeChild(child); }
}

É um pouco mais padronizado do que precisaríamos se construíssemos isso em um framework, mas a vantagem é que podemos reutilizá-lo em qualquer framework que quisermos - embora o React precise de um wrapper por enquanto, como discutimos .

Miudezas

Já mencionei o wrapper React do Lit. Mas se você estiver usando o Stencil, ele na verdade suporta um pipeline de saída separado apenas para React. E o pessoal da Microsoft também criou algo semelhante ao wrapper do Lit, anexado à biblioteca de componentes da Web Fast.

Como mencionei, todos os frameworks não chamados React cuidarão da configuração das propriedades dos componentes da web para você. Observe que alguns têm alguns sabores especiais de sintaxe. Por exemplo, com Solid.js, <your-wc value={12}> sempre assume que value é uma propriedade que você pode substituir por um attr prefixo, como <your-wc attr:value={12}>.

Resumindo

Os componentes da Web são uma parte interessante e frequentemente subutilizada do cenário de desenvolvimento da Web. Eles podem ajudar a reduzir sua dependência de qualquer estrutura JavaScript, gerenciando sua UI ou componentes “folha”. Embora criá-los como componentes da web — ao contrário dos componentes Svelte ou React — não seja tão ergonômico, a vantagem é que eles serão amplamente reutilizáveis.


Construindo Componentes Web Interoperáveis ​​Que Funcionam Até Com React publicado originalmente em Truques de CSS. Você deve receba o boletim informativo.

Carimbo de hora:

Mais de Truques CSS