Javascript bajo Node.js

Cuando se habla de Node.js se traduce en usar Javascript como lenguaje del lado de servidor, es decir, fuera del navegador. Node.js es un entorno que corre Javascript y se construyó con C++, permitiendo interactuar con el sistema operativo (Windows, macOS, Linux u otro). No obstante, a diferencia de Javascript para el navegador (o cliente) no se manipula directamente el esquema de HTML (DOM), mas bien Node.js envía los archivos al cliente para que estos se visualicen (distinto es que tales archivos también tengan código de cliente). Por tanto, se logra unificación del lenguaje y ampliación para contemplar, por ejemplo, acceso y manipulación de archivos y bases de datos, tu propio servidor web (http), micro-servicios web (API-Rest), una interfaz de línea de comandos, o simplemente scripts en el servidor escritos con Javascript.

Por otra parte, Node.js es una de las tecnologías que responden al denominado problema de las diez mil conexiones concurrentes (C10K), es decir, posibilita un buen acceso (muy superior a tecnologías web de una década hacia atrás que al día de hoy aún se encuentran en muchos sistemas y sólo gestionan un promedio de 100 peticiones concurrentes). Al ser ligero y potente, posibilita el uso de infraestructura mínima para ir creciendo o escalando, o bien, no se requiere una inversión inicial abismal en infraestructura para brindar un buen servicio (al decir infraestructura se descarta el servicio de alojamiento web convencional puesto que requiere alguna máquina, al menos VPS o servicio especializado).

Actualmente también se requiere Node.js para gestionar aplicaciones de Javascript (aunque se trate de una aplicacion para el navegador o UI).

Entorno de Desarrollo para Javascript moderno (ES6/ES2015+)

Para programar con Javascript moderno (ES6/ES2015+), bien sea del lado del cliente o del servidor (con Node.js) se requiere al menos lo siguiente:

  1. Un sistema operativo con entorno de escritorio (Microsoft Windows, macOS, Linux Ubuntu o Elementary OS)
  2. Descargar e instalar la versión de Node.js para el sistema operativo
  3. Un editor de código (se recomienda Microsoft Visual Studio Code)
  4. Un navegador moderno (se recomienda Google Chrome)
  5. Tener el conocimiento base de arquitectura web que será enrriquecido en la marcha

Algo de arquitectura antes de continuar

Sobre el último punto y salvo que se piense en algo distinto a la web, resulta significativo diferenciar entre servicios de alojamiento web convencional (web hosting) y máquinas (o infraestructura), dado que Node.js no funciona en el primero que es dominado por el lenguaje PHP. Sin embargo, curiosamente el lenguaje Javascript moderno ha evolucionado de un modo tan interesante dominando en el software cliente (o navegador) que nos hace pensar en la posibilidad de volver a los orígenes de páginas y sitios construidos con el estándar HTML y compañía (CSS y Javascript) que es lo que se requiere para el diseño web (sin otro lenguaje), lo que se conoce como páginas o sitios web estáticos (archivos que se entregan directamente al navegador). Comprendiendo que estamos hablando de las posibilidades actuales para páginas o sitios web que contienen información para públicar en Internet, cuando se piensa en aplicaciones también es posible salvo que suele requerirse de un lenguaje o entorno que opere en el servidor cuando se trata de involucrar datos y formularios (que genralmente requiren cierta dinámica).

Si esto resulta nuevo para tí, es de comprender que la arquitectura web que venía trabajándose de modo clásico proporcionaba páginas web dinámicas en relación a que en el servidor se procesaba algo semejante a plantillas HTML mezcladas con código (ejemplo: PHP, JSP, ASP) generando un resultado dinámico para mostrarlo en el navegador, sin embargo, al no tratarse de una arquitectura bidireccional requería una petición y una respuesta en cada ocasión, implementándose página a página dónde la interacción con el usuario sería menos fluida (esto se conoce ahora como “Multiple Pages Application” - MPA).

