Proyectos en Javascript#

Puesto que Javascript es un lenguaje “agnóstico” y muy flexible, programar un frontend puede ser una tarea muy libre. Esto da mucho poder para hacer cualquier cosa, pero exige una gran responsabilidad por parte de l@s programador@s. En este capítulo vamos a recopilar algunos consejos para enfocar un proyecto grande con Javascript.

Consejos generales#

Escribir Código Fácil de Leer#

El código se lee muchas más veces de las que se escribe. Si te conviertes en un ‘hacker’ demasiado sofisticado, tendrás que encargarte del proyecto tú solo porque los demás no entenderán tu código. Incluso tú mismo olvidarás cómo has hecho tu código con el tiempo. Por eso, es crucial seguir buenas prácticas para escribir código fácil de leer y mantener.

_images/comic-years.webp

Fig. 6 https://workchronicles.substack.com/p/comic-as-the-years-go-by#

Importancia de los Comentarios#

Los comentarios son esenciales para explicar por qué se hace algo, no solo cómo. Sin embargo, los comentarios no pueden reemplazar un código que ya sea legible por sí mismo. Usa comentarios para agregar contexto o explicar decisiones complejas, pero asegúrate de que el código sea lo más claro y autoexplicativo posible.

Guía de Estilo#

Seguir una guía de estilo es fundamental para mantener la coherencia y legibilidad del código en un equipo de trabajo. Algunas de las guías de estilo más recomendadas son:

Es importante leer varias guías y llegar a un consenso en el equipo de trabajo sobre cuál seguir. También puedes consultar la guía de código limpio para JavaScript de devictoribero.

Si queremos que la guía de estilo nos condiciones permanentemente, podemos usar un linter como EsLint.

Evitar el Código Espagueti#

El código espagueti es difícil de leer, entender y mantener. Aquí hay algunas prácticas para evitarlo:

  • Refactoriza pronto y con frecuencia: No esperes a que el código se vuelva inmanejable. Realiza pequeños ajustes continuamente.

  • Separa la vista del estado: Mantén la lógica de la interfaz de usuario separada de la lógica del estado de la aplicación.

  • Utiliza módulos: Divide tu código en módulos para mantenerlo organizado y reutilizable.

  • Exporta pocas cosas: Los módulos deben exportar solo lo necesario para mantener una interfaz clara y concisa.

  • Variable global para la aplicación: Si necesitas variables globales y no quieres usar algo sofisticado como Redux, considera usar un objeto global para encapsularlas:

    window.app = this;
    

Deuda Técnica#

La deuda técnica es un concepto crítico en el desarrollo de software que se refiere a la cantidad de trabajo adicional que es necesario realizar para modificar un programa debido a la elección de soluciones fáciles o rápidas en lugar de las adecuadas o más sostenibles a largo plazo. En otras palabras, es el costo acumulado de tomar atajos o soluciones temporales que, si bien pueden permitir un desarrollo más rápido inicialmente, generan una mayor carga de trabajo en el futuro.

Características de la Deuda Técnica#

  1. Es Inevitable: Es prácticamente imposible desarrollar software sin incurrir en algún nivel de deuda técnica. Las presiones de tiempo, las limitaciones de recursos y los cambios en los requisitos del proyecto contribuyen a la acumulación de deuda técnica.

  2. Tipos de Deuda Técnica:

    • Intencional: Cuando se toman decisiones conscientes de priorizar la velocidad sobre la calidad, sabiendo que habrá que corregirlo más adelante.

    • Inadvertida: Surge cuando los desarrolladores no se dan cuenta de que están acumulando deuda, debido a la falta de conocimiento o experiencia.

    • Deuda de Arquitectura: Relacionada con la estructura general del software.

    • Deuda de Código: Relacionada con la calidad del código escrito.

    • Deuda de Pruebas: Falta de pruebas suficientes o adecuadas.

  3. Impacto Negativo: La deuda técnica puede hacer que el desarrollo sea más lento, más costoso y más propenso a errores. También puede dificultar la implementación de nuevas funcionalidades y la corrección de errores existentes.

