Conceptos básicos de Node.js
Node.js es un entorno de ejecución de JavaScript construido sobre el motor V8 de Chrome JavaScript. En términos simples, Node.js permite que JavaScript se ejecute en el lado del servidor, no solo limitado al navegador.
Características principales de Node.js
1. Bucle de eventos de un solo hilo
- Node.js utiliza un modelo de bucle de eventos de un solo hilo
- Maneja solicitudes concurrentes mediante controladores de eventos y funciones de devolución de llamada (callbacks)
- Evita la sobrecarga del cambio de contexto de hilos en la programación tradicional de múltiples hilos
2. E/S sin bloqueo
- Todas las operaciones de E/S (lectura/escritura de archivos, solicitudes de red, etc.) son asíncronas
- Cuando se espera a que se complete una operación de E/S, el programa no se bloquea y puede continuar procesando otras tareas
- Mejora enormemente el rendimiento (throughput) de la aplicación
3. Multiplataforma
- Admite múltiples sistemas operativos: Windows, macOS, Linux, etc.
- Escríbelo una vez, ejecútalo en cualquier lugar
4. Ecosistema rico
- npm (Node Package Manager) cuenta con millones de paquetes de código abierto
- Comunidad activa de desarrolladores
Diferencias con las tecnologías tradicionales de servidor
1. Tecnologías tradicionales de servidor (como Apache + PHP):
请求1 → 创建线程1 → 处理请求 → 返回响应 → 销毁线程1 请求2 → 创建线程2 → 处理请求 → 返回响应 → 销毁线程2 请求3 → 创建线程3 → 处理请求 → 返回响应 → 销毁线程3

2. La forma de procesamiento de Node.js:
请求1 → 事件循环 → 处理请求 → 返回响应 请求2 → 事件循环 → 处理请求 → 返回响应 (复用同一线程) 请求3 → 事件循环 → 处理请求 → 返回响应

