Olá a todos, dev.family está em contato. Gostaríamos de falar sobre um projeto interessante no qual estamos trabalhando há quase seis meses e ainda continuamos. Durante esse tempo, muita coisa aconteceu nele, muita coisa mudou. Descobrimos algo interessante para nós mesmos, conseguimos preencher os solavancos.
Como será nossa história?
Então, no que ainda estamos trabalhando? Na verdade, essa questão em algum momento se tornou muito relevante, como foi, por exemplo, para o dono da corporação McDonalds em algum momento. Iniciamos o projeto como um programa de fidelidade criptográfica que oferece aos usuários finais recompensas por determinadas ações, e os clientes recebem análises sobre esses mesmos usuários. Sim, é bem superficial, mas não importa.
Foi necessário desenvolver módulos Shopify para conectar-se às lojas Shopify, um portal para marcas, uma extensão para Google Chrome, um aplicativo móvel + um servidor com banco de dados (bem, na verdade, em lugar nenhum sem eles). Em geral, com o que precisamos, decidimos e começamos a trabalhar. Como o projeto foi imediatamente assumido como grande, todos entenderam que ele poderia crescer como feijões mágicos de ação retardada.
Decidiu-se fazer tudo “corretamente” e “por todos os padrões”. Ou seja, tudo está escrito em uma linguagem - TypeScript. Para que todos escrevam da mesma forma, e não haja alterações desnecessárias nos arquivos, linters (muitos linters), para que tudo seja “fácil” de reutilizar, coloque TUDO em módulos separados e para que não roubem sob o token de acesso do Github.
Repositório para linters e ts config separados (guia de estilo)
Repositório para um aplicativo móvel (react native) e uma extensão do Chrome (react.js) (juntos, pois repetem a mesma funcionalidade, mas voltados para usuários diferentes)
Mais um repositório para o portal
Dois repositórios para módulos Shopify
Repositório para blockchain Repositório API (express.js) Repositório para infraestrutura
Hã... acho que listei tudo. Acabou um pouco demais, mas tudo bem, vamos continuar rolando. Ah, sim, por que dois repositórios foram alocados para os módulos do Shopify? Porque o primeiro repositório é UI-modules. Aí está toda a beleza dos nossos bebês e seus cenários. E o segundo são as integrações-Shopify. Esta é, de fato, sua implementação no próprio Shopify com todos os arquivos líquidos. No total, temos 8 repositórios, onde alguns devem se comunicar entre si.
Como estamos falando de desenvolvimento em TypeScript, também precisamos de gerenciadores de pacotes para instalar módulos, bibliotecas. Mas todos trabalhamos de forma independente em nossos repositórios e não importava para ninguém o que usar. Por exemplo, ao desenvolver um aplicativo móvel em React Native, não pensei muito e mantive o YARN1. Alguém pode estar mais acostumado a usar o bom e velho NPM, enquanto outros adoram tudo que é novo e usam o novo YARN3. Assim, em algum lugar havia NPM, em algum lugar YARN1 e em algum lugar YARN3.
Então todos nós começamos a fazer nossas aplicações. E quase imediatamente a diversão começou, mas não tão completa. Em primeiro lugar, alguns não pensaram sobre para que servia o TypeScript e usaram “Any” sempre que eram preguiçosos ou “não entendiam” como não podiam escrevê-lo. Alguém não percebeu todo o poder do TypeScript e o fato de que em alguns lugares tudo pode ser muito mais fácil. Portanto, os tipos vieram de dimensões cósmicas. Sim, esqueci de dizer, decidimos usar o Hasura GraphQL como banco de dados. A digitação manual de todas as respostas às vezes parecia outra coisa. E, em um caso, alguns até escreveram no bom e velho Javascript. Sim, a situação acabou sendo legal: o primeiro cara colocou “Qualquer” mais uma vez para não se esforçar muito, o segundo escreve telas de tipos com as próprias mãos e o terceiro ainda não escreve tipos.
Mais tarde, descobrimos que nos casos em que repetimos a lógica e, no bom sentido, deveria ter sido retirado em uma embalagem separada - ninguém faria isso. Todo mundo escreve e escreve código para si mesmo, para todo o resto - cuspa de um alto campanário.
O que nós temos? Temos 8 repositórios com aplicações diferentes. Alguns são necessários em todos os lugares, outros se comunicam. Portanto, todos nós criamos arquivos .NPMrc, prescrevemos créditos, criamos um token github e, em seguida, através do módulo gerenciador de pacotes. Em geral um leve incômodo, embora desagradável, mas nada incomum.
Apenas no caso de atualizar algo no pacote, você precisa atualizar a versão dele, depois fazer o upload, atualizar no seu aplicativo/módulo e só então você verá o que mudou. Mas isso é totalmente inapropriado! Especialmente se você puder apenas mudar a cor em algum lugar. Além disso, algum código é repetido e não reutilizado, mas simplesmente reescrito silenciosamente. Se estamos falando de um aplicativo móvel e uma extensão do navegador, a loja redux e todo o trabalho com a API são totalmente repetidos lá, algo é totalmente reescrito ou ligeiramente modificado.
No total, o que nos resta: um monte de repositórios, um lançamento bastante longo de aplicativos / módulos, muitas das mesmas coisas escritas pelas mesmas pessoas, muito tempo gasto em testes e introdução de novas pessoas no projeto, e outros problemas decorrentes do acima.
Em suma, isso nos levou ao fato de que as tarefas foram executadas por muito tempo. Claro que isso fazia com que os prazos fossem perdidos, era muito difícil apresentar alguém novo ao projeto, o que mais uma vez prejudicou a velocidade de desenvolvimento. Tudo seria bastante monótono e longo, em alguns casos, graças ao webpack por isso.
Então ficou claro que estávamos nos afastando de onde estávamos nos esforçando, mas quem sabe para onde. Depois de analisar todos os erros, tomamos uma série de decisões, que serão discutidas agora.
Provavelmente, o mais importante que influenciou muito no futuro foi a percepção de que não estamos construindo um aplicativo específico, mas uma plataforma. Temos vários tipos de usuários, existem diferentes aplicativos para eles, mas operam dentro de uma mesma plataforma. Portanto, encerramos imediatamente o problema com um grande número de repositórios: se estamos trabalhando em uma plataforma, por que dividi-la em repositórios quando é mais fácil trabalhar em uma.
Quero dizer que trabalhar em um monorepo facilitou muito a nossa vida. Alguns aplicativos ou módulos tinham um relacionamento direto entre si e agora você pode trabalhar neles com tranquilidade na mesma ramificação do mesmo repositório. Mas isso está longe de ser a principal vantagem.
Vamos continuar. Nós movemos tudo para um repositório. Legal! Continuamos a trabalhar no mesmo ritmo até chegar à reutilização. Aliás, essa é uma “regra de bom gosto” que temos em nosso trabalho. Percebendo que em alguns lugares usamos os mesmos algoritmos, funções, código e em alguns lugares pacotes separados que instalamos via github, decidimos que tudo isso “não cheira muito bem” e começamos a colocar tudo em pacotes separados dentro de um monorepo usando espaços de trabalho.
Espaços de trabalho (espaços de trabalho) são conjuntos de funções no NPM CLI, com os quais você pode gerenciar vários pacotes a partir de um único pacote raiz de nível superior.
Na verdade, são pacotes dentro de um pacote que são vinculados por meio de um gerenciador de pacotes específico (qualquer YARN / NPM / PNPM) e depois usados em outro pacote. Para falar a verdade, não reescrevemos tudo imediatamente nos espaços de trabalho, mas o fizemos conforme necessário.
De um arquivo
{ "type": "module", "name": "package-name-1", ... "types": "./src/index.ts", "exports": { ".": "./src/index.ts" }, },
Para outro arquivo
{ "type": "module", "name": "package-name-2", ... "dependencies": { "package-name-1": "workspace:*", }, },
Um exemplo usando PNPM
Nada complicado, na verdade, se você pensar bem: escreva alguns comandos e linhas e use o que quiser e onde quiser. Mas “há uma ressalva, camaradas”. Anteriormente, escrevi que todos usavam o gerenciador de pacotes que desejavam. Resumindo, temos um repositório com diferentes gerenciadores. Em alguns lugares foi engraçado quando alguém escreveu que não poderia vincular este ou aquele pacote, tendo em vista que ele usa NPM, e existe o YARN.
Acrescentarei que o problema não foi por causa de gerentes diferentes, mas porque as pessoas usaram os comandos errados ou configuraram algo errado. Por exemplo, algumas pessoas através do YARN 3 apenas fizeram um link do YARN e pronto, mas para o YARN 1 não funcionou da maneira que eles queriam devido à falta de compatibilidade com versões anteriores.
A essa altura, ficou claro que é melhor usar o mesmo gerenciador de pacotes. Mas você precisa escolher qual deles, então consideramos apenas 2 opções - YARN e PNPM . Descartamos o NPM na hora, porque era mais lento que os outros e mais feio. Houve uma escolha entre PNPM e YARN.
Inicialmente, o YARN funcionou bem - era mais rápido, mais simples e mais compreensível, e é por isso que todos o usavam na época. Mas a pessoa que fez o YARN deixou o Facebook e o desenvolvimento das próximas versões dele foi transferido para outros. Foi assim que o YARN 2 e o YARN 3 apareceram sem compatibilidade com versões anteriores do primeiro. Além disso, além do arquivo yarn.lock, eles geram uma pasta yarn, que às vezes pesa como node_modules e armazena caches em si.
Portanto, nós, como muitos outros desenvolvedores, voltamos nossa atenção para o PNPM. Acabou sendo tão conveniente quanto o primeiro YARN da época. Os espaços de trabalho podem ser facilmente usados aqui, alguns comandos têm a mesma aparência do primeiro YARN. Além disso, hoist vergonhosamente acabou sendo uma boa opção adicional - é mais conveniente instalar node_modules em todos os lugares ao mesmo tempo do que ir para alguma pasta todas as vezes e fazer a instalação do PNPM.
Além disso, decidimos experimentar o turborepo. Turborepo é uma ferramenta de CI/CD que possui seu próprio conjunto de opções, cli e configuração via arquivo turbo.json. Instalado e configurado o mais fácil possível. Colocamos uma cópia global do turbo cli através
PNPM add turbo --global.
Adicionando turbo.json ao projeto
turbo.json
{ "$schema": "https://turbo.build/schema.json", "pipeline": { "build": { "dependsOn": ["^build"] } } }
Depois disso, podemos usar todas as funções disponíveis do turborepo. Ficamos mais atraídos por seus recursos e pela possibilidade de usá-lo em um monorepo.
Compilações incrementais (Compilações incrementais - coletar compilações é bastante doloroso, o Turborepo vai lembrar o que foi construído e pular o que já foi calculado);
Hash com reconhecimento de conteúdo (Hashing com reconhecimento de conteúdo - o Turborepo analisa o conteúdo dos arquivos, não os timestamps, para descobrir o que precisa ser construído);
Cache remoto (hashing remoto - compartilhe um cache de build remoto com a equipe e CI/CD para builds ainda mais rápidos.);
Pipelines de tarefas (um pipeline de tarefas que define relacionamentos entre tarefas e otimiza o que e quando criar).
Execução paralela (Realiza compilações usando cada núcleo com o máximo de paralelismo, sem desperdiçar CPUs ociosas).
Também pegamos a recomendação de organizar um monorepo da documentação e a implementamos em nossa plataforma. Ou seja, dividimos todos os nossos pacotes em apps e pacotes. Para fazer isso, também criamos o arquivo PNPM-workspace.yaml e escrevemos:
PNPM-workspace.yaml
packages:
'apps/**/*'
'packages/**/*'
Aqui você pode ver um exemplo de nossa estrutura antes e depois:
Agora temos um monorep com espaços de trabalho personalizados e reutilização de código conveniente. Vou acrescentar mais alguns pontos que fizemos em paralelo. Mencionei duas coisas antes: tínhamos uma extensão do Chrome e decidimos - estávamos fazendo uma plataforma.
Como nossa plataforma trabalhava prioritariamente com o Shopify, decidimos que, em vez de uma extensão para o Chrome ou além dele, seria bom fazer outro módulo para o Shopify, que pode ser simplesmente instalado no site, para não forçar novamente as pessoas a baixar um aplicativo móvel ou uma extensão do Chrome . Mas deve repetir completamente a extensão. Inicialmente, fizemos em paralelo, mas percebemos que estávamos fazendo algo errado, porque simplesmente duplicamos o código. Em todos os sentidos, escrevemos a mesma coisa em lugares diferentes. Mas como agora temos todos os espaços de trabalho e reutilização configurados, movemos tudo facilmente para um pacote, que chamamos no módulo Shopify e na extensão do Chrome. Assim, economizamos muito tempo.
A segunda coisa que nos poupou muito tempo foi a eliminação do webpack e, em alguns lugares, das compilações em geral. O que há de errado com o webpack? Na verdade, existem dois pontos críticos: complexidade e velocidade. O que escolhemos é vite. Por que? É mais fácil de configurar, está ganhando popularidade rapidamente e já possui um grande número de plugins em funcionamento, e um exemplo do dock é suficiente para a instalação. Em comparação, a compilação no webpack de nossa extensão web do Chrome levou cerca de 15 segundos, no vite.js
cerca de 7 segundos (com geração de arquivo dts).
Sinta a diferença. O que há com a rejeição de compilações? Tudo é simples, como se viu, não precisávamos muito deles, pois são módulos reutilizáveis e no package.json, nas exportações, você poderia simplesmente substituir dist/index.js por src/index.ts.
Como foi
{... "exports": { "import": "./dist/built-index.js" }, ... }
Como está agora
{ ... "types": "./src/index.ts", "exports": { ".": "./src/index.ts" }, ... }
Assim, eliminamos a necessidade de executar o PNPM watch para rastrear atualizações de aplicativos relacionadas a esses módulos e fazer o PNPM build para obter atualizações. Acho que não vale a pena explicar quanto tempo isso nos poupou.
Na verdade, um dos motivos pelos quais coletamos compilações foi o TypeScript, mais precisamente os arquivos index.d.ts. Para que ao importar nossos módulos/pacotes saibamos quais tipos são esperados em determinadas funções ou quais tipos outras nos retornarão, como aqui:
Mas como você pode simplesmente exportar de index.tsx, havia outro motivo para abandonar as compilações.
Mas ainda assim, por que TypeScript? Acho que não faz sentido agora descrever todas as vantagens do TS: segurança de tipo, facilitar o processo de desenvolvimento devido à digitação, presença de interfaces e classes, código-fonte aberto, erros cometidos durante a modificação do código são visíveis imediatamente, e não em tempo de execução , e assim por diante.
Como eu disse logo no início, decidimos escrever tudo em um idioma para que, se alguém parar de trabalhar ou sair, possamos apoiar ou garantir. Primeiro escolhemos JS. Mas o JS não é muito seguro e, sem testes em grandes projetos, é bastante doloroso. Portanto, decidimos a favor do TS. Como a prática tem mostrado, é muito conveniente no monorepo, porque você pode simplesmente exportar arquivos *.ts e, ao usar componentes, os dados esperados e seus tipos são imediatamente claros.
Mas um dos principais recursos úteis foi a geração automática de tipos para consultas e mutações do GraphQl. Para quem não tem muito conhecimento, o GraphQl é uma tecnologia que permite ir ao banco de dados através da mesma consulta (para obter dados) e mutação (para alterar dados), e se parece com isto:
query getShop {shop { shopName shopLocation } }
Ao contrário da API REST, onde até recebê-lo, você não saberá o que virá até você, aqui você mesmo determina os dados de que precisa.
Voltemos ao nosso Presidente eleito. Usamos Hasura, que é um wrapper GraphQL sobre PostgreSQL. Já que estamos trabalhando com TS, então de uma boa forma devemos digitar os dados tanto das requisições quanto daqueles que enviamos para o payload. Se estivermos falando sobre o código do exemplo acima, não deve haver problemas, mais ou menos. Mas, na prática, uma consulta pode chegar a cem linhas, além disso, alguns campos podem vir ou não, ou ter tipos de dados diferentes. E digitar essas telas é uma tarefa muito longa e ingrata.
Alternativa? Claro que tenho! Deixe os tipos serem gerados via comandos. Em nosso projeto, fizemos o seguinte:
Usamos as seguintes bibliotecas: graphql e graphql-request
Primeiramente, foram criados arquivos com resolução *.graphql, nos quais foram escritas consultas e mutações.
Por exemplo:
test.graphql
query getAllShops {test_shops { identifier name location owner_id url domain type owner { name owner_id } } }
codegen.yaml
schema: ${HASURA_URL}:headers: x-hasura-admin-secret: ${HASURA_SECRET}
emitLegacyCommonJSImports: false
config: gqlImport: graphql-tag#gql scalars: numeric: string uuid: string bigint: string timestamptz: string smallint: number
generates: src/infrastructure/api/graphQl/operations.ts: documents: 'src/**/*.graphql'
plugins: - TypeScript - TypeScript-operations - TypeScript-graphql-request
Lá indicamos para onde estávamos indo e, no final, onde salvamos o arquivo com a API gerada (src/infrastructure/api/graphQl/operations.ts) e de onde obtemos nossas solicitações (src/**/*. gráficoql).
Depois disso, um script foi adicionado ao package.json que gerou os mesmos tipos para nós:
pacote.json
{... "scripts": { "generate": "HASURA_URL=http://localhost:9696/v1/graphql HASURA_SECRET=secret graphql-codegen-esm --config codegen.yml", ... }, ... }
Eles indicavam a URL que o script acessava para obter informações, o segredo e o próprio comando.
import { GraphQLClient } from "graphql-request"; import { getSdk } from "./operations.js"; export const createGraphQlClient = ({ getToken }: CreateGraphQlClient) => { const graphQLClient = new GraphQLClient('your url goes here...'); return getSdk(graphQLClient); };
Assim, obtemos uma função que gera um cliente com todas as consultas e mutações. O bônus em operation.ts coloca todos os nossos tipos que podemos exportar e usar, e há uma digitação completa de toda a solicitação: sabemos o que precisa ser dado e o que virá. Você não precisa pensar em mais nada, exceto executar o comando e aproveitar a beleza da digitação.
Assim, nos livramos de um grande número de repositórios desnecessários e da necessidade de fazer push constantemente das menores alterações para verificar como as coisas funcionam. Em vez disso, eles criaram um em que tudo é estruturado, decomposto de acordo com sua finalidade e tudo é facilmente reaproveitado. Assim facilitamos nossa vida e reduzimos o tempo para apresentar o projeto a novas pessoas, para lançar a plataforma e os módulos/aplicativos separadamente. Tudo foi digitado, e agora não há necessidade de entrar em cada pasta e ver o que esta ou aquela função/componente deseja. Como resultado, o tempo de desenvolvimento foi reduzido.
Concluindo, quero dizer que você nunca deve ter pressa. É melhor entender o que você está fazendo e como fazer com mais facilidade do que complicar deliberadamente sua vida. Os problemas estão em toda parte e sempre, mais cedo ou mais tarde eles aparecerão em algum lugar, e então a complicação deliberada vai atirar em você no joelho, mas não ajudará em nada.
A equipe dev.family esteve com vocês, até breve!