Gestión de la Deuda Técnica#

Gestionar la deuda técnica es crucial para mantener la salud y la sostenibilidad de un proyecto de software. Aquí hay algunas estrategias efectivas:

  1. Identificación y Monitoreo: Es importante identificar y documentar la deuda técnica desde el inicio. Utilizar herramientas de análisis estático y revisiones de código puede ayudar a detectar áreas problemáticas.

  2. Priorización: No toda la deuda técnica necesita ser pagada de inmediato. Priorizar las áreas que tienen el mayor impacto en el rendimiento y la mantenibilidad del software es esencial.

  3. Refactorización Continua: Refactorizar el código regularmente ayuda a reducir la deuda técnica. La refactorización implica reestructurar el código existente sin cambiar su comportamiento externo, mejorando así su estructura y claridad.

  4. Compromiso del Equipo: Todo el equipo de desarrollo debe estar comprometido con la gestión de la deuda técnica. Esto incluye los desarrolladores, los gerentes de proyecto y los stakeholders.

  5. Automatización de Pruebas: Implementar una suite de pruebas automatizadas puede ayudar a detectar problemas temprano y reducir la acumulación de deuda técnica relacionada con la calidad del código.

  6. Desarrollo Ágil: Las metodologías ágiles promueven la entrega continua y la adaptación al cambio, lo cual puede ayudar a gestionar la deuda técnica de manera más efectiva.

Estructura Propuesta de Proyecto#

Una estructura bien organizada facilita la navegación y el mantenimiento del código. Aquí hay una estructura propuesta:

  • Carpeta models: Contiene los modelos de datos.

  • Carpeta pages: Contiene las vistas organizadas por URL.

  • Carpeta views: Contiene los componentes de la vista.

  • Carpeta helpers: Contiene utilidades reutilizables.

  • Carpeta forms: Contiene los formularios.

  • Archivo app.js: Archivo principal de la aplicación.

  • Archivo router.js: Contiene la lógica de enrutamiento.

  • Carpeta libraries: Contiene bibliotecas externas como jQuery.

  • Archivo templates.js: Contiene plantillas de código.

Esta estructura no es rígida y puede ajustarse según las necesidades del proyecto, pero proporciona un buen punto de partida para organizar tu código de manera lógica y eficiente.

Principios de arquitectura#

Encapsular el código#

Encapsular el código es una práctica esencial en el desarrollo de software que implica restringir el acceso a ciertos componentes del código y sólo exponer lo que es necesario. Esto ayuda a mantener el código organizado, modular y más fácil de mantener. Si utilizas módulos correctamente, ya has logrado una encapsulación básica. Sin embargo, hay más técnicas y prácticas que puedes aplicar para mejorar aún más esta encapsulación. Estos son algunos de los motivos por los que buscar la encapsulación:

  1. Evita la Contaminación del Ámbito Global: Encapsular el código impide que las variables y funciones se definan en el ámbito global, reduciendo el riesgo de conflictos de nombres y mejorando la seguridad del código.

  2. Mantenimiento Más Fácil: Al encapsular, puedes modificar internamente las funciones y variables sin preocuparte por afectar otras partes del código, siempre y cuando la interfaz pública permanezca constante.

  3. Modularidad: Encapsular fomenta la creación de módulos independientes, lo que facilita la reutilización y prueba de componentes individuales del código.

  4. Claridad y Legibilidad: Una estructura clara y organizada dentro de cada módulo o función mejora la legibilidad del código, haciendo que sea más fácil de entender y seguir para otros desarrolladores.

  5. Seguridad: Al limitar el acceso a los componentes internos del módulo, se minimizan los riesgos de manipulación no deseada o acceso no autorizado, aumentando la seguridad del código.

