Guía inicial de Web Dev Server

Empaquetador Javascript enfocado en workflows sin compilación


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 web-dev-server, antiguamente conocido como es-dev-server.

Web-dev-server: Servidor local de desarrollo

Utilizaremos web-dev-server por varias razones:

  • 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 web-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 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 web-dev-server de forma global con npm. Para ello, simplemente escribimos:

# Instalamos web-dev-server en el proyecto
$ npm install -g @web/dev-server

Esto instalará el servidor local web-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:

$ web-dev-server --open --watch
Web Dev Server started...

  Root dir: /home/manz/workspace/project
  Local: http://localhost:8000/
  Network: http://192.168.1.1:8000/

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 wds -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 web-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.
--watch -w Vigila los cambios en el código. Si los hay, actualiza el navegador.

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, web-dev-server pretende fomentar arquitecturas inteligentes, reduciendo en desarrollo los procesos de build (transformaciones de código cada vez que este cambia) evitando cambiarlo continuamente, con la pérdida de tiempo que eso conlleva.

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 con 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:

# Creamos carpeta del proyecto
$ mkdir project

# Accedemos a ella
$ cd project

# Inicializamos el proyecto con NPM
$ npm init -y

Al igual que en la modalidad anterior, crearemos una estructura de carpetas. Sin embargo, será ligeramente diferente a la aproximación anterior. Probablemente, en algunas estructuras de carpetas te habrás dado cuenta que existe una carpeta src, donde se suelen incluir todos los archivos de código fuente que manejamos, mientras que los ficheros de configuración o «tooling» se encontrarán fuera.

Cuando tenemos una carpeta src en nuestro proyecto, se sobreentiende que trabajamos en una modalidad de transpilado. En dicha carpeta tenemos nuestro código fuente original en el que trabajamos: un código moderno, que utiliza características que muchas veces los navegadores no soportan.

La filosofía a seguir en la modalidad de transpilado es traducir (transpilar) el código moderno de la carpeta src a otro código de destino, que generalmente se guarda en una carpeta dist, fuera de src. Este código de destino puede variar, siendo generalmente un código más antiguo (y menos legible) compatible con todo tipo de navegadores menos modernos, como Internet Explorer o Safari.

La estructura de carpetas suele parecerse a la siguiente:

+ 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 la carpeta src, mientras que otros archivos como index.html, index.js e index.css se encuentran dentro de src.

Los archivos de tooling o configuración no existirán 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), que es lo que estamos analizando en este artículo. Con web-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 web-dev-server a nivel de proyecto. De esta forma, cualquier desarrollador que descargue el proyecto puede instalarlo con un simple npm install.

Si prefieres hacerlo a nivel de proyecto, escribimos:

# Instalamos web-dev-server en el proyecto
$ npm i --save-dev @web/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. El comando sería el siguiente:

$ npx web-dev-server --watch

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

  • Con --root-dir CARPETA podríamos indicar una carpeta diferente como raíz, y nunca se serviría nada fuera de ahí. En nuestro caso, podríamos indicar la carpeta src.

  • Con --app-index index.html podríamos indicar que index.html es nuestro punto de entrada para cualquier ruta. Ahora mismo no nos interesa, pero esto es muy interesante para emular facilmente una SPA y redireccionar rutas.

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 web-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";

Ten en cuenta que si tuvieramos una carpeta src y hubieramos aplicado el parámetro --root-dir src, habríamos definido por seguridad que todos los ficheros que pida el navegador se encuentren dentro de la carpeta src, y nunca fuera de ella. Como ahora querríamos que los paquetes de /node_modules/ también los cargue, nos daría un error.

Sin embargo, en nuestro caso, el siguiente comando sería suficiente para hacerlo funcionar:

$ npx wds --open --watch --node-resolve

Hasta ahora dependíamos de node para resolver esto. Sin embargo, los bare imports se pueden resolver con una característica nativa llamada import maps, que utilizará un fichero JSON como índice de dependencias:

Recuerda que es muy recomendable utilizar un sistema basado en Linux para trabajar desde la terminal. Si usas Windows, te recomiendo instalar WSL, el subsistema de Linux en Windows.

CommonJS vs ESM

Con lo que hemos hecho hasta ahora debería ser suficiente para poder utilizar librerías en nuestras páginas. Sin embargo, que funcione correctamente dependerá de cómo está hecha la librería en cuestión, y que sistemas de módulos utiliza. Actualmente, los más populares son los dos siguientes:

  • Módulos ESM: Se trata del estándar nativo de Javascript, comenzado a utilizar desde 2015. Actualmente, es soportado por los navegadores, pero aún no al 100% por NodeJS.

  • CommonJS: Se trata del sistema de módulos ideado por NodeJS cuando no existían los módulos ESM nativos. Actualmente, es soportado por NodeJS (obviamente) pero no será soportado nunca por los navegadores. Se trata de un sistema considerado legacy (marcado para ser obsoleto, a favor de 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), 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'

