Un ejemplo de uso práctico de módulos





¡Buen dia amigos!



Los módulos ES6 que utilizan la sintaxis de "importación / exportación" son herramientas bastante poderosas y un competidor digno de los componentes de los marcos populares.



Permítanme demostrarlo dibujando varias formas en un lienzo.



Inspirado en esta sección de la Guía de JavaScript de MDN.



Estas son las funciones que se implementarán en nuestra pequeña aplicación:



  • creación automática de un lienzo de dimensiones específicas y su representación en la página
  • la capacidad de dibujar cuadrados, círculos y triángulos de un tamaño y color determinados en el lienzo
  • dividir el código en módulos que contienen partes lógicas de la aplicación


En el proceso de creación de la aplicación, prestaremos especial atención a la exportación / importación predeterminada y con nombre, así como a las importaciones estáticas y dinámicas. La mayor parte de la aplicación se escribirá utilizando la sintaxis de clase.



Es deseable que tenga al menos un conocimiento básico del trabajo con clases y el lienzo.



El código del proyecto está aquí .



Puede ver una demostración de la aplicación aquí .



Empecemos por el apoyo.











En general bastante bien. En promedio, alrededor del 93%.



La estructura del proyecto será la siguiente (puede crear todos los archivos a la vez o crearlos según sea necesario):



modules
  helpers
    convert.js
  shapes
    circle.js
    square.js
    triangle.js
  canvas.js
index.html
main.js
main.css


El marcado se ve así:



<div>
  <section>
    <h3>Circle</h3>
    <label>
      X:
      <input type="number" value="75" data-prop="x" />
    </label>
    <label>
      Y:
      <input type="number" value="75" data-prop="y" />
    </label>
    <label>
      Radius:
      <input type="number" value="50" data-prop="radius" />
    </label>
    <label>
      Color:
      <input type="color" value="#ff0000" data-prop="color" />
    </label>
    <button data-btn="circle" class="draw_btn">Draw</button>
  </section>

  <section>
    <h3>Square</h3>
    <label>
      X:
      <input type="number" value="275" data-prop="x" />
    </label>
    <label>
      Y:
      <input type="number" value="175" data-prop="y" />
    </label>
    <label>
      Length:
      <input type="number" value="100" data-prop="length" />
    </label>
    <label>
      Color:
      <input type="color" value="#00ff00" data-prop="color" />
    </label>
    <button data-btn="square" class="draw_btn">Draw</button>
  </section>

  <section>
    <h3>Triangle</h3>
    <label>
      X:
      <input type="number" value="150" data-prop="x" />
    </label>
    <label>
      Y:
      <input type="number" value="100" data-prop="y" />
    </label>
    <label>
      Length:
      <input type="number" value="125" data-prop="length" />
    </label>
    <label>
      Color:
      <input type="color" value="#0000ff" data-prop="color" />
    </label>
    <button data-btn="triangle" class="draw_btn">Draw</button>
  </section>
</div>
<button>Clear Canvas</button>

<script src="main.js" type="module"></script>


¿A qué deberías prestar atención aquí?



Para cada forma, se crea una sección separada con campos para ingresar los datos necesarios y un botón para iniciar el proceso de dibujar la forma en el lienzo. En el ejemplo de una sección para un círculo, dichos datos son: coordenadas de inicio, radio y color. Establecemos los campos de entrada en valores iniciales para permitir una prueba rápida del estado de la aplicación. Los atributos "data-prop" están diseñados para obtener los valores de los campos para la entrada en el script. Los atributos "data-btn" se utilizan para determinar qué botón se presionó. El último botón se usa para limpiar el lienzo.



Preste atención a cómo está conectado el guión. Se requiere el atributo "tipo" con el valor "módulo". El atributo "aplazar" no es necesario en este caso, ya que la carga de módulos se aplaza (es decir, después de que la página está completamente cargada) de forma predeterminada. También tenga en cuenta que solo incluimos el archivo "main.js" en la página. Otros archivos se utilizan dentro de "main.js" como módulos.