Formas de encapsular en Javascript:

  • Encapsulación con IIFE (Immediately Invoked Function Expression): Una técnica eficaz para encapsular el código es utilizar una IIFE (Immediately Invoked Function Expression). Una IIFE es una función que se define y se ejecuta inmediatamente. Esto se hace para crear un ámbito local y evitar la contaminación del ámbito global.

  • Módulos ES6: Exportando lo necesario, se puede encapsular muy eficientemente. Además, mantienen un estado si es necesario.

  • Closures: Tećnica muy utilizada antes de los módulos, puede ser útil a nivel de funciones en técnicas como el Curring.

  • Espacios de Nombres: se trata de crear objetos literales donde añadir atributos.

  • Clases ES6: Mediante clases se pueden encapsular partes del código. Luego con closures o atributos privados se pueden ocultar cosas al exterior.

Dentro de cada función o archivo, es útil seguir una estructura clara y organizada, similar a la utilizada en lenguajes de programación más antiguos. Esta estructura generalmente sigue el orden:

  1. Variables

  2. Métodos o funciones

  3. Eventos

Acoplamiento#

Acoplamiento se refiere a la interdependencia entre diferentes módulos o unidades de software. Un alto grado de acoplamiento implica que los cambios en un módulo pueden afectar a otros, lo que dificulta la modificación y el mantenimiento del sistema. En general, se busca un bajo acoplamiento para mejorar la modularidad y flexibilidad del código.

Ejemplo de Alto Acoplamiento:

// Módulo A
function obtenerDatos() {
  return fetch('https://api.example.com/datos')
    .then(response => response.json());
}

// Módulo B
function procesarDatos() {
  obtenerDatos().then(datos => {
    // Procesa los datos
    console.log(datos);
  });
}

En este ejemplo, el módulo B depende directamente del módulo A para obtener los datos, creando un alto acoplamiento.

Ejemplo de Bajo Acoplamiento:

// Módulo A
function obtenerDatos(apiUrl) {
  return fetch(apiUrl)
    .then(response => response.json());
}

// Módulo B
function procesarDatos(apiUrl) {
  obtenerDatos(apiUrl).then(datos => {
    // Procesa los datos
    console.log(datos);
  });
}

Aquí, el módulo B no depende directamente del módulo A, sino que solo necesita una URL de API. Esto reduce el acoplamiento.

Cohesión#

Cohesión se refiere a cuán estrechamente relacionados y enfocados están los elementos dentro de un módulo. Un módulo altamente cohesivo tiene elementos que están fuertemente relacionados entre sí y juntos realizan una única tarea. Una alta cohesión es deseable porque facilita la comprensión, reutilización y mantenimiento del código.

Ejemplo de Baja Cohesión:

// Módulo con baja cohesión
function modulo() {
  // Calcula algo
  function calcular() {
    // ...
  }

  // Procesa datos
  function procesar() {
    // ...
  }

  // Gestiona la interfaz de usuario
  function gestionarUI() {
    // ...
  }
}

Este módulo realiza múltiples tareas no relacionadas, lo que indica baja cohesión.

Ejemplo de Alta Cohesión:

// Módulo con alta cohesión
function moduloCalculo() {
  // Calcula algo
  function calcular() {
    // ...
  }

  return {
    calcular
  };
}

function moduloProcesamiento() {
  // Procesa datos
  function procesar() {
    // ...
  }

  return {
    procesar
  };
}

function moduloUI() {
  // Gestiona la interfaz de usuario
  function gestionarUI() {
    // ...
  }

  return {
    gestionarUI
  };
}

En este ejemplo, cada módulo tiene una única responsabilidad claramente definida, indicando alta cohesión.

  • Mantenibilidad: Es más fácil hacer cambios en un sistema cuando los módulos están bien definidos y tienen poca interdependencia.

  • Reusabilidad: Los módulos cohesivos y con bajo acoplamiento pueden ser reutilizados en diferentes contextos.

  • Facilidad de prueba: Los módulos con alta cohesión y bajo acoplamiento son más fáciles de probar de forma independiente.

