Framework

13 ago, 2018

Apresentando o Fusion.js: um framework universal para a web baseado em plugins

Publicidade

Um fato pouco conhecido é que a Uber constrói muitos aplicativos baseados na web – centenas deles e contando, na verdade. Muitos deles são aplicativos internos para gerenciar vários aspectos do negócio, enquanto outros estão voltados para o público.

Um fato mais conhecido é que as tecnologias da Web mudam rapidamente e as melhores práticas estão em constante evolução. Proporcionar um framework de alta qualidade com recursos modernos para centenas de engenheiros da Web, mantendo a natureza dinâmica da plataforma da Web, tem sido historicamente um desafio.

Para enfrentar esse desafio, a equipe da Plataforma Web da Uber criou o Fusion.js, um framework da Web de código aberto que facilita o desenvolvimento web e produz aplicativos leves e de alto desempenho.

Motivação

À medida em que as melhores práticas da indústria da Web evoluíram, a Uber precisou reformular seu antigo framework web monolítico para algo que atendesse aos desafios impostos pelos longos anos de endividamento técnico. No entanto, também queríamos permitir que os engenheiros continuassem usando as tecnologias que eles adoram (por exemplo, React e Redux), mantendo a compatibilidade com a infraestrutura de monitoramento de integridade do aplicativo da Uber.

Especificamente, queríamos que o framework central abordasse os seguintes pontos problemáticos:

  • Configuração complexa e padrão exigido de várias ferramentas necessárias para renderização no lado do servidor, divisão de código e recarregamento de módulo quente
  • Falta de boas abstrações para implementar e compartilhar recursos que envolvem diferentes aspectos de aplicativos React renderizados pelo servidor (ou seja, abrangendo servidor e cliente, lidando com serialização/hidratação, comunicação servidor-cliente, etc)
  • Fragilidade resultante do acoplamento rígido de código localizado em lugares diferentes
  • Teste de dificuldades decorrentes de efeitos colaterais e singletons
  • Falta de flexibilidade de um framework monolítico

Embora as soluções existentes abordassem alguns desses desafios, descobrimos que a colagem de uma biblioteca no topo de um framework geralmente exigia alterações em vários arquivos não relacionados. Por exemplo, suportar o Redux em um aplicativo renderizável pelo servidor envolve adicionar código de configuração em algum lugar nos arquivos relacionados ao servidor, código semelhante em algum lugar no navegador, código de hidratação no modelo HTML, um componente React Provider, etc. Integrando uma biblioteca i18n ou biblioteca de métricas de desempenho do navegador leva ao mesmo problema.

Para tornar as coisas mais difíceis, muitos códigos específicos de aplicativos podem depender de bibliotecas que gerenciam efeitos colaterais (por exemplo, para registro ou persistência de dados), e pode ser difícil para um engenheiro integrar tal biblioteca de forma testável sem ajuda de uma abstração de camada de serviço.

Embora quiséssemos fornecer integrações testadas em batalha e fáceis de configurar com as várias bibliotecas usadas pelas equipes da Uber, também queríamos evitar um framework monolítico para manter os tamanhos dos pacotes pequenos.

Outra razão pela qual preferimos uma abordagem modular sobre nossa abordagem monolítica existente é que ela nos força a ser explícitos sobre dependências, o que torna mais fácil evitar fontes comuns de dívida técnica, como God objects, interfaces internas ad-hoc e acoplamento rígido.

O Fusion.js é o culminar dos nossos esforços.

Quem deve usar o Fusion.js?

O Fusion.js é uma boa opção para quem procura um padrão de código aberto para criar um aplicativo da web moderno e não trivial.

Resumidamente, o Fusion.js é um framework JavaScript licenciado pelo MIT que oferece suporte a bibliotecas populares, como React e Redux, e vem com recursos modernos como recarregamento de módulo quente, renderização do lado do servidor com reconhecimento de dados e suporte a divisão de pacote.

Além dos benefícios óbvios de um padrão otimizado e pré-configurado, o Fusion.js também fornece uma arquitetura flexível, baseada em plug-in. Isso o torna adequado para aplicativos de página única modernos e aplicativos da Web que dependem de camadas de serviço complexas para atender aos requisitos de qualidade, como observabilidade (por exemplo, registro de rastreamento, painéis de métricas, etc), teste completo (por exemplo, unidade/integração/E2E) e internacionalização.

Para mais informações sobre os benefícios do Fusion.js, confira nossa documentação.

Arquitetura baseada em plugins

Os aplicativos Fusion.js são universais, o que significa que os aplicativos têm um único arquivo de ponto de entrada e é possível reutilizar o código no servidor e no navegador. Em aplicativos universais, os componentes do React também podem buscar dados e renderizar HTML no servidor, melhorando assim o tempo de carregamento da página no navegador, aproveitando o analisador HTML nativo do navegador e evitando a sobrecarga da API DOM do JavaScript.

A arquitetura de ponto de entrada único permite que os próprios plug-ins do Fusion.js sejam universais, o que permite que os desenvolvedores de plug-ins co-localizem trechos de código com base na biblioteca à qual o código pertence, em oposição ao ambiente no qual o código é executado.

Figura 1. Os plug-ins do Fusion.js encapsulam a lógica com base em seu agrupamento lógico, em vez de se basear em onde o código precisa ser incluído.