Actualmente existe un panorama en el que pueden coexistir diversas arquitecturas o doble dinamismo gracias a Javascript moderno, aunque el código para el cliente se relacione con el término estático dado que se entrega directamente al navegador tal como se encuentra en el servidor pero al llegar al navegador se vuelve reactivo. No obstante si haces una aplicación nueva debe evaluarse lo que es más eficiente para cada caso. Por ejemplo, si procesas plantillas para “emails” podría ser interesante recurrir a un estilo clásico pero si quieres prestar un mejor servicio o diseñar micro-servicios están tecnologías como Node.js. Como generalmente se evoluciona, cabe aclarar que se hablaba de cierta arquitectura dominada por la popularidad de PHP en el contexto digital, lenguaje que actualmente también posibilita implementar micro-servicios con proveedores que te ofrecen algunos dólares menos que lo que cuesta el servicio de una máquina virtual pequeña (orientadas a recurso técnico o desarrollador con conocimientos de Linux en un nivel apropiado), pero volviendo a nuestro asunto de preparar un entorno de desarrollo para Javascript deberemos enfocarnos en este lenguaje.

¿Cómo funciona el bucle de eventos? (Event-Loop)

Javascript es en principio síncrono pero puede resolver sentencias asíncronas, ¿y cómo se explica esto? Gracias al loop de eventos que es un bucle que verifica ciertos criterios contando con ciertas características: La pila de llamadas (call-stack), la cola de tareas (task-queue), y Web-API’s como timers y funciones de red, principalmente. Veamos a modo general el flujo:

Esto se hace en medio de un ciclo sin interrumpción (hasta cancelación el proceso) y mediante un hilo de procesos.

Instalación bajo Windows

Como es usual en Windows, simplemente te diriges al sitio del fabricante (https://nodejs.org/, etc.) y descargas el instalador. Sin embargo, a continuación se indica la instalación bajo línea de comandos con privilegios de administrador si usas el gestor de paquetes Chocolatey. Básicamente ejecutas choco install nodejs-lts -y. Veamos entonces cómo aprovisionamos el ambiente con las herramientas citadas anteriormente.

choco install nodejs-lts -y
choco install vscode -y
choco install googlechrome -y
choco install git -y

La última línea sólo indica la conveniencia de instalar Git (un controlador de versiones) en caso de llegar a usar una herramienta que incopora una consola de comandos tipo Linux, denominada Git Bash.

Instalación bajo Linux Ubuntu

Básicamente bajas el paquete y ejecutas sudo apt install -y nodejs desde la línea de comandos. Veamos entonces cómo aprovisionamos el ambiente con las herramientas citadas anteriormente.

curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash -
sudo apt install -y nodejs
sudo apt install -y build-essential
sudo snap remove vscode
sudo snap install code --classic
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
sudo apt install -y libxss1 libgconf2-4 libappindicator1 libindicator7
sudo dpkg -i google-chrome-stable_current_amd64.deb
rm google-chrome-stable_current_amd64.deb

Bastarían las dos primeras líneas para lo referente a Node.js, quizás la tercera para ciertos casos requeridos por paquetes avanzados. Las demás líneas corresponden al editor y al navegador.

El primer programa de ejemplo

Abre un archivo de extensión .js, por ejemplo index.js, y coloca el siguiente código:

console.log('Hi there!')

Para ejecutarlo, haz lo siguiente:

node index.js

Si hacemos un ejercicio en versión web, se necesitaría el siguiente código:

var http = require('http');
http.createServer((req, res) => {
    res.write('Hi there!');
    res.end();
}).listen(8080);

En este caso, se indica que se requiere el módulo http y luego se invoca el método createServer dónde se procesa el mensaje, creando el servicio web para ser escuchado (listen) en el puerto 8080. En unas pocas línas tenemos un servidor web que podemos correr con la sentencia node index.js. Puede consultarse en un navegador usando la dirección localhost:8080.

Proyecto de inicio rápido

Generalmente al instalar Node.js viene el gestor de paquetes y módulos conocido como npm, así que para iniciar nuestro proyecto abrimos una carpeta nueva (por ejemplo, test) y ejecutamos dentro de ésta lo siguiente:

npm init

A las preguntas que hace esta sentencia, por ahora lo mas relevante es identificar el punto de entrada (enty point) que será nuestro script principal, por ejemplo, index.js, y se confirma afirmativamente a la última pregunta sobre si está “OK”. Esto creará un archivo de configuración de paquetes denominado package.json.

Se puede evitar usar npm init y en su lugar escribir nuestro archivo package.json indicando la dependencia de restana con el conocimiento respectivo, luego se ejecutaría npm install. En todo caso, si vemos éste archivo de configuración de paquetes debería asemejarse al siguiente contenido:

{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "restana": "^4.9.9"
  }
}