Una de las características principales de los módulos es que cada módulo tiene su propio alcance (contexto), incluido "main.js". Por un lado, esto es bueno porque evita la contaminación del espacio de nombres global y, por lo tanto, evita conflictos entre variables y funciones del mismo nombre. Por otro lado, si diferentes módulos necesitan acceder a los mismos elementos DOM, por ejemplo, debe crear un script separado con variables globales y conectarlo a la página antes del módulo principal, o crear explícitamente variables globales (window.variable = value), o crear las mismas variables dentro de cada módulo, o intercambiar variables entre módulos (que, de hecho, haremos).



También hay un cuarto enfoque: acceder directamente a los elementos DOM por ID. ¿Conocías esta posibilidad? Por ejemplo, si tenemos un elemento con el identificador "main" en nuestro marcado, podemos referirnos a él simplemente como main (main.innerHTML = "<p> Some Awesome Content <p />") sin primero definir (buscar) el elemento con utilizando "document.getElementById ()" o métodos similares. Sin embargo, este enfoque no es estándar y no se recomienda su uso, ya que no se sabe si será compatible en el futuro, aunque personalmente encuentro esta oportunidad muy conveniente.



Otra característica de los módulos estáticos es que solo se pueden importar una vez. Se ignorará la reimportación.



Finalmente, la tercera característica de los módulos es que el código del módulo no se puede cambiar después de la importación. En otras palabras, las variables y funciones declaradas en un módulo solo se pueden cambiar en este módulo, donde se importan, esto no se puede hacer. Esto recuerda un poco al patrón de diseño del módulo, implementado usando un objeto que contiene variables y funciones privadas, o usando una clase con campos y métodos privados.



Hacia adelante. Agreguemos algunos estilos mínimos:



body {
  max-width: 768px;
  margin: 0 auto;
  color: #222;
  text-align: center;
}
canvas {
  display: block;
  margin: 1rem auto;
  border: 1px dashed #222;
  border-radius: 4px;
}
div {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}
section {
  padding: 1rem;
}
label {
  display: block;
}
input {
  margin: 0.25rem 0;
}
input:not([type="color"]) {
  width: 50px;
}
button {
  margin: 0.25rem auto;
  cursor: pointer;
}
ul {
  list-style: none;
}
li {
  margin: 0.5rem auto;
  width: 320px;
  border-bottom: 1px dotted #222;
}
p {
  margin: 0.25rem 0;
}


Nada especial aqui. Puedes agregar belleza a tu gusto.



Pasemos a los módulos.



El archivo "canvas.js" contiene el código de clase para crear y representar un lienzo, así como una lista de mensajes que se muestran al crear una forma en particular (estos mensajes representan información sobre el área y el perímetro de la forma (convencionalmente)):



// export default      
//           ,  IIFE (   ,    )
//   , ..  class ClassName...,  export default ClassName
export default class Canvas {
  //     :  ,   
  constructor(parent, width, height) {
    this.parent = parent;
    this.width = width;
    this.height = height;
    //   
    this.ctx = null;
    //   
    this.listEl = null;
    //        ,          
    this.clearCanvas = this.clearCanvas.bind(this);
  }

  //     
  createCanvas() {
    //      
    //  ,       
    //        
    if (this.ctx !== null) {
      console.log("Canvas already created!");
      return;
    } else {
      //   "canvas"
      const canvasEl = document.createElement("canvas");

      //         
      //        
      canvasEl.setAttribute("width", this.width);
      canvasEl.setAttribute("height", this.height);

      //     
      this.parent.append(canvasEl);

      //     
      this.ctx = canvasEl.getContext("2d");
    }

    //          
    return this;
  }

  //     
  //        
  createReportList() {
    if (this.listEl !== null) {
      console.log("Report list already created!");
      return;
    } else {
      const listEl = document.createElement("ul");
      this.parent.append(listEl);

      this.listEl = listEl;
    }

    return this;
  }

  //       
  clearCanvas() {
    this.ctx.clearRect(0, 0, this.width, this.height);
    this.listEl.innerHTML = "";
  }
}


El archivo convert.js contiene una función para convertir grados a radianes:



//  
export const convert = (degrees) => (degrees * Math.PI) / 180;


