Back-End

26 mai, 2017

Modularizando apps ReactJS com Webpack

Publicidade

O desenvolvimento em JavaScript evoluiu muito nos últimos anos, inclusive aquele código executado no browser. O que antes era feito apenas com jQuery ou JavaScript puro, hoje pode ser construído com o auxílio de uma infinidade de frameworks e bibliotecas. Mas essa facilidade tem um preço que muitas vezes é deixado de lado: o desempenho.

Falar sobre desempenho pode parecer um assunto simples demais, porém ele é essencial para que a experiência da sua app seja a melhor possível. Afinal, quem nunca se irritou com um site lento?

Entendendo o problema

Com a chegada das SPAs (Single Page Applications), tornou-se muito comum a concatenação de todo o javascript client-side em apenas um ou dois arquivos minificados, pois esta prática reduz o número de requisições ao servidor e disponibiliza todo o código de uma só vez para o cliente, facilitando o desenvolvimento.

No geral, aplicações pequenas funcionam muito bem com esse tipo de abordagem. Contudo, em aplicações que contêm um grande volume de código no lado do cliente, este cenário muda bastante, prejudicando o tempo de carregamento inicial da app, aumentando o consumo de memória e CPU e podendo causar travamentos na interface durante os primeiros momentos.

Um outro caminho a seguir

Para evitar as desvantagens do método anterior devemos pensar um pouco diferente e, ao invés de carregar todo o javascript de uma vez, podemos dividi-lo em módulos que são carregados apenas quando uma determinada ação é realizada. Assim, diminuímos o tamanho do arquivo principal e priorizamos o carregamento inicial e a rápida exibição de algum conteúdo para o usuário.

Não existe uma fórmula mágica para modularizar o código da melhor maneira possível. Cada app possui suas particularidades que devem ser bem estudadas para conseguir chegar a um modelo que se encaixe na estrutura do projeto. Se seu projeto possui um menu principal, por exemplo, criar um módulo para cada item desse menu seria um bom começo na divisão do código.

Webpack em ação

Caso você ainda não conheça, o Webpack é um “module bundler” que consegue entender como estão distribuídos os seus arquivos estáticos, incluindo imagens e CSS, permitindo, assim, um fácil gerenciamento das dependências no client-side. Não entrarei em detalhes sobre todas as opções do Webpack pois existem ótimos artigos e tutoriais na web que tratam esse assunto com detalhes. Vou manter o foco na funcionalidade chamada “Code Splitting” e mostrar como aplicá-la em uma app com ReactJS. Então vamos lá!

A primeira coisa a se fazer é definir nosso javascript principal. Gosto da ideia de colocar em um arquivo separado todas as bibliotecas que serão utilizadas em qualquer parte da aplicação. Isso porque podemos nos aproveitar do cache nesse arquivo que raramente será modificado. Já as bibliotecas que são específicas de determinados contextos podem muito bem ser encaixadas em seus respectivos módulos. Sendo assim, a seção “entry” do nosso arquivo de configuração do webpack ficará da seguinte maneira:

...
entry: {
 app: './main.js',
 vendor: ['react', 'react-dom', 'react-router-dom']
}
...

Além disso, precisamos utilizar o “CommonsChunkPlugin” para remover quaisquer dependências duplicadas do arquivo “app” que já existam em “vendor”.

 

...
plugins: [
 new webpack.optimize.CommonsChunkPlugin('vendor')
]
...

Agora o Webpack já será capaz de dividir nosso código da forma tradicional que citei anteriormente. Nosso próximo objetivo é fazer a divisão nos pontos específicos e, para isso, podemos utilizar duas funções do Webpack, import() ou require.ensure(). As duas possuem o mesmo propósito, mas vamos utilizar a primeira por ser a recomendada pela ferramenta.

Minha proposta de modularizar uma app que utiliza ReactJS se baseia em criar componentes que são baixados e carregados apenas quando for necessário. Para que um componente tenha essa característica desenvolvi uma classe base para entendermos a ideia. Observe o código a seguir:

import React from 'react';

class AsyncComponent extends React.Component {

 constructor() {
   super(...arguments);

   this.state = {
     Component: this.getComponent()
   };
 }

 // Full module path for server-side require
 getRequirePath() { /* implement this method */ }

