Criando um carregador de layout esquelético para o Discourse

Olá :waving_hand:

A ideia básica

O objetivo era construir um carregador de esqueleto (skeleton loader) que seja gerado a partir da interface real do Discourse, em vez de depender de um modelo de esqueleto codificado.

O construtor permite que um administrador selecione elementos reais na página e os transforme em regiões de esqueleto.

Por exemplo:

.title
.avatar
.topic-excerpt
.btn
.category-breadcrumb

O componente então usa esses seletores para gerar o esqueleto em tempo de execução.

Prévia do Esqueleto

A parte interessante é que o administrador não precisa escrever os seletores manualmente. O construtor analisa o elemento selecionado e gera vários seletores candidatos.


Gerar seletores úteis se mostrou mais difícil do que o esperado

Um dos primeiros problemas com os quais me deparei foi a geração de seletores.

Uma implementação ingênua pode facilmente produzir algo como:

.container.list-container.--topic-list .row.full-width .contents ...

Tecnicamente válido, mas específico demais para uma configuração de esqueleto reutilizável.

Pode ficar ainda pior com ícones, onde o seletor gerado pode incluir detalhes de implementação, como classes relacionadas a SVG.

O que eu realmente queria era algo mais próximo de:

.badge-category__name

ou:

.badge-category__wrapper .d-icon

em vez de um seletor descrevendo todo o caminho do DOM.

Portanto, o construtor agora gera vários candidatos e os pontua com base em coisas como:

  • profundidade do seletor
  • número de classes
  • correspondências repetidas
  • classes relacionadas a estado
  • classes técnicas de SVG/ícone
  • se o seletor ainda corresponde ao elemento selecionado

O resultado é uma lista de seletores recomendados para que o administrador possa escolher ou editar manualmente.


Elementos ocultos

Há também um seletor separado para elementos que devem simplesmente desaparecer enquanto o esqueleto é exibido.

Por exemplo:

.alert.alert-info

Isso se mostrou útil para coisas como banners de anúncio ou avisos temporários que existem durante a construção/testes, mas não devem afetar o layout do esqueleto.

Um problema interessante aqui foi que ocultar um elemento não deve deixar um espaço vazio.

Portanto, os elementos excluídos não são tratados apenas como uma lista simples de display: none - o cálculo da geometria também precisa entender que o elemento não faz parte do layout final.

Prévia do Esqueleto


Navegação

Provavelmente o maior desafio foi a navegação.

O comportamento desejado era:

click

  ↓

mostrar esqueleto imediatamente

  ↓

Discourse muda a rota

  ↓

DOM de destino aparece

  ↓

ocultar esqueleto

A solução tentadora era se integrar profundamente ao ciclo de vida da navegação e esperar que o DOM se estabilizasse completamente.

Isso se mostrou a abordagem errada.

Em um momento, o esqueleto podia permanecer visível por vários segundos depois que o conteúdo real já estava lá.

A lição foi simples:

O esqueleto não deve se tornar uma porta de prontidão do DOM.

Assim que o destino tiver conteúdo real suficiente para assumir, o esqueleto deve sair do caminho.

Isso fez uma enorme diferença na velocidade percebida da navegação.


Viewports

O Discourse já tem um sistema de viewport responsivo, então o componente agora usa a mesma abstração de breakpoint:

xs
sm
md
lg
xl
2xl

A configuração do esqueleto pode ser agrupada adicionalmente como:

mobile → xs / sm
tablet → md
desktop → lg / xl / 2xl
all → everything

Isso significa que o componente não precisa conhecer os valores de pixels reais de forma alguma.

Se o Discourse alterar os valores de breakpoint, o componente de esqueleto não precisa ser reescrito em torno de novos números codificados.


Cache de geometria

Os seletores nos dizem o que deve ser renderizado, mas não nos dizem exatamente onde as formas do esqueleto devem aparecer.

Para isso, adicionei a captura de geometria.

O construtor pode medir as regiões renderizadas reais e armazenar sua geometria para que o carregador possa renderizar um esqueleto de destino imediatamente durante a navegação SPA.

Há também uma opção explícita de bloqueio de geometria para casos em que não quero que visitas posteriores mudem continuamente a geometria de referência.

Esta foi outra distinção importante:

definição do seletor e geometria renderizada são duas coisas diferentes.


Rascunhos

Outra coisa que se tornou necessária foi o estado de rascunho.

Eu não queria este fluxo de trabalho:

abrir construtor

→ gastar 10 minutos configurando

→ fechar construtor

→ tudo está perdido

Portanto, o construtor mantém um rascunho em andamento separadamente da configuração real do tema.

O rascunho é limitado à combinação de página/rota/viewport, então, por exemplo:

topic-list / lg
topic-list / md
topic-list / xs

não se sobrescrevem acidentalmente.

Fechar o construtor não destrói o trabalho.


Desfazer

À medida que o construtor se tornou mais interativo, um sistema de Desfazer se tornou quase inevitável.

O construtor armazena snapshots de seu estado de configuração:

{
  "regions": \[\],
  "excludes": \[\]
}

em vez de tentar manter um histórico de operações de DOM.

Isso torna o sistema de desfazer muito mais fácil de raciocinar e também o mantém independente do DOM real da página.


Este projeto está em desenvolvimento ativo. Espero que esteja pronto para um componente de tema em breve! :slightly_smiling_face:

3 curtidas