Node.js & Express (una mirada)

El marco de trabajo express es reconocido para implementar un servidor web en Node.js y gestionar sus peticiones en lugar de usar el modo nativo. Veamos un ejemplo de inicio sobre como establecer un servidor web bajo la propuesta de esta tecnología.

const express = require('express');  // import the module for Node.js
const service = express();

service.get('/', (req, res) => {  // initial function
    res.send('Express says: Hi there!');
});

const server = service.listen(3000);  // open the server

Debe instalarse el módulo y luego lanzas el servicio, suponiendo que el archivo se nombra como index.js se ejecuta cada uno de los siguientes comandos:

npm install express -S
node index.js

Node.js & Restana

Para crear un servicio Web, usaremos un módulo publicado en npm denominado restana (gracias a Rolando Santamaria Maso). Los ejemplos convencionales suelen usar express que es el framework popular, pero éste lo recomiendo por tratarse de una librería ligera enfocada en API’s que tiene un tiempo de respuesta mejor y simpatiza con algunos aspectos esenciales de express. Lo instalamos de la siguiente manera:

npm install restana -S

Ahora abrimos un nuevo archivo para nuestro programa con nombre server.js y colocamos (copiar y pegar) el siguiente contenido:

const service = require('restana')();  // import the module for Node.js

service.get('/', (req, res) => {  // initial function
    res.send('Restana says: Hi there!');
});

service.start(3000);  // start the server

Con tan sólo estas simples líneas, hemos elaborado un servicio http propio para ser escuchado en un puerto (3000). Después de guardarlo se invoca desde la línea de comandos de la siguiente manera:

node server.js

Se debe abrir un navegador y consultar la dirección http://localhost:3000. En este caso no necesariamente se trata de una página web sino de un servicio web que se encuentra disponible para ser consumido o requerido por otro servicio, o en nuestro caso un usuario. Usualmente lo que debe retornar es un JSON que no corresponde al nivel de usuario sino a un nivel de aplicación o información que puede ser procesada por una app en el navegador.

