Otimizando aplicativos Next.js com Nx

Publicados: 2022-03-10
Resumo rápido ↬ Nx é uma estrutura de construção que facilita a otimização, dimensionamento eficiente de aplicativos e outros recursos, como bibliotecas e componentes compartilhados. Neste artigo, veremos como podemos dimensionar aplicativos Next.js com eficiência usando o Nx.

Neste artigo, veremos como otimizar e criar um aplicativo Next.js de alto desempenho usando Nx e seus recursos avançados. Veremos como configurar um servidor Nx, como adicionar um plugin a um servidor existente e o conceito de um monorepo com uma visualização prática.

Se você é um desenvolvedor que busca otimizar aplicativos e criar componentes reutilizáveis ​​entre aplicativos com eficiência, este artigo mostrará como dimensionar rapidamente seus aplicativos e como trabalhar com o Nx. Para acompanhar, você precisará de conhecimento básico da estrutura Next.js e do TypeScript.

O que é Nx?

O Nx é uma estrutura de compilação de código aberto que ajuda você a arquitetar, testar e compilar em qualquer escala, integrando-se perfeitamente a tecnologias e bibliotecas modernas, ao mesmo tempo em que fornece uma interface de linha de comando (CLI) robusta, armazenamento em cache e gerenciamento de dependências. O Nx oferece aos desenvolvedores ferramentas e plug-ins avançados da CLI para estruturas, testes e ferramentas modernas.

Para este artigo, focaremos em como o Nx funciona com os aplicativos Next.js. O Nx fornece ferramentas padrão para teste e estilo em seus aplicativos Next.js, como Cypress, Storybook e componentes com estilo. O Nx facilita um monorepo para seus aplicativos, criando um espaço de trabalho que pode conter o código-fonte e as bibliotecas de vários aplicativos, permitindo compartilhar recursos entre aplicativos.

Por que usar Nx?

O Nx fornece aos desenvolvedores uma quantidade razoável de funcionalidades prontas para uso, incluindo clichês para testes de ponta a ponta (E2E) de seu aplicativo, uma biblioteca de estilos e um monorepo.

Muitas vantagens vêm com o uso do Nx, e veremos algumas delas nesta seção.

  • Execução de tarefas com base em gráficos
    O Nx usa execução de tarefas baseadas em gráficos distribuídos e armazenamento em cache de computação para acelerar as tarefas. O sistema agendará tarefas e comandos usando um sistema gráfico para determinar qual nó (ou seja, aplicativo) deve executar cada tarefa. Isso lida com a execução de aplicativos e otimiza o tempo de execução com eficiência.
  • Teste
    O Nx fornece ferramentas de teste pré-configuradas para testes de unidade e testes E2E.
  • Cache
    O Nx também armazena o gráfico do projeto em cache. Isso permite reanalisar apenas os arquivos atualizados. O Nx acompanha os arquivos alterados desde o último commit e permite que você teste, construa e execute ações apenas nesses arquivos; isso permite a otimização adequada quando você está trabalhando com uma grande base de código.
  • Gráfico de dependência
    O gráfico de dependência visual permite inspecionar como os componentes interagem entre si.
  • Armazenamento na núvem
    O Nx também fornece armazenamento em nuvem e integração com o GitHub, para que você possa compartilhar links com membros da equipe para revisar os logs do projeto.
  • Compartilhamento de código
    Criar uma nova biblioteca compartilhada para cada projeto pode ser bastante desgastante. O Nx elimina essa complicação, liberando você para se concentrar na funcionalidade principal do seu aplicativo. Com o Nx, você pode compartilhar bibliotecas e componentes entre aplicativos. Você pode até compartilhar código reutilizável entre seus aplicativos front-end e back-end.
  • Suporte para monorepos
    O Nx fornece um espaço de trabalho para vários aplicativos. Com essa configuração, um repositório do GitHub pode abrigar o código-fonte de vários aplicativos em seu espaço de trabalho.
Mais depois do salto! Continue lendo abaixo ↓

Nx para bibliotecas publicáveis

Nx permite que você crie bibliotecas publicáveis. Isso é essencial quando você tem bibliotecas que usará fora do monorepo. Em qualquer instância em que você esteja desenvolvendo componentes de interface do usuário organizacional com a integração do Nx Storybook, o Nx criará componentes publicáveis ​​junto com suas histórias. Os componentes publicáveis ​​podem compilar esses componentes para criar um pacote de biblioteca que você pode implantar em um registro externo. Você usaria a opção --publishable ao gerar a biblioteca, ao contrário de --buildable , que é usado para gerar bibliotecas que são usadas apenas no monorepo. O Nx não implanta as bibliotecas publicáveis ​​automaticamente; você pode invocar a compilação por meio de um comando como nx build mylib (onde mylib é o nome da biblioteca), que produzirá um pacote otimizado na pasta dist / mylib que pode ser implantado em um registro externo.