| Característica | Modelo tradicional de múltiples hilos | Modelo de un solo hilo de Node.js |
|---|---|---|
| Uso de memoria | Cada hilo ocupa alrededor de 2 MB | Un solo hilo, menor uso de memoria |
| Manejo de concurrencia | El número de hilos limita la concurrencia | El bucle de eventos maneja alta concurrencia |
| Cambio de contexto | Sobrecarga por cambios frecuentes de hilo | Sin sobrecarga por cambio de hilo |
| Complejidad de programación | Requiere manejar sincronización de hilos | Evita problemas de bloqueos y sincronización de hilos |
| Escenarios de aplicación | Tareas intensivas en CPU | Tareas intensivas en E/S |
Casos de uso de Node.js
1. Casos de uso adecuados
Aplicaciones web
- Servicios de API RESTful
- Backend para aplicaciones de una sola página (SPA)
- Aplicaciones web en tiempo real
Aplicaciones en tiempo real
- Aplicaciones de chat
- Juegos en línea
- Herramientas de colaboración (como edición de documentos en línea)
Arquitectura de microservicios
- Microservicios ligeros
- Puerta de enlace de API (API Gateway)
- Comunicación entre servicios
Herramientas y aplicaciones de línea de comandos
- Herramientas de construcción (como Webpack, Gulp)
- Herramientas de andamiaje (scaffolding)
- Scripts de automatización
Aplicaciones de Internet de las cosas (IoT)
- Recopilación de datos de dispositivos
- Procesamiento de datos de sensores
2. Casos de uso no adecuados
Tareas intensivas en CPU
- Procesamiento de imágenes/video
- Cálculos matemáticos complejos
- Análisis de big data
Aplicaciones que requieren muchos cálculos
- Entrenamiento de aprendizaje automático
- Cálculo científico
- Minería de criptomonedas
Modelo de E/S sin bloqueo y controlado por eventos
Ejemplo del modelo tradicional de E/S bloqueante:
Ejemplo
// Cada readFileSync "congela" el programa, esperando a que la lectura del archivo termine para continuar con la ejecución
const data1 = readFileSync('file1.txt'); // El programa se detiene aquí, esperando que la lectura termine
const data2 = readFileSync('file2.txt'); // Después de leer data1, se detiene y espera nuevamente
const data3 = readFileSync('file3.txt'); // Después de leer data2, se detiene y espera nuevamente
console.log('Todos los archivos han sido leídos'); // Solo después de que los tres archivos se hayan leído por completo se ejecuta esta línea
// Tiempo total = tiempo de lectura de file1 + tiempo de lectura de file2 + tiempo de lectura de file3
Ejemplo del modelo de E/S sin bloqueo de Node.js:
Ejemplo
const fs = require('fs');
// Las tres operaciones de lectura de archivos se lanzan casi al mismo tiempo, el programa no espera a que ninguna termine
fs.readFile('file1.txt', (err, data1) => {
// Esta función de devolución de llamada se invocará automáticamente cuando file1.txt se haya leído por completo
console.log('Lectura del archivo 1 completada');
});
fs.readFile('file2.txt', (err, data2) => {
// Esta función de devolución de llamada se invocará automáticamente cuando file2.txt se haya leído por completo
console.log('Lectura del archivo 2 completada');
});
fs.readFile('file3.txt', (err, data3) => {
// Esta función de devolución de llamada se invocará automáticamente cuando file3.txt se haya leído por completo
console.log('Lectura del archivo 3 completada');
});
// Nota: ¡esta línea de código se ejecutará primero! Porque las tres operaciones de lectura de archivos anteriores son asíncronas,
// Después de realizar la solicitud de lectura, el programa continúa inmediatamente sin esperar a que la lectura del archivo termine
console.log('El programa continúa ejecutándose sin esperar la lectura de archivos');
// Ejemplo del orden real de salida:
// El programa continúa ejecutándose sin esperar la lectura de archivos ← Se imprime primero
// Lectura del archivo 2 completada ← Se imprime el archivo que termine primero, el orden no es fijo
// Lectura del archivo 1 completada
// Lectura del archivo 3 completada
// Tiempo total ≈ tiempo del archivo más lento de los tres (ejecución concurrente, no suma secuencial)
Mecanismo del bucle de eventos:
El bucle de eventos es el mecanismo central de Node.js para lograr E/S sin bloqueo. Se ejecuta continuamente, revisando constantemente si hay tareas pendientes, logrando así el efecto de "un solo hilo que maneja concurrencia":
- Pila de llamadas (Call Stack): ejecuta código síncrono; las llamadas a funciones entran a la pila y salen cuando terminan
- Cola de eventos (Event Queue): almacena las funciones de devolución de llamada de operaciones asíncronas completadas, y las ejecuta cuando la pila de llamadas esté libre
- Bucle de eventos (Event Loop): supervisa continuamente la pila de llamadas y la cola de eventos; cuando la pila está vacía, toma las devoluciones de llamada de la cola y las coloca en la pila para ejecutarlas
El bucle de eventos procesa las siguientes etapas en un orden fijo:
┌───────────────────────────┐
┌─>│ timers │ ← 执行 setTimeout、setInterval 的到期回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │ ← 执行上一轮循环中延迟的 I/O 回调(如某些系统错误回调)
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │ ← Node.js 内部使用,开发者一般不需要关心
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ poll │ ← 核心阶段:获取新的 I/O 事件并执行其回调(如文件读取、网络请求完成)
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ check │ ← 执行 setImmediate 的回调(在 poll 阶段之后立即执行)
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │ ← 执行关闭事件的回调,如 socket.on('close', ...)
└───────────────────────────┘
Para los principiantes, lo más importante a recordar es:El código síncrono siempre se ejecuta primero; las devoluciones de llamada asíncronas (como el manejo de la lectura de archivos completada) se ejecutan solo después de que la pila de llamadas se haya vaciado.
Entorno de ejecución de JavaScript
Introducción al motor V8
V8 es un motor de JavaScript de alto rendimiento desarrollado por Google y es un componente central del navegador Chrome.
Node.js utiliza el motor V8 para ejecutar código JavaScript.
Características del motor V8:
Compilación justo a tiempo (JIT)
- Compila el código JavaScript directamente a código máquina
- Sin necesidad de bytecode intermedio, mayor eficiencia de ejecución
Recolección de basura
- Gestión automática de memoria; los desarrolladores no necesitan liberar memoria manualmente
- Utiliza un algoritmo de recolección de basura generacional, dividiendo la memoria en generación joven y generación vieja, aumentando la eficiencia de la recolección
Técnicas de optimización
- Caché en línea (Inline Caching): almacena en caché los resultados de búsqueda de propiedades de objetos, evitando búsquedas repetidas.
- Clases ocultas (Hidden Classes): estatizan la estructura de objetos de los lenguajes dinámicos, acelerando el acceso a propiedades
- Optimización dinámica: analiza código caliente en tiempo de ejecución y realiza optimizaciones específicas
JavaScript en el navegador vs. JavaScript en Node.js
Aunque ambos usan el lenguaje JavaScript, las diferencias en el entorno de ejecución conllevan algunas diferencias importantes:
Puntos en común:
- Ambos usan la misma sintaxis de JavaScript
- Ambos admiten características de ES6+
- Ambos usan el motor V8 (navegador Chrome)
Diferencias:
| Característica | JavaScript en el navegador | Node.js JavaScript |
|---|---|---|
| Objeto global | window |
global |
| Sistema de módulos | ES6 modules, AMD | CommonJS, ES6 modules |
| Acceso al sistema de archivos | No accesible (por restricciones de seguridad) | Acceso completo |
| Peticiones de red | XMLHttpRequest, Fetch | Módulos http, https |
| Manipulación del DOM | Soportado (manipular elementos de la página) | No soportado (no hay páginas en el servidor) |
| Control de procesos | No soportado | Soportado (puede leer variables de entorno, salir del proceso, etc.) |
Ejemplo en el entorno del navegador:
Ejemplo
console.log(window); // Objeto global del navegador, incluye todas las API del navegador
document.getElementById('app'); // Obtiene el elemento de página por ID (no disponible en Node.js)
localStorage.setItem('key', 'value'); // Almacenamiento local del navegador (no disponible en Node.js)
Ejemplo en el entorno de Node.js:
Ejemplo
console.log(global); // Objeto global de Node.js (corresponde a window en el navegador)
const fs = require('fs'); // Importa el módulo integrado del sistema de archivos (no disponible en el navegador)
fs.readFile('data.txt', 'utf8', callback); // Lee archivos locales del servidor (no disponible en el navegador)
Diferencias en los objetos globales
Objeto global en el navegador:
Ejemplo
console.log(this === window); // true, el this de nivel superior es el objeto window
var globalVar = 'hello';
console.log(window.globalVar); // 'hello', las variables declaradas con var se convierten en propiedades de window
Objeto global en Node.js:
Ejemplo
// Nota: en Node.js cada archivo es un módulo independiente, el this dentro del módulo no es igual a global
console.log(this === global); // false (en el ámbito del módulo, this apunta a module.exports)
var globalVar = 'hello';
console.log(global.globalVar); // undefined, las variables dentro del módulo no se montan automáticamente en global
// Ámbito del módulo en Node.js
console.log(this); // {} objeto vacío, es decir, el valor inicial de module.exports
console.log(module.exports === this); // true, this dentro del módulo apunta a module.exports
Variables globales específicas de Node.js:
Ejemplo
console.log(__filename); // Ruta absoluta del archivo del módulo actual, por ejemplo: /home/user/project/app.js
console.log(process); // Objeto de proceso, puede leer argumentos de línea de comandos (process.argv), variables de entorno (process.env), etc.
console.log(Buffer); // Constructor para manejar datos binarios, comúnmente usado en lectura/escritura de archivos y comunicación de red
Ventajas y limitaciones de Node.js
Explicación detallada de las ventajas
1. Alta capacidad de manejo de concurrencia
Un servidor tradicional de múltiples hilos necesitaría alrededor de 20 GB de memoria para manejar 10,000 conexiones concurrentes (aproximadamente 2 MB por hilo), mientras que Node.js solo necesita muy poca memoria para manejar el mismo número de conexiones.
Ejemplo
const http = require('http');
const server = http.createServer((req, res) => {
// Simula una operación asíncrona (como consultar una base de datos), devuelve la respuesta después de 100ms
// Durante estos 100ms, el bucle de eventos puede seguir procesando otras solicitudes sin bloquearse
setTimeout(() => {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
}, 100);
});
server.listen(3000, () => {
console.log('Servidor ejecutándose en http://localhost:3000/');
});
// Gracias a la E/S no bloqueante y al mecanismo de bucle de eventos, este servidor puede manejar miles de solicitudes a la vez sin bloqueo
2. Desarrollo rápido
La pila de lenguaje JavaScript unificada (tanto el frontend como el backend usan JavaScript) hace que el desarrollo frontend y backend sea más eficiente. El desarrollador solo necesita dominar un lenguaje para escribir código tanto de frontend como de backend, y puede compartir lógica de código entre ambos lados:
Ejemplo
function validateEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
// Uso en frontend (se ejecuta en el navegador): al enviar el formulario, se valida primero en el frontend para evitar que solicitudes inválidas lleguen al servidor
if (validateEmail(userInput)) {
// Validación correcta, enviar al servidor
}
// Uso en backend (se ejecuta en Node.js): el servidor valida nuevamente para evitar que se solicite la API directamente sin pasar por el frontend
if (validateEmail(req.body.email)) {
// Validación correcta, guardar en la base de datos
}
3. Ventajas de la pila de lenguaje unificada
- Reutilización de código: el frontend y el backend pueden compartir funciones de utilidad, lógica de validación de datos, definiciones de constantes, etc.
- Eficiencia del equipo: los desarrolladores pueden encargarse simultáneamente del desarrollo frontend y backend, reduciendo los costos de comunicación.
- Simplificación de la pila tecnológica: solo es necesario dominar JavaScript, reduciendo el costo de aprendizaje y la complejidad técnica.
4. Ecosistema npm rico
# npm 提供了数百万个开源包,几乎任何功能都能找到现成的库 npm search express # 搜索名为 express 的包 npm install express # 安装 express 包到当前项目 npm update # 更新当前项目中所有包到最新兼容版本
Análisis de las limitaciones
1. Problemas de rendimiento con tareas de uso intensivo de CPU
Ejemplo
// fibonacciSync es una tarea puramente computacional, ocupa la CPU continuamente, durante ese tiempo el bucle de eventos no puede procesar otras solicitudes
function fibonacciSync(n) {
if (n < 2) return n;
return fibonacciSync(n - 1) + fibonacciSync(n - 2);
}
// Esto bloquea toda la aplicación durante varios segundos, y durante ese tiempo ninguna solicitud puede responder
console.log(fibonacciSync(40));
// Solución: usar Worker Threads para ejecutar tareas de uso intensivo de CPU en hilos independientes,
// así el bucle de eventos del hilo principal no se bloqueará
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
if (isMainThread) {
// Hilo principal: crea un Worker y le pasa el argumento { n: 40 }
// El Worker ejecutará el archivo actual en un hilo independiente (__filename se refiere a la ruta del archivo actual)
const worker = new Worker(__filename, {
workerData: { n: 40 }
});
// Escucha los mensajes enviados por el Worker (resultado del cálculo)
worker.on('message', (result) => {
console.log('Resultado:', result); // Cuando el Worker termina el cálculo, el hilo principal recibe el resultado aquí
});
} else {
// Hilo Worker: obtiene los parámetros pasados por el hilo principal a través de workerData, realiza el cálculo y envía el resultado
const result = fibonacciSync(workerData.n);
parentPort.postMessage(result); // Envía el resultado de vuelta al hilo principal
}
2. Problema del infierno de callbacks
Ejemplo
fs.readFile('file1.txt', (err, data1) => {
if (err) throw err;
fs.readFile('file2.txt', (err, data2) => { // Primer nivel de anidamiento
if (err) throw err;
fs.readFile('file3.txt', (err, data3) => { // Segundo nivel de anidamiento
if (err) throw err;
// En proyectos reales, este anidamiento puede tener más de diez niveles, afectando seriamente el mantenimiento del código
console.log('Todos los archivos se leyeron correctamente');
});
});
});
// Solución moderna: usar Promise + async/await para que el código asíncrono se escriba de forma tan intuitiva como el código síncrono
const fsPromises = require('fs').promises;
async function readFiles() {
try {
// await espera a que cada archivo se lea por completo, pero la función completa no bloquea el bucle de eventos
// Nota: aquí se lee secuencialmente; si se necesita leer varios archivos de forma concurrente, se puede usar Promise.all()
const data1 = await fsPromises.readFile('file1.txt');
const data2 = await fsPromises.readFile('file2.txt');
const data3 = await fsPromises.readFile('file3.txt');
console.log('Todos los archivos se leyeron correctamente');
} catch (err) {
// try/catch puede manejar todos los errores asíncronos de forma unificada, sin necesidad de comprobarlos por separado en cada callback
console.error('Error al leer el archivo:', err);
}
}
3. Fragilidad del subproceso único
Ejemplo
// A diferencia de la programación multihilo, en un solo hilo un error no manejado detiene todo el servicio
setTimeout(() => {
throw new Error('Excepción no capturada'); // Esto provoca el colapso de toda la aplicación y todas las solicitudes quedan sin respuesta
}, 1000);
// Solución: registrar un manejador global de excepciones, registrar el log antes de que la aplicación colapse y salir de forma elegante
process.on('uncaughtException', (err) => {
console.error('Excepción no capturada:', err);
// Aquí se debe registrar el log del error antes de salir, para evitar que un colapso silencioso haga difícil la depuración
process.exit(1); // El código de salida 1 indica salida anormal
});
process.on('unhandledRejection', (reason, promise) => {
// Se activa cuando una Promise es rechazada pero no hay un .catch() que la maneje
console.error('Rechazo de Promise no manejado:', reason);
process.exit(1);
});
Análisis de escenarios de aplicación
1. Servicios de API RESTful
Ejemplo
// Primero necesitas instalar express: npm install express
const express = require('express');
const app = express();
app.use(express.json()); // Middleware: analiza automáticamente los datos JSON del cuerpo de la solicitud
// Solicitud GET: obtener lista de usuarios (corresponde al método HTTP GET, ruta /api/users)
app.get('/api/users', (req, res) => {
const users = [
{ id: 1, name: 'Zhang San' },
{ id: 2, name: 'Li Si' }
];
res.json(users); // Devuelve los datos en formato JSON
});
// Solicitud POST: crear un nuevo usuario (el cliente envía la información del usuario en el cuerpo de la solicitud)
app.post('/api/users', (req, res) => {
const newUser = req.body; // Obtiene los datos JSON enviados por el cliente
// En proyectos reales, aquí normalmente se guarda la información del usuario en la base de datos
res.status(201).json({ message: 'Usuario creado correctamente', user: newUser });
});
// Solicitud PUT: actualizar la información del usuario especificado (:id es un parámetro de ruta, por ejemplo, /api/users/1 indica actualizar el usuario con ID 1)
app.put('/api/users/:id', (req, res) => {
const { id } = req.params; // Obtiene el ID del usuario de la ruta
const updatedData = req.body; // Obtiene el contenido de actualización del cuerpo de la solicitud
res.json({ message:`Usuario ${id}actualizado correctamente`, data: updatedData });
});
// Solicitud DELETE: eliminar el usuario especificado
app.delete('/api/users/:id', (req, res) => {
const { id } = req.params;
res.json({ message:`Usuario ${id}eliminado correctamente`});
});
app.listen(3000, () => {
console.log('El servidor API se ejecuta en http://localhost:3000/');
});
2. Aplicaciones en tiempo real
Ejemplo
// Primero necesitas instalar: npm install socket.io
const io = require('socket.io')(server);
// Escucha el evento de conexión del cliente, se dispara cada vez que un nuevo usuario se conecta
io.on('connection', (socket) => {
// Escucha el evento 'chat message' enviado por el cliente actualmente conectado
socket.on('chat message', (msg) => {
io.emit('chat message', msg); // Transmite el mensaje a todos los clientes en línea (incluido el remitente)
});
});
3. Middleware y servicios proxy
Ejemplo
// Reenvía las solicitudes /api/xxx del frontend al servidor API real del backend, oculta la dirección real, se usa comúnmente para resolver problemas de CORS
// Primero necesitas instalar: npm install http-proxy-middleware
const httpProxy = require('http-proxy-middleware');
const proxy = httpProxy({
target: 'http://api.example.com', // Dirección real del servidor API del backend
changeOrigin: true, // Cambia el Host en los encabezados de la solicitud a la dirección de destino
pathRewrite: {
'^/api': '' // Elimina el prefijo /api de la ruta antes de reenviar, por ejemplo, /api/users → /users
}
});