O autor do material, cuja tradução publicamos hoje, diz que o
repositório do
GitHub , no qual ele trabalhou e vários outros freelancers, recebeu, por várias razões, cerca de 8.200 estrelas em 3 dias. Este repositório ficou em primeiro lugar no HackerNews e no GitHub Trending, e 20.000 usuários do Reddit votaram nele.

Este repositório reflete a metodologia para o desenvolvimento de aplicativos de pilha completa, aos quais este artigo é dedicado.
Antecedentes
Eu ia escrever esse material por algum tempo. Acredito que não consigo encontrar um momento melhor que esse, quando nosso repositório é muito popular.
Nº 1 em tendências do GitHubEu trabalho em uma equipe de
freelancers . Nossos projetos usam React / React Native, NodeJS e GraphQL. Este material é destinado a quem deseja aprender sobre como desenvolvemos aplicativos. Além disso, será útil para aqueles que se juntarão à nossa equipe no futuro.
Agora vou falar sobre os princípios básicos que usamos no desenvolvimento de projetos.
Quanto mais simples, melhor.
"Quanto mais simples, melhor" é mais fácil dizer do que fazer. A maioria dos desenvolvedores sabe que a simplicidade é um princípio importante no desenvolvimento de software. Mas esse princípio nem sempre é fácil de seguir. Se o código for simples, isso facilita o suporte ao projeto e simplifica o trabalho em equipe nesse projeto. Além disso, a conformidade com esse princípio ajuda a trabalhar com o código que foi escrito, digamos, seis meses atrás.
Aqui estão os erros que me deparei com esse princípio:
- Desejo injustificado de cumprir o princípio DRY. Às vezes, copiar e colar código é bastante normal. Não há necessidade de abstrair cada dois fragmentos de código que são um pouco semelhantes um ao outro. Eu mesmo cometi esse erro. Todos, talvez, cometeram. O DRY é uma boa abordagem de programação, mas a escolha de uma abstração com falha só pode piorar a situação e complicar a base de código. Se você quiser saber mais sobre essas idéias, recomendo a leitura da Programação AHA de Kent A Dodds.
- Recusa em usar as ferramentas disponíveis. Um exemplo desse erro é o uso de
reduce
vez de map
ou filter
. Obviamente, o uso de reduce
pode reproduzir o comportamento do map
. Mas é provável que isso leve a um aumento no tamanho do código e ao fato de que será mais difícil para outras pessoas entender esse código, uma vez que "simplicidade do código" é um conceito subjetivo. Às vezes, pode ser necessário usar apenas reduce
. E se você comparar a velocidade de processamento do conjunto de dados usando as chamadas de map
e filter
combinadas em uma cadeia e usando reduce
, a segunda opção funciona mais rapidamente. Na opção com reduce
é necessário examinar o conjunto de valores uma vez, não dois. Diante de nós está um debate sobre produtividade e simplicidade. Na maioria dos casos, eu preferiria a simplicidade e tentaria evitar a otimização prematura do código, ou seja, escolheria um par de map
/ filter
vez de reduce
. E se a construção de map
e filter
se tornasse o gargalo do sistema, ele traduziria o código para reduce
.
Muitas das idéias que serão discutidas abaixo visam tornar a base de código o mais simples possível e mantê-la nesse estado.
Mantenha entidades semelhantes próximas umas das outras
Esse princípio, o "princípio da colocação", se aplica a muitas partes do aplicativo. Essa é a estrutura das pastas nas quais o código do cliente e do servidor está armazenado, esse é o armazenamento do código do projeto em um repositório, e também é a tomada de decisão sobre qual código está em um determinado arquivo.
Posit Repositório
É recomendável manter o código do cliente e do servidor no mesmo repositório. É simples Não complique o que não é necessário. Com essa abordagem, é conveniente organizar um trabalho em equipe coordenado em um projeto. Trabalhei em projetos que usavam vários repositórios para armazenar materiais. Isso não é um desastre, mas os mono-repositórios tornam a vida muito mais fácil.
▍ Estrutura do projeto da parte do cliente do aplicativo
Estamos escrevendo aplicativos de pilha completa. Ou seja, o código do cliente e o código do servidor. A estrutura de pastas de um projeto típico do cliente fornece diretórios separados para componentes, contêineres, ações, redutores e rotas.
Ações e redutores estão presentes nos projetos que usam Redux. Eu me esforço para ficar sem essa biblioteca. Estou certo de que existem projetos de alta qualidade que usam a mesma estrutura. Alguns dos meus projetos têm pastas separadas para componentes e contêineres. Uma pasta de componente pode armazenar algo como arquivos com código para entidades como
BlogPost
e
Profile
. Na pasta do contêiner, há arquivos que armazenam o código do contêiner
BlogPostContainer
e
ProfileContainer
. O contêiner recebe dados do servidor e os passa para o componente filho "estúpido", cuja tarefa é exibir esses dados na tela.
Esta é uma estrutura de trabalho. É pelo menos homogêneo, e isso é muito importante. Isso leva ao fato de que o desenvolvedor, que ingressou no trabalho no projeto, entenderá rapidamente o que está acontecendo nele e qual o papel de suas partes individuais. A desvantagem dessa abordagem, devido à qual eu tenho tentado recentemente não usá-la, é que ela força o programador a se mover constantemente pela base de código. Por exemplo, as
BlogPostContainer
ProfileContainer
e
BlogPostContainer
têm nada em comum, mas seus arquivos estão próximos um do outro e distantes dos arquivos em que são realmente usados.
Há algum tempo, tenho me esforçado para colocar arquivos cujo conteúdo está planejado para ser compartilhado nas mesmas pastas. Essa abordagem para a estruturação do projeto baseia-se no agrupamento de arquivos com base em seus recursos. Graças a essa abordagem, você pode simplificar bastante sua vida se, por exemplo, colocar o componente pai e seu componente filho "estúpido" na mesma pasta.
Geralmente usamos a pasta
routes
/
screens
e a pasta
components
. A pasta do componente normalmente armazena código para itens como
Button
ou
Input
. Este código pode ser usado em qualquer página do aplicativo. Cada pasta na pasta para rotas é uma página de aplicativo separada. Ao mesmo tempo, os arquivos com o código do componente e o código lógico do aplicativo relacionados a esta rota estão localizados na mesma pasta. E o código dos componentes usados em várias páginas cai na pasta de
components
.
Na pasta de rota, você pode criar pastas adicionais nas quais o código responsável pela formação de diferentes partes da página está agrupado. Isso faz sentido nos casos em que a rota é representada por uma grande quantidade de código. Aqui, no entanto, gostaria de alertar o leitor que não vale a pena criar estruturas a partir de pastas com um nível muito alto de aninhamento. Isso complica o movimento do projeto. Estruturas de pastas aninhadas profundas são um dos sinais de supercomplicar um projeto. Deve-se notar que o uso de ferramentas especializadas, como comandos de busca, fornece ao programador ferramentas convenientes para trabalhar com o código do projeto e encontrar o que ele precisa. Mas a estrutura de arquivos do projeto também afeta a usabilidade dele.
Estruturando o código do projeto, você pode agrupar arquivos com base não na rota, mas nos recursos do projeto implementados por esses arquivos. No meu caso, essa abordagem se mostra perfeitamente em projetos de página única que implementam muitos recursos em sua única página. Mas deve-se notar que o agrupamento dos materiais do projeto por rota é mais fácil. Essa abordagem não requer esforços mentais especiais para tomar decisões sobre quais entidades devem ser colocadas próximas umas das outras e para procurar algo.
Se formos agrupar o código ainda mais, podemos decidir que o código de contêineres e componentes será justificadamente colocado no mesmo arquivo. E você pode ir ainda mais longe - coloque o código de dois componentes em um arquivo. Suponho que você pode estar pensando agora que recomendar essas coisas é realmente uma blasfêmia. Mas, na realidade, tudo está longe de ser tão ruim. De fato, essa abordagem é totalmente justificada. E se você usar ganchos React ou código gerado (ou ambos), eu recomendaria essa abordagem.
De fato, a questão de como decompor o código em arquivos não é de suma importância. A verdadeira questão é por que você pode precisar dividir os componentes em inteligentes e estúpidos. Quais são os benefícios dessa separação? Existem várias respostas para esta pergunta:
- Os aplicativos criados dessa maneira são mais fáceis de testar.
- O desenvolvimento de tais aplicativos facilita o uso de ferramentas como o Storybook.
- Componentes estúpidos podem ser usados com muitos componentes inteligentes diferentes (e vice-versa).
- Componentes inteligentes podem ser usados em plataformas diferentes (por exemplo, nas plataformas React e React Native).
Todos esses são argumentos reais em favor da divisão dos componentes em "inteligente" e "estúpido", mas não são aplicáveis a todas as situações. Por exemplo, geralmente usamos o
Apollo Client com ganchos ao criar projetos. Para testar esses projetos, você pode criar zombarias de resposta da Apollo ou zombarias de gancho. O mesmo vale para o livro de histórias. Se falamos sobre misturar e compartilhar componentes "inteligentes" e "estúpidos", então, na verdade, nunca encontrei isso na prática. Em relação ao uso do código em várias plataformas, havia um projeto no qual eu faria algo semelhante, mas nunca o fiz. Era para ser o
mono-repositório Lerna. Atualmente, em vez dessa abordagem, você pode optar por React Native Web.
Como resultado, podemos dizer que na separação de componentes em “inteligente” e “bobo” existe um certo significado. Este é um conceito importante que vale a pena conhecer. Mas, muitas vezes, você não precisa se preocupar muito com isso, principalmente considerando a aparência recente dos ganchos React.
O ponto forte da combinação dos recursos de componentes "inteligentes" e "tolos" em uma entidade é que ele acelera o desenvolvimento e simplifica a estrutura do código.
Além disso, se surgir essa necessidade, um componente sempre pode ser dividido em dois componentes separados - "inteligente" e "estúpido".
Estilização
Utilizamos
componentes de emoção /
estilo para aplicações de estilo. Sempre há a tentação de separar estilos em um arquivo separado. Eu já vi alguns desenvolvedores fazer isso. Mas, depois de experimentar as duas abordagens, no final, não consegui encontrar um motivo para mover os estilos para um arquivo separado. Como no caso de muitas outras coisas, das quais estamos falando aqui, um desenvolvedor pode facilitar sua vida combinando os estilos e componentes aos quais eles se relacionam em um arquivo.
Estrutura do projeto da parte do servidor do aplicativo
Todas as opções acima são verdadeiras com relação à estruturação do código do servidor do aplicativo. Uma estrutura típica que eu pessoalmente tento evitar pode ser algo como
isto :
src │ app.js # └───api # Express └───config # └───jobs # agenda.js └───loaders # └───models # └───services # - └───subscribers # └───types # (d.ts) Typescript
Geralmente usamos o GraphQL em nossos projetos. Portanto, eles usam arquivos que armazenam modelos, serviços e reconhecedores. Em vez de espalhá-los em diferentes locais do projeto, coleciono-os em uma pasta. Na maioria das vezes, esses arquivos serão compartilhados e será mais fácil trabalhar com eles se estiverem armazenados na mesma pasta.
Não substitua as definições de tipo várias vezes
Utilizamos muitas soluções em nossos projetos que, de alguma forma, estão relacionadas aos tipos de dados. Estes são TypeScript, GraphQL, esquemas de banco de dados e, às vezes, MobX. Como resultado, pode acontecer que os tipos para as mesmas entidades sejam descritos 3-4 vezes. Coisas assim devem ser evitadas. Devemos nos esforçar para usar ferramentas que geram automaticamente descrições de tipo.
No servidor, uma combinação de TypeORM / Typegoose e TypeGraphQL pode ser usada para essa finalidade. Isso é suficiente para descrever todos os tipos usados. TypeORM / Typegoose permite descrever o esquema do banco de dados e os tipos TypeScript correspondentes. TypeGraphQL ajudará na criação dos tipos de GraphQL e TypeScript.
Aqui está um exemplo de determinação dos tipos de TypeORM (MongoDB) e TypeGraphQL em um arquivo:
import { Field, ObjectType, ID } from 'type-graphql' import { Entity, ObjectIdColumn, ObjectID, Column, CreateDateColumn, UpdateDateColumn, } from 'typeorm' @ObjectType() @Entity() export default class Policy { @Field(type => ID) @ObjectIdColumn() _id: ObjectID @Field() @CreateDateColumn({ type: 'timestamp' }) createdAt: Date @Field({ nullable: true }) @UpdateDateColumn({ type: 'timestamp', nullable: true }) updatedAt?: Date @Field() @Column() name: string @Field() @Column() version: number }
O GraphQL Code Generator também pode gerar muitos tipos diferentes. Usamos essa ferramenta para criar tipos TypeScript no cliente, bem como ganchos de reação que acessam o servidor.
Se você usa o MobX para controlar o estado do aplicativo, usando algumas linhas de código, pode obter tipos de TS gerados automaticamente. Se você também usa o GraphQL, consulte o novo pacote -
MST-GQL , que gera uma árvore de estados a partir do esquema GQL.
O uso dessas ferramentas em conjunto evitará a reescrita de grandes quantidades de código e ajudará a evitar erros comuns.
Outras soluções, como
Prisma ,
Hasura e
AWS AppSync , também podem ajudar a evitar declarações de tipo duplicadas. O uso de tais ferramentas, é claro, tem seus prós e contras. Nos projetos que criamos, essas ferramentas nem sempre são usadas, pois precisamos implantar o código em nossos próprios servidores das organizações.
Sempre que possível, recorra a meios de geração automática de código
Se você observar o código criado sem usar as ferramentas acima para gerar código automaticamente, os programadores precisam constantemente escrever a mesma coisa. O principal conselho que posso dar sobre isso é que você precisa criar trechos para tudo o que costuma usar. Se você costuma inserir o comando
console.log
, crie um snippet como
cl
, que se transforma automaticamente em
console.log()
. Se você não fizer isso e me pedir para ajudá-lo na depuração de código, isso me incomodará bastante.
Existem muitos pacotes com trechos, mas é fácil criar seus próprios trechos. Por exemplo, usando o
gerador de trechos .
Aqui está o código que me permite adicionar alguns dos meus snippets favoritos ao VS Code:
{ "Export default": { "scope": "javascript,typescript,javascriptreact,typescriptreact", "prefix": "eid", "body": [ "export { default } from './${TM_DIRECTORY/.*[\\/](.*)$$/$1/}'", "$2" ], "description": "Import and export default in a single line" }, "Filename": { "prefix": "fn", "body": ["${TM_FILENAME_BASE}"], "description": "Print filename" }, "Import emotion styled": { "prefix": "imes", "body": ["import styled from '@emotion/styled'"], "description": "Import Emotion js as styled" }, "Import emotion css only": { "prefix": "imec", "body": ["import { css } from '@emotion/styled'"], "description": "Import Emotion css only" }, "Import emotion styled and css only": { "prefix": "imesc", "body": ["import styled, { css } from ''@emotion/styled'"], "description": "Import Emotion js and css" }, "Styled component": { "prefix": "sc", "body": ["const ${1} = styled.${2}`", " ${3}", "`"], "description": "Import Emotion js and css" }, "TypeScript React Function Component": { "prefix": "rfc", "body": [ "import React from 'react'", "", "interface ${1:ComponentName}Props {", "}", "", "const ${1:ComponentName}: React.FC<${1:ComponentName}Props> = props => {", " return (", " <div>", " ${1:ComponentName}", " </div>", " )", "}", "", "export default ${1:ComponentName}", "" ], "description": "TypeScript React Function Component" } }
Além dos trechos, os geradores de código podem ajudar a economizar tempo. Você pode criá-los você mesmo. Eu gosto de usar o
plop para isso.
O Angular possui seus próprios geradores de código. Usando as ferramentas de linha de comando, você pode criar um novo componente, composto por 4 arquivos, que apresenta tudo o que você espera encontrar no componente. É uma pena que o React não tenha esse recurso padrão, mas algo semelhante pode ser criado independentemente usando o plop. Se cada novo componente que você criar deve ser apresentado na forma de uma pasta que contém um arquivo com o código do componente, um arquivo de teste e um arquivo de Storybook, o gerador ajudará a criar tudo isso com um único comando. Em muitos casos, isso facilita muito a vida do desenvolvedor. Por exemplo, ao adicionar um novo recurso ao servidor, basta executar um comando na linha de comando. Depois disso, os arquivos da entidade, serviços e reconhecedores serão criados automaticamente, contendo todas as estruturas básicas necessárias.
Outro ponto forte dos geradores de código é que eles contribuem para a uniformidade no desenvolvimento da equipe. Se todos usarem o mesmo gerador de plop, o código será muito uniforme para todos.
Formatação automática de código
Formatar o código é uma tarefa simples, mas, infelizmente, nem sempre é resolvido corretamente. Não perca tempo alinhando manualmente o código ou inserindo ponto e vírgula nele. Use o
Prettier para formatar automaticamente o código ao confirmar.
Sumário
Neste artigo, falei sobre algumas coisas que aprendemos ao longo dos anos de trabalho, durante os anos de tentativa e erro. Existem muitas abordagens para estruturar a base de código dos projetos. Mas entre eles não há ninguém que possa ser chamado o único certo.
A coisa mais importante que eu queria transmitir a você é que o programador deve se esforçar pela simplicidade da organização dos projetos, pela homogeneidade, pelo uso de uma estrutura compreensível e fácil de trabalhar. Isso simplifica os projetos de desenvolvimento de equipe.
Caros leitores! O que você acha das idéias para o desenvolvimento de aplicativos JavaScript de pilha completa descritos neste artigo?