Es posible modificar el archivo package.json para indicar el comando de script de inico de nuestro programa servidor. Comprendiendo lo anterior, se agrega start bajo scripts y ese bloque quedaría así:

  "scripts": {
    "start": "node server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

De este modo, se puede ejecutar de modo genérico el comando:

npm start

Ahora vamos a mejorar nuestro ejercicio dejando el contenido de server.js de la siguiente manera:

const service = require('restana')();

service.get('/', (req, res) => {
    res.send('Hi there!');
});

service.get('/yourname/:name', (req, res) => {
    res.send({ "yourname": req.params.name });
});

// start the server
service.start(3000).then((server) => {
    console.log(`Service listening as ${server._connectionKey}`);
});

Nótese que en la constante service se recibe el módulo o librería importándola con require (en lugar de import), de modo que en adelante podemos usar sus funciones. En este caso usamos get para incorporar la ruta yourname que recibe como parámetro name y reporta una respuesta JSON. De este modo puedes consultarla en la barra de direcciones del navegador agregando /yourname/john, dónde “john” es el nombre enviado. Adicionalmente dejamos un mensaje en consola sobre el servicio en ejecución.

Debes interrumpir el proceso anterior (CTRL-C) y lanzarlo de nuevo (node server.js), luego refrescas el navegador.

Para finalizar, en este tipo de servicio es posible encadenar el flujo de solicitudes web con el uso de next(). Asegurate de que el archivo contenga ahora el siguiente código y recuerda reiniciar el servicio.

const service = require('restana')();

function audit (req, res, next) {
    console.log(req.url);
    next();  // Next middleware
}

service.get('/', (req, res) => {
    res.send('Hi there!');
});

service.get('/yourname/:name', audit, (req, res, next) => {
    if (req.params.name.length < 3)
        return res.send({ status: 'error', message: 'invalid name' });
    res.send({ yourname: req.params.name });
});

// start the server
service.start(3000).then((server) => {
    console.log(`Service listening as ${server._connectionKey}`);
});

Aunque no se trate de un ejemplo cercano a la realidad (pues no se debería saturar el uso de console.log), puede observarse que la diferencia más significativa consiste en el uso de la función audit que invoca a next(). Lo que se intenta ilustrar es que pueden existir bloques de lógica independientes (middleware) que pueden reutilizarse, incluso incorporar un servicio de terceros según los momentos del flujo (por ejemplo, enviar un email). Por otra parte, agregamos una validación para dar una respuesta con un mensaje de error cuando el nombre tiene menos de 3 caracteres.

Ahora reorganicemos el código en una nueva versión quedando con el siguiente contenido:

const fs = require('fs');
const service = require('restana')();
const port = 3000;

const audit = (req, res, next) => {
    console.log(req.url);
    next();
}

const names = (req, res, next) => {
    if (req.params.name.length < 3)
        return res.send({ status: 'error', message: 'invalid name' })
    res.send({ yourname: req.params.name });
}

const files = (res, file) => {
    fs.readFile(file, 'utf8', (err, data) => {
        if (err)
            return res.send(err);
        return res.end(data);
    });
}

service.get('/', (req, res) => { files(res, './web/index.html') });
service.get('/yourname/:name', audit, names);

service.start(port).then((server) => {
    console.log(`Service listening on ${port}`);
});

Observa los cambios. Lo más significativo es el uso del módulo fs (nativo de Node.js, por eso no se instala) para gestionar archivos, en este caso para leer ./web/index.html por lo que debe existir o crearse con algún contenido (de lo contrario reporta error). Además, se reorganizó el código usando funciones tipo arrow que es otra manera de expresarlas, así como también se incorpora una constante para el puerto del servicio.

En cuanto al uso de archivos estáticos debería exponerse una carpeta destinada para ello (web) y no un archivo. Para esto ya existe el módulo serve-static que se puede instalar (npm install serve-static -S), y al combinarlo con restana tendríamos finalmente el siguiente código:

const files = require('serve-static');
const service = require('restana')();
const port = 3000;

audit = (req, res, next) => {
    console.log(req.url);
    next();
}

names = (req, res, next) => {
    if (req.params.name.length < 3)
        return res.send({ status: 'error', message: 'invalid name' });
    res.send({ yourname: req.params.name });
}

service.use(files('./web'));
service.get('/yourname/:name', audit, names);

service.start(port).then((server) => {
    console.log(`Service listening on ${port}`);
});

Nótese que, además de importar el nuevo módulo, el cambio se centra en utilizar service.use(files('./web')), dónde use indica que debe ejecutarse la gestión de files, que ahora corresponde al servicio que expone la carpeta de archivos estáticos.

Node.js & low-http-server (una mirada)

Podemos combinar la anterior tecnología (restana) para potenciarla con low-http-server y obtener un tiempo de respuesta superior. Para instalarlo ejecutamos lo siguiente:

npm install low-http-server -S

Y como ejemplo correspondiente para un servidor web, tendríamos un código como el siguiente:

const server = require('low-http-server')({});
const service = require('restana')({
	server: server,
	prioRequestsProcessing: false  // for great performance
});
const port = 3000;

service.get('/', (req,res) => {
	res.send('Hi there!');
});

server.listen(3000, () => {
    console.log(`Service listening on ${port}`);
});

low-http-server esta basado en uWebSockets que brinda un mejor desempeño respecto al modo convencional de operar en Node.js.

Rutas con Express

Una de las ventajas de usar Express se debe su gestión de rutas para orgonizar la aplicación. Lo que se busca es que cuando se repite un patrón de ruta se puede usar route y encadenar el verbo http respectivo. Veamos el ejemplo:

const express = require('express');  // import the module for Node.js
const service = express();

service.route('/thing')
.get((req, res) => {
    res.send('GET thing');
})
.post((req, res) => {
    res.send('POST thing');
});

const server = service.listen(3000);  // open the server

Nótese que se usa route y se encadena get y post

Sin embargo, podemos usar nuestra aplicación con la parte que expresa la ruta y modularizar las rutas en otro archivo gracias a Router. De este modo, nuestro archivo principal se vería así:

const express = require('express');  // import the module for Node.js
const service = express();
const thing = require('./thing');

service.use('/thing', thing);

const server = service.listen(3000);  // open the server

Y en un nuevo archivo (thing) tendríamos el siguiente contenido:

const express = require('express');  // import the module for Node.js
const router = express.Router();

router('/')
.get((req, res) => {
    res.send('GET thing');
})
.post((req, res) => {
    res.send('POST thing');
});

module.exports = router;

En este archivo no se incluye la ruta /thing sino las subrutas correspondientes (/...) usando la instancia de Router y el verbo http.

De este modo, se puede hacer un CRUD según el módulo que consistiría en un archivo separado. Además, se pueden incluir rutas con parámetros o expresiones regulares.

Tambien podría usarse express-generator para agilizar la creación de una aplicación con rutas ejecutando, por ejemplo, los siguientes comandos:

npm i express-generator -g
express --no-view project
cd project
npm install
npm start

Para ejecutar lo anterior debe existir una carpeta destinada como espacio de trabajo de proyectos de código, de modo que nos ubicamos dentro de ella desde la línea de comandos.
project correspondería al nombre asignado para el proyecto.

Socket.io (una mirada)

Además de modulos como express encontramos socket.io que es reconocido para implementar websockets usados en Node.js para aplicaciones en tiempo real, como es caso de un “chat”. Veamos un ejemplo de inicio sobre como establecer websockets con esta tecnología.

const server = require('http').createServer();  // Web server
const io = require('socket.io')(server);  // import the module for Node.js

io.on('connection', (browser) => {
  browser.on('my-event', (data) => {  // event from the browser/client
      console.log('Hi there!');
  });

  browser.on('disconnect', () => {
      console.log('Bye!');
  });
});

server.listen(3000);  // open the server

Debe instalarse el módulo y luego lanzas el servicio, suponiendo que el archivo se nombra como index.js se ejecuta cada uno de los siguientes comandos:

npm install socket.io -S
node index.js

Un ejercicio completo implementaría un código para una aplicación en el navegador que invoca el evento usando también socket.io como cliente.

Ejemplo de cluster con Node.js

En el siguiente ejemplo se ilustra la lógica que se puede implementar para usar un cluster con Node.js. Dado que Node.js corre en un sólo hilo, con cluster es posible levantar un proceso por cada procesador, identificando uno maestro. Veamos el ejempplo:

const http = require('http');
const cluster = require('cluster');
const cpus = require('os').cpus().length;

if (cluster.isMaster) {
    for (var i = 0; i < cpus; i++)
        cluster.fork();

    cluster.on('exit', (worker, code, signal) => {
        console.log('pid ' + worker.process.pid + ' bye.');
    });
}
else
    http.createServer((req, res) => {
        res.send(`Hi there!\n`);
    }).listen(8000);

cluster.isMaster permite identificar si se trata del proceso maestro sobre el cual se derivan los otros procesos (hilos) que se crearían con cluster.fork(). Mediante cluster.on es posible capturar el evento de salida (exit) de cada proceso.

Ejemplo de Worker Threads con Node.js

Mientras un cluster utiliza multiples procesos en Node.js, los hilos de trabajos (Worker Threads) posibilitan operaciones paralelas dentro de un solo proceso compartiendo la memoria. Veamos el ejempplo:

const { Worker, isMainThread, parentPort } = require('worker_threads');

if (isMainThread) {
    const wkr = new Worker(__filename);

    wkr.on('message', msg => {
        console.log(`Worker says: ${msg}`);
    });

    wkr.on('error', error => {
        console.error(error);
    });

    wkr.on('exit', exitCode => {
        if (exitCode !== 0)
            console.error(`The worker got end with ${exitCode}`);
    });
}
else
    parentPort.postMessage('Hi, this is the worker');

Se puede identificar si se cumple isMainThread, permitiendo capturar los eventos para message, errory exit
parentPort.postMessage es usando desde el worker para enviar mensajes o eventos al hilo principal.

Métodos y variables esenciales de Node.js

Node.js provee un conjunto de librerías con métodos que convienen conocer. Las librerías nativas que podemos mencionar inicialmente son fs para gestionar archivos, path para el manejo de ubicaciones locales, url para el manejo de rutas web, y http para el servicio web (aunque hemos usado en su lugar una libería no nativa de nombre restana), entre otras.

Método Descripción
require Importa librerías (o archivos) tanto nativas como de terceros. En nuevas versiones es posible usar import (con herramienta webpack que veremos avanzando el texto)
__dirname Variable del entorno que devuevle el directorio desde donde se ejecuta el entorno.
console.log Al igual que javascript del lado del navegador, es usado para imprimir salida de consola (en este caso, en la pantalla).
fs.readFile Lee un archivo
fs.writeFile Escribe un archivo
path.join Concatena rutas con el separador apropiado para el sistema
url.format Formatea una url apropiadamente basándose en parámetros (protocolo, ruta, slashes)
http.createServer Inicia un servidor para peticiones web (http)

El objeto Process

Existe un objeto en Node.js denominado process, el cual nos permite acceder a ciertas características. Veamos algunos ajemplos:

Algunas diferencias entre Javascript para el cliente y para servidor

Comprendiendo que lo que llega al navegador sería el cliente y lo que corre fuera del navegador (bajo Node.js) lo designamos como servidor, podríamos citar algunas diferencias claves sobre este asunto.

Concepto JS para cliente JS para servidor
Acceso al DOM (HTML) No
Setencia FETCH para peticiones web No
Gestión de archivos de modo nativo No
Acceso a bases de datos convencionales No
Acceso a base de datos local para navegador No
Servicio Web o de páginas HTML No
Renderizado desde el servidor - SSR No
API de microservicios Web No
Aplicaciones de línea de comandos - CLI No
Scripts para servidor No
Aplicaciones para el escritorio - PWA No

Algún punto puede ser considerable dado que existen tecnologías como Electron o NW.js que integran ambas partes posibilitando justamente aplicaciones para el escritorio, también denominadas aplicaciones web progresivas (PWA).

Webpack

Si se piensa que existen unos archivos de orígen que pueden ser organizados obteniendo unos archivos distribuibles algo distintos se requiere de algún mecanismo que lo facilite. Esta es la manera de trabajar que se propone para el desarrollo web moderno. Por tanto, dejan de usarse scripts y otros archivos de modo directo. Siendo así, aunque un script corresponda al lado del cliente, en tiempo de desarrollo se utilizaría Node.js y sus facultades. De hecho, bajo esta filosofía de desarrollo para componentes de naturaleza estática, aunque se use PHP, Python, Java u otro lenguaje del lado del servidor, se usaría igual Node.js para el entorno de desarrollo.

Webpack es una herramienta que opera bajo Node.js simplificando las tareas de empaquetamiento de archivos distribuibles sin necesidad de un gestor de tareas (podemos citar gulp, también se pueden combinar), es decir, hace posible organizar la entrega de programas generalmente relacionados con archivos html, css, js. Adicionalmente ofuzca el código y dadas las diferentes versiones de Javascript (es5, es6, es7, etc.) se aplica la transpilación (término usado como sinónimo de transformación considerando que tampoco es compilación) a la versión distribuible conforme a los navegadores objetivo (por ejemplo los modernos: edge, chrome 80+, firefox 70+, safari 10+), así como otro tipos de lenguajes que transpilan a js, como por ejemplo Typescript (ts), Dart, Kotlin, y librerías como React (jsx) y Riot, entre muchas otras tecnologías.

En el contexto de desarrollo moderno con Javascript, es indispensable una herrammienta como ésta o similar (por ejemplo, ParcelJS), siendo actualmente Webpack la más popular (teniendo acogida para ser usado con otras tecnologías como el gestor de tareas Gradle en la JVM).

Para empezar se debe contar con una carpeta que corresponda al proyecto de ejemplo y en la que se encuentra el archivo package.json. Para nuestro ejercicio con Webpack instalaremos lo siguiente:

npm install webpack webpack-cli -D

Nótese que se usa -D dado que son módulos útiles en tiempo de desarrollo y no como dependencia final.

Debemos reorganizar la estructura del proyecto para distinguir nuestros fuentes del código generado por Webpack como destino. Como estándar para los fuentes se busca una carpeta src, de modo que allí debe quedar nuestro archivo Javascript.

Además, se modifica el archivo package.json para indicar el comando de script de construcción de nuestro programa. Comprendiendo lo anterior, se agrega build bajo scripts y ese bloque quedaría así:

  "scripts": {
    "start": "node server.js",
    "build": "webpack --mode development",
    "build:prod": "webpack -p --progress",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

De este modo, se podría ejecutar el comando:

npm run build

En un escenario sencillo, Webpack funciona sin configuración asumiendo que encuentra un archivo src\index.js y lo distribuye por defecto como dist\main.js, como se puede observar a continuación:

📂 project
├── src
│⋅⋅⋅⋅⋅⋅└── index.js
├── dist
│⋅⋅⋅⋅⋅⋅└── main.js
└── index.html

Sin embargo, para especificar algunas cosas o personalizarlo es usual el uso de un archivo de configuración denominado por defecto webpack.config.js. Procedemos a inicializarlo (creándolo en un editor) con el siguiente contenido:

const path = require('path');

module.exports = {
    entry: path.join(__dirname, 'src', 'index.js'),
    output: {
        filename: 'main.js',
        path: path.join(__dirname, 'web', 'dist')
    },
    module: {
        rules: [
            {
                exclude: /node_modules/,
            },
        ],
    },
}

Como se puede observar, se usa un archivo JS que exporta un objeto de módulo, el cual tiene principalmente cinco elementos esenciales que son: entry, output, mode, module, plugins.

Este archivo de ejemplo utiliza los elementos entry, output y module (aplicando una regla para excluir los módulos Node.js). En entry se reporta el punto de entrada que se trata de un script principal, mientras en output se indica el destino para su distribución. Adicionalmente, en este caso se usa la variable de entorno __dirname y el módulo path (nativos de Node.js) para garantizar las ubicaciones (aplicando el separador respectivo para el sistema operativo).

Para comprender mejor la disposición de nuestros archivos, tendríamos lo siguiente:

📂 project
├── src
│⋅⋅⋅⋅⋅⋅└── index.js
├── web
│⋅⋅⋅⋅⋅⋅├── index.html
│⋅⋅⋅⋅⋅⋅└── dist
│⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅└── main.js
├── package.json
└── webpack.config.js

Archivo Descripción
./package.json Archivo de configuración de proyecto con Node.js
./webpack.config.js Archivo de configuración de Webpack
./src/index.js “script” original del lado del cliente
./web/dist/main.js “script” que se generaría con Webpack como versión distribuible de index.js
./web/index.html Plantilla de página web que usaría el “script” distribuible (main.js)

Podemos agregar en esta lista el servicio web creado en un ejercicio anterior (./server.js), sólo que se ilustra de este modo para distinguir lo que corresponde al cliente del código para el servidor, que en nuestro caso también es bajo Node.js.

Ahora agreguemos nuestro archivo src/index.js con el siguiente contenido:

onSend = (e) => {
    e.preventDefault();
    let input = document.querySelector('input');
    let uri = 'http://localhost:3000/yourname/' + input.value;
    if (!input.value)
        uri = 'http://localhost:3000/yourname/john';

    fetch(uri)
    .then(res => res.json())
    .then(data => {
        let holder = document.getElementById('holder')
        if (holder) holder.remove();
        let code = document.querySelector('code');
        code.innerHTML = JSON.stringify(data, null, '  ');
        hljs.highlightBlock(code);  // use highlight.js
    })
    .catch(err => {
        document.querySelector('code').innerHTML = `${uri} => ${err.message}`;
    });
}

De modo separado, abrimos nuestra plantilla web/index.html con el siguiente contenido:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Template with Semantic-UI styles</title>

    <!-- Semantic-UI Styles -->
    <link href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.5.0/semantic.min.css" rel="stylesheet" media="screen">
    <!-- HighlightsJS -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.9/styles/ocean.min.css">
  </head>
  <body>
    <div class="ui text container">
        <br/>
        <form onsubmit="onSend(event)">
            <div class="ui labeled action fluid input">
                <div class="ui label">http://localhost:3000/yourname/</div>
                <input type="text" placeholder="john">
                <button class="ui icon button">
                    <i class="search icon"></i>
                </button>
            </div>
        </form>
        <div class="ui placeholder segment" id="holder">
            <div class="ui icon header">
                <i class="search blue icon"></i>
                Try it!
            </div>
        </div>
        <pre>
            <code style="max-height: 300px; overflow-y: scroll;"></code>
        </pre>
    </div>

    <script src="https://code.jquery.com/jquery-3.6.1.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.5.0/semantic.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.9/highlight.min.js"></script>
    <script defer src="dist/main.js"></script>
  </body>
</html>

Ejecutamos el comando npm run build (o npm run build:prod) e iniciamos nuestro servidor (con el comando node server.js o npm start). Si todo esta bien, nos dirigimos al navegador y revisamos nuestra aplicación web en el puerto respectivo.

Para un siguiente ejercicio podríamos ver interactuar Webpack con una librería como Riot en su versión 5, así continuamos avanzando en el mundo Javascript.


© 2019 by César Arcila