Servidor local de desarrollo

Cuando nos dedicamos al desarrollo web, es muy frecuente el uso de servidores locales (development web servers). Esto no son más que pequeñas herramientas que nos muestran nuestra web tal y como quedaría si la subieramos a un servidor final de producción, sólo que en nuestra máquina, de una forma más rápida y sencilla.

Existen muchas herramientas de este tipo, como live-server, http-server o sirv. Con ellas podemos ir viendo en un navegador como va quedando la página a medida que vamos escribiendo código, puesto cada vez que guardamos recarga los cambios. En este artículo nos vamos a basar en es-dev-server, que forma parte de la iniciativa open-wc, por varias razones:

ES-dev-server: Servidor local de desarrollo

  • Flexibilidad: Sirve para ejemplos sencillos, pero es personalizable para avanzados.
  • Caché eficiente: Usa ESM y el caché del navegador para acelerar actualizaciones.
  • Auto-recarga rápida a medida que vamos escribiendo código y guardando cambios.
  • Transforma código para ser compatible con navegadores antiguos.
  • Resuelve importaciones de node para funcionar en el navegador.
  • Enrutamiento de peticiones 404 (history API fallback), muy útil para SPA.
  • Fácil de integrar con herramientas como Typescript, Babel (u otras).

Usuarios que comienzan en el mundo del desarrollo web suelen hacerlo con plataformas como CodePen o CodeSandbox, que te abstraen un poco de toda la infraestructura de ficheros, pero este artículo vamos a ver como ponernos a preparar un proyecto con nuestros propios ficheros utilizando es-dev-server, primero con un enfoque sencillo y luego con uno más complejo.

Modalidad simple (directa)

En primer lugar, vamos a ver una modalidad de trabajar en la que trabajamos directamente con el código, sin que sea preprocesado. La hemos llamado modalidad simple o directa. En esta modalidad, el contenido de la carpeta se subirá directamente a nuestro servidor final sin ningún cambio específico. Más adelante veremos otro enfoque donde utilizaremos herramientas que modifican el código final (preprocesado o transpilado).

Lo primero que tenemos que hacer es crear una carpeta para nuestro proyecto, que en un alarde de originalidad hemos llamado «project» (nos sirve como primer ejemplo, pero a partir de ahora debe ser un nombre representativo para nuestro proyecto). Desde una terminal escribimos:

# Creamos una carpeta "project"
$ mkdir project

# Accedemos a ella
$ cd project

# Editamos nuestra página principal con VSCode
$ code index.html

El siguiente paso será crear una estructura de carpetas que nos resulte cómoda e interesante. Fijate que project es la carpeta raíz (la principal). Veamos un ejemplo de estructura de carpetas:

+ project
   + assets
   + components
   - index.html
   - index.js
   - index.css

Observa que en el interior de nuestra carpeta project tenemos dos carpetas: assets y components. La primera de ellas, assets se suele utilizar para guardar todos los ficheros considerados estáticos (imágenes, videos, tipografías, sonidos, etc...), mientras que components se utilizará para guardar todos los archivos .js que contengan un WebComponent.

Nota: A medida que el proyecto crece, es posible que el desarrollador prefiera crear subcarpetas para organizar mejor el código. Por ejemplo, en los componentes, se suelen crear carpetas para agrupar por temáticas o contextos si tenemos mucha cantidad de componentes.

Por otro lado, tenemos los tres ficheros principales index.html, index.js y index.css. Recuerda que en el index.html debemos asociar correctamente los ficheros javascript y css, para que todo comience a funcionar de forma adecuada. Veamos un ejemplo del fichero index.html:

<!DOCTYPE html>
<html lang="es">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Título del documento</title>
    <link rel="stylesheet" href="index.css">
    <script type="module" src="index.js"></script>
  </head>
  <body>
    <h1>First example</h1>
  </body>
</html>

Observa las etiquetas <link> y <script> que hacen referencia a nuestros archivos index.css e index.js. Muchos usuarios prefieren incluso tener estos archivos en carpetas css o js, para tenerlo todo más organizado. Aquí se han colocado en el raíz para simplificar lo máximo posible.

Nota: No te olvides de colocar el atributo type="module" si quieres utilizar import y export en tus archivos Javascript desde el navegador. En caso de que no lo vayas a hacer, no es necesario.

