Toda vez que preciso me sentar para criar um novo aplicativo, caio em um estupor fácil. Minha cabeça está girando com a necessidade de escolher qual biblioteca ou estrutura levar esse tempo. A última vez que escrevi na biblioteca X, mas agora a estrutura Y cresceu e foi entregue, e ainda existe um UI Kit Z legal, e muito trabalho foi deixado em projetos anteriores.
A partir de algum momento, percebi que a estrutura realmente não importa - o que eu preciso, posso fazer em qualquer uma delas. Aqui parece que você deveria ser feliz, pegar algo com o máximo de estrelas no github e se acalmar. Mesmo assim, surge constantemente um desejo irresistível de fazer algo próprio, sua própria bicicleta. Bem então. Algumas idéias gerais sobre esse assunto e uma estrutura chamada Chorda estão esperando por você.
De fato, o problema não é que a decisão de outra pessoa seja ruim ou ineficaz. Não. O problema é que a decisão de outra pessoa nos faz pensar de maneiras que podem não ser convenientes para nós. Mas espera. O que significa “conveniente-inconveniente” e como isso pode afetar o desenvolvimento? Lembre-se de que existe um DX, de fato - um conjunto de práticas pessoais estabelecidas e geralmente aceitas. A partir daqui, podemos dizer que é conveniente para nós quando nosso próprio DX coincide com o DX do autor da biblioteca ou estrutura. E no caso em que divergem, surge o próprio desconforto, irritação e busca de algo novo.
Um pouco de história
Ao desenvolver uma interface do usuário para um aplicativo corporativo, você se depara com um grande número de formulários de usuário. E um dia um pensamento brilhante me vem à cabeça: por que eu crio um formulário da Web toda vez que posso simplesmente listar os campos no JSON e alimentar a estrutura resultante para o gerador? E, embora no mundo das empresas sangrentas essa abordagem não funcione muito bem (por que isso é uma conversa à parte), mas a ideia de passar de um estilo imperativo para um declarativo geralmente não é ruim. Prova disso é o grande número de geradores de formulários da Web, páginas e até sites inteiros que podem ser facilmente encontrados na Web.
Então, em algum momento, eu não era estranha ao desejo de melhorar meu código devido à transição para a declaratividade. Mas, assim que precisávamos não apenas de elementos html padrão, mas de componentes de widget complexos e interativos, não poderíamos fugir com um simples gerador. Os requisitos de reutilização, integrabilidade, extensibilidade etc. de código rapidamente foram adicionados a isso. O desenvolvimento de sua própria biblioteca de componentes com uma API declarativa não demorou a chegar.
Mas aqui, a felicidade não aconteceu. Provavelmente, a melhor situação refletirá a opinião do meu colega, que deveria usar a biblioteca criada. Ele olhou para os exemplos, para a documentação e disse: "A biblioteca é legal. Linda, dinâmica. Mas como posso fazer um aplicativo com tudo isso agora?" E ele estava certo. Aconteceu que fabricar um componente não é o mesmo que combinar vários componentes e fazê-los funcionar perfeitamente.
Muito tempo se passou desde então. E quando mais uma vez fui visitado pelo desejo de reunir pensamentos e desenvolvimentos, decidi agir um pouco de maneira diferente e ir não de baixo para cima, mas de cima para baixo.
Gerenciamento de Aplicativos == Gerenciamento de Estado
Estou mais acostumado a considerar o aplicativo como uma máquina de estados finitos com algum conjunto de estados clones. E o trabalho do aplicativo como um conjunto de transições de um estado para outro, no qual a alteração do modelo leva à criação de uma nova versão da exibição. No futuro , chamarei alguns dados fixos (um objeto, uma matriz, um tipo primitivo etc.) relacionados à sua única representação - um documento .
Há um problema óbvio - para muitos valores do modelo, é necessário descrever muitas opções para o documento. Duas abordagens são geralmente usadas aqui:
- Templates. Usamos nossa linguagem de marcação favorita e a complementamos com diretivas de ramificação e loop.
- Funções Descrevemos em nossas funções nossas ramificações e loops em nossa linguagem de programação favorita.
Como regra, essas duas abordagens são declaradas declarativas. O primeiro é considerado declarativo, porque se baseia, embora ligeiramente expandido, nas regras da linguagem de marcação. O segundo - porque se concentra na composição de funções, algumas das quais atuam como regras. O que é digno de nota, agora não há limites claros entre modelos e funções.
Por um lado, gosto de modelos, mas, por outro, queria usar os recursos do javascript. Por exemplo, algo como isto:
createFromConfig({ data: { name: 'Alice' }, tag: 'div', class: 'clickable box', onClick: function () { alert('Click') } })
O resultado é uma configuração JS que descreve um estado específico inteiro. Para descrever os muitos estados, será necessário obter extensibilidade dessa configuração. E qual é a maneira mais conveniente de tornar um conjunto de opções extensível? Não vamos inventar nada aqui - as opções de sobrecarga já existem há muito tempo. Como funciona pode ser visto no exemplo do Vue com sua API de opções. Mas, diferentemente do mesmo Vue, eu me perguntava se o estado completo, incluindo dados e documento, poderia ser descrito da mesma maneira.
Estrutura do aplicativo e declaratividade
O termo “componente” tornou-se muito vago, especialmente após o surgimento do chamado componentes funcionais. À medida que avançamos na estrutura do aplicativo, chamarei o componente de elemento estrutural .
Muito rapidamente, cheguei à conclusão de que o elemento estrutural (componente) não é um elemento de documento, mas alguma entidade, que:
- combina dados e documentos (encadernação e eventos)
- conectado com outras entidades similares (estrutura em árvore)
Como mencionei anteriormente, se você percebe o aplicativo como um conjunto de estados, para esses estados deve ter um método de descrição. Além disso, é necessário encontrar esse método para que ele não contenha operadores imperativos "espúrios". Estamos falando desses elementos muito auxiliares que são introduzidos nos modelos - #if , #elsif , v-for , etc. Acho que muitas pessoas já conhecem a solução - é necessário transferir a lógica para o modelo, deixando no nível da apresentação uma API que permite controlar elementos estruturais por meio de tipos de dados simples.
Pela administração, entendo a presença de variabilidade e ciclicidade.
Variabilidade (caso contrário)
Vamos ver como você pode controlar as opções de exibição usando o exemplo de um componente de cartão no Chorda:
const isHeaderOnly = true const card = new Html({ $header: { }, $footer: { }, components: {header: true, footer: !isHeaderOnly}
Ao definir o valor da opção de componentes , você pode controlar os componentes exibidos. E ao vincular componentes ao armazenamento reativo, obtemos que nossa estrutura ficará sob gerenciamento de dados. Há uma ressalva - o objeto é usado como valor e as chaves nele não são ordenadas, o que impõe algumas restrições aos componentes .
Ciclo (para)
Trabalhar com dados cuja quantidade é conhecida apenas no tempo de execução exigirá iteração nas listas.
const drinks = ['Coffee', 'Tea', 'Milk'] const html = new Html({ html: 'ul', css: 'list', defaultItem: { html: 'li', css: 'list-item' }, items: drinks })
O valor da opção de itens é Matriz, respectivamente, obtemos um conjunto ordenado de componentes. Vincular itens ao armazenamento, como no caso de componentes, transferirá o controle para os dados.
Elementos estruturais são conectados entre si em uma hierarquia de árvore. Se combinarmos os exemplos anteriores, para exibir a lista no corpo do cartão, obtemos o seguinte:
Aproximadamente dessa maneira, a estrutura de dados do aplicativo é criada. É suficiente ter dois tipos de geradores - baseados em Object e em Array. Resta apenas entender como ocorre a transformação de elementos estruturais em um documento.
Quando tudo já está inventado para nós
Em geral, sou a favor do fato de que o sistema de renderização de documentos deve ser implementado no nível do navegador (embora pelo menos o mesmo VDOM). E nossa tarefa será apenas conectá-lo cuidadosamente à árvore de componentes. Afinal, não importa quanto a velocidade da biblioteca cresça, o navegador tem mais ainda.
Sinceramente, tentei fazer minha função de renderização algum dia, mas depois de um tempo desisti, porque não consigo desenhar mais rápido que o VanillaJS (infelizmente!). Agora está na moda usar o VDOM para renderização, e suas implementações são, talvez, mesmo em abundância. Então, mais uma implementação da árvore virtual, decidi não adicioná-la ao cofrinho do github - apenas a próxima estrutura é suficiente.
Inicialmente, um adaptador para a biblioteca Maquette foi criado em Chorda para renderização, mas assim que as tarefas "do mundo real" começaram a aparecer, era mais prático ter uma gaveta no React. Nesse caso, por exemplo, você pode simplesmente usar o React DevTools existente e não escrever o seu próprio.
Para conectar o VDOM a elementos estruturais, você precisa de um layout . Pode ser chamada de função de documento de um elemento estrutural. O que é importante é uma função pura.
Considere um exemplo com um cartão com cabeçalho, corpo e porão. Já foi mencionado que os componentes não são pedidos, ou seja, se começarmos a ligar / desligar os componentes durante a operação, eles aparecerão sempre em um novo pedido. Vamos ver como isso é resolvido pelo layout:
function orderedByKeyLayout (h, type, props, components) { return h(type, props, components.sort((a, b) => a.key - b.key).map(c => c.render())) } const html = new Html({ $header: {}, $content: {}, $footer: {}, layout: orderedByKeyLayout
O layout permite configurar o chamado O elemento host ao qual o componente está associado e seus filhos ( itens e componentes ). Normalmente, o layout padrão é suficiente, mas em alguns casos, o layout requer a presença de elementos de wrapper (por exemplo, para grades) ou a atribuição de classes especiais, que não queremos levar ao nível dos componentes.
Pitada de reatividade
Tendo declarado e desenhado a estrutura dos componentes, obtemos um estado correspondente a um conjunto de dados específico. Em seguida, precisamos descrever os muitos conjuntos de dados e a reação à mudança deles.
Ao trabalhar com dados, não gostei de duas coisas:
- Imunidade. Uma coisa boa para acompanhar as mudanças é a versão para os pobres, que funciona muito bem em objetos primitivos e planos. Porém, assim que a estrutura se torna mais complexa e o número de investimentos aumenta, fica difícil manter a imunidade de um objeto complexo.
- Substituição. Se eu colocar algum objeto no armazém de dados, quando o solicitar, posso retornar uma cópia dele ou outro objeto ou proxy em geral que tenha semelhança estrutural com ele.
Eu queria ter um repositório que se comporte como imutável, mas dentro dele contém dados mutáveis, que também mantêm a persistência do link. No caso ideal, seria assim: eu crio um repositório, escrevo um objeto vazio para ele, começo a inserir dados no formulário de inscrição e, depois de clicar no botão enviar, recebo o mesmo objeto (vinculo o mesmo!) Com as propriedades preenchidas. Eu chamo esse caso de ideal, pois geralmente não acontece que o modelo de armazenamento corresponda ao modelo de apresentação.
Outra tarefa que precisa ser resolvida é fornecer dados do armazenamento para os elementos estruturais. Novamente, não vamos inventar nada e usar a abordagem de conexão com um contexto comum. No caso de Chorda, não temos acesso ao contexto em si, mas apenas à sua exibição, chamada escopo . Além disso, o escopo do componente é o contexto para seus componentes filhos. Essa abordagem permite restringir, expandir ou substituir dados relacionados em qualquer nível de nosso aplicativo, e essas alterações serão isoladas.
Um exemplo de como os dados contextuais são distribuídos por uma árvore de componentes:
const html = new Html({
O momento mais difícil de entender é que cada componente tem seu próprio contexto, e não aquele declarado no topo da estrutura, como costumamos fazer ao trabalhar com modelos.
E quanto à sobrecarga de opções?
Certamente você se depara com uma situação em que há um componente grande e é necessário alterar um pequeno componente aninhado em algum lugar no fundo. Eles dizem que a granulação e a composição devem ajudar aqui. E também, que componentes e arquitetura devem ser projetados imediatamente. A situação fica muito triste se o componente grande não for seu, mas fizer parte de uma biblioteca desenvolvida por outra equipe ou mesmo por uma comunidade independente. E se eles pudessem facilmente fazer alterações no componente base, mesmo se não tivessem sido planejadas originalmente?
Normalmente, os componentes nas bibliotecas são projetados como classes e, em seguida, podem ser usados como base para criar novos componentes. Mas aqui está oculto um pequeno recurso que eu nunca gostei: às vezes criamos uma classe apenas para aplicá-la em um único local. Isso é estranho. Por exemplo, estou acostumado a usar classes para digitar, criar relacionamentos entre grupos de objetos e não usá-los para resolver o problema de decomposição.
Vamos ver como as classes funcionam com a configuração no Chorda.
Eu gosto mais dessa opção do que criar uma classe TitledCard especial que será usada apenas uma vez. E se você precisar fazer parte das opções, poderá usar o mecanismo de impureza. Bem, ninguém cancelou o Object.assign.
Em Chorda, uma classe é essencialmente um contêiner para configuração e desempenha o papel de um tipo especial de impureza.
Por que outra estrutura?
Repito que, na minha opinião, a estrutura é mais sobre o modo de pensar e a experiência do que sobre a tecnologia. Meus hábitos e DX pediram declaratividade em JS, o que não encontrei em outras soluções. Mas a implementação de um recurso atraiu novos, e depois de um tempo eles simplesmente deixaram de se encaixar na estrutura de uma biblioteca especializada.
No momento, Chorda está em desenvolvimento ativo. As principais direções já estão visíveis, mas os detalhes estão constantemente mudando.
Obrigado por ler até o fim. Eu ficaria feliz em comentários.
Onde eu posso ver?
A documentação
Fontes do GitHub
Exemplos de CodePen