O Nx oferece a opção de criar uma nova área de trabalho com Next.js como uma predefinição ou adicionar Next.js a uma área de trabalho existente.

Para criar um novo espaço de trabalho com Next.js como uma predefinição, você pode usar o seguinte comando:

 npx create-nx-workspace happynrwl \ --preset=next \ --style=styled-components \ --appName=todo

Este comando criará um novo espaço de trabalho Nx com um aplicativo Next.js chamado “todo” e com styled-components como a biblioteca de estilos.

Em seguida, podemos adicionar o aplicativo Next.js a um workspace Nx existente com o seguinte comando:

 npx nx g @nrwl/next:app

Criando um aplicativo Next.js e Nx

O plugin Nx para Next.js inclui ferramentas e executores para executar e otimizar um aplicativo Next.js. Para começar, precisamos criar um novo espaço de trabalho Nx com next como predefinição:

 npx create-nx-workspace happynrwl \ --preset=next \ --style=styled-components \ --appName=todo

O bloco de código acima gerará um novo espaço de trabalho Nx e o aplicativo Next.js. Receberemos um prompt para usar o Nx Cloud. Para este tutorial, selecionaremos “Não” e aguardaremos a instalação de nossas dependências. Feito isso, devemos ter uma árvore de arquivos semelhante a esta:

 happynrwl ┣ apps ┃ ┣ todo ┃ ┣ todo-e2e ┃ ┗ .gitkeep ┣ libs ┣ node_modules ┣ tools ┣ .editorconfig ┣ .eslintrc.json ┣ .gitignore ┣ .prettierignore ┣ .prettierrc ┣ README.md ┣ babel.config.json ┣ jest.config.js ┣ jest.preset.js ┣ nx.json ┣ package-lock.json ┣ package.json ┣ tsconfig.base.json ┗ workspace.json

Na pasta apps , teremos nosso aplicativo Next.js “todo”, com o teste E2E pré-configurado para o app de tarefas. Tudo isso é gerado automaticamente com a poderosa ferramenta Nx CLI.

Para executar nosso aplicativo, use o comando npx nx serve todo . Quando terminar de servir o aplicativo, você deverá ver a tela abaixo:

A página inicial gerada pelo Nx para um novo aplicativo
Nx página padrão para um novo aplicativo. (Visualização grande)

Construindo a API

Neste ponto, configuramos o espaço de trabalho. O próximo passo é construir a API CRUD que usaremos no aplicativo Next.js. Para fazer isso, usaremos o Express; para demonstrar o suporte ao monorepo, construiremos nosso servidor como um aplicativo no espaço de trabalho. Primeiro, temos que instalar o plugin Express para Nx executando este comando:

 npm install --save-dev @nrwl/express

Feito isso, estamos prontos para configurar nosso aplicativo Expresso no espaço de trabalho fornecido. Para gerar um aplicativo Express, execute o comando abaixo:

 npx nx g @nrwl/express:application --name=todo-api --frontendProject=todo

O comando nx g @nrwl/express:application irá gerar uma aplicação Express para a qual podemos passar parâmetros de especificação adicionais; para especificar o nome do aplicativo, use o sinalizador --name ; para indicar o aplicativo front-end que usará o aplicativo Express, passe o nome de um aplicativo em nosso workspace para --frontendProject . Algumas outras opções estão disponíveis para um aplicativo Express. Feito isso, teremos uma estrutura de arquivos atualizada na pasta apps com a pasta todo-api adicionada a ela.

 happynrwl ┣ apps ┃ ┣ todo ┃ ┣ todo-api ┃ ┣ todo-e2e ┃ ┗ .gitkeep …

A pasta todo-api é um clichê do Express com um arquivo de entrada main.ts

 /** * This is not a production server yet! * This is only minimal back end to get started. */ import * as express from 'express'; import {v4 as uuidV4} from 'uuid'; const app = express(); app.use(express.json()); // used instead of body-parser app.get('/api', (req, res) => { res.send({ message: 'Welcome to todo-api!' }); }); const port = process.env.port || 3333; const server = app.listen(port, () => { console.log(`Listening at http://localhost:${port}/api`); }); server.on('error', console.error);