Por último, y como estamos trabajando en la modalidad simple, vamos a instalar el servidor local es-dev-server como paquete global de npm, es decir, en nuestro sistema para que esté siempre disponible. Más adelante veremos como hacerlo a nivel de proyecto.

# Instalamos es-dev-server de forma global (en el sistema)
$ npm install -g es-dev-server

Esto instalará el servidor local es-dev-server en nuestro sistema, disponible para utilizar en cualquier proyecto. Recuerda que necesitas tener NodeJS instalado (mínimo, versión 10).

Situados en la carpeta raíz de nuestro proyecto (es decir, en project), escribimos lo siguiente:

$ es-dev-server --open --watch
es-dev-server started on http://localhost:8000
  Serving files from '/home/manz/workspace/project'.
  Opening browser on '/'
  Using auto compatibility mode, transforming code on older browsers based on user agent.

Aunque las opciones las veremos a continuación, vamos a comenzar indicando --open para abrir un navegador con la página principal (index.html) y --watch para vigilar los cambios del código, y si los hay, actualice el navegador «automágicamente».

Observa que la ruta a acceder, en principio, sería http://localhost:8000. También puedes usar el formato abreviado de parámetros y escribir es-dev-server -o -w, que es más corto.

En este punto, deberíamos poder ver nuestro primer ejemplo, y si cambiamos código en nuestros ficheros index.html, index.js o index.css se deberían ver reflejados en el navegador sobre la marcha.

Parámetros de es-dev-server

Aunque de momento solo hemos utilizado los parámetros --open y --watch, existen muchos otros parámetros bastante interesantes:

Parámetro Descripción
--port port -p Indica el puerto a utilizar. Por defecto, 8000.
--hostname host -h Indica el host a utilizar. Por defecto, localhost.
--app-index index.html -a Indica la página principal y/o SPA routing. Por defecto, index.html.
--root-dir rootdir -r Indica la carpeta con los ficheros a servir. Por defecto, la carpeta actual.
--open path -o Abre el navegador en la ruta indicada. Por defecto, app-index o root-dir.
--base-path path Indica ruta de base para servir la app. Util para GitHub Pages, por ejemplo.
--cors Activa CORS. Desactivado por defecto.
--watch -w Vigila los cambios en el código. Si los hay, actualiza el navegador.
--watch-excludes path Indica una ruta (o varias rutas) a excluir de la vigilancia anterior.
--http2 -t Lanza el servidor local utilizando HTTP/2 con certificados autofirmados.
--ssl-key keypath Usado con -t permite indicar la ruta del fichero .key con clave privada.
--ssl-cert certpath Usado con -t permite indicar la ruta del fichero .cert con clave pública.

Probablemente, al principio, los parámetros más interesantes serán:

  • --port permite usar un puerto diferente para el servidor local (por defecto usa 8000).
  • --app-index permite cambiar el nombre del fichero principal (emula routing, útil en SPA).
  • --root-dir permite establecer lo que consideraremos nuestra carpeta raíz.

A medida que crezca la cantidad de parámetros a usar, puede ser interesante crear un fichero de configuración. Lo veremos al final del artículo.

Modalidad de transpilado

En muchas ocasiones, nos interesará utilizar un enfoque más complejo que la modalidad directa, donde utilizamos preprocesadores, transpiladores o herramientas que modifican nuestro código final, por lo que la modalidad anterior se nos queda corta. Vamos a echar un vistazo para ver que pasos seguir para crear un proyecto análogo al anterior, pero utilizando este enfoque.

A diferencia de otros servidores, es-dev-server pretende fomentar arquitecturas inteligentes, reduciendo en desarrollo los procesos de build (transformaciones de código cada vez que este cambia) y manteniéndolo solo cuando sea necesario. En su lugar, da prioridad a sistemas rápidos de caché, lazy load y al enfoque moderno de módulos ECMAScript. Este enfoque choca frontalmente con los más utilizados hoy en día en el ecosistema Javascript, como los que usan frameworks como React o Vue, o herramientas como Webpack, Parcel y otros, donde hay una fuerte dependencia de Node y se utilizan sistemas de módulos legacy como CommonJS.

En esta modalidad, los proyectos suelen gestionarse con npm, por lo que vamos a crear una carpeta para un proyecto, e inicializarlo:

$ mkdir project

$ cd project

$ npm init -y

