Web Components - Distribuindo funcionalidade em SSR / SSG
Em algum momento como desenvedores Front-end nós iremos bater no dilema de distribuir funcionalidades de maneira mais agnóstica. Isso, porque conforme cresce a complexidade do sistema aumenta consigo a pluralidade de tecnologias.
Um caso claro, quando sua empresa começa a agregar partes das funcionalidades de produto de outras empresas, ou mesmo, internamente acaba separando os times mirando para uma melhor governança dos seus produtos.
Com diferentes tecnologias, diferentes frameworks, como compartilhar funcionalidades a fim de ganhar produtividade?
Em termos de Front-end existem algumas soluções que nos possibilitam compartilhar experiências inteiras entre diferentes times. A mais difundida e comum são os micro-frontends. Existe uma série de tópicos no assunto hoje sobre o tema, mas eu gostaria de fato tratar nesse post o caso dos Web Components.
Em algum momento você vai cair na necessidade de usar Web Components para não ter que se conectar à nenhum framework e possibilitar o reuso em qualquer aplicação.
Mas qual o dilema aqui?
A questão é, trabalhar com Web Components costuma ser tão burocrático quanto trabalhar com frameworks muitas vezes, as soluções atuais geralmente entregam um formato que é comum ao se utilizar os frameworks que é o acoplamento do Html com o Javascript.
Seja utilizando template strings ou utilizando JSX, as soluções acabam gerando build que muitas vezes são incompatíveis ao rodar no servidor, seja na entrega via SSR, seja na entrega de estáticos através do SSG.
Além disso o bundle costuma ser maior também, já que o seu componente conterá tanto a lógica, quanto o html para funcionar.
A solução
Uma forma de resolver isso é separar o Html da sua lógica Javascript. Quais os benefícios disso?
- Bundle Menor - Uma vez que você apenas precisa distribuir a parte Javascript, seu html fica de fora aqui.
- Flexibilidade & Simplicidade - Dado um web component que não tenha um html atrelado a si, ele requer menos lógica para endereçar mudanças possíveis de layout
- SSG & SSR Habilitado - A parte que você precisa rodar no servidor é a parte do HTML estático, portanto, não há a complexidade de se virtualizar a aplicação no ambiente node, apenas entregar um html padrão.
Beleza, mas vamos por partes...
A distribuição
Eu vou utilizar um repositório que fiz há algum tempo para demonstrar alguns conceitos de Micro-frontends, mas que vai ser útil para esse post aqui.
Esse repositório utiliza o Parcel. Este setup me permite desenvolver utilizando .ts, pug ( para tornar meu html dinâmico ) sem muita configuração.
A estrutura de pasta é mais ou menos essa:
mfes/
├── mfe-a
├── mfe-b
└── mfe-swiper/
├── app.ts
├── index.ts
├── index.css
├── index.pug
└── page.pug
-
page.pug
é um shell, onde eu faço os imports de dependencias do meu web component e me permite desenvolver local. Estas dependencias são injetadas no meu Web Component, assim ele consegue ficar bem leve e delegar para a página essa tarefa de prover estas dependencias. -
index.pug
é o html do meu Web component. -
index.css
é o Css do meu Web Component. -
index.ts
é o entrypoint para a saída do js do meu Web Component.
Esse repositório vai dar um output nestes arquivos:
https://mfe-jails.netlify.app/mfe-swiper/index.js
https://mfe-jails.netlify.app/mfe-swiper/index.css
https://mfe-jails.netlify.app/mfe-swiper/index.html
https://mfe-jails.netlify.app/mfe-swiper/page.html
O Componente Javascript
Fazer Componentes usando Javascript puro é bem trabalhoso, especialmente quando começa a ter lógicas de validação de formulário, adição / remoção de campos, reatividade utilizando chamadas de api's etc.
Como eu disse anteriormente, a maioria das bibliotecas / frameworks para Web Components acabam trabalhando com o html dentro do js, e como pode ver nos arquivos gerados, a idéia aqui é separar, especialmente se você deseja ter o html pronto no page load ( SSR / SSG ), dá uma olhada no source-code:
https://mfe-jails.netlify.app/mfe-swiper/page.html
Você vai ver que o html do meu componente está renderizado já. Então, qual biblioteca me permitiria esse tipo de vantagem?
Nesse post vou utilizar o Jails. A proposta dele é trazer uma série de helpers
que simplificam demais o desenvolvimento de componentes se beneficiando de algumas técnicas e features como: pub/sub
, reatividade
, event delegation
, template system
, dependency injection
, state management
, dom diffing
etc.
A biblioteca em si pesa 9kb gzippada. Essa é uma parte importante porque não adianta o componente ser leve se o framework / lib é pesado. Com ela conseguimos desenvolver de maneira sucinta, reativa e gerando bundles leves.
Você escreve seu componente em um padrão funcional, recebendo uma série de helpers para te ajudar na produtividade, e este componente irá enxergar todo o html do seu conteúdo e trabalhar em cima de algumas diretivas como: html-inner
, html-for
por exemplo.
Ou seja, ele enriquece seu Custom Element de forma a adicionar lógica no seu html já renderizado.
O Exemplo
Para facilitar a didática, eu quis fazer uma espécie de Banner
como um Web Component. Daqueles que você pode ver o próximo item e o anterior via navegação por cliques.
A cara deste componente utilizando o Jails é essa:
export default function appSwiper ({ main, on, elm, state, dependencies }) {
const wrapper = elm.querySelector('.swiper')
const { Swiper } = dependencies
const swiper = new Swiper(wrapper)
main(() => {
// Opcional
// Deixando pública as funções: next, prev, goto etc.
elm.next = next
elm.prev = prev
elm.goto = goto
elm.useSwiper = useSwiper
// Definindo eventos
on('click', '[data-next]', next)
on('click', '[data-prev]', prev)
swiper.on('slideChange', onchange)
})
const onchange = (instance) => {
state.set({ page: instance.activeIndex + 1 })
}
const next = () => {
swiper.slideNext()
}
const prev = () => {
swiper.slidePrev()
}
const goto = (n) => {
swiper.slideTo(n-1)
}
const useSwiper = (fn) => {
fn(swiper)
}
}
export const model = {
page: 1
}
Explicando o código por partes.
-
Na definição da função, nós recebemos os helpers que a biblioteca fornece.
main
é uma função construtora, que permite que referenciemos as funções debaixo antes delas serem definidas, assim nos ajudando a organizar o código melhor, detalhes da implementação para baixo, chamadas iniciais e definições de variáveis em cima. -
Deixamos acessiveis as funções atrelando-as ao elemento
<app-swiper>
que é nosso web component. Assim, conseguimos interagir e utilizar as funcionalidades do web component. Para isso, basta referenciá-lo no DOM, e chamar as funções atreladas ao elemento.
const wc = document.querySelector('app-swiper')
wc.next() // vai para o próximo slide
wc.prev() // vai para o slide anterior
wc.useSwiper((swiper) => {
console.log( swiper ) // Faça algo com a instância do swiper
})
-
Adiciona eventos. A interface
on
adiciona os eventos diretamente no elemeto customizado do web component, portanto, mesmo se voce adicionar mais elementos dinamicamente, os eventos continuam vivos ( Event Delegation ).
Qualquer elemento que possuir um atributodata-next
oudata-prev
, disparará as ações denext
eprev
. Dessa forma, o web component se torna flexível o suficiente para você criar sua própria estrutura html, sem necessáriamente ter que criar uma serie de atributos opcionais para alterar elementos de UI. -
Por fim, a cada mudança de slide, o componente atualiza seu estado local, disponibilizando uma variável
page
para que voce possa usar no seu html para mostrar a página atual.
Uma parte importante para deixar o Web Component leve. Ele depende da lib Swiper, que é uma lib de Carrossel que implementa toda a lógica de UI. Eu não importo ela diretamente no meu código, de forma estática. Eu adiciono-a como uma dependencia externa, assim ela não pesa no bundle do arquivo js final do web component.
A forma como instanciamos e damos vida para o componente passando suas dependencias de maneira geral no Jails é assim:
import Swiper from 'swiper'
import { register, start } from 'jails-js'
import * as appSwiper from 'components/app-swiper'
register('app-swiper', appSwiper, { Swiper })
start()
Utilizando o Web Component
Bom, dada a explicação de como é feita essa estrutura que criei para as minhas demos de Web Component, fica a questão, como utilizar?
Nesse exemplo, existem dois principais assets a serem considerados para o reuso, o Javascript e o Css:
- https://mfe-jails.netlify.app/mfe-swiper/index.js
- https://mfe-jails.netlify.app/mfe-swiper/index.css
E o html?
O html é por conta nossa.
Ahhh mas eu não reutilizo o html???
Vai por mim, o html é a parte menos trabalhosa para nós. Onde de fato perdemos tempo mesmo é no Css e no Javascript.
Com isso, basta você criar um componente na sua estrutura do seu projeto que já tem o html montado do seu gosto, e aí reutilizá-lo em outras partes da sua aplicação, por exemplo:
<app-swiper>
<div class="swiper" html-static style="height: 300px">
<div class="swiper-wrapper">
<div class="swiper-slide">
<img src="https://picsum.photos/800/300?random=1" />
</div>
<div class="swiper-slide">
<img src="https://picsum.photos/800/300?random=2" />
</div>
<div class="swiper-slide">
<img src="https://picsum.photos/800/300?random=3" />
</div>
<div class="swiper-slide">
<img src="https://picsum.photos/800/300?random=4" />
</div>
</div>
</div>
<p>Página: <strong html-inner="page">1</strong></p>
<div class="d-flex justify-content-between">
<button class="btn btn-primary" data-prev>Anterior</button>
<button class="btn btn-primary" data-next>Próximo</button>
</div>
</app-swiper>
Na documentação do seu Web Component, é importante deixar um exemplo de uso, e definições obrigatórias para que seu componente funcione corretamente. No caso deste componente, a estrtura swiper > swiper-wrapper > swiper-slide é obrigatória para os seus slides. porém, fora da div.swiper
você tem a liberdade de montar o html da forma como desejar, e esse é o grande valor do Separation of Concerns.
Sabendo que você tem disponível a variável page
para ser utilizada dentro do seu html gerado, e que você pode usar os atributos data-next
, data-prev
em qualquer elemento para poder fazer a navegação acontecer, a partir daqui, seu componente pode se beneficiar da funcionalidade pronta, te dando a flexibilidade de mudar a sua UI sem precisar recompilar seu código novamente.
Mais um exemplo e Fim.
Deixei uma versão completa para que possam brincar ali e ver como funciona na prática:
https://stackblitz.com/edit/jails-web-component-app-swiper
Era isso que eu gostaria de mostrar aqui para vocês. O Jails já completa um pouco mais de 8 anos, nessa versão atual o foco é evoluir as maneiras de distribuir funcionalidades, seja através de web components, seja através de Micro-frontends.
É possível ainda renderizar esse componente como ele é, sem a opção da flexibilidade de alterar o html mas com o beneficio de simplificar sua reutilização através de uma funcionalidade de rendering de micro-frontend.
Mas isso fica para um próximo post.