Estaremos criando nossas rotas dentro deste aplicativo. Para começar, inicializaremos uma matriz de objetos com dois pares de valores-chave, item e id , logo abaixo da declaração do aplicativo.

 /** * This is not a production server yet! * This is only minimal back end to get started. */ import * as express from 'express'; import {v4 as uuidV4} from 'uuid'; const app = express(); app.use(express.json()); // used instead of body-parser let todoArray: Array<{ item: string; id: string }> = [ { item: 'default todo', id: uuidV4() }, ]; …

Em seguida, configuraremos a rota para buscar todas as listas de tarefas em app.get() :

 … app.get('/api', (req, res) => { res.status(200).json({ data: todoArray, }); }); …

O bloco de código acima retornará o valor atual de todoArray . Posteriormente, teremos rotas para criar, atualizar e remover pendências do array.

 … app.post('/api', (req, res) => { const item: string = req.body.item; // Increment ID of item based on the ID of the last item in the array. let id: string = uuidV4(); // Add the new object to the array todoArray.push({ item, id }); res.status(200).json({ message: 'item added successfully', }); }); app.patch('/api', (req, res) => { // Value of the updated item const updatedItem: string = req.body.updatedItem; // ID of the position to update const id: string = req.body.id; // Find index of the ID const arrayIndex = todoArray.findIndex((obj) => obj.id === id); // Update item that matches the index todoArray[arrayIndex].item = updatedItem res.status(200).json({ message: 'item updated successfully', }); }); app.delete('/api', (req, res) => { // ID of the position to remove const id: string = req.body.id; // Update array and remove the object that matches the ID todoArray = todoArray.filter((val) => val.id !== id); res.status(200).json({ message: 'item removed successfully', }); }); …

Para criar um novo item de tarefa, tudo o que precisamos é o valor do novo item como uma string. Vamos gerar um ID incrementando o ID do último elemento do array no servidor. Para atualizar um item existente, passaríamos o novo valor do item e o ID do objeto do item a ser atualizado; no servidor, percorreríamos cada item com o método forEach e atualizaríamos o item no local onde o ID corresponde ao ID enviado com a solicitação. Finalmente, para remover um item do array, enviaríamos o ID do item a ser removido com a solicitação; em seguida, filtramos o array e retornamos um novo array de todos os itens que não correspondem ao ID enviado com a solicitação, atribuindo o novo array à variável todoArray .

Observação: se você procurar na pasta do aplicativo Next.js, deverá ver um arquivo proxy.conf.json com a configuração abaixo:

 { "/api": { "target": "http://localhost:3333", "secure": false } }

Isso cria um proxy, permitindo que todas as chamadas de API para rotas que correspondam a /api como destino o servidor todo-api .

Gerando páginas Next.js com Nx

Em nosso aplicativo Next.js, geraremos uma nova página, home e um componente de item. O Nx fornece uma ferramenta CLI para criarmos facilmente uma página:

 npx nx g @nrwl/next:page home

Ao executar este comando, obteremos um prompt para selecionar a biblioteca de estilos que queremos usar para a página; para este artigo, selecionaremos styled-components . Voilá! Nossa página está criada. Para criar um componente, execute npx nx g @nrwl/next:component todo-item ; isso criará uma pasta de component com o componente todo-item .

Consumo de API no aplicativo Next.js

Em cada pendência teremos dois botões, para editar e deletar a pendência. As funções assíncronas que executam essas ações são passadas como props da página inicial.

 … export interface TodoItemProps { updateItem(id: string, updatedItem: string): Promise<void>; deleteItem(id: string): Promise<void>; fetchItems(): Promise<any>; item: string; id: string; } export const FlexWrapper = styled.div` width: 100%; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #ccc; padding-bottom: 10px; margin-top: 20px; @media all and (max-width: 470px) { flex-direction: column; input { width: 100%; } button { width: 100%; } } `; export function TodoItem(props: TodoItemProps) { const [isEditingItem, setIsEditingItem] = useState<boolean>(false); const [item, setNewItem] = useState<string | null>(null); return ( <FlexWrapper> <Input disabled={!isEditingItem} defaultValue={props.item} isEditing={isEditingItem} onChange={({ target }) => setNewItem(target.value)} /> {!isEditingItem && <Button onClick={() => setIsEditingItem(true)} > Edit </Button>} {isEditingItem && <Button onClick={async () => { await props.updateItem(props.id, item); //fetch updated items await props.fetchItems(); setIsEditingItem(false) }}> Update </Button>} <Button danger onClick={async () => { await props.deleteItem(props.id); //fetch updated items await await props.fetchItems(); }} > Delete </Button> </FlexWrapper> ); }

Para a funcionalidade de atualização, temos uma entrada que é desabilitada quando o estado isEditingItem é false . Uma vez que o botão “Editar” é clicado, ele alterna o estado isEditingItem para true e exibe o botão “Atualizar”. Aqui, o componente de entrada é habilitado e o usuário pode inserir um novo valor; quando o botão "Atualizar" é clicado, ele chama a função updateItem com os parâmetros passados ​​e alterna isEditingItem de volta para false .

No componente home page, temos as funções assíncronas realizando a operação CRUD.

 … const [items, setItems] = useState<Array<{ item: string; id: string }>>([]); const [newItem, setNewItem] = useState<string>(''); const fetchItems = async () => { try { const data = await fetch('/api/fetch'); const res = await data.json(); setItems(res.data); } catch (error) { console.log(error); } }; const createItem = async (item: string) => { try { const data = await fetch('/api', { method: 'POST', body: JSON.stringify({ item }), headers: { 'Content-Type': 'application/json', }, }); } catch (error) { console.log(error); } }; const deleteItem = async (id: string) => { try { const data = await fetch('/api', { method: 'DELETE', body: JSON.stringify({ id }), headers: { 'Content-Type': 'application/json', }, }); const res = await data.json(); alert(res.message); } catch (error) { console.log(error); } }; const updateItem = async (id: string, updatedItem: string) => { try { const data = await fetch('/api', { method: 'PATCH', body: JSON.stringify({ id, updatedItem }), headers: { 'Content-Type': 'application/json', }, }); const res = await data.json(); alert(res.message); } catch (error) { console.log(error); } }; useEffect(() => { fetchItems(); }, []); …

No bloco de código acima, temos fetchItems , que retorna todoArray do servidor. Então, temos a função createItem , que recebe uma string; o parâmetro é o valor do novo item de pendência. A função updateItem recebe dois parâmetros, o ID do item a ser atualizado e o valor updatedItem . E a função deleteItem remove o item correspondente ao ID que é passado.

Para renderizar o item pendente, mapeamos o estado dos items :

 … return ( <StyledHome> <h1>Welcome to Home!</h1> <TodoWrapper> {items.length > 0 && items.map((val) => ( <TodoItem key={val.id} item={val.item} id={val.id} deleteItem={deleteItem} updateItem={updateItem} fetchItems={fetchItems} /> ))} </TodoWrapper> <form onSubmit={async(e) => { e.preventDefault(); await createItem(newItem); //Clean up new item setNewItem(''); await fetchItems(); }} > <FlexWrapper> <Input value={newItem} onChange={({ target }) => setNewItem(target.value)} placeholder="Add new item…" /> <Button success type="submit"> Add + </Button> </FlexWrapper> </form> </StyledHome> ); …

Nosso servidor e front-end já estão configurados. Podemos servir o aplicativo de API executando npx nx serve todo-api e, para o aplicativo Next.js, executamos npx nx serve todo . Clique no botão "Continuar" e você verá uma página com o item de tarefa padrão exibido.

Página de itens de tarefas do aplicativo, com a lista de tarefas padrão no servidor.
A página de itens de tarefas do aplicativo. (Visualização grande)

Agora temos um aplicativo Next.js e Express funcionando juntos em um espaço de trabalho.

O Nx possui outra ferramenta CLI que nos permite visualizar o gráfico de dependência de nossa aplicação em nossa execução de terminal. Execute npx nx dep-graph , e devemos ver uma tela semelhante à imagem abaixo, representando o gráfico de dependência de nossa aplicação.

Gráfico de dependência mostrando a relação entre aplicativos no espaço de trabalho
Dep-gráfico de aplicação. (Visualização grande)

Outros comandos CLI para Nx

  • nx list
    Lista os plugins Nx atualmente instalados.
  • nx migrate latest
    Atualiza os pacotes em package.json para a versão mais recente.
  • nx affected
    Executa a ação apenas nos aplicativos afetados ou modificados.
  • nx run-many --target serve --projects todo-api,todo
    Executa o comando de destino em todos os projetos listados.

Conclusão

Como uma visão geral do Nx, este artigo abordou o que o Nx oferece e como ele facilita o trabalho para nós. Também passamos pela configuração de um aplicativo Next.js em um espaço de trabalho Nx, adicionando um plug-in Express a um espaço de trabalho existente e usando o recurso monorepo para hospedar mais de um aplicativo em nosso espaço de trabalho.

Você encontrará o código-fonte completo no repositório do GitHub. Para obter informações adicionais sobre o Nx, confira a documentação ou a documentação do Nx para Next.js.