Al igual que en la modalidad anterior, crearemos una estructura de carpetas. Sin embargo, esta será ligeramente diferente ya que tendremos nuestros archivos de código fuente en una carpeta src, mientras que los de configuración o «tooling» se encontrarán fuera:

+ project
   + src
      + assets
      + components
      - index.html
      - index.js
      - index.css
   + node_modules
   - package.json
   - package-lock.json

Como se puede ver, el fichero package.json generado por npm (configuración de nuestro proyecto), el fichero package-lock.json (versionado e histórico de las dependencias de nuestro proyecto) y la carpeta node_modules (paquetes instalados con npm) permanecen fuera de src, mientras que los archivos de nuestra web estarán en su interior.

Los archivos de tooling o configuración no deben existir en nuestra web final de producción, sino que sólo son utilizados en desarrollo (gestión del proyecto y/o generar nuevos archivos procesados). Con es-dev-server se propone favorecer la velocidad en entornos de desarrollo, y utilizar automatizadores como rollup para generar las versiones de producción.

En muchos casos, en lugar de seguir el enfoque global de la modalidad que vimos anteriormente, es muy habitual instalar es-dev-server a nivel de proyecto. De esta forma, cualquier desarrollador que descargue el proyecto puede instalarlo con un simple npm install. Para ello, escribimos:

$ npm install -D es-dev-server

Una vez lo tengamos instalado, hay que ejecutarlo utilizando npx, una herramienta de nodeJS que buscará el paquete en el proyecto local, ya que si no lo tenemos instalado como global, puede que no lo encuentre escribiendo el comando sin npx:

$ npx es-dev-server --open --watch --root-dir src

En primer lugar, hay que tener en cuenta varios detalles:

  • Con --root-dir src indicamos src como carpeta pública. No se servirá nada fuera de ahí.
  • Con --app-index index.html indicamos que index.html es nuestro punto de entrada para cualquier ruta. Ahora mismo quizás no nos interesa, pero esto es muy interesante para emular facilmente una SPA y redireccionar rutas.
  • Con --base-path /ruta podríamos simular una ruta URL prefijo, interesante en contextos como GitHub Pages, donde tenemos que usar una ruta obligatoriamente.

Hasta aquí todo bien, sin embargo, podemos tener problemas particulares desde que comencemos a utilizar npm para instalar dependencias que usemos en nuestra página. Vamos a detallarlos en los siguientes apartados.

Bare imports (Paquetes de node)

Los bare imports (imports «desnudos») son aquellos que hacemos desde Javascript y no indicamos una ruta al cargarlos, sino que solo indicamos el nombre del paquete. Estos bare imports no forman parte del estándar, sino que fue una mecánica de node para hacer más cómoda la importación de paquetes desde la carpeta node_modules (donde las busca):

// Import por defecto (ruta relativa)
import AppElement from "./components/AppElement.js";

// Imports específicos (ruta relativa desde el raíz)
import { getData, getObject } from "/js/library.js";

// Imports específicos (desde una ruta absoluta)
import { html, render } from "https://unpkg.com/[email protected]";

// Bare imports específicos (desde node_modules)
import { html, render } from "lit-html";

// Direct code import (ruta relativa)
import "./components/AppElement.js";

Por lo tanto, si intentamos realizar un bare import desde un navegador, probablemente nos aparezca algo parecido al siguiente mensaje, ya que detecta que no le estamos indicando una ruta:

Uncaught TypeError: Failed to resolve module specifier "package". Relative references must start with either "/", "./", or "../".

La filosofía de es-dev-server es ir resolviendo estos problemas y «vicios» que se alejan del estándar, proporcionando una forma de transición para ir resolviéndolo mientras nos adaptamos a las nuevas formas de trabajar. Con el parámetro --node-resolve lo podemos resolver, ya que se encarga de reemplazar dichos bare imports por su equivalente en rutas:

import { html, render } from "./node_modules/lit-html/lit-html.js";

Sin embargo, con esto aparece otro problema. Al haber aplicado el parámetro --root-dir src hemos definido por seguridad que todos los ficheros que pida el navegador deben estar dentro de la carpeta src, nunca fuera de ella. Como queremos que los paquetes de /node_modules/ también los cargue, debemos buscar una forma de solucionarlo.

