Apollo: 9 meses - voo normal

imagem


Olá pessoal, meu nome é Semyon Levenson, trabalho como líder de equipe no projeto Stream do Rambler Group e quero falar sobre a nossa experiência com a Apollo.


Vou explicar o que é o "Stream". Este é um serviço automatizado para empreendedores, que permite atrair clientes da Internet para seus negócios sem se envolver em publicidade e criar rapidamente sites simples sem ser um especialista em layout.


A captura de tela mostra uma das etapas para criar uma página de destino.



Qual foi o começo?


E no começo havia MVP, muitos Twig, jQuery e prazos muito apertados. Mas seguimos um caminho fora do padrão e decidimos fazer um novo design. O redesenho não está no sentido de "estilos corrigidos", mas decidiu revisar completamente o sistema. E esse foi um bom palco para montarmos o frontend perfeito. Afinal, nós, a equipe de desenvolvimento, continuamos apoiando isso e implementando outras tarefas com base nisso, alcançando novos objetivos estabelecidos pela equipe de produto.


Nosso departamento já acumulou experiência suficiente no uso do React. Como não queria passar duas semanas configurando o webpack, decidi usar o CRA (Create React App). Para estilos, os Componentes com estilo foram usados ​​e onde, sem digitar, eles usaram o Flow . Eles levaram o Redux para a Administração do Estado, mas, como resultado, verificou-se que não precisamos disso, mas mais sobre isso mais tarde.


Reunimos nossa interface perfeita e percebemos que havíamos esquecido algo. Como se viu, esquecemos o back-end, ou melhor, a interação com ele. Quando você pensou sobre o que podemos usar para organizar essa interação, a primeira coisa que veio à mente - é claro, é Descanse. Não, não fomos descansar (sorrir), mas começamos a falar sobre a API RESTful. Em princípio, a história é familiar, se estende por um longo tempo, mas também sabemos sobre os problemas com ela. Nós falaremos sobre eles.


O primeiro problema é a documentação. O RESTful, é claro, não diz como organizar a documentação. Aqui há uma opção para usar a mesma arrogância, mas na verdade é a introdução de uma entidade adicional e a complicação de processos.


O segundo problema é como organizar o suporte para controle de versão da API.


A terceira questão importante é um grande número de consultas ou pontos de extremidade personalizados que podemos recompensar. Suponha que precisamos solicitar postagens, para essas postagens - comentários e mais autores desses comentários. No descanso clássico, temos que fazer pelo menos 3 consultas. Sim, podemos recompensar pontos de extremidade personalizados e tudo isso pode ser reduzido a 1 solicitação, mas isso já é uma complicação.



Obrigado Sashko Stubailo pela ilustração .


Solução


E, neste momento, o Facebook vem em nosso auxílio com o GraphQL. O que é o GraphQL? Essa é uma plataforma, mas hoje veremos uma de suas partes - essa é a linguagem de consulta para sua API, apenas uma linguagem e bastante primitiva. E funciona da maneira mais simples possível - à medida que solicitamos algum tipo de entidade, também a obtemos.


Pedido:


{ me { id isAcceptedFreeOffer balance } } 

A resposta é:


 { "me": { "id": 1, "isAcceptedFreeOffer": false, "balance": 100000 } } 

Mas o GraphQL não se refere apenas à leitura, mas também à alteração de dados. Para fazer isso, há mutações no GraphQL. As mutações são dignas de nota, pois podemos declarar a resposta desejada do back-end, com uma alteração bem-sucedida. No entanto, existem algumas nuances. Por exemplo, se nossa mutação afeta dados fora dos limites do gráfico.


Um exemplo de mutação na qual usamos uma oferta gratuita:


 mutation { acceptOffer (_type: FREE) { id isAcceptedFreeOffer } } 

Em resposta, obtemos a mesma estrutura solicitada


 { "acceptOffer": { "id": 1, "isAcceptedFreeOffer": true } } 

