Otimizando aplicativos Next.js com Nx
Publicados: 2022-03-10Neste 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.
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:
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.
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.
Outros comandos CLI para Nx
-
nx list
Lista os plugins Nx atualmente instalados. -
nx migrate latest
Atualiza os pacotes empackage.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.