Podríamos probar diferentes configuraciones, meter la carpeta node_modules en src, utilizar la característica middleware para reescribir rutas (que veremos más adelante), etc. Pero probablemente la más sencilla sea crear un enlace simbólico que apunte a nuestra carpeta node_modules. Para ello, escribimos desde la carpeta raíz del proyecto:

$ ln -s node_modules src/node_modules

Recuerda que para crear enlaces simbólicos con ln necesitas utilizar un sistema basado en Linux. Si usas Windows, te recomiendo instalar WSL, el subsistema de Linux en Windows.

Convertir CommonJS a ESM

Con lo que hemos hecho hasta ahora debería ser suficiente para poder utilizar librerías en nuestras páginas, sin embargo, esto dependerá de que la librería utilice un sistema de módulos ESM (estándar Javascript) o un sistema de módulos CommonJS (Node).

CommonJS vs ESM

En el caso de que la librería implemente el primero (ESM), como por ejemplo lit-html, no tendríamos problemas al utilizarlo en nuestra página. Sin embargo, si utilizamos otra librería que sólo implemente el segundo (CommonJS), como dayjs, obtendríamos un error similar al siguiente:

Uncaught SyntaxError: The requested module '../node_modules/dayjs/dayjs.min.js' does not provide an export named 'default'

Presumiblemente, con el tiempo, las librerías irán implementando el estándar ESM pero si de momento queremos utilizar una librería que utiliza módulos CommonJS, podemos instalar el siguiente plugin:

$ npm install -D es-dev-commonjs-transformer

Una vez instalado, creamos un fichero de configuración es-dev-server.config.js en la carpeta raíz de nuestro proyecto (hablaremos de esto más adelante), donde escribiremos el siguiente contenido. Básicamente se trata de añadir la propiedad responseTransformers a un module.exports. Esto nos permitirá convertir los módulos CommonJS a ESM y hacerlos compatibles con el navegador:

const cjsTransformer = require("es-dev-commonjs-transformer");

module.exports = {
  responseTransformers: [cjsTransformer()],
};

Ahora vamos a hacer una prueba y con las dos librerías mencionadas anteriormente: dayjs (CommonJS) y lit-html (ESM). Lo primero, las instalamos con npm y ejecutamos nuestro servidor local es-dev-server, sin olvidarnos de aplicar el parámetro --node-resolve:

$ npm install -D lit-html dayjs

$ npx es-dev-server --open --watch --node-resolve --root-dir src

Posteriormente, modificamos nuestro archivo index.js para importar las dos librerías y utilizarlas. Lo que haremos en el siguiente fragmento de código es crear una fecha con dayjs y renderizarla en un <div id="app"></div> que colocaremos en nuestro HTML:

import { render, html } from "lit-html";
import dayjs from "dayjs";

const app = document.querySelector("#app");
const day = dayjs().format("YYYY-MM-DD HH:mm:ss");
const template = html`<p>¡Hola a todos! ${day}</p>`;
render(template, app);

Si todo ha ido bien, debería aparecernos un saludo, seguido de la fecha actual.

Parámetros de transpilación

Aunque en el apartado anterior sólo usamos el parámetro --node-resolve, existen varios parámetros más que podemos utilizar y que tienen relación con la modalidad de transpilación, donde incluso podemos utilizar transformaciones de babel:

Parámetro Descripción
--node-resolve -n Traduce los bare imports a rutas de node_modules.
--module-dirs paths -m Indica rutas para buscar módulos. Por defecto, node_modules.
--dedupe -d Unifica los paquetes con diferentes versiones en un solo paquete.
--preserve-symlinks Mantiene enlaces simbólicos (por ej. npm link). Por defecto, off.
--babel -b Transforma el código con babel. Requiere un .babelrc.
--babel-exclude paths Ficheros (patrones) a excluir de la transformación de babel.
--babel-modern-exclude paths Idem, pero sólo en navegadores modernos.
--file-extensions paths Extensiones de ficheros (patrones) a aplicar con babel.
--compatibility level Modo para garantizar compatibilidad en navegadores antiguos.
--debug Activa el modo verbose y muestra información detallada.
--config file -c Usa un fichero de configuración en lugar de parámetros CLI.

Mención especial al parámetro --compatibility, que nos permite activar el servidor local con cierto grado de compatibilidad hacia navegadores antiguos, realizando ciertas tareas dependiendo del valor indicado.

