Optimizarea aplicațiilor Next.js cu Nx
Publicat: 2022-03-10În acest articol, vom analiza cum să optimizați și să construim o aplicație Next.js de înaltă performanță folosind Nx și caracteristicile sale bogate. Vom analiza cum să configurați un server Nx, cum să adăugați un plugin la un server existent și conceptul de monorepo cu o vizualizare practică.
Dacă sunteți un dezvoltator care dorește să optimizeze aplicațiile și să creeze componente reutilizabile în aplicații în mod eficient, acest articol vă va arăta cum să vă scalați rapid aplicațiile și cum să lucrați cu Nx. Pentru a continua, veți avea nevoie de cunoștințe de bază despre cadrul Next.js și TypeScript.
Ce este Nx?
Nx este un cadru de compilare open-source care vă ajută să creați, să testați și să construiți la orice scară — integrându-se perfect cu tehnologiile și bibliotecile moderne, oferind în același timp o interfață robustă de linie de comandă (CLI), stocarea în cache și gestionarea dependenței. Nx oferă dezvoltatorilor instrumente CLI și plugin-uri avansate pentru cadre, teste și instrumente moderne.
Pentru acest articol, ne vom concentra asupra modului în care funcționează Nx cu aplicațiile Next.js. Nx oferă instrumente standard pentru testare și stilare în aplicațiile dvs. Next.js, cum ar fi Cypress, Storybook și componentele cu stil. Nx facilitează un monorepo pentru aplicațiile dvs., creând un spațiu de lucru care poate conține codul sursă și bibliotecile mai multor aplicații, permițându-vă să partajați resurse între aplicații.
De ce să folosiți Nx?
Nx oferă dezvoltatorilor o cantitate rezonabilă de funcționalități, incluzând boilerplate pentru testarea end-to-end (E2E) a aplicației dvs., o bibliotecă de stiluri și un monorepo.
Multe avantaje vin odată cu utilizarea Nx și vom trece prin câteva dintre ele în această secțiune.
- Execuția sarcinilor bazată pe grafic
Nx folosește execuția sarcinilor bazată pe grafice distribuite și stocarea în cache de calcul pentru a accelera sarcinile. Sistemul va programa sarcini și comenzi folosind un sistem grafic pentru a determina ce nod (adică aplicația) ar trebui să execute fiecare sarcină. Aceasta se ocupă de execuția aplicațiilor și optimizează timpul de execuție în mod eficient. - Testare
Nx oferă instrumente de testare preconfigurate pentru testarea unitară și testele E2E. - Memorarea în cache
Nx stochează, de asemenea, graficul proiectului în cache. Acest lucru îi permite să reanalizeze numai fișierele actualizate. Nx ține evidența fișierelor modificate de la ultima comitere și vă permite să testați, să construiți și să efectuați acțiuni numai pe acele fișiere; acest lucru permite o optimizare adecvată atunci când lucrați cu o bază de cod mare. - Graficul dependenței
Graficul dependenței vizuale vă permite să inspectați modul în care componentele interacționează între ele. - Stocare in cloud
Nx oferă, de asemenea, stocare în cloud și integrare GitHub, astfel încât să puteți partaja link-uri cu membrii echipei pentru a revizui jurnalele de proiect. - Distribuie codul
Crearea unei noi biblioteci partajate pentru fiecare proiect poate fi destul de dificilă. Nx elimină această complicație, eliberându-vă să vă concentrați pe funcționalitatea de bază a aplicației dvs. Cu Nx, puteți partaja biblioteci și componente între aplicații. Puteți chiar să partajați cod reutilizabil între aplicațiile dvs. front-end și back-end. - Suport pentru monorepos
Nx oferă un spațiu de lucru pentru mai multe aplicații. Cu această configurare, un depozit GitHub poate găzdui sursa de cod pentru diverse aplicații în spațiul dvs. de lucru.
Nx pentru biblioteci care pot fi publicate
Nx vă permite să creați biblioteci care pot fi publicate. Acest lucru este esențial atunci când aveți biblioteci pe care le veți folosi în afara monorepo. În orice situație în care dezvoltați componente UI organizaționale cu integrarea Nx Storybook, Nx va crea componente care pot fi publicate împreună cu poveștile dvs. Componentele care pot fi publicate pot compila aceste componente pentru a crea un pachet de biblioteci pe care îl puteți implementa într-un registru extern. Veți folosi opțiunea --publishable
atunci când generați biblioteca, spre deosebire --buildable
, care este folosită pentru a genera biblioteci care sunt utilizate numai în monorepo. Nx nu implementează automat bibliotecile care pot fi publicate; puteți invoca construirea printr-o comandă precum nx build mylib
(unde mylib
este numele bibliotecii), care va produce apoi un pachet optimizat în folderul dist
/ mylib
care poate fi implementat într-un registru extern.
Nx vă oferă opțiunea de a crea un nou spațiu de lucru cu Next.js ca presetare sau de a adăuga Next.js la un spațiu de lucru existent.
Pentru a crea un nou spațiu de lucru cu Next.js ca presetare, puteți utiliza următoarea comandă:
npx create-nx-workspace happynrwl \ --preset=next \ --style=styled-components \ --appName=todo
Această comandă va crea un nou spațiu de lucru Nx cu o aplicație Next.js numită „todo” și cu styled-components
ca bibliotecă de stiluri.
Apoi, putem adăuga aplicația Next.js la un spațiu de lucru Nx existent cu următoarea comandă:
npx nx g @nrwl/next:app
Construirea unei aplicații Next.js și Nx
Pluginul Nx pentru Next.js include instrumente și executori pentru rularea și optimizarea unei aplicații Next.js. Pentru a începe, trebuie să creăm un nou spațiu de lucru Nx cu next
ca presetare:
npx create-nx-workspace happynrwl \ --preset=next \ --style=styled-components \ --appName=todo
Blocul de cod de mai sus va genera un nou spațiu de lucru Nx și aplicația Next.js. Vom primi o solicitare pentru a folosi Nx Cloud. Pentru acest tutorial, vom selecta „Nu”, apoi vom aștepta ca dependențele noastre să se instaleze. Odată ce s-a terminat, ar trebui să avem un arbore de fișiere similar cu acesta:
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
În dosarul de apps
, vom avea aplicația noastră Next.js „todo”, cu testul E2E preconfigurat pentru aplicația de făcut. Toate acestea sunt generate automat cu instrumentul puternic Nx CLI.
Pentru a rula aplicația noastră, utilizați comanda npx nx serve todo
. După ce ați terminat de servit aplicația, ar trebui să vedeți ecranul de mai jos:
Construirea API-ului
În acest moment, am configurat spațiul de lucru. În continuare, construim API-ul CRUD pe care îl vom folosi în aplicația Next.js. Pentru a face acest lucru, vom folosi Express; pentru a demonstra suportul monorepo, vom construi serverul nostru ca o aplicație în spațiul de lucru. Mai întâi, trebuie să instalăm pluginul Express pentru Nx, rulând această comandă:
npm install --save-dev @nrwl/express
Odată ce s-a terminat, suntem gata să configuram aplicația noastră Express în spațiul de lucru oferit. Pentru a genera o aplicație Express, rulați comanda de mai jos:
npx nx g @nrwl/express:application --name=todo-api --frontendProject=todo
Comanda nx g @nrwl/express:application
va genera o aplicație Express căreia îi putem transmite parametri de specificație suplimentari; pentru a specifica numele aplicației, utilizați steag-ul --name
; pentru a indica aplicația front-end care va folosi aplicația Express, transmiteți numele unei aplicații în spațiul nostru de lucru către --frontendProject
. Alte câteva opțiuni sunt disponibile pentru o aplicație Express. Când se face acest lucru, vom avea o structură de fișiere actualizată în folderul apps
cu folderul todo-api
adăugat.
happynrwl ┣ apps ┃ ┣ todo ┃ ┣ todo-api ┃ ┣ todo-e2e ┃ ┗ .gitkeep …
Dosarul todo-api
este un boilerplate Express cu un fișier de intrare 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);
Ne vom crea rutele în cadrul acestei aplicații. Pentru a începe, vom inițializa o matrice de obiecte cu două perechi cheie-valoare, item
și id
, chiar sub declarația aplicației.
/** * 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() }, ]; …
În continuare, vom seta ruta pentru a prelua toate listele de sarcini sub app.get()
:
… app.get('/api', (req, res) => { res.status(200).json({ data: todoArray, }); }); …
Blocul de cod de mai sus va returna valoarea curentă a todoArray
. Ulterior, vom avea rute pentru crearea, actualizarea și eliminarea elementelor de făcut din matrice.
… 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', }); }); …
Pentru a crea un nou articol de făcut, tot ce ne trebuie este valoarea noului articol ca șir. Vom genera un ID prin creșterea ID-ului ultimului element din matricea de pe server. Pentru a actualiza un articol existent, vom trece noua valoare pentru articol și ID-ul obiectului articol de actualizat; pe server, vom parcurge fiecare articol cu metoda forEach
și vom actualiza articolul în locul în care ID-ul se potrivește cu ID-ul trimis cu cererea. În cele din urmă, pentru a elimina un articol din matrice, vom trimite ID-ul articolului pentru a fi eliminat odată cu cererea; apoi, filtrăm prin matrice și returnăm o nouă matrice cu toate elementele care nu se potrivesc cu ID-ul trimis cu cererea, atribuind noua matrice variabilei todoArray
.
Notă: Dacă te uiți în folderul aplicației Next.js, ar trebui să vezi un fișier proxy.conf.json
cu configurația de mai jos:
{ "/api": { "target": "http://localhost:3333", "secure": false } }
Acest lucru creează un proxy, permițând tuturor apelurilor API către rutele care se potrivesc cu /api
să vizeze serverul todo-api
.
Generarea paginilor Next.js cu Nx
În aplicația noastră Next.js, vom genera o nouă pagină, home
și o componentă de articol. Nx ne oferă un instrument CLI pentru a crea cu ușurință o pagină:
npx nx g @nrwl/next:page home
La rularea acestei comenzi, vom primi o solicitare pentru a selecta biblioteca de stiluri pe care dorim să o folosim pentru pagină; pentru acest articol, vom selecta styled-components
. Voila! Pagina noastră este creată. Pentru a crea o componentă, rulați npx nx g @nrwl/next:component todo-item
; aceasta va crea un folder de component
cu componenta todo-item
.
Consumul API în aplicația Next.js
În fiecare articol de făcut, vom avea două butoane, pentru a edita și șterge elementul de făcut. Funcțiile asincrone care efectuează aceste acțiuni sunt transmise ca elemente de recuzită din pagina de pornire.
… 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> ); }
Pentru funcționalitatea de actualizare, avem o intrare care este dezactivată atunci când starea isEditingItem
este false
. Odată ce se face clic pe butonul „Editare”, acesta comută starea isEditingItem
la true
și afișează butonul „Actualizare”. Aici, componenta de intrare este activată, iar utilizatorul poate introduce o nouă valoare; când se face clic pe butonul „Actualizare”, apelează funcția updateItem
cu parametrii trecuți și comută isEditingItem
înapoi la false
.
În componenta paginii home
, avem funcțiile asincrone care efectuează operația 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(); }, []); …
În blocul de cod de mai sus, avem fetchItems
, care returnează todoArray
de la server. Apoi, avem funcția createItem
, care ia un șir; parametrul este valoarea noului element de rezolvat. Funcția updateItem
ia doi parametri, ID-ul articolului care trebuie actualizat și valoarea updatedItem
. Și funcția deleteItem
elimină elementul care se potrivește cu ID-ul care este transmis.
Pentru a reda elementul de făcut, mapăm starea 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> ); …
Serverul și front-end-ul nostru sunt acum configurate. Putem servi aplicația API rulând npx nx serve todo-api
, iar pentru aplicația Next.js, rulăm npx nx serve todo
. Faceți clic pe butonul „Continuați” și veți vedea o pagină cu elementul prestabilit implicit afișat.
Acum avem o aplicație Next.js și Express care funcționează împreună într-un singur spațiu de lucru.
Nx are un alt instrument CLI care ne permite să vedem graficul de dependență al aplicației noastre în rularea terminalului nostru. Rulați npx nx dep-graph
și ar trebui să vedem un ecran similar cu imaginea de mai jos, ilustrând graficul de dependență al aplicației noastre.
Alte comenzi CLI pentru Nx
-
nx list
Listează pluginurile Nx instalate în prezent. -
nx migrate latest
Actualizează pachetele dinpackage.json
la cea mai recentă versiune. -
nx affected
Efectuează acțiunea numai asupra aplicațiilor afectate sau modificate. -
nx run-many --target serve --projects todo-api,todo
Rulează comanda țintă în toate proiectele listate.
Concluzie
Ca o prezentare generală a Nx, acest articol a acoperit ce oferă Nx și cum ne ușurează munca. De asemenea, am parcurs configurarea unei aplicații Next.js într-un spațiu de lucru Nx, adăugarea unui plugin Express la un spațiu de lucru existent și utilizarea caracteristicii monorepo pentru a găzdui mai mult de o aplicație în spațiul nostru de lucru.
Veți găsi codul sursă complet în depozitul GitHub. Pentru informații suplimentare despre Nx, consultați documentația sau documentația Nx pentru Next.js.