Arquitectura#

La arquitectura de una aplicación frontend se basa en la separación de responsabilidades para lograr un código más limpio, mantenible y escalable. Esta separación permite que los diferentes componentes de la aplicación interactúen de manera organizada. A continuación, se describen los elementos básicos que deben separarse y su función:

  1. Interfaz de Usuario (Vista y Controlador)

    • Vista: Es la parte de la aplicación que interactúa directamente con el usuario, mostrando datos y capturando entradas.

    • Controlador: Gestiona la lógica de la interfaz de usuario, facilitando la comunicación entre la vista y el modelo.

  2. Estado de la Aplicación (Modelo)

    • Modelo: Representa y gestiona los datos y la lógica de negocio de la aplicación.

  3. Lógica (Controlador y Servicios)

    • Controlador: Implementa la lógica de la aplicación, conectando la vista con el modelo.

    • Servicios: Gestionan la comunicación con el servidor y otras fuentes de datos externas.

  4. Comunicación HTTP (Modelo y Servicios)

    • Servicios: Encargados de realizar solicitudes HTTP para obtener y enviar datos al servidor.

  5. Rutas (Router)

    • Router: Gestiona la navegación y las rutas de la aplicación.

Componentes#

Utilización de Modelos#

Es crucial separar el Document Object Model (DOM) de los datos de la aplicación. Entre los datos recibidos en formato JSON y los mostrados en HTML, es necesario tener objetos que gestionen esos datos, conocidos como modelos.

  • Funcionalidad de los Modelos:

    • Validan la integridad de los datos.

    • Obtienen y modifican datos del servidor.

  • Limitaciones de los Modelos:

    • No deben manipular el DOM.

    • No deben manejar eventos.

Vistas#

Las vistas se encargan de la interacción con el DOM. Permiten modificar toda la vista sin alterar los modelos ni el servidor y gestionar eventos sin interferir con los modelos. Muchos frameworks separan las vistas de los modelos y, en algunos casos, también los modelos de los controladores.

  • Método render(): Las vistas suelen tener un método render() que muestra la vista en las plantillas correspondientes y las subvistas si es necesario.

  • Plantillas: Pueden ser <template>, literales o funciones de template literals etiquetados.

  • Comunicación con el Controlador: Puede ser a través de eventos, eventos personalizados o observables.

Controlador#

El controlador conecta inicialmente la vista y el modelo, gestionando la interacción del usuario y modificando el modelo. En el patrón Model-View-Presenter (MVP), el presentador desacopla totalmente la vista del modelo y actúa como intermediario.

  • Funcionalidad del Controlador:

    • Gestiona la interacción del usuario y modifica el modelo.

Servicios (Opcionales)#

Los servicios se encargan de obtener y enviar datos al servidor o al almacenamiento local, permitiendo que los modelos se mantengan manejables y que la lógica de comunicación no afecte al modelo.

  • Funcionalidad de los Servicios:

    • Facilitan la comunicación con el servidor.

    • Permiten cambiar de servidor, protocolos o APIs sin afectar al modelo.

    • Pueden gestionar el estado de la aplicación.

  • Ventajas:

    • Mantienen los modelos ligeros y enfocados en la gestión de datos locales.

    • Desacoplan la lógica de negocio de la lógica de comunicación.

Rutas (Routers) en Aplicaciones SPA#

Las Single Page Applications (SPA) presentan un problema importante: no tienen rutas reales en la URL. Esto implica varios problemas:

  • No se puede volver atrás mediante el botón de retroceso del navegador.

  • No se pueden crear enlaces a partes específicas de la aplicación.

  • Es necesario crear rutas artificiales que nuestro código JavaScript pueda entender para mostrar diferentes contenidos.

