Node
Tutorial Node: Fundamentos del entorno Node.js
Descubre cómo el event loop de Node.js revoluciona la programación asincrónica, gestionando múltiples tareas sin bloquear ejecución.
Aprende Node GRATIS y certifícateCómo funciona el Event Loop en Node.js y su importancia
El event loop es el mecanismo fundamental que permite a Node.js manejar operaciones asíncronas. A diferencia de los entornos tradicionales, donde cada petición podría bloquear un hilo, Node.js utiliza un único hilo para gestionar múltiples tareas sin bloquear la ejecución.
En Node.js, cuando se realiza una operación de entrada/salida (E/S), el sistema delega esa tarea al sistema operativo, permitiendo que el hilo principal continúe ejecutando otras operaciones. Una vez que la operación E/S finaliza, el event loop se encarga de ejecutar la función de devolución de llamada (callback) asociada.
El funcionamiento interno del event loop se divide en varias fases, las cuales se repiten continuamente:
- Timers: Ejecución de callbacks programados con
setTimeout
ysetInterval
. - Pending Callbacks: Ejecución de callbacks de algunas operaciones del sistema.
- Idle, Prepare: Operaciones internas para Node.js.
- Poll: Recuperación de nuevas E/S; aquí es donde el event loop pasa la mayor parte del tiempo.
- Check: Ejecución de callbacks de
setImmediate
. - Close Callbacks: Manejo de eventos de cierre, como
socket.on('close', ...)
.
Un ejemplo ilustrativo es el siguiente código:
console.log('Inicio');
setTimeout(() => {
console.log('Timeout 0 ms');
}, 0);
setImmediate(() => {
console.log('Immediate');
});
console.log('Fin');
El resultado de este script será:
Inicio
Fin
Immediate
Timeout 0 ms
Aunque pueda parecer contraintuitivo, el callback de setImmediate
se ejecuta antes que el de setTimeout
con 0 milisegundos. Esto se debe a que setImmediate
se encola en la fase check del event loop, mientras que setTimeout
se encola en la fase timers, y el temporizador inicia después de completar la fase actual.
Es crucial comprender que las funciones asíncronas no bloquean el hilo de ejecución. Esto permite que Node.js maneje múltiples conexiones simultáneas con alto rendimiento. Sin embargo, si se ejecuta una operación sincrónica pesada, como una función de cálculo intensivo, se bloqueará el event loop y afectará negativamente a la escalabilidad de la aplicación.
Para mitigar este problema, Node.js introdujo los Worker Threads, que permiten ejecutar operaciones de CPU intensivas en hilos separados:
const { Worker, isMainThread, parentPort } = require('worker_threads');
if (isMainThread) {
// Este bloque se ejecuta en el hilo principal
const worker = new Worker(__filename);
worker.on('message', (resultado) => {
console.log(`Resultado: ${resultado}`);
});
worker.on('error', (error) => {
console.error(`Error del worker: ${error}`);
});
worker.on('exit', (code) => {
if (code !== 0) {
console.error(`Worker finalizó con código de salida ${code}`);
}
});
} else {
// Este bloque se ejecuta en el worker
const calcularPrimos = (limite) => {
const primos = [];
for (let i = 2; i <= limite; i++) {
let esPrimo = true;
for (let j = 2; j < i; j++) {
if (i % j === 0) {
esPrimo = false;
break;
}
}
if (esPrimo) {
primos.push(i);
}
}
return primos;
};
const resultado = calcularPrimos(100000);
parentPort.postMessage(resultado);
}
Con esta estructura, las operaciones pesadas no bloquean el event loop principal, manteniendo la aplicación responsiva.
Manejo de require, import y export en Node.js
En Node.js, los módulos son fundamentales para organizar y reutilizar el código. Un módulo es simplemente un archivo que contiene código de JavaScript que puede ser exportado y luego importado en otros archivos, permitiendo una estructura más mantenible y modular en las aplicaciones.
Existen dos tipos principales de módulos en Node.js: los módulos internos y los módulos externos. Los módulos internos, también conocidos como módulos nativos o incorporados, son parte del núcleo de Node.js y proporcionan funcionalidades esenciales sin necesidad de instalación adicional. Ejemplos de estos son fs
para operaciones de archivos, http
para crear servidores web y path
para manipular rutas de archivos.
Por otro lado, los módulos externos son paquetes desarrollados por la comunidad que se pueden instalar mediante herramientas como npm
o yarn
. Estos módulos amplían las capacidades de Node.js y permiten integrar funcionalidades adicionales. Para utilizarlos, es necesario instalarlos previamente en el proyecto utilizando comandos como npm install
.
Históricamente, Node.js ha utilizado el sistema de módulos CommonJS, que hace uso de require
para importar módulos y module.exports
o exports
para exportarlos. Un ejemplo básico de uso de require
con un módulo interno sería:
const fs = require('fs');
fs.readFile('/ruta/al/archivo.txt', 'utf8', (err, datos) => {
if (err) throw err;
console.log(datos);
});
Para exportar funcionalidades en CommonJS, se utiliza module.exports
. Supongamos que tenemos un archivo matematicas.js
:
function suma(a, b) {
return a + b;
}
module.exports = { suma };
Luego, en otro archivo, podemos importar y utilizar esta función:
const { suma } = require('./matematicas');
console.log(suma(2, 3)); // Salida: 5
Con la evolución de JavaScript, se introdujeron los módulos ES6 (ES Modules), que utilizan las palabras clave import
y export
. Node.js, a partir de versiones recientes, soporta de forma nativa los módulos ES6, permitiendo escribir código más alineado con los estándares modernos.
Para utilizar exportaciones en ES Modules, se puede hacer de la siguiente manera:
// matematicas.mjs
export function suma(a, b) {
return a + b;
}
Y para importar:
// app.mjs
import { suma } from './matematicas.mjs';
console.log(suma(2, 3)); // Salida: 5
Es importante destacar que, por defecto, Node.js trata los archivos con extensión .js
como módulos CommonJS. Para indicar que un proyecto utiliza ES Modules, se debe añadir el campo "type": "module"
en el archivo package.json
. Alternativamente, se pueden utilizar extensiones .mjs
para archivos de módulos ES6.
La diferencia entre require
y import
radica en el sistema de módulos que utilizan. require
es propio de CommonJS, mientras que import
pertenece a los módulos ES6. Aunque cumplen funciones similares, tienen diferencias en su comportamiento. Por ejemplo, import
es estático y se procesa en tiempo de compilación, mientras que require
es dinámico y se ejecuta en tiempo de ejecución.
Al decidir entre utilizar CommonJS o ES Modules, es recomendable optar por ES Modules si se está desarrollando un nuevo proyecto, ya que es el estándar actual y facilita la interoperabilidad con herramientas modernas. Sin embargo, si se trabaja en proyectos existentes que utilizan CommonJS o se requiere compatibilidad con módulos que aún no soportan ES Modules, es aceptable continuar con require
y module.exports
.
Al importar módulos externos instalados mediante npm
, el proceso es similar. Por ejemplo, si instalamos el paquete axios
para realizar peticiones HTTP, podemos importarlo de la siguiente manera:
Usando CommonJS:
const axios = require('axios');
axios.get('https://api.ejemplo.com/datos')
.then(response => {
console.log(response.data);
})
.catch(error => {
console.error(error);
});
Usando ES Modules:
import axios from 'axios';
try {
const response = await axios.get('https://api.ejemplo.com/datos');
console.log(response.data);
} catch (error) {
console.error(error);
}
Es posible utilizar import()
como una función dinámica para cargar módulos de forma asíncrona. Esto es útil cuando se desea cargar módulos condicionalmente o reducir el tiempo de carga inicial. Por ejemplo:
if (condicion) {
const modulo = await import('./moduloEspecial.mjs');
modulo.funcionEspecial();
}
Al exportar módulos, también se pueden utilizar exportaciones por defecto (default exports) y exportaciones nombradas. Las exportaciones por defecto permiten exportar un único valor predeterminado, mientras que las exportaciones nombradas permiten exportar múltiples valores. Ejemplo de exportación por defecto:
// utilidades.mjs
export default function saludar(nombre) {
console.log(`Hola, ${nombre}`);
}
E importación:
// app.mjs
import saludar from './utilidades.mjs';
saludar('Carlos'); // Salida: Hola, Carlos
Al combinar exportaciones nombradas y por defecto, se puede ofrecer una API más flexible en los módulos. Es importante mantener consistencia en el estilo y ser consciente de cómo se estructuran las exportaciones para evitar confusiones al importar.
Es importante considerar que algunos módulos externos pueden no ser compatibles con ES Modules. En esos casos, se pueden utilizar herramientas como Babel para transpilar el código o emplear soluciones híbridas que permitan interoperabilidad entre CommonJS y ES Modules.
Diferencias entre global y window en entornos de ejecución
En el mundo de JavaScript, el contexto global es un concepto fundamental que varía dependiendo del entorno de ejecución. En los navegadores web, el objeto global se conoce como window
, mientras que en Node.js es global
. Comprender las diferencias entre global y window es crucial para desarrollar aplicaciones que funcionen correctamente en el servidor y en el navegador.
En el navegador, el objeto window
representa la ventana del navegador y actúa como el objeto global del entorno. Todas las variables y funciones declaradas en el ámbito global se convierten en propiedades de window
. Por ejemplo:
// En el navegador
var nombre = 'Ana';
function saludar() {
console.log('Hola');
}
console.log(window.nombre); // 'Ana'
window.saludar(); // 'Hola'
En este caso, tanto nombre
como saludar
se añaden al objeto window
. Esto permite acceder a ellas desde cualquier parte del código que tenga acceso al contexto global del navegador.
Por otro lado, en Node.js, el objeto global se denomina global
. Sin embargo, a diferencia del navegador, las variables y funciones definidas en el ámbito global de un módulo no se agregan automáticamente al objeto global
. Esto se debe a que Node.js utiliza el sistema de módulos CommonJS, donde cada archivo es un módulo independiente con su propio ámbito. Por ejemplo:
// En Node.js
var nombre = 'Carlos';
function saludar() {
console.log('Hola');
}
console.log(global.nombre); // undefined
global.saludar(); // TypeError: global.saludar is not a function
En este ejemplo, nombre
y saludar
no están disponibles en el objeto global
. Para hacer que una variable o función esté disponible globalmente en Node.js, es necesario asignarla explícitamente al objeto global
:
global.nombre = 'Lucía';
global.saludar = function() {
console.log('Hola');
};
console.log(global.nombre); // 'Lucía'
global.saludar(); // 'Hola'
Es importante destacar que agregar propiedades al objeto global
en Node.js no es una práctica recomendada, ya que puede generar conflictos y dificultar el mantenimiento del código. En su lugar, es preferible utilizar módulos y exportaciones para compartir código entre diferentes partes de la aplicación.
Además, en el contexto de Node.js, existe el objeto process
, que proporciona información y control sobre el proceso en ejecución. Por ejemplo, process.env
permite acceder a las variables de entorno, y process.argv
contiene los argumentos pasados al script:
console.log(process.env.PATH); // Muestra la variable de entorno PATH
console.log(process.argv); // Muestra los argumentos del script
Otra diferencia clave es que en el navegador, ciertas APIs como el DOM y funciones como alert()
o confirm()
están disponibles a través del objeto window
, mientras que en Node.js, dichas APIs no existen. Node.js proporciona sus propias APIs para interactuar con el sistema operativo, como el módulo fs
para manipular el sistema de archivos:
const fs = require('fs');
fs.readFile('/ruta/al/archivo.txt', 'utf8', (err, datos) => {
if (err) throw err;
console.log(datos);
});
Además, en los navegadores modernos, el objeto window
también actúa como el objeto global para las APIs de fetch y WebSockets, permitiendo realizar peticiones HTTP y comunicación en tiempo real. En Node.js, aunque inicialmente estas APIs no estaban disponibles, a partir de versiones recientes, se ha incorporado de forma nativa el soporte para fetch
:
// En Node.js v23.3.0
const respuesta = await fetch('https://api.ejemplo.com/datos');
const datos = await respuesta.json();
console.log(datos);
Es crucial tener en cuenta que el uso de var
para declarar variables globales en el navegador y en Node.js tiene comportamientos distintos. En el navegador, como se mencionó, las variables globales declaradas con var
se añaden al objeto window
. Sin embargo, en Node.js, aunque se utilice var
, las variables se mantienen dentro del ámbito del módulo. Para evitar problemas de ámbito y promocionar buenas prácticas, es recomendable utilizar const
y let
para declarar variables, limitando su alcance al bloque donde se definen.
Además, Node.js introduce el objeto globalThis
que es un estándar de JavaScript para referirse al objeto global en cualquier entorno. Tanto en el navegador como en Node.js, globalThis
apunta al objeto global correspondiente:
// Funciona en ambos entornos
globalThis.miVariableGlobal = 'Valor global';
console.log(globalThis.miVariableGlobal); // 'Valor global'
El uso de globalThis
proporciona una forma unificada de acceder al objeto global sin preocuparse por el entorno de ejecución, lo que mejora la portabilidad del código entre el servidor y el navegador.
Sin embargo, es importante ser cauteloso al utilizar el objeto global, ya que manipular el espacio de nombres global puede llevar a conflictos y errores difíciles de depurar. En entornos complejos, es preferible encapsular el código y utilizar patrones como módulos o clases para mantener un código limpio y modular.
Asincronía 101: callbacks, promesas y async/await en Node.js
La asincronía es fundamental en Node.js, permitiendo manejar operaciones de E/S sin bloquear el event loop. Para gestionar estas operaciones, Node.js ofrece diversas técnicas: callbacks, promesas y async/await, cada una aportando diferentes ventajas en la escritura y legibilidad del código.
Un callback es una función pasada como argumento a otra función, que se ejecuta después de que una operación asíncrona ha completado. Este patrón ha sido ampliamente utilizado desde los inicios de Node.js. Por ejemplo:
const fs = require('fs');
fs.readFile('archivo.txt', 'utf8', (err, datos) => {
if (err) {
console.error('Error al leer el archivo:', err);
return;
}
console.log('Contenido del archivo:', datos);
});
En este ejemplo, la función de callback maneja tanto el error como el resultado. Sin embargo, el uso intensivo de callbacks puede conducir al llamado "infierno de callbacks" o callback hell, donde la anidación excesiva dificulta la lectura y mantenimiento del código.
Para abordar este problema, se introdujeron las promesas, una abstracción que permite manejar operaciones asíncronas de manera más estructurada. Una promesa representa un resultado eventual de una operación asíncrona y puede estar en uno de tres estados: pendiente, cumplida o rechazada. El código con promesas mejora la legibilidad:
const fs = require('fs').promises;
fs.readFile('archivo.txt', 'utf8')
.then((datos) => {
console.log('Contenido del archivo:', datos);
})
.catch((err) => {
console.error('Error al leer el archivo:', err);
});
Al utilizar promesas, se evita la anidación profunda de callbacks y se facilita el manejo de errores. Además, es posible encadenar múltiples promesas para ejecutar operaciones secuencialmente o en paralelo.
La llegada de async/await simplificó aún más el código asíncrono, permitiendo escribirlo de manera similar al código sincrónico. Para utilizar await
, la función que lo contiene debe declararse con async
. Por ejemplo:
const fs = require('fs').promises;
async function leerArchivo() {
try {
const datos = await fs.readFile('archivo.txt', 'utf8');
console.log('Contenido del archivo:', datos);
} catch (err) {
console.error('Error al leer el archivo:', err);
}
}
leerArchivo();
Con async/await, se puede manejar la asincronía sin necesidad de encadenar métodos .then()
y .catch()
, utilizando en su lugar bloques try...catch
para el manejo de errores.
Para ejecutar operaciones asíncronas en paralelo, se puede emplear Promise.all
junto con await
:
async function procesarArchivos() {
try {
const [datos1, datos2] = await Promise.all([
fs.readFile('archivo1.txt', 'utf8'),
fs.readFile('archivo2.txt', 'utf8')
]);
console.log('Archivo 1:', datos1);
console.log('Archivo 2:', datos2);
} catch (err) {
console.error('Error al leer los archivos:', err);
}
}
procesarArchivos();
En este caso, ambas operaciones se inician simultáneamente, mejorando la eficiencia.
Es importante manejar correctamente los errores en operaciones asíncronas. Con async/await, los errores se capturan mediante try...catch
, lo que permite un manejo más claro y centralizado. Los errores no capturados pueden provocar comportamientos inesperados o la terminación abrupta del proceso de Node.js.
Además de utilizar funciones que ya devuelven promesas, es posible crear promesas personalizadas:
function esperar(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function ejecutar() {
console.log('Esperando 2 segundos...');
await esperar(2000);
console.log('Listo');
}
ejecutar();
En este ejemplo, esperar
retorna una promesa que se resuelve después de un tiempo determinado, simulando una operación asíncrona.
Todas las lecciones de Node
Accede a todas las lecciones de Node y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Node.js
Introducción Y Entorno
Fundamentos Del Entorno Node.js
Introducción Y Entorno
Módulo Http Y Https
Http Y Api Rest
Http Params, Headers Y Body
Http Y Api Rest
Validación De Datos
Http Y Api Rest
Conexión A Bases De Datos Sin Orm
Persistencia
Creación De Consultas Básicas (Crud) Sin Orm
Persistencia
Módulo Fs
Sistema De Archivos
Introducción A La Seguridad
Seguridad
Sesiones Y Cookies
Seguridad
Roles Y Permisos
Seguridad
Testing En Node.js
Testing
Estructura De Carpetas
Arquitectura
Configuración Y Variables De Entorno
Arquitectura
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender el funcionamiento del event loop en Node.js.
- Distinguir las diferentes fases del event loop y su impacto.
- Analizar el rol de callbacks en la ejecución de código asíncrono.
- Comparar operaciones asíncronas y sincrónicas en Node.js.
- Explorar el uso de Worker Threads para operaciones de CPU intensivas.