O artigo contém uma análise mais detalhada dos princípios e exemplos detalhados da prática que não se enquadraram no relatório. Recomendo a leitura se você quiser se aprofundar no tópico e aprender como escrevemos componentes reutilizáveis. Se você deseja se familiarizar com o mundo dos componentes reutilizáveis em termos gerais, então, na minha opinião, o registro do relatório é mais adequado para você.
Todo mundo sabe que a duplicação de código é ruim porque é frequentemente falada: em um livro sobre sua primeira linguagem de programação, em cursos de programação, em livros sobre como escrever código de qualidade, como Perfect Code e Clean Code.
Vamos ver por que é tão difícil evitar a duplicação no frontend e como escrever componentes reutilizáveis corretamente. E os princípios do SOLID nos ajudarão.
Por que é difícil parar de duplicar o código?
Parece que o princípio parece simples. E ao mesmo tempo é fácil verificar se está sendo respeitado: se não houver duplicação na base de código, está tudo bem. Por que é tão difícil na prática?
Vamos analisar um exemplo com uma biblioteca de componentes Ya. Tutorial. Era uma vez, o projeto era um monólito. Mais tarde, por conveniência, os desenvolvedores decidiram mover os componentes reutilizáveis para uma biblioteca separada. Um dos primeiros a chegar lá foi um componente de botão. O componente evoluiu, ao longo do tempo, novas "habilidades" e configurações para o botão apareceram, o número de personalizações visuais aumentou. Depois de algum tempo, o componente se tornou tão sofisticado que se tornou inconveniente usá-lo para novas tarefas e expandir ainda mais.
E assim, na próxima iteração, apareceu uma cópia do componente - Button2. Isso aconteceu há muito tempo, ninguém se lembra dos motivos exatos do aparecimento. No entanto, o componente foi criado.
Parece que está tudo bem - deixe haver dois botões. Afinal, é apenas um botão. No entanto, na realidade, ter dois botões em um projeto tinha consequências muito desagradáveis a longo prazo.
Cada vez que era necessário atualizar os estilos, não estava claro em qual componente fazer isso. Tive que verificar onde qual componente é usado para não quebrar acidentalmente estilos em outros lugares. Quando uma nova versão da exibição do botão apareceu, decidimos qual dos componentes estender. Cada vez que víamos um novo recurso, pensávamos em qual botão usar. E às vezes em um lugar precisávamos de vários botões diferentes e, em seguida, importamos dois componentes de botão em um componente de projeto de uma vez.
Apesar de a longo prazo a existência dos dois componentes do botão se revelar dolorosa, não compreendemos imediatamente a gravidade do problema e conseguimos fazer algo semelhante com os ícones. Criamos um componente e quando percebemos que não era muito conveniente para nós, criamos o Icon2, e quando se revelou inadequado para novas tarefas, escrevemos o Icon3.
Quase todo o conjunto de efeitos negativos da duplicação de botões foi repetido nos componentes do ícone. Foi um pouco mais fácil porque os ícones são usados com menos frequência no projeto. Embora, para ser honesto, tudo depende do recurso. Além disso, tanto para o botão quanto para o ícone, o componente antigo não foi excluído na criação de um novo, pois a exclusão exigiu muita refatoração com o possível aparecimento de bugs ao longo do projeto. Então, o que os casos de botão e ícone têm em comum? O mesmo esquema para o aparecimento de duplicados no projeto. Era difícil para nós reutilizar o componente atual, adaptá-lo às novas condições, então criamos um novo.
Ao criar uma duplicata de um componente, complicamos nossa vida futura. Queríamos montar uma interface a partir de blocos prontos, como um construtor. Para fazer isso convenientemente, você precisa de componentes de qualidade que possa simplesmente pegar e usar. A raiz do problema é que o componente que planejávamos reutilizar foi gravado incorretamente. Era difícil estendê-lo e aplicá-lo em outro lugar.
Um componente reutilizável deve ser versátil e simples ao mesmo tempo. Trabalhar com ele não deve causar dor e assemelhar-se a atirar em um pardal de um canhão. Por outro lado, o componente deve ser personalizável o suficiente para que, com uma pequena alteração no script, não fique claro que é mais fácil escrever "Component2".
SÓLIDO para componentes reutilizáveis
Para escrever componentes de qualidade, precisamos de um conjunto de regras por trás da sigla SOLID. Essas regras explicam como combinar funções e estruturas de dados em classes e como as classes devem ser combinadas entre si.
Por que exatamente SOLID e nenhum outro conjunto de princípios? As regras SOLID informam como arquitetar corretamente seu aplicativo. Para que você possa desenvolver o projeto com segurança, adicionar novas funções, alterar as existentes e ao mesmo tempo não quebrar tudo ao seu redor. Quando tentei descrever o que, na minha opinião, deve ser um bom componente, percebi que meus critérios estão próximos dos princípios do SOLID.
- S é o princípio da responsabilidade exclusiva.
- O - o princípio de abertura / proximidade.
- L é o princípio de substituição de Liskov.
- I - o princípio da separação das interfaces.
- D - Princípio de Inversão de Dependências.
Alguns desses princípios funcionam bem para descrever componentes. Outros parecem mais rebuscados em um contexto de front-end. Mas, todos juntos, eles descrevem bem minha visão de um componente de qualidade.
Seguiremos os princípios fora da ordem, mas do simples ao complexo. Primeiro, vamos examinar as coisas básicas que podem ser úteis em um grande número de situações e, em seguida, as mais poderosas e específicas.
O artigo fornece exemplos de código em React + TypeScript. Escolhi React como a estrutura com a qual mais trabalho. Em seu lugar pode haver qualquer outra estrutura que você goste ou se adapte. Em vez de TS, pode haver JS puro, mas o TypeScript permite que você descreva explicitamente os contratos no código, o que simplifica o desenvolvimento e o uso de componentes complexos.
Básico
O princípio de abrir / fechar
As entidades de software devem ser abertas para extensão e fechadas para modificação. Em outras palavras, devemos ser capazes de estender a funcionalidade com o novo código sem alterar o existente. Por que isso é importante? Se toda vez que você tiver que editar um monte de módulos existentes para adicionar novas funcionalidades, todo o projeto se tornará instável. Haverá muitos lugares que podem quebrar devido ao fato de estarem constantemente sendo alterados.
Vamos considerar a aplicação do princípio no exemplo de um botão. Criamos um componente de botão e tem estilos. Até agora, tudo está funcionando bem. Mas então surge uma nova tarefa, e acontece que em um local específico para esse botão, estilos diferentes precisam ser aplicados.
O botão é escrito de tal forma que não pode ser alterado sem editar o código
Para aplicar estilos diferentes na versão atual, você terá que editar o componente do botão. O problema é que o componente não é personalizável. Não consideraremos a opção de escrever estilos globais, uma vez que não é confiável. Qualquer coisa pode quebrar com qualquer edição. As consequências são fáceis de imaginar se você colocar algo mais complexo no lugar do botão, por exemplo, um componente selecionador de data.
De acordo com o princípio de abertura / fechamento, devemos escrever o código de forma que, ao adicionar um novo estilo, não tenhamos que reescrever o código do botão. Tudo funcionará se parte dos estilos do componente puderem ser jogados de fora. Para fazer isso, criaremos um suporte no qual passaremos a classe necessária para descrever os novos estilos do componente.
// ,
import cx from 'classnames';
// — mix
const Button = ({ children, mix }) => {
return (
<button
className={cx("my-button", mix)}
>
{children}
</button>
}
Pronto, agora você não precisa editar seu código para personalizar um componente.
Este método bastante popular permite que você personalize a aparência de um componente. É chamado de mix porque a classe adicional é combinada com as próprias classes do componente. Observe que passar em uma classe não é a única maneira de estilizar um componente de fora. Você pode passar um objeto com propriedades CSS para um componente. Você pode usar soluções CSS-in-JS, a essência não mudará. As misturas são usadas por muitas bibliotecas de componentes, por exemplo: MaterialUI, Vuetify, PrimeNG e outros.
Que conclusão pode ser tirada sobre as misturas? Eles são fáceis de implementar, versáteis e permitem que você personalize com flexibilidade a aparência de seus componentes com o mínimo de esforço.
Mas essa abordagem também tem suas desvantagens. Permite muita liberdade, o que pode levar a problemas com a especificidade dos seletores. Ele também quebra o encapsulamento. Para gerar o seletor css correto, você precisa conhecer a estrutura interna do componente. Isso significa que esse código pode falhar ao refatorar um componente.
Variabilidade de componentes
Um componente possui partes que são seu núcleo. Se os mudarmos, obteremos um componente diferente. Para um botão, é um conjunto de estados e comportamentos. Os usuários distinguem um botão de, por exemplo, uma caixa de seleção, graças ao seu efeito de passar o mouse e clicar. Existe uma lógica geral de trabalho: quando o usuário clica, o manipulador de eventos é acionado. Este é o núcleo do componente, o que torna um botão um botão. Sim, existem exceções, mas é assim que funciona na maioria dos casos de uso.
Existem também peças no componente que podem mudar dependendo do local de uso. Os estilos pertencem a este grupo. Talvez precisemos de um botão de tamanho ou cor diferente. Com um traço e filete diferentes ou com um efeito de foco diferente. Todos os estilos são parte modificável do componente. Não queremos reescrever ou criar um novo componente sempre que o botão parecer diferente.
O que muda com frequência deve ser ajustado sem alterar o código. Caso contrário, nos encontraremos em uma situação em que é mais fácil criar um novo componente do que personalizar e adicionar um antigo, que acabou por não ser flexível o suficiente.
Temas
Vamos voltar a personalizar o visual do componente usando o exemplo de um botão. A próxima maneira é aplicar temas. Por temas, quero dizer a capacidade de um componente aparecer em vários modos, de maneira diferente em lugares diferentes. Esta interpretação é mais ampla do que temática no contexto de temas claros e escuros.
O uso de temas não exclui o método anterior com mixagens, mas o complementa. Dizemos explicitamente que um componente possui vários métodos de exibição e, quando usado, requer que você especifique o desejado.
import cx from 'classnames';
import b from 'b_';
const Button = ({ children, mix, theme }) => (
<button
className={cx(
b("my-button", { theme }), mix)}
>
{children}
</button>
)
O tema permite evitar o zoológico de estilos, quando, por exemplo, você tem 20 botões em seu projeto e todos eles parecem um pouco diferentes devido ao fato de que os estilos de cada botão são definidos no local de aplicação. A abordagem pode ser aplicada a todos os novos componentes sem medo de engenharia excessiva. Se você entender que um componente pode ter uma aparência diferente, é melhor definir um tema explicitamente desde o início. Isso simplificará o desenvolvimento de outros componentes.
Mas também há uma desvantagem - o método só serve para customizar o visual e não permite influenciar o comportamento do componente.
Aninhamento de componentes
Não listei todas as maneiras de evitar a alteração do código do componente ao adicionar novas funções. Outros serão demonstrados examinando o resto dos princípios. Aqui, eu gostaria de mencionar slots e componentes filhos.
A página da web é uma hierarquia de componentes em forma de árvore. Cada componente decide por si mesmo o que e como renderizar. Mas nem sempre é o caso. Por exemplo, um botão permite que você especifique qual conteúdo será renderizado internamente. No React, a ferramenta principal são os adereços infantis e renderizadores. O Vue tem um conceito de slots mais poderoso. Não há problemas ao escrever componentes simples usando esses recursos. Mas é importante não esquecer que mesmo em componentes complexos, você pode usar o lançamento de alguns dos elementos que o componente deve exibir de cima.
Avançado
Os princípios descritos abaixo são adequados para projetos maiores. As técnicas correspondentes fornecem mais flexibilidade, mas aumentam a complexidade do design e do desenvolvimento.
Princípio de Responsabilidade Única
O princípio da responsabilidade única significa que um módulo deve ter um e apenas um motivo para mudar.
Por que isso é importante? As consequências da violação do princípio incluem:
- Risco de quebrar outro ao editar uma parte do sistema.
- Abstrações ruins. O resultado são componentes que podem executar várias funções, o que torna difícil entender o que exatamente o componente deve fazer e o que não.
- Trabalho inconveniente com componentes. É muito difícil fazer melhorias ou corrigir bugs em um componente que faz tudo de uma vez.
Vamos voltar ao exemplo do tema e ver se o princípio da responsabilidade única é respeitado. Já na sua forma atual, o theming cumpre as suas tarefas, mas isso não significa que a solução não tenha problemas e não possa ser melhorada.
Um módulo é editado por pessoas diferentes por motivos diferentes.
Digamos que colocamos todos os estilos em um arquivo css. Ele pode ser editado por pessoas diferentes por motivos diferentes. Acontece que o princípio da responsabilidade exclusiva foi violado. Alguém pode refatorar os estilos e outro desenvolvedor ajustará o novo recurso. Assim, você pode quebrar algo facilmente.
Vamos dar uma olhada em como podem ser os temas compatíveis com SRP. A imagem perfeita: temos um botão e, separadamente, um conjunto de temas para ele. Podemos aplicar um tema a um botão e obter um botão temático. Como bônus, gostaria de poder montar um botão com vários temas disponíveis, por exemplo, para ser colocado em uma biblioteca de componentes.
Pintura desejada. Um tema é uma entidade separada e pode ser aplicado a um botão. Um
tema envolve um botão. Esta é a abordagem usada no Lego, nossa biblioteca de componentes internos. Usamos HOC (High Order Components) para envolver o componente base e adicionar novos recursos a ele. Por exemplo, a capacidade de exibir com um tema.
HOC é uma função que pega um componente e retorna outro componente. Um HOC com um tema pode lançar um objeto com estilos dentro do botão. Abaixo está uma opção bastante educacional, na vida real você pode usar soluções mais elegantes, por exemplo, jogar uma classe no componente, os estilos dos quais são importados para o HOC, ou usar soluções CSS-in-JS.
Um exemplo de um HOC simples para o tema de um botão:
const withTheme1 = (Button) =>
(props) => {
return (
<Button
{...props}
styles={theme1Styles}
/>
)
}
const Theme1Button = withTheme1(Button);
O HOC só pode aplicar estilos se um tema específico for especificado, caso contrário, ele não faz nada. Assim, podemos montar um botão com um conjunto de temas e ativar o que precisamos especificando o tema prop.
Usando vários HOCs para coletar um botão com os temas desejados:
import "./styles.css";
// .
const ButtonBase = ({ style, children }) => {
console.log("styl123e", style);
return <button style={style}>{children}</button>;
};
const withTheme1 = (Button) => (props) => {
// HOC , "theme1"
if (props.theme === "theme1") {
return <Button {...props} style={{ color: "red" }} />;
}
return <Button {...props} />;
};
const withTheme2 = (Button) => (props) => {
// HOC , "theme2"
if (props.theme === "theme2") {
return <Button {...props} style={{ color: "green" }} />;
}
return <Button {...props} />;
};
// - HOC
const compose = (...hocs) => (BaseComponent) =>
hocs.reduce((Component, nextHOC) => nextHOC(Component), BaseComponent);
// ,
const Button = compose(withTheme1, withTheme2)(ButtonBase);
export default function App() {
return (
<div className="App">
<Button theme="theme1">"Red"</Button>
<Button theme="theme2">"Green"</Button>
</div>
);
}
E aqui chegamos à conclusão de que precisamos dividir as áreas de responsabilidade. Mesmo que pareça que você tem um componente, pense - é realmente assim? Talvez deva ser dividido em várias camadas, cada uma das quais será responsável por uma função específica. Em quase todos os casos, a camada visual pode ser desacoplada da lógica do componente.
Separar um tema em uma entidade separada oferece vantagens para a usabilidade do componente: você pode colocar um botão em uma biblioteca com um conjunto básico de temas e permitir que os usuários escrevam seus próprios, se necessário; os tópicos podem ser convenientemente atrapalhados entre os projetos. Isso permite que você preserve a consistência da interface e não sobrecarregue a biblioteca original.
Existem diferentes opções para implementar a divisão em camadas. O exemplo acima foi com HOC, mas a composição também é possível. No entanto, acredito que no caso da theming, os HOCs são mais adequados, uma vez que o tema não é um componente autônomo.
Não é apenas o visual que pode ser colocado em uma camada separada. Mas não pretendo considerar em detalhes a transferência da lógica de negócios para o HOC, porque a questão é muito holística. Minha opinião é que você pode fazer isso se entender o que está fazendo e por que precisa disso.
Componentes compostos
Vamos prosseguir para componentes mais complexos. Vamos pegar o Select como exemplo e ver qual é o uso do Princípio da Responsabilidade Única. A seleção pode ser considerada uma composição de componentes menores.
- Container - comunicação entre outros componentes.
- Campo - o texto para a seleção usual e a entrada para o componente CobmoBox, onde o usuário insere algo.
- Ícone - um ícone tradicional no campo para seleção.
- Menu é um componente que exibe uma lista de itens para seleção.
- O item é um item separado no menu.
Para cumprir o princípio de responsabilidade única, você precisa dividir todas as entidades em componentes separados, deixando todos com apenas um motivo para editar. Quando cortamos o arquivo, surge a pergunta: como agora personalizar o conjunto de componentes resultante? Por exemplo, se você precisar definir um tema escuro para o campo, aumente o ícone e altere a cor do menu. Existem duas maneiras de fazer isso.
Substituições
A primeira maneira é direta. Mova todas as configurações dos componentes aninhados para os adereços do original. Porém, se você aplicar a solução "de frente", verifica-se que o select tem um grande número de adereços, que são difíceis de entender. É necessário organizá-los de forma conveniente. É aqui que entra a substituição. Este é um config que é encaminhado para um componente e permite que você personalize cada um de seus elementos.
<Select
...
overrides={{
Field: {
props: {theme: 'dark'}
},
Icon: {
props: {size: 'big'},
},
Menu: {
style: {backgroundColor: '#CCCCCC'}
},
}}
/>
Dei um exemplo simples em que substituímos os adereços. Mas a substituição pode ser considerada uma configuração global - ela configura tudo o que os componentes suportam. Você pode ver como isso funciona na prática na biblioteca BaseWeb .
Em suma, usando a substituição, você pode personalizar os componentes compostos de maneira flexível, e essa abordagem também se adapta bem. Contras: as configurações de componentes complexos são muito grandes e o poder de substituição tem um lado negativo. Temos controle total sobre os componentes internos, o que nos permite fazer coisas estranhas e expor configurações inválidas. Além disso, se você não usa bibliotecas, mas deseja implementar a abordagem sozinho, terá que ensinar os componentes a entender a configuração ou escrever wrappers que irão lê-lo e configurar os componentes corretamente.
Princípio de Inversão de Dependência
Para entender a alternativa de substituir as configurações, vamos voltar para a letra D em SOLID. Este é o Princípio de Inversão de Dependência. Ele argumenta que o código que implementa a política de alto nível não deve depender do código que implementa os detalhes de baixo nível.
Voltemos à nossa seleção. O contêiner é responsável pela comunicação entre outras partes do componente. Na verdade, é a raiz que controla a renderização do resto dos blocos. Para fazer isso, ele deve importá-los.
Esta é a aparência da raiz de qualquer componente complexo, se você não usar a inversão de dependência:
import InputField from './InputField';
import Icon from './Icon';
import Menu from './Menu';
import Option from './Option';
Vamos analisar as dependências entre os componentes para entender o que pode dar errado. Agora, o Select de nível superior depende do Menu de nível inferior, porque ele o importará para si mesmo. O princípio de inversão de dependência foi quebrado. Isso cria problemas.
- Primeiro, se você alterar o Menu, terá que editar Selecionar.
- Em segundo lugar, se quisermos usar um componente de menu diferente, também temos que editar o componente selecionado.
Não está claro o que fazer quando você precisa do Select com um menu diferente.
Você precisa expandir a dependência. Faça com que o componente do menu dependa do select. A inversão de dependência é feita por meio de injeção de dependência - a seleção deve aceitar um componente de menu como um dos parâmetros, props. É aqui que a digitação é útil. Vamos indicar qual componente o Select está esperando.
// Select
const Select = ({
Menu: React.ComponentType<IMenu>
}) => {
return (
...
<Menu>
...
</Menu>
...
)
...
}
É assim que declaramos que o select precisa de um componente de menu cujos adereços satisfaçam uma determinada interface. Em seguida, as setas apontarão na direção oposta, conforme determina o princípio DI.
A seta está expandida, é assim que a Inversão de Dependência funciona.
Resolvemos o problema da dependência, mas um pouco de açúcar sintático e ferramentas auxiliares são bem-vindos aqui.
Sempre, colocar todas as dependências em um componente no local de renderização é tedioso, mas a biblioteca bem-react tem um registro de dependências e um processo de composição. Com a ajuda deles, você pode empacotar dependências e configurações uma vez e, em seguida, apenas usar o componente pronto.
import { compose } from '@bem-react/core'
import { withRegistry, Registry } from '@bem-react/di'
const selectRegistry = new Registry({ id: cnSelect() })
...
selectRegistry.fill({
'Trigger': Button,
'Popup': Popup,
'Menu': Menu,
'Icon': Icon,
})
const Select = compose(
...
withRegistry(selectRegistry),
)(SelectDesktop)
O exemplo acima mostra parte da montagem do componente usando o exemplo bem-react. O código de exemplo completo e a sandbox podem ser encontrados no livro de histórias da IU do yandex .
O que ganhamos com o uso da Inversão de Dependência?
- Controle total - a liberdade de personalizar todos os componentes de um componente.
- Encapsulamento flexível - a capacidade de tornar os componentes muito flexíveis e totalmente personalizáveis. Se necessário, o desenvolvedor substituirá todos os blocos que compõem o componente e obterá o que deseja. Nesse caso, sempre existe a opção de criar componentes já configurados e prontos.
- Escalabilidade - este método funciona bem para bibliotecas de qualquer tamanho.
Nós da Yandex.Tutorial escrevemos nossos próprios componentes usando DI. A biblioteca interna de componentes Lego também segue essa abordagem. Mas tem uma desvantagem significativa - um desenvolvimento muito mais complexo.
Dificuldades no desenvolvimento de componentes reutilizáveis
Qual é a dificuldade em desenvolver componentes reutilizáveis?
Primeiro, um design longo e cuidadoso. Você precisa entender de quais partes os componentes são feitos e quais partes podem ser alteradas. Se tornarmos todas as partes mutáveis, acabaremos com um grande número de abstrações que são difíceis de entender. Se houver poucas peças substituíveis, o componente não será flexível o suficiente. Terá de ser melhorado para evitar futuros problemas de reutilização.
Em segundo lugar, os altos requisitos para os componentes. Você entende em que partes consistirão os componentes. Agora você precisa escrevê-los de forma que não saibam nada um do outro, mas possam ser usados juntos. É mais difícil do que desenvolver sem levar em conta a reutilização.
Em terceiro lugar, uma estrutura complexa como consequência dos pontos anteriores. Se você precisar de personalização séria, terá que reconstruir todas as dependências do componente. Para fazer isso, você precisa entender profundamente em que partes ele consiste. Uma boa documentação é essencial no processo.
O Tutorial possui uma biblioteca de componentes internos onde a mecânica educacional está localizada - uma parte da interface com a qual as crianças interagem enquanto resolvem problemas. E então há uma biblioteca compartilhada de serviços educacionais. Colocamos lá os componentes que queremos reutilizar entre diferentes serviços.
A transferência de um mecânico leva várias semanas, desde que já tenhamos um componente funcionando e não estejamos adicionando novas funcionalidades. A maior parte desse trabalho é dividir o componente em pedaços independentes e permitir que sejam compartilhados.
Princípio de Substituição de Liskov
Os princípios anteriores eram sobre o que fazer e os dois últimos serão sobre o que não quebrar.
Vamos começar com o princípio de substituição de Barbara Liskov. Ele diz que os objetos em um programa devem ser substituídos por instâncias de seus subtipos sem interromper a execução correta do programa.
Normalmente não escrevemos componentes como classes e não usamos herança. Todos os componentes são intercambiáveis fora da caixa. Esta é a base do frontend moderno. Uma digitação forte ajuda a evitar erros e manter a compatibilidade.
Como a substituibilidade out-of-the-box pode ser quebrada? O componente possui uma API. Por API, quero dizer um conjunto de adereços para um componente e mecanismos integrados à estrutura, como um mecanismo de manipulador de eventos. A forte tipagem e linting no IDE podem destacar a incompatibilidade na API, mas o componente pode interagir com o mundo externo e ignorar a API:
- leia e escreva algo para a loja global,
- interagir com a janela,
- interagir com cookies,
- ler / escrever armazenamento local,
- fazer solicitações à rede.
Tudo isso é inseguro, pois o componente depende do ambiente e pode quebrar se você movê-lo para outro local ou para outro projeto.
Para cumprir o princípio de substituição Liskov, você precisa:
- usar recursos de digitação,
- evite a interação ignorando a API do componente,
- evitar efeitos colaterais.
Como evitar interação não API? Coloque tudo de que o componente depende na API e escreva um wrapper que encaminhe dados do mundo externo para os props. Por exemplo, assim:
const Component = () => {
/*
, , .
, .
, , .
*/
const {userName} = useStore();
// , ( , ).
const userToken = getFromCookie();
// — window .
const {taskList} = window.ssrData;
const handleTaskUpdate = () => {
// API . .
fetch(...)
}
return <div>{'...'}</div>;
};
/*
.
, .
*/
const Component2 = ({
userName, userToken, onTaskUpdate
}) => {
return <div>{'...'}</div>;
};
Princípio de Segregação de Interface
Muitas interfaces de uso especial são melhores do que uma interface de uso geral. Não fui capaz de transferir o princípio para os componentes front-end de forma tão inequívoca. Então eu entendo isso como uma necessidade de ficar de olho na API.
É necessário transferir o mínimo de entidades possível para o componente e não transferir dados que não sejam usados por ele. Um grande número de adereços em um componente é um motivo para ser cauteloso. Muito provavelmente, ele viola os princípios SOLID.
Onde e como reutilizamos?
Discutimos princípios para ajudá-lo a escrever componentes de qualidade. Agora vamos ver onde e como vamos reutilizá-los. Isso ajudará você a entender quais outros problemas você pode encontrar.
O contexto pode ser diferente: você precisa usar um componente em outro lugar na mesma página ou, por exemplo, deseja reutilizá-lo em outros projetos da empresa - são coisas completamente diferentes. Destaco várias opções:
Nenhuma reutilização é necessária ainda.Você escreveu um componente, acha que é específico e não planeja usá-lo em nenhum outro lugar. Não há necessidade de fazer esforços adicionais. E você pode seguir alguns passos simples que serão úteis se ainda quiser voltar a ele. Portanto, por exemplo, você pode verificar se o componente não está muito vinculado ao ambiente e se as dependências foram agrupadas. Você também pode fazer uma reserva para personalização para o futuro: adicionar temas ou a capacidade de alterar a aparência de um componente de fora (como no exemplo com um botão) - não leva muito tempo.
Reutilize no mesmo projeto.Você escreveu um componente e tem certeza de que desejará reutilizá-lo em outro lugar em seu projeto atual. Tudo o que foi escrito acima é relevante aqui. Só agora é imperativo remover todas as dependências em wrappers externos e é altamente desejável poder personalizar externamente (temas ou mixagens). Se um componente contém muita lógica, você deve pensar se ele é necessário em todos os lugares ou se deve ser modificado em alguns lugares. Para a segunda opção, considere a possibilidade de personalização. Também é importante aqui pensar sobre a estrutura do componente e dividi-lo em partes, se necessário.
Reutilize em uma pilha semelhante.Você entende que o componente será útil em um projeto vizinho que tenha a mesma pilha que você. É aqui que todos os itens acima se tornam obrigatórios. Além disso, aconselho você a monitorar de perto as dependências e tecnologias. O projeto vizinho usa exatamente as mesmas versões das bibliotecas que você? O SASS e o TypeScript estão usando a mesma versão?
Também gostaria de destacar a reutilização em outro ambiente de execução , por exemplo, em SSR. Decida se seu componente pode e deve ser renderizado em SSR. Em caso afirmativo, certifique-se de que ele renderiza conforme o esperado de antemão. Lembre-se de que existem outros tempos de execução, como deno ou GraalVM. Considere seus recursos se você usá-los.
Bibliotecas de componentes
Se os componentes precisam ser reutilizados entre vários repositórios e / ou projetos, eles devem ser movidos para a biblioteca.
Pilha
Quanto mais tecnologias forem utilizadas em projetos, mais difícil será resolver problemas de compatibilidade. É melhor reduzir o zoológico e minimizar o número de tecnologias usadas: frameworks, linguagens, versões de grandes bibliotecas. Se você entender que realmente precisa de muita tecnologia, terá que aprender a conviver com ela. Por exemplo, você pode usar wrappers sobre componentes da web, coletar tudo em JS puro ou usar adaptadores para componentes.
O tamanho
Se usar um componente simples de sua biblioteca adiciona alguns megabytes ao pacote, não está tudo bem. Esses componentes não precisam ser reutilizados, porque a perspectiva de escrever sua própria versão light do zero parece justificada. Você pode resolver o problema usando ferramentas de controle de tamanho, por exemplo, limite de tamanho.
Não se esqueça da modularidade - um desenvolvedor que deseja usar seu componente deve ser capaz de pegar apenas ele, e não arrastar todo o código da biblioteca para o projeto.
É importante que a biblioteca modular não seja compilada em um arquivo. Você também precisa controlar a versão JS que a biblioteca vai usar. Se você construir a biblioteca em ES.NEXT e projetos em ES5, haverá problemas. Você também precisa configurar adequadamente o assembly para versões mais antigas de navegadores e certificar-se de que todos os usuários da biblioteca saibam no que está acontecendo. Se isso for muito complicado, existe uma alternativa - configurar suas próprias regras de construção de biblioteca em cada projeto.
Atualizar
Pense com antecedência sobre como você atualizará a biblioteca. É bom se você conhece todos os clientes e seus scripts personalizados. Isso ajudará você a pensar melhor sobre migrações e mudanças significativas. Por exemplo, uma equipe que usa sua biblioteca achará extremamente frustrante aprender sobre uma atualização importante com mudanças importantes antes do lançamento.
Ao mover componentes para uma biblioteca que outra pessoa está usando, você perde a facilidade de refatoração. Para evitar que o fardo da refatoração se torne excessivo, aconselho você a não arrastar novos componentes para as bibliotecas. Eles provavelmente mudarão, o que significa que você terá que gastar muito tempo atualizando e mantendo a compatibilidade.
Personalização e design
O design não afeta a capacidade de reutilização, mas é uma parte importante da personalização. Em nosso Tutorial, os componentes não vivem por conta própria, sua aparência é projetada por designers. Os designers têm um sistema de design. Se um componente parecer diferente no sistema e no repositório, os problemas não podem ser evitados. Designers e desenvolvedores não têm as mesmas idéias sobre a aparência da interface, o que pode levar a decisões erradas.
Vitrine de componentes
A vitrine de componentes ajudará a simplificar a interação com designers. Uma das soluções de vitrine populares é o Storybook . Usando esta ou outra ferramenta adequada, você pode mostrar os componentes do projeto para qualquer pessoa fora do desenvolvimento.
Adicione interatividade à vitrine - os designers devem ser capazes de interagir com os componentes e ver como eles são exibidos e funcionam com diferentes parâmetros.
Não se esqueça de configurar a vitrine para atualizar automaticamente quando você atualizar componentes. Para fazer isso, você precisa mover o processo para CI. Agora os designers podem sempre ver quais componentes prontos estão no projeto e usá-los.
Sistema de design
Para um desenvolvedor, um sistema de design é um conjunto de regras que governam a aparência dos componentes em um projeto. Para evitar que o zoológico de componentes cresça, você pode limitar a personalização aos seus limites.
Outro ponto importante é que o sistema de design e o tipo de componentes do projeto às vezes são diferentes. Por exemplo, quando há uma grande reformulação e nem tudo pode ser atualizado no código, ou você tem que ajustar um componente, mas não há tempo para fazer alterações no sistema de design. Nestes casos, é do seu interesse e do interesse dos designers sincronizar o sistema de design e o projeto assim que surgir a oportunidade.
O último conselho universal e óbvio: comunique-se e negocie. Não há necessidade de perceber os designers como aqueles que se afastam do desenvolvimento e apenas criam e editam layouts. Trabalhar em estreita colaboração com eles o ajudará a projetar e implementar uma interface de qualidade. Em última análise, isso beneficiará a causa comum e encantará os usuários do produto.
conclusões
O código duplicado leva a dificuldades no desenvolvimento e uma diminuição na qualidade do frontend. Para evitar as consequências, você precisa monitorar a qualidade dos componentes e os princípios do SOLID ajudam a escrever componentes de qualidade.
É muito mais difícil escrever um bom componente com uma reserva para o futuro do que um que resolva rapidamente o problema aqui e agora. Ao mesmo tempo, bons "tijolos" são apenas parte da solução. Se você trouxer componentes para a biblioteca, precisará tornar o trabalho com eles conveniente e eles também precisam ser sincronizados com o sistema de design.
Como você pode ver, a tarefa não é fácil. É difícil e demorado desenvolver componentes reutilizáveis de alta qualidade. Vale a pena? Acredito que cada um responderá a essa pergunta por si mesmo. Para projetos pequenos, a sobrecarga pode ser muito alta. Para projetos em que o desenvolvimento de longo prazo não é planejado, investir esforços na reutilização de código também é uma decisão controversa. No entanto, tendo dito "não precisamos disso agora", é fácil ignorar como você acaba em uma situação em que a falta de componentes reutilizáveis trará muitos problemas que poderiam não ter acontecido. Portanto, não repita nossos erros e não se repita!
Veja o relatório