Las opciones posibles son:

  • El valor auto considera navegadores modernos a las últimas 2 versiones de los navegadores populares, en los que no hace cambios. En navegadores antiguos, utiliza @babel/preset-env aplicando opciones concretas dependiendo del agente de usuario detectado. Si el navegador no soporta ESM, utiliza systemjs. Ofrece compatibilidad incluso en IE11.

  • El valor always es idéntico al anterior, pero no considera navegadores modernos y le aplica los criterios descritos anteriormente a todos.

  • El valor min aplica sólo los cambios necesarios para garantizar compatibilidad en las últimas 2 versiones de navegadores populares.

  • El valor max compila todo a Javascript utilizando módulos systemjs.

  • El valor none desactiva por completo los sistemas de compatibilidad.

Fichero de configuración

Cuando estamos haciendo pruebas (o tenemos pocas opciones que indicar) es interesante usar los parámetros mediante la línea de comandos, como hemos visto hasta ahora. Sin embargo, desde que estos parámetros crecen en número, es muy probable que nos resulte mucho más cómodo crear un fichero de configuración llamado es-dev-server.config.js en la carpeta raíz del proyecto e indicar todos los parámetros de configuración ahí, ejecutando posteriormente con un simple npx es-dev-server.

Dicho archivo de configuración pueden indicarse tanto los parámetros de configuración que hemos visto anteriormente, como algunos nuevos que nos permiten personalizar el funcionamiento de es-dev-server:

module.exports = {
  port: "1234",       // Usamos el puerto 1234 en lugar de 8000
  watch: true,        // Vigilamos cambios (actualiza automágicamente)
  appIndex: "index.html", // Routing SPA (los 404 envían a esa ruta)
  nodeResolve: true,  // Resolvemos paquetes a node_modules
  open: true          // Abrimos la página principal
  debug: false,       // Se puede activar modo verbose
  http2: true         // Utilizamos HTTPS en lugar de HTTP
  rootDir: "src"      // Utilizamos esta carpeta raíz
}

Entre esos nuevos parámetros para realizar ciertas tareas de más bajo nivel, se encuentran algunos detalles interesantes como por ejemplo:

  • Middlewares: tareas que se realizan antes de procesar las peticiones.
  • Plugins: mecanismos concretos para ampliar funcionalidades.
  • Babel: transformaciones automáticas de código.
  • Polyfills: Parches para añadir funcionalidades inexistentes en navegadores antiguos.
Parámetro Descripción
middlewares Incluye tareas de middleware a realizar, como las redirecciones citadas anteriormente.
plugins Permite añadir plugins que extiendan la funcionalidad de es-dev-server.
babelConfig Permite indicar una configuración personalizada de babel.
polyfillsLoader Si la compatibilidad está activada, se pueden añadir polyfills.

Una muestra de middlewares podría ser el siguiente ejemplo teórico, donde indicamos que cualquier ruta que termine en .pcss la reescriba como .pcss.css:

module.exports = {
  /* ... */
  rootDir: "src",
  middlewares: [
    function rewriteIndex(context, next) {
      if (context.url.endsWith(".pcss")) {
        context.url = `${context.url}.css`;
      }
      return next();
    },
  ],
};

Recuerda que todo lo que abarca este artículo es para el servidor local de desarrollo, y por lo tanto, son operaciones que se realizan en nuestro equipo local. El objetivo es acelerar lo máximo la carga de archivos en el navegador, fomentar el estándar y evitar en lo posible añadir código de herramientas no estándar y fomentar el uso del caché del navegador para evitar esperas y acelerar el desarrollo.

En otro artículo abordaremos buenas prácticas para generar nuestros archivos de producción, que son los que finalmente se suben al servidor definitivo para ser accedido por el público. Por norma general, junto a es-dev-server se suele utilizar RollUp, ya que es uno de los automatizadores que mejor preparado está para ESM.

En el caso de querer utilizar otro nombre para nuestro archivo de configuración (por ejemplo, server.config.js), podemos lanzar el servidor local con npx es-dev-server -c server.config.js.

Manz
Publicado por Manz

Docente, divulgador informático y freelance. Autor de Emezeta.com, es profesor en la Universidad de La Laguna y dirige el curso de Programación web FullStack y Diseño web FrontEnd de EOI en Tenerife (Canarias). En sus ratos libres, busca GIF de gatos en Internet.