 // Returns a promise created by import() to split the module into a new chunk
 getChunk() { /* implement this method */ }

 // Used to retrieve the module id
 getModuleId() { /* implement this method */ }

 getComponent() {
   if (typeof window === 'undefined') {
     let requirePath = this.getRequirePath();
     return require(requirePath).default;
   } else {
     let moduleId = this.getModuleId();
     try {
       if (__webpack_modules__[moduleId])
         return __webpack_require__(moduleId).default;
     } catch (e) {}
   }
   return null;
 }

 componentWillMount() {
   if (!this.state.Component) {
     this
       .getChunk()
       .then((Component) => {
         this.setState({ Component });
       });
   }
 }

 render() {
   const { Component } = this.state;
   if (Component) {
     return <Component {...this.props} />;
   }
   return null;
 }
}

export default AsyncComponent;

A ideia é que um componente faça herança dessa classe e implemente apenas alguns métodos para fornecer informações sobre o módulo a ser criado. A mágica realmente acontece no método getChunk() pois é nele que devemos chamar a função import() e retornar uma Promise do módulo.

class HomeModule extends AsyncComponent {

 getRequirePath() {
   return __dirname + '/home';
 }

 getChunk() {
   return import( /* webpackChunkName: "home-chunk" */ './home')
     .then(module => module.default);
 }

 getModuleId() {
   return require.resolveWeak('./home');
 }
}

No exemplo acima, estamos separando o componente localizado em ‘./home’ e suas dependências em um novo arquivo chamado ‘home-chunk’. É importante lembrar que o Webpack utiliza por padrão um identificador nos nomes dos módulos. Então, devemos alterar a configuração “chunkFilename” para que seja possível, por exemplo, carregar os módulos com base nas rotas.

Utilizando a técnica descrita acima, seu frontend já conseguirá tratar de forma automática a injeção dos componentes quando eles forem requisitados. Mas, não estamos nos esquecendo de algo?

Server-side Rendering

Uma das vantagens do ReactJS é permitir a escrita de um código isomórfico, ou seja, o mesmo código criado para executar no cliente é utilizado também no servidor. Podemos utilizar essa prática em nossos componentes para que eles sejam renderizados também no backend. Com isso, os outros dois métodos da classe AsyncComponent que ainda não citei ajudarão a atender este requisito.

O método getRequirePath() serve para fornecer o caminho absoluto do módulo, em que seu backend em NodeJS poderá usá-lo para fazer um require normal. No backend não precisamos carregar os componentes de forma assíncrona pois tudo pode ser carregado instantaneamente.

O método getModuleId() deve retornar o identificador do módulo no Webpack, que nos ajuda a descobrir se um módulo já foi ou não carregado anteriormente. Este é um ponto importante a ser observado quando uma app com React utiliza renderização no servidor e módulos no frontend. Veja o porquê a seguir.

Imagine o seguinte cenário: Um usuário carrega uma página de sua app com uma url que aponta para um componente que, no frontend, foi dividido em um módulo. O backend conseguirá montar o html exatamente como deve ser pois ele tem acesso a tudo. Mas quando o React do frontend tentar entender esse html, irá ocorrer um alerta no console dizendo que a página renderizada no backend é diferente do que o frontend conhece. Isso acontece porque o componente que o usuário tentou carregar é um módulo e, assim, seu respectivo arquivo javascript não está disponível no frontend até que o Webpack perceba sua necessidade. Para contornar este problema seu servidor deve ser capaz de detectar quando um módulo do frontend é necessário e adicioná-lo ao html para ser carregado antes do arquivo principal da aplicação.  Assim o React cliente terá o necessário para exibir a página da mesma forma conhecida pelo servidor.

Conclusão

Modularizar apps com React não é uma tarefa muito simples, e quando lidamos com renderização no servidor isso pode se tornar uma verdadeira dor de cabeça. Diversos bugs e erros podem aparecer até que tudo se encaixe. No entanto, temos a nossa disposição o Webpack, que torna extremamente mais simples a criação de módulos não só para o React mas para qualquer código javascript que seja executado no cliente. Além disso, a complexidade envolvida nesse processo é direcionada para o usuário como uma navegação mais rápida e fluida.

Confira também o exemplo completo no meu Github.