Cada archivo en el directorio de formas es un módulo para una forma particular. En general, el código de estos módulos es idéntico, a excepción de los métodos de dibujo, así como las fórmulas para calcular el área y el perímetro de una forma. Considere un módulo que contiene el código para dibujar un círculo (circle.js):



//    
//     
//       -      "Module"
import { convert } from "../helpers/convert.js";

//  
//         
export class Circle {
  //     "" 
  //     
  //    ,    ""  ctx  listEl
  constructor({ ctx, listEl, radius, x, y, color }) {
    this.ctx = ctx;
    this.listEl = listEl;
    this.radius = radius;
    this.x = x;
    this.y = y;
    this.color = color;
    //  
    this.name = "Circle";
    //   
    this.listItemEl = document.createElement("li");
  }

  //    
  draw() {
    //   
    this.ctx.fillStyle = this.color;
    //  
    this.ctx.beginPath();
    //  arc  6 :
    //     "x",     "y", ,  ,  
    // (      "0, 2 * Math.PI")
    //   ,    :     
    this.ctx.arc(this.x, this.y, this.radius, convert(0), convert(360));
    //  
    this.ctx.fill();
  }

  //        
  report() {
    // 
    this.listItemEl.innerHTML = `<p>${this.name} area is ${Math.round(Math.PI * (this.radius * this.radius))}px squared.</p>`;
    // 
    this.listItemEl.innerHTML += `<p>${this.name} circumference is ${Math.round(2 * Math.PI * this.radius)}px.</p>`;

    this.listEl.append(this.listItemEl);
  }
}


Finalmente, en el archivo "main.js" se realiza una importación estática por defecto del módulo de clase "Canvas", se crea una instancia de esta clase y se manejan las pulsaciones de botones, que consiste en importar dinámicamente el módulo de clase de figura correspondiente y llamar a sus métodos:



//   
//      
import Canvas from "./modules/canvas.js";

//   ,         
//     :
//   ,       
const { ctx, listEl, clearCanvas } = new Canvas(document.body, 400, 300).createCanvas().createReportList();

//    "" 
//    ,      
//      "async"
document.addEventListener("click", async (e) => {
  //     
  if (e.target.tagName !== "BUTTON") return;

  //     
  if (e.target.className === "draw_btn") {
    //   
    //  ,     
    //     
    const { btn: btnName } = e.target.dataset;

    //      
    //      -      
    const shapeName = `${btnName[0].toUpperCase()}${btnName.slice(1)}`;

    //     
    const shapeParams = {};

    //     
    const inputsEl = e.target.parentElement.querySelectorAll("input");

    //  
    inputsEl.forEach((input) => {
      //   
      //   
      const { prop } = input.dataset;
      //   
      //  ,   ,   
      const value = !isNaN(input.value) ? input.valueAsNumber : input.value;
      //      
      shapeParams[prop] = value;
    });
    //          
    shapeParams.ctx = ctx;
    shapeParams.listEl = listEl;

    console.log(shapeParams);

    //  
    //   "Module"
    const ShapeModule = await import(`./modules/shapes/${btnName}.js`);
    //      -  
    //    
    //             "Module" (  )       
    const shape = new ShapeModule[shapeName](shapeParams);

    //   
    shape.draw();
    //         
    shape.report();
  } else {
    //     
    //     "Canvas"
    clearCanvas();
  }
});


Puedes jugar con el código aquí .



Como puede ver, los módulos ES6 brindan posibilidades bastante interesantes relacionadas con la división del código en bloques relativamente autónomos que contienen partes lógicas de la aplicación que se pueden cargar inmediatamente o bajo demanda. Junto con los literales de plantilla, son una buena alternativa a los componentes de los marcos populares. Quiero decir, en primer lugar, renderizar páginas en el lado del cliente. Además, este enfoque le permite volver a renderizar solo aquellos elementos DOM que han sufrido cambios, lo que, a su vez, elimina la necesidad de un DOM virtual. Pero más sobre eso en uno de los siguientes artículos.



Espero que hayas encontrado algo interesante para ti. Gracias por su atención.



All Articles