Este error nos aparece al intentar importar la librería dayjs como una librería ESM. Esto ocurre porque dayjs usa CommonJS por defecto. Sin embargo, también soporta ESM, como se puede ver en el siguiente ejemplo:

import dayjs from "dayjs";      // Error: no encuentra modulo con export default
import dayjs from "dayjs/esm";  // OK

Existen repositorios como SkyPack o unpkg que favorecen o permiten utilizar librerías en formato de módulos ESM, pudiendo utilizarlo directamente desde una URL, al estilo Deno. Presumiblemente, con el tiempo, las librerías irán implementando el estándar ESM.

Opciones de transpilación

Aunque en el apartado anterior sólo usamos el parámetro --node-resolve, existen más configuraciones que podemos utilizar y que tienen relación con la modalidad de transpilación, donde incluso podemos utilizar transformaciones de código, como las que habitualmente se han venido haciendo en babel.

Los parámetros de web-dev-server en relación a esto son los siguientes:

Parámetro Descripción
--node-resolve -n Traduce los bare imports a rutas de node_modules.
--esbuild-target TARGET Traduce Javascript a versiones anteriores de ECMAScript (retrocompatibilidad).
--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.

Si uno de nuestros objetivos es dar soporte a navegadores antiguos como Safari, versiones especificas de ECMAScript o incluso de Node, podemos utilizar el parámetro --esbuild-target de web-dev-server.

Esbuild es un automatizador y bundler de Javascript extremadamente rápido, que permite hacer traducciones y transpilado de código a una velocidad asombrosa. Podremos utilizarlo desde web-dev-server para convertir nuestro código Javascript en código Javascript o similar, y conseguir un mejor soporte en navegadores más antiguos.

Para ello, el primer paso será instalar el complemento @web/dev-server-esbuild:

npm install -D @web/dev-server-esbuild

Ahora, al ejecutar, indicamos el parámetro --esbuild-target, indicando a continuación una palabra clave de las que explicaremos a continuación:

$ npx web-dev-server --open --watch --node-resolve --esbuild-target auto

Los parámetros posibles para --esbuild-target son:

  • El valor auto hace que esbuild mire el agente de usuario (identificación del navegador) para saber si debe transformar o no el código Javascript. Si utiliza las últimas versiones de Chrome, Firefox o Edge, omite el proceso de compilación porque no es necesario.

  • El valor auto-always es igual al anterior, con la diferencia que realiza proceso de compilación aunque sea la última versión de un navegador moderno. Esto es útil cuando queremos dar soporte a una característica que ni los navegadores modernos tienen compatibilidad.

  • Un valor por navegador. Con valores como chrome80, safari11, firefox57, edge16, node9 o similares, puedes especificar explícitamente el navegador mínimo al que quieres dar soporte.

  • Un valor por versión de ECMAScript. Con valores como es2020, es2019, es2018, es2017 o es2016 puedes indicar hasta que versión de ECMAScript quieres transformar.

  • El valor esnext desactiva por completo la compilación de esbuild.

Fichero de configuración

Cuando estamos probando web-dev-server (o tenemos pocas opciones) 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 web-dev-server.config.mjs en la carpeta raíz del proyecto e indicar todos los parámetros de configuración ahí, ejecutando posteriormente con un simple npx web-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 web-dev-server:

export default {
  port: 4321,             // Usamos el puerto 4321 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
  preserveSymlinks: true  // Sigue los enlaces simbólicos con nodeResolve
  esbuildTarget: ["auto", "safari11", "chrome80"],  // Compatibilidad JS
  open: true,             // Abrimos la página principal en el navegador
  debug: false,           // Se puede activar modo verbose
  http2: true,            // Utilizamos HTTPS en lugar de HTTP
  sslKey: "./ssl.key",    // Clave privada HTTPS
  sslCert: "./ssl.cert",  // Clave pública HTTPS
  rootDir: ".",           // Utilizamos esta carpeta raíz
  plugins: []             // Plugins a utilizar
}

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.
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 web-dev-server.

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 web-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.mjs), podemos lanzar el servidor local con npx web-dev-server -c server.config.mjs.

¿Quién soy yo?

Soy Manz, vivo en Tenerife (España) y soy streamer partner en Twitch y profesor. Me apasiona el universo de la programación web, el diseño y desarrollo web y la tecnología en general. Aunque soy full-stack, mi pasión es el front-end, la terminal y crear cosas divertidas y locas.

Puedes encontrar más sobre mi en Manz.dev