Para solucionar esto, se implementan routers que pueden o no modificar la URL, pero que igualmente deben proporcionar un sistema para navegar hacia atrás.

  1. Navegación Fluida: Los routers permiten una navegación fluida y natural dentro de una SPA, simulando la navegación de una aplicación multipágina.

  2. URL Amigables: Facilitan la creación de URLs amigables que los usuarios pueden compartir y que los motores de búsqueda pueden indexar.

  3. Gestión del Estado: Permiten gestionar el estado de la aplicación a través de la URL, haciendo posible volver a estados previos o específicos.

  4. Modularidad: Ayudan a mantener el código modular y organizado, separando claramente la lógica de navegación de la lógica de presentación y negocio.

Para ilustrar cómo se puede implementar un router básico en JavaScript Vanilla, consideremos el siguiente ejemplo:

// Establecer una ruta inicial
window.location.hash = "#/login";

// Llamar al router cuando se establece la ruta inicial
router(window.location.hash);

// Agregar un listener para cambios en el hash de la URL
window.addEventListener("hashchange", () => {
  router(window.location.hash);
});

// Definir la función del router
const router = (route) => {
  console.log(route);

  // Rutas con expresiones regulares
  if (/#\/city\/[0-9]+/.test(route)) {
    let cityId = route.split("/")[2];
    CityDetails(cityId);
  } else if (/#\/building\/[0-9]+/.test(route)) {
    let buildingId = route.split("/")[2];
    BuildingDetails(buildingId);
  } else if (/#\/survivor\/[0-9]+/.test(route)) {
    let survivorId = route.split("/")[2];
    SurvivorDetails(survivorId);
  } 
  // Rutas a páginas específicas
  else {
    switch (route) {
      case "#/":
        let home = new Home(); 
        home.renderHome();
        break;
      case "#/survivors":
        let survivors = new SurvivorsPage(); 
        survivors.render(app.container);
        break;
      // Añadir más rutas según sea necesario
      default:
        let notFound = new NotFoundPage();
        notFound.render(app.container);
    }
  }
};

Explicación del Código

  1. Establecer una Ruta Inicial:

    window.location.hash = "#/login";
    

    Se define una ruta inicial #/login que será la primera vista cuando se cargue la aplicación.

  2. Llamar al Router con la Ruta Inicial:

    router(window.location.hash);
    

    Se llama a la función router con la ruta inicial para que la aplicación muestre el contenido correspondiente a esa ruta.

  3. Listener para Cambios en el Hash:

    window.addEventListener("hashchange", () => {
      router(window.location.hash);
    });
    

    Se añade un event listener para escuchar los cambios en el hash de la URL. Cada vez que cambia el hash, se llama a la función router con el nuevo valor del hash.

  4. Definición del Router:

    const router = (route) => {
      console.log(route);
      // Rutas con expresiones regulares
      if (/#\/city\/[0-9]+/.test(route)) {
        let cityId = route.split("/")[2];
        CityDetails(cityId);
      } else if (/#\/building\/[0-9]+/.test(route)) {
        let buildingId = route.split("/")[2];
        BuildingDetails(buildingId);
      } else if (/#\/survivor\/[0-9]+/.test(route)) {
        let survivorId = route.split("/")[2];
        SurvivorDetails(survivorId);
      } 
      // Rutas a páginas específicas
      else {
        switch (route) {
          case "#/":
            let home = new Home(); 
            home.renderHome();
            break;
          case "#/survivors":
            let survivors = new SurvivorsPage(); 
            survivors.render(app.container);
            break;
          // Añadir más rutas según sea necesario
          default:
            let notFound = new NotFoundPage();
            notFound.render(app.container);
        }
      }
    };
    

    La función router toma una ruta como parámetro y determina qué contenido mostrar basándose en esa ruta.

    • Rutas con Expresiones Regulares:

      if (/#\/city\/[0-9]+/.test(route)) {
        let cityId = route.split("/")[2];
        CityDetails(cityId);
      } 
      

      Este bloque de código utiliza expresiones regulares para identificar rutas dinámicas, como #/city/123. Extrae el ID de la ciudad de la ruta y llama a una función CityDetails con ese ID.

    • Rutas a Páginas Específicas:

      else {
        switch (route) {
          case "#/":
            let home = new Home(); 
            home.renderHome();
            break;
          case "#/survivors":
            let survivors = new SurvivorsPage(); 
            survivors.render(app.container);
            break;
          default:
            let notFound = new NotFoundPage();
            notFound.render(app.container);
        }
      }
      

      Este bloque maneja rutas estáticas, como la página de inicio #/ y la página de sobrevivientes #/survivors. Dependiendo de la ruta, se instancia y se renderiza la vista correspondiente.

