Optimización de aplicaciones Next.js con Nx
Publicado: 2022-03-10En este artículo, veremos cómo optimizar y crear una aplicación Next.js de alto rendimiento utilizando Nx y sus ricas funciones. Veremos cómo configurar un servidor Nx, cómo agregar un complemento a un servidor existente y el concepto de un monorepo con una visualización práctica.
Si es un desarrollador que busca optimizar aplicaciones y crear componentes reutilizables en todas las aplicaciones de manera efectiva, este artículo le mostrará cómo escalar rápidamente sus aplicaciones y cómo trabajar con Nx. Para seguir, necesitará conocimientos básicos del marco Next.js y TypeScript.
¿Qué es Nx?
Nx es un marco de compilación de código abierto que lo ayuda a diseñar, probar y compilar a cualquier escala, integrándose perfectamente con tecnologías y bibliotecas modernas, al tiempo que proporciona una sólida interfaz de línea de comandos (CLI), almacenamiento en caché y administración de dependencias. Nx ofrece a los desarrolladores herramientas y complementos de CLI avanzados para marcos, pruebas y herramientas modernos.
Para este artículo, nos centraremos en cómo funciona Nx con las aplicaciones Next.js. Nx proporciona herramientas estándar para probar y diseñar en sus aplicaciones Next.js, como Cypress, Storybook y componentes con estilo. Nx facilita un monorepo para sus aplicaciones, creando un espacio de trabajo que puede contener el código fuente y las bibliotecas de múltiples aplicaciones, lo que le permite compartir recursos entre aplicaciones.
¿Por qué usar Nx?
Nx proporciona a los desarrolladores una cantidad razonable de funciones listas para usar, incluidas plantillas para pruebas de extremo a extremo (E2E) de su aplicación, una biblioteca de estilos y un monorepo.
Muchas ventajas vienen con el uso de Nx, y veremos algunas de ellas en esta sección.
- Ejecución de tareas basada en gráficos
Nx utiliza la ejecución de tareas distribuidas basadas en gráficos y el almacenamiento en caché de cómputo para acelerar las tareas. El sistema programará tareas y comandos utilizando un sistema gráfico para determinar qué nodo (es decir, aplicación) debe ejecutar cada tarea. Esto maneja la ejecución de aplicaciones y optimiza el tiempo de ejecución de manera eficiente. - Pruebas
Nx proporciona herramientas de prueba preconfiguradas para pruebas unitarias y pruebas E2E. - almacenamiento en caché
Nx también almacena el gráfico del proyecto en caché. Esto le permite volver a analizar solo los archivos actualizados. Nx realiza un seguimiento de los archivos modificados desde la última confirmación y le permite probar, compilar y realizar acciones solo en esos archivos; esto permite una optimización adecuada cuando se trabaja con una gran base de código. - gráfico de dependencia
El gráfico de dependencia visual le permite inspeccionar cómo interactúan los componentes entre sí. - Almacenamiento en la nube
Nx también proporciona almacenamiento en la nube e integración con GitHub, para que pueda compartir enlaces con los miembros del equipo para revisar los registros del proyecto. - compartir código
Crear una nueva biblioteca compartida para cada proyecto puede ser bastante agotador. Nx elimina esta complicación, permitiéndole concentrarse en la funcionalidad central de su aplicación. Con Nx, puede compartir bibliotecas y componentes entre aplicaciones. Incluso puede compartir código reutilizable entre sus aplicaciones de front-end y back-end. - Soporte para monorepos
Nx proporciona un espacio de trabajo para múltiples aplicaciones. Con esta configuración, un repositorio de GitHub puede albergar el código fuente de varias aplicaciones en su espacio de trabajo.
Nx para bibliotecas publicables
Nx le permite crear bibliotecas publicables. Esto es esencial cuando tiene bibliotecas que usará fuera del monorepo. En cualquier instancia en la que esté desarrollando componentes de UI organizacionales con la integración de Nx Storybook, Nx creará componentes publicables junto con sus historias. Los componentes publicables pueden compilar estos componentes para crear un paquete de biblioteca que puede implementar en un registro externo. Usaría la opción --publishable
al generar la biblioteca, a diferencia de --buildable
, que se usa para generar bibliotecas que se usan solo en el monorepo. Nx no implementa las bibliotecas publicables automáticamente; puede invocar la compilación a través de un comando como nx build mylib
(donde mylib
es el nombre de la biblioteca), que luego producirá un paquete optimizado en la carpeta dist
/ mylib
que se puede implementar en un registro externo.
Nx le ofrece la opción de crear un nuevo espacio de trabajo con Next.js como valor predeterminado o agregar Next.js a un espacio de trabajo existente.
Para crear un nuevo espacio de trabajo con Next.js como ajuste preestablecido, puede usar el siguiente comando:
npx create-nx-workspace happynrwl \ --preset=next \ --style=styled-components \ --appName=todo
Este comando creará un nuevo espacio de trabajo Nx con una aplicación Next.js llamada "todo" y con styled-components
como biblioteca de estilos.
Luego, podemos agregar la aplicación Next.js a un espacio de trabajo Nx existente con el siguiente comando:
npx nx g @nrwl/next:app
Creación de una aplicación Next.js y Nx
El complemento Nx para Next.js incluye herramientas y ejecutores para ejecutar y optimizar una aplicación Next.js. Para comenzar, necesitamos crear un nuevo espacio de trabajo Nx con el next
como valor preestablecido:
npx create-nx-workspace happynrwl \ --preset=next \ --style=styled-components \ --appName=todo
El bloque de código anterior generará un nuevo espacio de trabajo Nx y la aplicación Next.js. Recibiremos un aviso para usar Nx Cloud. Para este tutorial, seleccionaremos "No" y luego esperaremos a que se instalen nuestras dependencias. Una vez hecho esto, deberíamos tener un árbol de archivos similar a este:
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
En la carpeta de apps
, tendremos nuestra aplicación Next.js "todo", con la prueba E2E preconfigurada para la aplicación de tareas pendientes. Todo esto se genera automáticamente con la poderosa herramienta Nx CLI.
Para ejecutar nuestra aplicación, use el npx nx serve todo
. Una vez que haya terminado de servir la aplicación, debería ver la siguiente pantalla:
Construyendo la API
En este punto, hemos configurado el espacio de trabajo. Lo siguiente es construir la API CRUD que usaremos en la aplicación Next.js. Para hacer esto, usaremos Express; para demostrar la compatibilidad con monorepo, construiremos nuestro servidor como una aplicación en el espacio de trabajo. Primero, tenemos que instalar el complemento Express para Nx ejecutando este comando:
npm install --save-dev @nrwl/express
Una vez hecho esto, estamos listos para configurar nuestra aplicación Express en el espacio de trabajo proporcionado. Para generar una aplicación Express, ejecute el siguiente comando:
npx nx g @nrwl/express:application --name=todo-api --frontendProject=todo
El comando nx g @nrwl/express:application
generará una aplicación Express a la que podemos pasar parámetros de especificación adicionales; para especificar el nombre de la aplicación, use el indicador --name
; para indicar la aplicación front-end que usará la aplicación Express, pase el nombre de una aplicación en nuestro espacio de trabajo a --frontendProject
. Algunas otras opciones están disponibles para una aplicación Express. Cuando termine, tendremos una estructura de archivos actualizada en la carpeta de apps
con la carpeta todo-api
agregada.
happynrwl ┣ apps ┃ ┣ todo ┃ ┣ todo-api ┃ ┣ todo-e2e ┃ ┗ .gitkeep …
La carpeta todo-api
es un modelo Express con un archivo 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 creando nuestras rutas dentro de esta aplicación. Para comenzar, inicializaremos una matriz de objetos con dos pares clave-valor, item
e id
, justo debajo de la declaración de la aplicación.
/** * 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() }, ]; …
A continuación, configuraremos la ruta para obtener todas las listas de tareas pendientes en app.get()
:
… app.get('/api', (req, res) => { res.status(200).json({ data: todoArray, }); }); …
El bloque de código anterior devolverá el valor actual de todoArray
. Posteriormente, tendremos rutas para crear, actualizar y eliminar elementos pendientes de la matriz.
… 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 crear un nuevo elemento pendiente, todo lo que necesitamos es el valor del nuevo elemento como una cadena. Generaremos una ID incrementando la ID del último elemento de la matriz en el servidor. Para actualizar un elemento existente, pasaríamos el nuevo valor del elemento y el ID del objeto del elemento que se actualizará; en el servidor, recorreríamos cada elemento con el método forEach
y actualizaríamos el elemento en el lugar donde la ID coincide con la ID enviada con la solicitud. Finalmente, para eliminar un elemento de la matriz, enviaríamos la identificación del elemento para que se elimine con la solicitud; luego, filtramos a través de la matriz y devolvemos una nueva matriz de todos los elementos que no coinciden con la ID enviada con la solicitud, asignando la nueva matriz a la variable todoArray
.
Nota: Si busca en la carpeta de la aplicación Next.js, debería ver un archivo proxy.conf.json
con la configuración a continuación:
{ "/api": { "target": "http://localhost:3333", "secure": false } }
Esto crea un proxy, lo que permite que todas las llamadas API a rutas que coincidan con /api
se dirijan al servidor todo-api
.
Generación de páginas Next.js con Nx
En nuestra aplicación Next.js, generaremos una nueva página, home
y un componente de elemento. Nx proporciona una herramienta CLI para que podamos crear fácilmente una página:
npx nx g @nrwl/next:page home
Al ejecutar este comando, obtendremos un mensaje para seleccionar la biblioteca de estilo que queremos usar para la página; para este artículo, seleccionaremos styled-components
. ¡Voila! Nuestra página está creada. Para crear un componente, ejecute npx nx g @nrwl/next:component todo-item
; esto creará una carpeta de component
con el componente todo-item
.
Consumo de API en la aplicación Next.js
En cada tarea pendiente, tendremos dos botones, para editar y eliminar la tarea pendiente. Las funciones asincrónicas que realizan estas acciones se pasan como accesorios desde la página de inicio.
… 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 la funcionalidad de actualización, tenemos una entrada que está deshabilitada cuando el estado isEditingItem
es false
. Una vez que se hace clic en el botón "Editar", cambia el estado isEditingItem
a true
y muestra el botón "Actualizar". Aquí, el componente de entrada está habilitado y el usuario puede ingresar un nuevo valor; cuando se hace clic en el botón "Actualizar", llama a la función updateItem
con los parámetros pasados y cambia isEditingItem
a false
.
En el componente de la página home
, tenemos las funciones asíncronas que realizan la operación 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(); }, []); …
En el bloque de código anterior, tenemos fetchItems
, que devuelve todoArray
desde el servidor. Luego, tenemos la función createItem
, que toma una cadena; el parámetro es el valor del nuevo elemento pendiente. La función updateItem
toma dos parámetros, el ID del elemento que se actualizará y el valor del updatedItem
actualizado. Y la función deleteItem
elimina el elemento que coincide con la ID que se pasa.
Para representar el elemento pendiente, mapeamos a través del estado de los 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> ); …
Nuestro servidor y front-end ahora están configurados. Podemos servir la aplicación API ejecutando npx nx serve todo-api
, y para la aplicación Next.js, ejecutamos npx nx serve todo
. Haga clic en el botón "Continuar" y verá una página con el elemento de tarea predeterminado que se muestra.
Ahora tenemos una aplicación Next.js y Express funcionando juntas en un espacio de trabajo.
Nx tiene otra herramienta CLI que nos permite ver el gráfico de dependencia de nuestra aplicación en nuestra ejecución de terminal. Ejecute npx nx dep-graph
y deberíamos ver una pantalla similar a la imagen a continuación, que muestra el gráfico de dependencia de nuestra aplicación.
Otros comandos CLI para Nx
-
nx list
Enumera los complementos de Nx actualmente instalados. -
nx migrate latest
Actualiza los paquetes enpackage.json
a la última versión. -
nx affected
Realiza la acción solo en las aplicaciones afectadas o modificadas. -
nx run-many --target serve --projects todo-api,todo
Ejecuta el comando de destino en todos los proyectos enumerados.
Conclusión
Como descripción general de Nx, este artículo cubre lo que ofrece Nx y cómo nos facilita el trabajo. También explicamos cómo configurar una aplicación Next.js en un espacio de trabajo Nx, agregar un complemento Express a un espacio de trabajo existente y usar la función monorepo para albergar más de una aplicación en nuestro espacio de trabajo.
Encontrará el código fuente completo en el repositorio de GitHub. Para obtener información adicional sobre Nx, consulte la documentación o la documentación de Nx para Next.js.