A interação com o back-end do GraphQL pode ser feita usando a busca regular.


 fetch('/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: '{me { id balance } }' }) }); 

Quais são as vantagens do GraphQL?


A primeira vantagem muito interessante que pode ser apreciada quando você começa a trabalhar com ela é que esse idioma é fortemente tipado e auto-documentado. Ao projetar o esquema GraphQL no servidor, podemos descrever imediatamente tipos e atributos diretamente no código.



Como mencionado acima, o RESTful tem um problema de versão. O GraphQL implementou uma solução muito elegante para isso - descontinuada.



Suponha que tenhamos um filme, que o expandamos e que tenhamos um diretor. E, em algum momento, tornamos o diretor um tipo separado. A questão é: o que fazer com o último campo de diretor? Existem duas respostas para ele: ou excluímos este campo ou o marcamos como obsoleto e ele desaparece automaticamente da documentação.


Decidimos independentemente o que precisamos.


Recordamos a imagem anterior, onde tudo correu com o REST, aqui temos tudo combinado em uma solicitação e não requer nenhuma personalização do desenvolvimento do back-end. Uma vez que todos descreveram, nós torcemos, torcemos, fizemos malabarismos.



Mas não sem uma mosca na pomada. Em princípio, não existem muitas desvantagens no GraphQL no frontend, porque ele foi originalmente desenvolvido para resolver problemas de frontend. Mas o back-end não está indo tão bem ... Eles têm um problema como o N + 1. Tome a consulta como um exemplo:


 { landings(_page: 0, limit: 20) { nodes { id title } totalCount } } 

Uma solicitação simples, solicitamos 20 sites e o número de sites que temos. E no back-end, isso pode se transformar em 21 consultas ao banco de dados. Este problema é conhecido, resolvido. Para o Nó JS, existe um pacote dataloader do Facebook. Para outros idiomas, você pode encontrar suas próprias soluções.


Há também o problema de aninhamento profundo. Por exemplo, temos álbuns, esses álbuns têm músicas e, através da música, também podemos obter álbuns. Para fazer isso, faça as seguintes consultas:


 { album(id: 42) { songs { title artists } } } 

 { song(id: 1337) { title album { title } } } 

Assim, obtemos uma consulta recursiva, que também nos fornece uma base elementar.


 query evil { album(id: 42) { songs { album { songs { album { 

Esse problema também é conhecido, a solução para o Nó JS é o limite de profundidade do GraphQL, para outros idiomas também existem soluções.


Assim, decidimos pelo GraphQL. É hora de escolher uma biblioteca que funcione com a API GraphQL. O exemplo em algumas linhas com busca, mostrado acima, é apenas um transporte. Mas, graças ao esquema e à declaratividade, também podemos armazenar consultas em cache no início e trabalhar com maior desempenho com o back-end do GraphQL.


Portanto, temos dois jogadores principais - Relay e Apollo.


Relé


Relay é um desenvolvimento do Facebook, eles usam eles mesmos. Como Oculus, Circle CI, Arsti e sexta-feira.


Quais são os profissionais do Relay?


A vantagem imediata é que o desenvolvedor é o Facebook. React, Flow e GraphQL são desenvolvimentos do Facebook, todos os quais são quebra-cabeças personalizados um para o outro. Onde estamos sem estrelas no Github, o Relay tem quase 11.000, o Apollo tem 7600 para comparação.O que o Relay tem é o Relay-compiler, uma ferramenta que otimiza e analisa suas consultas GraphQL no nível de compilação do seu projeto . Podemos assumir que isso é apenas uglify para o GraphQL:


 #  Relay-compiler foo { # type FooType id ... on FooType { # matches the parent type, so this is extraneous id } } #  foo { id } 

Quais são os contras do Relay?


O primeiro sinal de menos * é a falta de SSR pronto para uso. O Github ainda tem um problema em aberto. Por que sob o asterisco - porque já existem soluções, mas eles são de terceiros e, além disso, bastante ambíguos.



Novamente, o relé é uma especificação. O fato é que o GraphQL já é uma especificação e Relay é uma especificação sobre uma especificação.



Por exemplo, a paginação de retransmissão é implementada de maneira diferente, os cursores aparecem aqui.


 { friends(first: 10, after: "opaqueCursor") { edges { cursor node { id name } } pageInfo { hasNextPage } } } 

Não usamos mais as compensações e os limites usuais. Para feeds no feed, esse é um ótimo tópico, mas quando começamos a fazer todos os tipos de grades, ocorre dor.


O Facebook resolveu seu problema escrevendo uma biblioteca para o React. Existem soluções para outras bibliotecas, como vue.js, por exemplo - vue-relay . Mas se prestarmos atenção ao número de estrelas e cometer s, também aqui, nem tudo é tão suave e pode ser instável. Por exemplo, a opção Criar aplicativo de reação fora da caixa CRA impede que você use o compilador de retransmissão. Mas você pode contornar essa limitação com react-app-rewired .



Apollo


Nosso segundo candidato é Apollo . Desenvolvido por sua equipe Meteor . A Apollo usa comandos conhecidos como: AirBnB, ticketmaster, Opentable, etc.


Quais são as vantagens do Apollo?


A primeira vantagem significativa é que o Apollo foi desenvolvido como uma biblioteca independente de framework. Por exemplo, se agora queremos reescrever tudo no Angular, isso não será um problema, a Apollo trabalha com isso. E você pode até escrever tudo em Vanilla.


A Apollo possui uma documentação interessante, existem soluções prontas para problemas comuns.



Outra vantagem do Apollo - uma API poderosa. Em princípio, quem trabalhou com o Redux encontrará abordagens comuns aqui: existe o ApolloProvider (como o Provux for Redux) e, em vez da loja para o Apollo, isso é chamado de cliente:


 import { ApolloProvider } from 'react-apollo'; import { ApolloClient } from './ApolloClient'; const App = () => ( <ApolloProvider client={ApolloClient}> ... </ApolloProvider> ); 

No nível do componente em si, temos o graphql HOC fornecido como connect. E nós escrevemos a consulta GraphQL já dentro, como MapStateToProps no Redux.


 import { graphql } from 'react-apollo'; import gql from 'graphql-tag'; import { Landing } from './Landing'; graphql(gql` { landing(id: 1) { id title } } `)(Landing); 

Mas quando fazemos o MapStateToProps no Redux, coletamos os dados locais. Se não houver dados locais, o próprio Apollo vai ao servidor para eles. Suportes muito convenientes caem no próprio componente.


 function Landing({ data, loading, error, refetch, ...other }) { ... } 

Isto é:
Dados;
• status do download;
• um erro se ocorreu;
funções auxiliares como refetch para recarregar dados ou fetchMore para paginar. Há também uma enorme vantagem para o Apollo e o Relay, que é a interface do usuário otimista. Permite confirmar / desfazer / refazer no nível da solicitação:


 this.props.setNotificationStatusMutation({ variables: { … }, optimisticResponse: { … } }); 

Por exemplo, o usuário clicou no botão "curtir", e o curtir contou imediatamente. Nesse caso, uma solicitação do servidor será enviada em segundo plano. Se ocorrer algum erro durante o processo de envio, os dados mutáveis ​​retornarão ao seu estado original por conta própria.


A renderização no servidor foi implementada bem, definimos um sinalizador no cliente e tudo está pronto.


 new ApolloClient({ ssrMode: true, ... }); 

Mas aqui eu gostaria de falar sobre o estado inicial. Quando Apollo cozinha sozinho, tudo funciona bem.


 <script> window.__APOLLO_STATE__ = client.extract(); </script> const client = new ApolloClient({ cache: new InMemoryCache().restore(window.__APOLLO_STATE__), link }); 

Mas não temos renderização no servidor, e o back-end envia uma consulta específica do GraphQL para a variável global. Aqui você precisa de uma pequena muleta, precisa escrever uma função Transform que a resposta do GraphQL do back-end já se transformará no formato necessário para o Apollo.


 <script> window.__APOLLO_STATE__ = transform({…}); </script> const client = new ApolloClient({ cache: new InMemoryCache().restore(window.__APOLLO_STATE__), link }); 

Outra vantagem do Apollo é que ele é bem personalizável. Todos nos lembramos do middleware do Redux, tudo aqui é o mesmo, apenas isso é chamado de link.



Gostaria de anotar separadamente dois links: apollo-link-state , necessário para armazenar o estado local na ausência de Redux, e apollo-link-rest , se quisermos escrever consultas GraphQL na API Rest. No entanto, com o último você precisa ser extremamente cuidadoso, porque certos problemas podem surgir.


Apollo também tem contras


Vejamos um exemplo. Ocorreu um problema de desempenho inesperado: 2.000 itens foram solicitados no front-end (era um diretório) e os problemas de desempenho foram iniciados. Depois de visualizá-lo no depurador, descobriu-se que o Apollo consome muitos recursos durante a leitura, o problema está basicamente fechado, agora está tudo bem, mas houve um pecado.


Além disso, a refetch acabou sendo muito óbvia ...


 function Landing({ loading, refetch, ...other }) { ... } 

Parece que, quando solicitamos novamente os dados, se a solicitação anterior terminou com um erro, o carregamento deve se tornar verdadeiro. Mas não!


Para que isso ocorra, é necessário especificar notifyOnNetworkStatusChange: true no graphql HOC ou armazenar o estado de busca localmente.


Apollo vs. Relé


Assim, chegamos a essa mesa, todos pesamos, contamos e tínhamos 76% atrás da Apollo.



Então escolhemos a biblioteca e fomos trabalhar.


Mas eu gostaria de dizer mais sobre a cadeia de ferramentas.


Tudo é muito bom aqui, existem vários complementos para editores, em algum lugar melhor, em algum lugar pior. Também existe o apollo-codegen, que gera arquivos úteis, por exemplo, tipos de fluxo e basicamente extrai o esquema da API do GraphQL.


O cabeçalho "Mãos loucas" ou o que fizemos em casa


A primeira coisa que encontramos foi que, basicamente, precisamos solicitar dados.


 graphql(BalanceQuery)(BalanceItem); 

Temos condições comuns: carregamento, tratamento de erros. Nós escrevemos nosso próprio falcão (asyncCard), que é conectado através da composição de graqhql e asyncCard.


 compose( graphql(BalanceQuery), AsyncCard )(BalanceItem); 

Eu também gostaria de falar sobre fragmentos. Há um componente LandingItem e ele sabe quais dados precisam da API do GraphQL. Definimos a propriedade do fragmento, onde especificamos os campos da entidade Landing.


 const LandingItem = ({ content }: Props) => ( <LandingItemStyle></LandingItemStyle> ); LandingItem.fragment = gql` fragment LandingItem on Landing { ... } `; 

Agora, no nível de uso do componente, usamos seu fragmento na solicitação final.


 query LandingsDashboard { landings(...) { nodes { ...LandingItem } totalCount } ${LandingItem.Fragment} } 

Digamos que uma tarefa seja executada para adicionar status a esta página de destino - não é um problema. Nós adicionamos uma propriedade ao render e ao fragmento. E está tudo pronto. Princípio de responsabilidade única em toda a sua glória.


 const LandingItem = ({ content }: Props) => ( <LandingItemStyle><LandingItemStatus … /> </LandingItemStyle> ); LandingItem.fragment = gql` fragment LandingItem on Landing { ... status } `; 

Que outro problema tivemos?


Temos vários widgets em nosso site que fizeram suas solicitações individuais.



Durante o teste, tudo ficou mais lento. Temos verificações de segurança muito longas e cada solicitação é muito cara. Isso também acabou não sendo problema, existe o Apollo-link-batch-http


 new BatchHttpLink({ batchMax: 10, batchInterval: 10 }); 

Está configurado da seguinte maneira: passamos o número de solicitações que podemos combinar e quanto tempo esse link aguardará após a primeira solicitação aparecer.
E aconteceu assim: ao mesmo tempo, tudo carrega, e ao mesmo tempo tudo vem. Vale ressaltar que, se durante essa mesclagem, qualquer uma das subconsultas retornar com um erro, o erro estará apenas com ele, e não com toda a solicitação.


Eu gostaria de dizer separadamente que no outono passado houve uma atualização do primeiro Apollo para o segundo


No começo eram Apollo e Redux


 'react-apollo' 'redux' 

Então a Apollo se tornou mais modular e expansível, esses módulos podem ser desenvolvidos de forma independente. A mesma apollo-cache-inmemory.


 'react-apollo' 'apollo-client' 'apollo-link-batch-http' 'apollo-cache-inmemory' 'graphql-tag' 

Vale a pena notar que o Redux não é e, como se viu, não é, em princípio, necessário.


Conclusões:


  1. O tempo de entrega dos recursos diminuiu, não perdemos tempo descrevendo ações, reduzimos no Redux e tocamos menos no back-end
  2. A antifragilidade apareceu porque A análise estática da API permite que você anule os problemas quando o front-end espera uma coisa e o back-end retorna um problema completamente diferente.
  3. Se você começar a trabalhar com o GraphQL - tente o Apollo, não fique desapontado.

PS: Você também pode assistir a um vídeo da minha apresentação no Rambler Front & Meet up # 4


Source: https://habr.com/ru/post/pt418417/


All Articles