Ottimizzazione delle applicazioni Next.js con Nx
Pubblicato: 2022-03-10In questo articolo, analizzeremo come ottimizzare e creare un'applicazione Next.js ad alte prestazioni utilizzando Nx e le sue ricche funzionalità. Analizzeremo come configurare un server Nx, come aggiungere un plug-in a un server esistente e il concetto di monorepo con una visualizzazione pratica.
Se sei uno sviluppatore che cerca di ottimizzare le applicazioni e creare componenti riutilizzabili in modo efficace tra le applicazioni, questo articolo ti mostrerà come ridimensionare rapidamente le tue applicazioni e come lavorare con Nx. Per seguire, avrai bisogno di una conoscenza di base del framework Next.js e TypeScript.
Cos'è Nx?
Nx è un framework di compilazione open source che ti aiuta a progettare, testare e creare su qualsiasi scala, integrandosi perfettamente con le moderne tecnologie e librerie, fornendo al contempo una solida interfaccia a riga di comando (CLI), memorizzazione nella cache e gestione delle dipendenze. Nx offre agli sviluppatori strumenti CLI avanzati e plug-in per framework, test e strumenti moderni.
Per questo articolo, ci concentreremo su come Nx funziona con le applicazioni Next.js. Nx fornisce strumenti standard per il test e lo styling nelle tue applicazioni Next.js, come Cypress, Storybook e styled-components. Nx facilita un monorepo per le tue applicazioni, creando uno spazio di lavoro che può contenere il codice sorgente e le librerie di più applicazioni, permettendoti di condividere le risorse tra le applicazioni.
Perché usare Nx?
Nx fornisce agli sviluppatori una quantità ragionevole di funzionalità pronte all'uso, inclusi standard per il test end-to-end (E2E) dell'applicazione, una libreria di stili e un monorepo.
Molti vantaggi derivano dall'utilizzo di Nx e in questa sezione ne analizzeremo alcuni.
- Esecuzione di attività basata su grafici
Nx utilizza l'esecuzione di attività basate su grafici distribuiti e la memorizzazione nella cache di calcolo per accelerare le attività. Il sistema pianificherà attività e comandi utilizzando un sistema grafico per determinare quale nodo (ad esempio applicazione) dovrebbe eseguire ciascuna attività. Questo gestisce l'esecuzione delle applicazioni e ottimizza il tempo di esecuzione in modo efficiente. - Test
Nx fornisce strumenti di test preconfigurati per unit test e test E2E. - Memorizzazione nella cache
Nx memorizza anche il grafico del progetto memorizzato nella cache. Ciò consente di rianalizzare solo i file aggiornati. Nx tiene traccia dei file modificati dall'ultimo commit e ti consente di testare, creare ed eseguire azioni solo su quei file; ciò consente una corretta ottimizzazione quando si lavora con una base di codice di grandi dimensioni. - Grafico delle dipendenze
Il grafico della dipendenza visiva consente di esaminare il modo in cui i componenti interagiscono tra loro. - Archiviazione su cloud
Nx fornisce anche l'archiviazione su cloud e l'integrazione con GitHub, in modo da poter condividere i collegamenti con i membri del team per rivedere i registri del progetto. - Condivisione codice
Creare una nuova libreria condivisa per ogni progetto può essere piuttosto faticoso. Nx elimina questa complicazione, permettendoti di concentrarti sulle funzionalità principali della tua app. Con Nx puoi condividere librerie e componenti tra le applicazioni. Puoi persino condividere codice riutilizzabile tra le tue applicazioni front-end e back-end. - Supporto per monorepos
Nx fornisce un'area di lavoro per più applicazioni. Con questa configurazione, un repository GitHub può ospitare il codice sorgente per varie applicazioni nel tuo spazio di lavoro.
Nx per le biblioteche pubblicabili
Nx ti consente di creare librerie pubblicabili. Questo è essenziale quando hai librerie che utilizzerai al di fuori del monorepo. In ogni caso in cui stai sviluppando componenti dell'interfaccia utente dell'organizzazione con l'integrazione di Nx Storybook, Nx creerà componenti pubblicabili insieme alle tue storie. I componenti pubblicabili possono compilare questi componenti per creare un bundle di librerie che è possibile distribuire in un registro esterno. Dovresti utilizzare l'opzione --publishable
durante la generazione della libreria, a differenza --buildable
, che viene utilizzato per generare librerie che vengono utilizzate solo nel monorepo. Nx non distribuisce automaticamente le librerie pubblicabili; puoi invocare la build tramite un comando come nx build mylib
(dove mylib
è il nome della libreria), che produrrà quindi un bundle ottimizzato nella cartella dist
/ mylib
che può essere distribuito in un registro esterno.
Nx ti dà la possibilità di creare un nuovo spazio di lavoro con Next.js come predefinito o di aggiungere Next.js a uno spazio di lavoro esistente.
Per creare un nuovo spazio di lavoro con Next.js come predefinito, puoi utilizzare il comando seguente:
npx create-nx-workspace happynrwl \ --preset=next \ --style=styled-components \ --appName=todo
Questo comando creerà un nuovo spazio di lavoro Nx con un'app Next.js denominata "todo" e con styled-components
come libreria di stili.
Quindi, possiamo aggiungere l'applicazione Next.js a un'area di lavoro Nx esistente con il seguente comando:
npx nx g @nrwl/next:app
Creazione di un'applicazione Next.js e Nx
Il plug-in Nx per Next.js include strumenti ed esecutori per eseguire e ottimizzare un'applicazione Next.js. Per iniziare, dobbiamo creare un nuovo spazio di lavoro Nx con next
come predefinito:
npx create-nx-workspace happynrwl \ --preset=next \ --style=styled-components \ --appName=todo
Il blocco di codice sopra genererà un nuovo spazio di lavoro Nx e l'applicazione Next.js. Riceveremo una richiesta per utilizzare Nx Cloud. Per questo tutorial, selezioneremo "No" e quindi attenderemo l'installazione delle nostre dipendenze. Una volta fatto, dovremmo avere un albero di file simile a questo:
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
Nella cartella delle apps
, avremo la nostra applicazione Next.js "todo", con il test E2E preconfigurato per l'app da fare. Tutto questo viene generato automaticamente con il potente strumento Nx CLI.
Per eseguire la nostra app, usa il comando npx nx serve todo
. Una volta che hai finito di servire l'app, dovresti vedere la schermata qui sotto:
Costruire l'API
A questo punto, abbiamo impostato l'area di lavoro. Il prossimo passo è la creazione dell'API CRUD che utilizzeremo nell'applicazione Next.js. Per fare ciò, utilizzeremo Express; per dimostrare il supporto di monorepo, creeremo il nostro server come applicazione nell'area di lavoro. Innanzitutto, dobbiamo installare il plug-in Express per Nx eseguendo questo comando:
npm install --save-dev @nrwl/express
Una volta fatto, siamo pronti per configurare la nostra app Express nell'area di lavoro fornita. Per generare un'app Express, esegui il comando seguente:
npx nx g @nrwl/express:application --name=todo-api --frontendProject=todo
Il comando nx g @nrwl/express:application
genererà un'applicazione Express a cui possiamo passare parametri di specifica aggiuntivi; per specificare il nome dell'applicazione, utilizzare il flag --name
; per indicare l'applicazione front-end che utilizzerà l'app Express, passa il nome di un'app nel nostro spazio di lavoro a --frontendProject
. Sono disponibili alcune altre opzioni per un'app Express. Al termine, avremo una struttura di file aggiornata nella cartella delle apps
con la cartella todo-api
aggiunta ad essa.
happynrwl ┣ apps ┃ ┣ todo ┃ ┣ todo-api ┃ ┣ todo-e2e ┃ ┗ .gitkeep …
La cartella todo-api
è un boilerplate Express con un file di ingresso 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);
Creeremo i nostri percorsi all'interno di questa app. Per iniziare, inizializzeremo un array di oggetti con due coppie chiave-valore, item
e id
, appena sotto la dichiarazione dell'app.
/** * 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() }, ]; …
Successivamente, imposteremo il percorso per recuperare tutti gli elenchi di cose da fare in app.get()
:
… app.get('/api', (req, res) => { res.status(200).json({ data: todoArray, }); }); …
Il blocco di codice sopra restituirà il valore corrente di todoArray
. Successivamente, avremo percorsi per creare, aggiornare e rimuovere le cose da fare dall'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', }); }); …
Per creare un nuovo elemento da fare, tutto ciò di cui abbiamo bisogno è il valore del nuovo elemento come stringa. Genereremo un ID incrementando l'ID dell'ultimo elemento nell'array sul server. Per aggiornare un articolo esistente, passeremo il nuovo valore per l'articolo e l'ID dell'oggetto articolo da aggiornare; sul server, scorreremo ogni elemento con il metodo forEach
e aggiorneremo l'elemento nel punto in cui l'ID corrisponde all'ID inviato con la richiesta. Infine, per rimuovere un elemento dall'array, invieremo l'ID dell'elemento da rimuovere con la richiesta; quindi, filtriamo l'array e restituiamo un nuovo array di tutti gli elementi che non corrispondono all'ID inviato con la richiesta, assegnando il nuovo array alla variabile todoArray
.
Nota: se guardi nella cartella dell'applicazione Next.js, dovresti vedere un file proxy.conf.json
con la configurazione seguente:
{ "/api": { "target": "http://localhost:3333", "secure": false } }
Questo crea un proxy, consentendo a tutte le chiamate API di instradare la corrispondenza /api
per indirizzare il server todo-api
.
Generazione di pagine Next.js con Nx
Nella nostra applicazione Next.js, genereremo una nuova pagina, home
e un componente elemento. Nx fornisce uno strumento CLI per creare facilmente una pagina:
npx nx g @nrwl/next:page home
Dopo aver eseguito questo comando, avremo un prompt per selezionare la libreria di stili che vogliamo usare per la pagina; per questo articolo, selezioneremo styled-components
. Ecco! La nostra pagina è stata creata. Per creare un componente, esegui npx nx g @nrwl/next:component todo-item
; questo creerà una cartella del component
con il componente todo-item
.
Consumo API nell'applicazione Next.js
In ogni cosa da fare, avremo due pulsanti, per modificare ed eliminare la cosa da fare. Le funzioni asincrone che eseguono queste azioni vengono passate come prop dalla home page.
… 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> ); }
Per la funzionalità di aggiornamento, abbiamo un input che è disabilitato quando lo stato isEditingItem
è false
. Dopo aver fatto clic sul pulsante "Modifica", lo stato isEditingItem
viene impostato su true
e viene visualizzato il pulsante "Aggiorna". Qui, il componente di input è abilitato e l'utente può inserire un nuovo valore; quando si fa clic sul pulsante "Aggiorna", chiama la funzione updateItem
con i parametri passati e riporta isEditingItem
a false
.
Nel componente della home
page, abbiamo le funzioni asincrone che eseguono l'operazione 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(); }, []); …
Nel blocco di codice sopra, abbiamo fetchItems
, che restituisce todoArray
dal server. Quindi, abbiamo la funzione createItem
, che accetta una stringa; il parametro è il valore del nuovo elemento da fare. La funzione updateItem
accetta due parametri, l'ID dell'elemento da aggiornare e il valore updatedItem
. E la funzione deleteItem
rimuove l'elemento corrispondente all'ID passato.
Per rendere l'elemento da fare, mappiamo attraverso lo stato degli 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> ); …
Il nostro server e il front-end sono ora configurati. Possiamo servire l'applicazione API eseguendo npx nx serve todo-api
e per l'applicazione Next.js, eseguiamo npx nx serve todo
. Fai clic sul pulsante "Continua" e vedrai una pagina con l'elemento da fare predefinito visualizzato.
Ora abbiamo un'applicazione Next.js e Express funzionante che lavorano insieme in un'unica area di lavoro.
Nx ha un altro strumento CLI che ci consente di visualizzare il grafico delle dipendenze della nostra applicazione nel nostro terminale. Esegui npx nx dep-graph
e dovremmo vedere una schermata simile all'immagine qui sotto, raffigurante il grafico delle dipendenze della nostra applicazione.
Altri comandi CLI per Nx
-
nx list
Elenca i plugin Nx attualmente installati. -
nx migrate latest
Aggiorna i pacchetti inpackage.json
all'ultima versione. -
nx affected
Esegue l'azione solo sulle app interessate o modificate. -
nx run-many --target serve --projects todo-api,todo
Esegue il comando target su tutti i progetti elencati.
Conclusione
Come panoramica generale di Nx, questo articolo ha coperto ciò che offre Nx e come semplifica il lavoro per noi. Abbiamo anche esaminato la configurazione di un'applicazione Next.js in un'area di lavoro Nx, l'aggiunta di un plug-in Express a un'area di lavoro esistente e l'utilizzo della funzione monorepo per ospitare più di un'applicazione nel nostro spazio di lavoro.
Troverai il codice sorgente completo nel repository GitHub. Per ulteriori informazioni su Nx, consulta la documentazione o la documentazione di Nx per Next.js.