Implementar routers en JavaScript Vanilla es una técnica esencial para desarrollar aplicaciones SPA. Muchos frameworks modernos como React, Vue y Angular ya incluyen sistemas de enrutamiento avanzados.

Componentes Web#

En el desarrollo de aplicaciones web modernas, la idea de componentes ha ganado una gran popularidad. Los componentes, o widgets, son elementos que encapsulan tanto la vista como la lógica de una parte específica de la aplicación. Son la base de algunos frameworks modernos.

Los componentes web son un conjunto de tecnologías que permiten crear elementos personalizados reutilizables en aplicaciones web, con su propia funcionalidad y estilo encapsulados. Esto significa que puedes definir nuevos elementos HTML, con comportamiento y apariencia específicos, que se pueden utilizar como cualquier otro elemento estándar de HTML. Los componentes, bien hechos, pueden proporcionar los siguientes beneficios:

  1. Encapsulamiento: Los componentes web permiten encapsular completamente el HTML, CSS y JavaScript necesarios para un elemento, evitando conflictos con el resto del código.

  2. Reutilización: Una vez creados, los componentes pueden ser reutilizados en diferentes partes de una aplicación o en múltiples aplicaciones, lo que ahorra tiempo y esfuerzo.

  3. Mantenibilidad: Al tener una lógica encapsulada, los componentes facilitan el mantenimiento del código, ya que los cambios en un componente no afectan al resto de la aplicación.

  4. Modularidad: Permiten dividir la aplicación en partes más pequeñas y manejables, mejorando la organización y estructura del código.

Los componentes web se basan en cuatro tecnologías principales:

  1. Custom Elements: Permiten definir nuevos tipos de elementos HTML.

  2. Shadow DOM: Proporciona encapsulamiento de estilo y estructura para evitar colisiones en el DOM global.

  3. HTML Templates: Permiten definir fragmentos de HTML que se pueden reutilizar.

  4. ES Modules: Proporcionan una manera de organizar y cargar código JavaScript de manera modular.

En el capítulo de Web Component se explican de forma técnica.

Estado de la Aplicación#

El estado de la aplicación es un concepto complicado en el desarrollo de aplicaciones web modernas. Controlar y gestionar correctamente el estado puede hacer la diferencia entre una aplicación mantenible y una que sea difícil de manejar y propensa a errores.

Dónde está el estado#

Si estamos separando las páginas y, dentro de estas, hacemos componentes independientes, estos pueden tener su propio estado. Pueden ser autocontenidos, lo cual puede llevar a varios problemas:

  • Estados compartidos: Puede que varios componentes tengan un estado que se ha de actualizar a la vez. En ese caso, es mejor unirlos en una única variable.

  • Contradicciones en el estado: Puede que el estado está estructurado de forma que se contradiga.

  • Redundancias: Puede que un componente padre tenga una información y también la tengan los hijos y al mismo tiempo esté en otro sitio. El problema es actualizar a la vez todos esos datos.

  • Estados anidados: Puede que unos componentes padres tengan parte del estado y los hijos mantengan otra parte, si es muy profundo ese estado jerárquico es mejor “aplanarlo”.

  • Pasar datos profundamente: Pasar datos de unos componentes a otros mediante propiedades, atributos… puede ser conveniente. El problema es cuando para por muchos niveles que no hacen nada con los datos a parte de enviarlos a sus componentes hijos. Este anti patrón se llama Prop Drilling y hay que evitarlo. En React, por ejemplo, lo evitan con el concepto de context, un objeto que tiene el estado y es accesible desde todos los componentes. Leer: https://plainvanillaweb.com/blog/articles/2024-10-07-needs-more-context/

