Introducción a Node.js
Por Arrecio
En el anterior artículo hablé de JavaScript y TypeScript, y entre otras cosas comenté que en general todos los lenguajes de familia JavaScript (o más bien ECMAScript) ha estado históricamente vinculados a los navegadores web como motor de ejecución de scripts que de alguna manera dotaban de cierto dinamismo a una web desde el lado del cliente.
Aunque este fuera el uso más tradicional desde los inicios ya se pensó en darle otros usos más en la línea de lenguajes multipropósito y no mucho después de los albores de JavaScript en los laboratorios de Netscape construyeron un entorno de ejecución de lado de servidor al estilo de las CGI mediante Perl, y antes incluso que las ASP y poco después PHP.
Y es que Netscape no proveía únicamente navegadores web siendo su propósito empresarial distribuir el servidor web Netscape Enterprise Server (ver Oracle iPlanet Web Server). De hecho JavaScript propiamente dicho es actualmente una marca de la que es propietaria Oracle, empresa que ha publicado una guía para un uso de JavaScript en el lado del servidor.
Algunas de las implementaciones que se han realizado están en esta lista sorprendiéndome encontrar, todo sea dicho, a MongoDB.
Entre los elementos de la lista aparece Node.js, el niño guapo de la familia, el que todos conocen.
Node.js está construido como una arquitectura general de servidor, es decir que su principal función es atender peticiones de usuario, procesarlas y ofrecer una respuesta. Cada petición no es más que una tarea a resolver, y en el caso de Node.js estas se resuelve de manera asíncrona no bloqueante.
El término asíncrono, cuando se relaciona con el software, suele referirse a arquitectura dirigida por eventos. Un caso general es el que se solicita algo al servidor y con la solicitud se envía el método a utilizar para dar a conocer la respuesta. El uso más tradicional es el de los llamados callbacks.
Con "no bloqueante" se dota de una característica al servidor por la cual estamos diciendo que está garantizado que ninguna de las tareas tendrá que esperar por la finalización de otras tareas, algo que podría suceder si alguna de las tareas bloqueara un servicio necesario para resolver también otra tarea.
Sumando ambos conceptos decimos el funcionamiento es que el servidor recibe una petición y se pone a procesarla mientras sigue admitiendo peticiones. Finalizará las mismas en el mayor tiempo posible (las más sencillas probablemente acaben antes) y tras ello responde al cliente con el resultado sin más demora.
La parte asíncrona se resuelve mediante la librería libuv que es la que gestiona el bucle de eventos o event loop que corre en un único hilo, y la parte no bloqueante a través de la implementación de worker threads (multi-hilo) encargados de realizar todo aquello que requiera llamadas al sistema o a librerías externas. Si un worker se bloquea bloqueara únicamente la tarea que está tratando de resolver.
Existe 4 hilos adicionales al que sostiene el event loop que se utilizan específicamente para operaciones de compresión, llamadas al sistema de archivos, criptografía y resolución de nombres que son entendidas como potencialmente bloqueantes. Estos 4 hilos y su utilización son responsabilidad de Node.Js. Para cualquier otra operación que corre el riesgo de bloquear el event loop el programador es responsable de mantener el event loop sin bloqueos (regla de oro de Node.Js) y a para ello es que utilizaremos los workers.
Todo esto se ve mejor con un dibujo:
En el dibujo se ha expuesto un caso de uso de un worker que finaliza por completo una tarea pero en muchas tareas sencillas no necesitaremos hacer uso de los mismos, ni tan siquiera para ejecutar micro-tareas (partes de una tarea), pudiendo en estos casos devolver el resultado directamente a través del respectivo callback.
El dibujo simboliza a unos clientes que realizan peticiones que son enviadas al bucle de eventos, allí para cada petición habrá que realizar una serie de operaciones que conllevarán llamar a una o varias funciones. Los intérpretes de JavaScript van ejecutando el código mediante una pila de llamadas. Cuando alguna función se corresponde a una llamada potencialmente bloqueante de las que gestiona Node.Js entonces pasa su ejecución al hilo correspondiente.
Por otro lado si el programador considera que la tarea o cierta parte de la tarea corre el riesgo de bloquear el event loop puede lanzar la ejecución de ese código mediante un nuevo hilo mediante lo que se conoce como worker thread que se van obteniendo de un pool en el que descansan todos los workers no ocupados y que pasaría a correr en un nuevo hilo.
Cuando las tareas van siendo completamente finalizadas su resultado se inserta en el callback queue desde donde el event loop lo despachará hacia el cliente que solicitó el trabajo.
Entorno de desarrollo de apps JavaScript
Podríamos definir entorno de desarrollo como el conjunto de especificación del lenguaje, intérprete (o compilador) y gestor de paquetes (o librerías) + repositorios.
Por poner similitudes podríamos hablar de otros entornos de desarrollo de otros lenguajes como por ejemplo para el lenguaje Python cuyo interprete oficial es CPython. Cuando descargas Python a través de esta web en realidad estás descargando CPython. Con él descargas el gestor de paquetes pip que trabaja sobre un repositorio llamado PyPi (siendo esta su web). Todo ello: especificación del lenguaje, intérprete y gestor de paquetes + repositorios, forman el entorno de desarrollo de Python.
Si programas en Python y quieres probar una alternativa al intérprete CPython puedes echar un vistazo a PyPy (no confundir con PyPi).
Igualmente podríamos hablar de LuaJit como interprete del lenguaje Lua y luarocks como gestor de paquetes tradicional existiendo alternativas más modernas como lux del proyecto lumen.
Node.js es más que un intérprete, funcionalidad que realiza el motor de JavaScript V8 de Google. Node.js es una infraestructura de ejecución que envuelve al intérprete y proporciona una serie de librerías "estándar", pero que puede perfectamente importar librerías de terceros para ampliar sus capacidades. A estas librerías se las denomina packages o paquetes.
Actualmente hay 4 gestos de paquetes JavaScript muy conocidos:
npm, que siempre ha sido el gestor oficial que acompaña a Node.js. El que a día de hoy tiene mayor número de paquetes en sus repositorios.yarnque surgió más adelante y pronto se hizo muy popular.pnpm, más reciente y que promete eliminar problemas y realizar una gestión más optimizada. Es compatible con los paquetes npm.bunque dice ser increíblemente más rápido que los demás.
Supongo que para gustos los colores. En mi caso me conformo de momento con npm. Aunque todos tienen una buena documentación, por referir a una guía de eso tenemos por ejemplo esta. Para bum por ejemplo esta. Internet está plagado de guías de todo lo que rodea a Node.js, y también de artículos con comparativas.
Lo que hace grande a infraestructuras como npmjs (o sucedáneos), PyPi o luarocks es que son completamente abiertas para que cualquiera pueda subir sus propios paquetes confiando en la responsabilidad de los usuarios. Algunos autores refieren a este tipo de infraestructuras como ecosistema digital.
Frameworks JavaScript
Por encima de las infraestructuras estarían los frameworks cuya traducción viene a ser eso, estructuras de trabajo que envuelven a la infraestructura en un entorno de librerías más complejas orientadas a facilitar las tareas para las que está diseñado el framework. Por ejemplo ya he hablado en este blog de Django que es un framework de Python para la realización de aplicaciones web.
En el caso de Node.js un framework muy conocido es Express.js. Visita este sitio donde resumidamente explican las diferencias entre ellos.
Para no llegar a confusión, comentar que existen otros frameworks para desarrollar aplicaciones con JavaScript y paquetes npm que no descansan realmente sobre Node.js, sino que tienen su propia infraestructura. Este es el caso por ejemplo de React (by Facebook), AnguarJs (by Google), Vue.js o el relativamente reciente Astro que está pensado como una envoltura de otros frameworks (ver Wikipedia) pero más orientado a la generación de contenido estático. En este caso los que hemos nombrado están orientados a la creación de aplicaciones del lado del cliente, es decir la mayoría del código se traslada con la propia web y se ejecuta en el navegador (usan su motor), mientras Node.js está más bien orientado a ejecutar el código en el lado del servidor (sobre el motor V8). Decir que todos ellos trabajan con paquetes del ecosistema de JavaScript y que pueden ser gestionados con cualquiera de los gestores que hemos visto.
Veamos una imagen con una comparativa:
Hello World con Node.js
Para instalar Node.js es tan sencillo como acudir a su web de descarga. En este sitio están disponibles tanto los instaladores como las instrucciones para instalar utilizando algún gestor de paquetes del sistema operativo. Siguiendo esta opción también te deja elegir entre varios gestores de paquetes de Node.js que serán igualmente instalados en el sistema. Falta la opción de bun la cual puede instalarse en Windows siguiendo estas instrucciones (incluso puede instarse con npm).
En mi caso he instalado usando la gestor de paquetes de Windows chocolatey y como lo uso habitualmente únicamente he tenido que hacer:
choco install nodejs
Esto deja instalado Node.js y npm, algo que podemos comprobar abriendo una nueva ventana de terminal y haciendo:
node --version
npm --version
No voy a complicarme la vida y voy a realizar el mismo ejemplo que en w3schools. Creamos un archivo llamado myfirst.js con el siguiente contenido:
let http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/html'});
res.end('Hello World!');
}).listen(8080);
A continuación hacemos:
node ./myfirst.js
Y si accedemos mediante un navegador web a la dirección http://127.0.0.1:8080 veremos nuestro previsible mensaje "Hello World!".
El código básicamente importa la librería http y utiliza su función createServer para crear un servidor http que escucha en el puerto 8080 del equipo local. La función createServer recibe una función que será llamada cada vez que el servidor recibe una solicitud como primer parámetro, pudiendo escribir un resultado a través del objeto recibido como segundo parámetro.
Al contrario de lo que se pueda pensar al ejecutar el script, la función listen no es bloqueante, algo que podemos demostrar añadiendo una línea a continuación en la que escribamos cualquier cosa en la consola, por ejemplo. Recordemos que nada debería ser bloqueante con Node.js. De alguna manera listen lo que hace es colocar al servidor en el Event Loop como tarea permanente hasta que se sale de Node.js. Si el lector quiere profundizar sobre ello, la función listen se implementa aquí pero este análisis puede terminar llevándote por todo el código de Node.js. Como desarrollador probablemente no necesites conocer nada más de lo que la documentación de las librerías de indican acerca de uso y funcionalidad y para este caso tenemos la documentación de la función que hemos usado aquí.
Estas capacidades de Node.js como servidor web lo convierten en una alternativa a las soluciones tradicionales.
Como siempre, a partir de aquí hay todo un mundo de posibilidades. Esto tan sólo es un artículo introductorio.