Os plug-ins têm acesso ao ciclo de vida das solicitações HTTP por meio de middlewares e podem acessar a árvore React para adicionar componentes do Provider. Eles também podem inicializar o código do navegador.

Por fim, essas qualidades tornam possível instalar uma biblioteca em um aplicativo com uma única linha de código, independentemente de quantos pontos de integração diferentes a biblioteca requer.

Como os plug-ins são fáceis de adicionar e remover, também é fácil raciocinar sobre o acoplamento, o impacto no tamanho do pacote e outros atributos de qualidade de código durante a refatoração. Eles também podem inicializar o código do navegador.

Injeção de dependência digitada

Os plug-ins usam a injeção de dependência, o que significa que eles podem expor APIs bem definidas como serviços para outros plug-ins, e as dependências de um plug-in podem ser facilmente ridicularizadas durante os testes.

Isso é especialmente importante quando as dependências são responsáveis pela comunicação com a infraestrutura de armazenamento de dados ou quando se relacionam à observabilidade (por exemplo, registro, análise e métricas).

/*
This example implements an endpoint that reads from session data.
Session is an injected dependency.
SessionToken is a label (and it helps enforce type safety).
*/

// src/plugins/user.js
import {createPlugin} from 'fusion-core';
import {SessionToken} from 'fusion-tokens';

export default __NODE__ && createPlugin({
  deps: {Session: SessionToken},
  middleware({Session}) {
    return async (ctx, next) => {
      if (ctx.path === '/api/user') {
        ctx.body = JSON.parse(await Session.from(ctx).get('user'));
      }
      return next();
    }
  }
});

Também é possível garantir a estabilidade do tipo estaticamente entre as dependências por meio do Flow.js, conforme ilustrado abaixo:

Figura 2. Erros de tipo de superfície diretamente no editor de código ajudam a capturar erros antes da execução do código.

Gerenciamento de middleware

Um desafio que se tornou aparente anos atrás foi que a popular biblioteca de servidores HTTP Express tem uma API que encoraja os efeitos colaterais ávidos, o que dificulta a encapsulação e o teste de transformações de resposta complexas.

Para nossa arquitetura anterior, os desenvolvedores de aplicativos frequentemente precisavam recorrer a correções de objetos de solicitação/resposta de Express ad-hoc e à cuidadosa colocação de preocupações não relacionadas de maneiras que só faziam sentido em termos das funções de ordem necessárias para que as coisas funcionassem como esperado. Naturalmente, dado o alto acoplamento de requisitos de tempo para subsistemas ricos em efeitos colaterais, os testes se tornaram extremamente difíceis.

Esse problema tem sido uma preocupação desde os estágios de projeto do Fusion.js. Depois de muita pesquisa, optamos por adotar o Koa, que fornece uma API baseada em contexto mais amigável para testes de unidade e uma abstração elegante e leve para o gerenciamento da duração da solicitação com base no conceito de downstreams e upstreams.

Acontece que as decisões de projeto adotadas pela Koa complementam muito bem as decisões de projeto no Fusion.js.

O middleware Koa fornece um ponto de integração lógica para os componentes do Provedor React e a abstração downstream/upstream se alinha perfeitamente ao ciclo de vida do contexto renderizado pelo servidor React. Os efeitos colaterais da rede são dissociados da lógica do aplicativo, melhorando a testabilidade.

Os problemas com God objects e os de ordem de operação que afetaram nossos aplicativos mais antigos agora são resolvidos pelos mecanismos de injeção de dependência e resolução de gráficos do Fusion.js.

Figura 3. O núcleo do Fusion.js separa os efeitos colaterais da rede do estado do aplicativo e aproveita o Koa e o DI para obter um baixo acoplamento entre os subsistemas.

Testabilidade

Nos últimos anos houve uma proliferação de ferramentas de teste de alta qualidade no ecossistema JavaScript e uma maior conscientização das técnicas de teste.

Além de oferecer suporte a ferramentas de teste modernas, como Jest, Enzyme e Puppeteer, o Fusion.js também fornece ferramentas para desenvolvedores testarem plugins. O pacote fusion-test-utils permite ridicularizar o próprio servidor, possibilitando a execução rápida de testes de integração entre qualquer permutação de plugins e mocks.

Apenas o começo

Dentro da Uber, já existem mais de 60 repositórios usando o Fusion.js desde seu lançamento interno. Esperamos que esse número aumente rapidamente devido a uma combinação de novos projetos da web e migração automatizada de projetos antigos para o Fusion.js. Dada essa demanda, as melhorias no nível do framework devem melhorar significativamente a linha de base da qualidade do software para esses projetos.

Nosso roteiro inclui a adição de mais otimizações de desempenho e ferramentas orientadas para testes, bem como melhor suporte a Fluxo.

Se você estiver interessado em usar o Fusion.js e/ou quiser contribuir, confira a documentação e nossa organização do Github. Se você tiver comentários ou perguntas, também pode entrar em contato conosco no Slack.

Assine nosso newsletter para acompanhar as mais recentes inovações da Engenharia da Uber.

***

Este artigo é do Uber Engineering. Ele foi escrito por Leo Horie. A tradução foi feita pela Redação iMasters com autorização. Você pode conferir o original em: https://eng.uber.com/fusionjs/