Separación del Estado#

Es importante separar el estado en dos categorías:

  1. Estado del Servidor: Datos que se obtienen y almacenan en el servidor, como información de usuarios, productos, etc.

  2. Estado del Cliente: Datos que solo existen en el cliente, como la interfaz de usuario, configuraciones temporales, credenciales…

Inmutabilidad#

Trabajar con datos inmutables significa que, una vez creados, los datos no pueden cambiar. En lugar de modificar un objeto o array existente, se crea una nueva versión del mismo con las modificaciones requeridas. Este enfoque ofrece varios beneficios:

  • Facilidad de Depuración: Si los datos no cambian, es más fácil rastrear de dónde provienen los errores.

  • Predictibilidad: Las funciones puras y los datos inmutables hacen que el comportamiento de la aplicación sea más predecible.

  • Evitar Efectos Colaterales: Al no modificar directamente los objetos, se evitan efectos colaterales que puedan afectar otras partes de la aplicación.

El estado debe tener métodos para acceder (getters) y modificar (setters) sus valores. No se debe modificar el estado directamente, ya que esto puede llevar a inconsistencias y errores difíciles de rastrear.

Centralización del Control del Estado#

Centralizar el control del estado significa tener un único lugar donde se maneja el estado de la aplicación. Esto facilita su gestión y mejora la coherencia en cómo se accede y modifica el estado. Algunos enfoques para centralizar el estado incluyen:

  • Espacio de Nombres: Utilizar un objeto global para guardar el estado. Aunque es una solución simple, puede volverse inmanejable a medida que la aplicación crece. Este espacio de nombres puede estar en un Subject de RxJS, por ejemplo, para conseguir reactividad.

  • Servicios: Los servicios centralizan la gestión del estado y pueden ser accesible mediante funciones importadas. Para no hacer que los componentes estén demasiado acoplados a los servicios se puede usar el patrón de Inyección de dependencias.

  • Redux: Una biblioteca para manejar el estado de manera predecible en aplicaciones JavaScript.

Beneficios del Uso de Redux#

Redux es una biblioteca popular para gestionar el estado de aplicaciones JavaScript, especialmente en el contexto de aplicaciones de React. Algunos de sus beneficios incluyen:

  • Estado Centralizado: Todo el estado de la aplicación se almacena en un solo objeto.

  • Inmutabilidad: Se asegura que el estado sea inmutable.

  • Debugging: Herramientas como Redux DevTools facilitan la depuración.

  • Middleware: Facilita la integración de lógica asíncrona y otras funcionalidades adicionales.

A continuación se muestra un ejemplo básico de cómo usar Redux para gestionar el estado de una aplicación de tareas.

import { createStore } from 'redux';

// Estado inicial
const initialState = {
  tasks: [],
  user: null
};

// Reducer
const reducer = (state = initialState, action) => {
  switch (action.type) {
    case 'SET_USER':
      return {
        ...state,
        user: action.payload
      };
    case 'ADD_TASK':
      return {
        ...state,
        tasks: [...state.tasks, action.payload]
      };
    default:
      return state;
  }
};

// Crear el store
const store = createStore(reducer);

// Acciones
const setUser = (user) => ({ type: 'SET_USER', payload: user });
const addTask = (task) => ({ type: 'ADD_TASK', payload: task });

// Usar el store
store.dispatch(setUser({ id: 1, name: 'John Doe' }));
store.dispatch(addTask({ id: 1, title: 'Learn JavaScript', completed: false }));

console.log(store.getState());

I18N (Internacionalización)#

La internacionalización, comúnmente abreviada como I18N (debido a las 18 letras que hay entre la “I” inicial y la “N” final en inglés), es el proceso de diseñar una aplicación de software para que pueda ser adaptada a diferentes idiomas y regiones sin necesidad de realizar cambios en el código base.

Uso de Librerías para I18N#

Existen muchas librerías que facilitan el proceso de internacionalización en aplicaciones web. Algunas de las más populares incluyen:

  • i18next: Una potente librería de JavaScript que proporciona una solución completa para la internacionalización.

  • react-i18next: Una extensión de i18next para aplicaciones React.

  • Polyglot.js: Una librería ligera para manejar la internacionalización en aplicaciones JavaScript.

  • Intl: Una API nativa de JavaScript para la internacionalización.

Ejemplo con i18next#

A continuación se muestra un ejemplo básico de cómo utilizar i18next para internacionalizar una aplicación web:

  1. Instalación:

    npm install i18next
    
  2. Configuración:

    import i18next from 'i18next';
    
    i18next.init({
      lng: 'en', // Idioma por defecto
      debug: true,
      resources: {
        en: {
          translation: {
            "welcome": "Welcome to our application"
          }
        },
        es: {
          translation: {
            "welcome": "Bienvenido a nuestra aplicación"
          }
        }
      }
    });
    
    document.getElementById('output').innerText = i18next.t('welcome');
    
  3. HTML:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Internacionalización</title>
    </head>
    <body>
        <div id="output"></div>
        <button onclick="changeLanguage('en')">Inglés</button>
        <button onclick="changeLanguage('es')">Español</button>
    
        <script src="path/to/your/js/file.js"></script>
    </body>
    </html>
    
  4. Función para cambiar el idioma:

    function changeLanguage(language) {
      i18next.changeLanguage(language, () => {
        document.getElementById('output').innerText = i18next.t('welcome');
      });
    }
    

Aunque existen múltiples librerías que facilitan este proceso, también es posible implementar soluciones personalizadas utilizando variables y funciones en JavaScript. Independientemente del enfoque elegido, la clave está en separar los textos del código y proporcionar mecanismos para cambiar fácilmente entre diferentes idiomas.

Lazy Loading#

Lazy Loading es una técnica utilizada en el desarrollo de software para retrasar la carga o la inicialización de un recurso hasta el momento en que realmente se necesita. Esta técnica es especialmente útil en el desarrollo web, ya que permite optimizar el rendimiento de las aplicaciones, reduciendo el tiempo de carga inicial y la cantidad de recursos consumidos.

En JavaScript, se puede implementar Lazy Loading para cargar módulos de manera dinámica cuando se necesiten. Esto es particularmente beneficioso para aplicaciones grandes y complejas donde no es necesario cargar todos los módulos en el momento de iniciar la aplicación.

Lazy Loading en Vanilla JavaScript#

En JavaScript Vanilla, se pueden cargar módulos de manera dinámica utilizando la sintaxis import(). Esta sintaxis devuelve una promesa que se resuelve con el módulo cargado, permitiendo así cargar solo los módulos necesarios en el momento adecuado.

Ejemplo de Lazy Loading en Vanilla JavaScript#

Supongamos que tenemos un módulo que queremos cargar de manera dinámica:

export class WelcomeModule {
    constructor() {
        console.log("WelcomeModule loaded");
    }
    greet() {
        console.log("Hello from WelcomeModule");
    }
}
document.getElementById('loadModuleBtn').addEventListener('click', async () => {
    const { WelcomeModule } = await import('./welcome/ModuleImpl.js');
    const module = new WelcomeModule();
    module.greet();
});

En este ejemplo, el módulo WelcomeModule se carga solo cuando el usuario hace clic en el botón “Load Module”. Esto significa que el código del módulo no se incluye en la carga inicial de la página, lo que puede mejorar significativamente el tiempo de carga y el rendimiento de la aplicación.

Tanto en JavaScript Vanilla como en frameworks modernos, implementar Lazy Loading es una práctica recomendada para gestionar aplicaciones complejas y de gran escala.