Herramientas de usuario

Herramientas del sitio


informatica:programacion:cursos:programacion_avanzada_javascript:modulos_javascript

Diferencias

Muestra las diferencias entre dos versiones de la página.

Enlace a la vista de comparación

Ambos lados, revisión anteriorRevisión previa
Próxima revisión
Revisión previa
informatica:programacion:cursos:programacion_avanzada_javascript:modulos_javascript [2024/10/29 12:04] – [DEMO: Módulos ES6] tempwininformatica:programacion:cursos:programacion_avanzada_javascript:modulos_javascript [2024/10/30 16:07] (actual) – [Recursos] tempwin
Línea 611: Línea 611:
 Lamentablemente cuando salió ES2015, el cargador de módulos no estaba definido, ni tampoco la API que debía usarse para configurarlo. Esto significaba que **no había en el mercado ningún motor de ECMAScript que soportase los módulos de ES2015 de forma nativa**. Ningún navegador, ni Node.js en ninguna de sus versiones. Afortunadamente **eso ha cambiado y en la actualidad tenemos una especificación completa** del sistema de módulos, y todas las versiones modernas de navegadores //evergreen// ya podemos usarlo, tal y [[https://caniuse.com/#feat=es6-module-dynamic-import|como nos indica caniuse]]: Lamentablemente cuando salió ES2015, el cargador de módulos no estaba definido, ni tampoco la API que debía usarse para configurarlo. Esto significaba que **no había en el mercado ningún motor de ECMAScript que soportase los módulos de ES2015 de forma nativa**. Ningún navegador, ni Node.js en ninguna de sus versiones. Afortunadamente **eso ha cambiado y en la actualidad tenemos una especificación completa** del sistema de módulos, y todas las versiones modernas de navegadores //evergreen// ya podemos usarlo, tal y [[https://caniuse.com/#feat=es6-module-dynamic-import|como nos indica caniuse]]:
  
-<WRAP center round todo 60%> +{{ :informatica:programacion:cursos:programacion_avanzada_javascript:21-es6-dynamic-module-soporte-caniuse.png |}}
-soporte de módulos ES6 dinámicos +
-</WRAP>+
  
 La imagen anterior es para el soporte de módulos dinámicos, que son aquellos que se cargan usando el propio lenguaje ECMAScript, a través de la palabra clave ''import'' (de una forma similar a como usábamos ''require'' en módulos CommonJS). Pero el sistema de módulos de ES2015 va mucho más allá y **se soporta también la carga de módulos usando etiquetas ''<script>''**. De nuevo [[https://caniuse.com/#feat=es6-module|caniuse nos muestra el soporte]] de esta otra característica: La imagen anterior es para el soporte de módulos dinámicos, que son aquellos que se cargan usando el propio lenguaje ECMAScript, a través de la palabra clave ''import'' (de una forma similar a como usábamos ''require'' en módulos CommonJS). Pero el sistema de módulos de ES2015 va mucho más allá y **se soporta también la carga de módulos usando etiquetas ''<script>''**. De nuevo [[https://caniuse.com/#feat=es6-module|caniuse nos muestra el soporte]] de esta otra característica:
  
-<WRAP center round todo 60%> +{{ :informatica:programacion:cursos:programacion_avanzada_javascript:21-es6-script-module-soporte-caniuse.png |}}
-soporte de módulos ES6 con etiqueta script +
-</WRAP> +
 ==== Definiendo un módulo ECMAScript 2015 ==== ==== Definiendo un módulo ECMAScript 2015 ====
  
Línea 919: Línea 914:
 ==== Exports nombrados ==== ==== Exports nombrados ====
  
 +Una de las limitaciones existentes tanto en CommonJS como en AMD es la imposibilidad de exportar más de un valor. En CommonJS se exporta siempre el valor de ''module.exports'' y en AMD se exporta lo que se devuelva en la función que contiene el módulo. Por supuesto puedes exportar un objeto que contenga tres funciones pero no puedes exportar las tres funciones por separado. El sistema de módulos de ECMAScript 2015 sí que lo permite.
 +
 +Lo habitual es que cada módulo exporte un valor por defecto, ya hemos visto como: usando ''export default''. Pero un módulo puede **exportar otros valores además del valor por defecto** o incluso no exportar valor por defecto y exportar otros valores. A los otros valores exportados por un módulo se les llama "**exports nombrados**".
 +
 +Quizá te puede parecer innecesario que un módulo pueda exportar varios elementos. Pero tiene mucha utilidad si adoptas un punto de vista de desarrollo más funcional. En este paradigma tus módulos pueden ser colecciones de funciones relacionadas, puedes exportarlas todas y luego cada módulo cliente puede elegir exactamente cuáles quiere importar.
 +
 +Vamos a ver un ejemplo de un módulo que exporte dos funciones. Asumamos que el código está en ''math_functions.js'':
 +
 +<code javascript>
 +export function add(...a) {
 +    let accum = 0;
 +    for (let v of a) {accum += v;}
 +    return accum;
 +}
 +
 +export function substract(start, ...a) {
 +    let accum = start;
 +    for (let v of a) {accum -= v;}
 +    return accum;
 +}
 +</code>
 +
 +Observa cómo se usa ''export'' sin ''default'' para importar las dos funciones. A esos exports los llamamos "exports nombrados".
 +
 +Recuerda: un módulo puede exportar tan solo un valor por defecto y todos los nombrados que desee.
 +
 +==== Importación de exports nombrados ====
 +
 +¿Cómo podemos importar un export nombrado? Pues seleccionamos todos aquellos que queremos exportar y aplicamos la desestructuración:
 +
 +<code javascript>
 +import {add} from './math_functions';        // importamos el export nombrado 'add'
 +let r1 = add(1,2,3);
 +let r2 = substract(10,1,2,3);       // Error: substract no está definido
 +</code>
 +
 +Observa ahora por un lado que la función ''substract'' no está definida dentro del segundo módulo ya que no la hemos importado. Y por otro lado, ahora la función ''add'' se ha importado dentro del //namespace// del propio módulo.
 +
 +Si quisiéramos importar tanto ''add'' como ''substract'' podemos hacerlo en una sola sentencia ''import'':
 +
 +<code javascript>
 +import {add, substract} from './math_functions';
 +</code>
 +
 +==== Asignar un nombre local a los exports nombrados ====
 +
 +Al importar un ''export'' nombrado el nombre se preserva, por lo que en el módulo que importa el elemento se llamará tal y como se llamaba en el módulo importado (en nuestro ejemplo ''add''). ¿Hay alguna manera de cambiarlo? Pues la respuesta es que sí, y se debe usar esa sintaxis especial:
 +
 +<code javascript>
 +import {add as sum} from './math_functions';        // importamos el export nombrado 'add'
 +let r1 = sum(1,2,3);
 +let r2 = add(1,2,3);       // Error: add no está definido
 +</code>
 +
 +La clave es el ''as sum''. Eso asigna el nombre ''sum'' al ''export'' nombrado ''add''. Así en nuestro módulo ahora debemos usar siempre ''sum'' ya que a todos los efectos ''add'' no existe.
 ==== DEMO: Exports nombrados ==== ==== DEMO: Exports nombrados ====
  
 +Usaremos el entorno que creamos antes para ejecutar módulos ES6.
 +
 +Fichero ''math.js'':
 +
 +<code javascript>
 +// Función que suma todos los parámetros
 +export function sum(...a) {
 +    let accum = 0;
 +    
 +    for (let v of a) {
 +        accum += v;
 +    }
 +    
 +    return accum;
 +}
 +
 +// Cálculo de la media de los valores
 +export function avg(...a) {
 +    if (a.length == 0) {
 +        return 0;
 +    }
 +    return sum(...a) / a.length;
 +}
 +</code>
 +
 +Ya tenemos listos los módulos para importarlos. Creamos ahora el fichero ''main.js'' con:
 +
 +<code javascript>
 +import {sum, avg} from './math';
 +
 +console.log(sum(1, 2, 3));
 +
 +console.log(avg(1, 2, 3));
 +</code>
 +
 +Creamos el //bundle//:
 +
 +<code bash>
 +npm run bundle
 +</code>
 +
 +Iniciamos un servidor web local (podemos usar [[https://www.npmjs.com/package/local-web-server|local-web-server]], de Node.js):
 +
 +<code bash>
 +ws -p 5000
 +</code>
 +
 +Y navegamos a ''%%http://localhost:5000%%''. Nos fijamos en la pestaña **Consola** de las herramientas para desarrolladores.
 +
 +Podríamos cambiar el nombre de lo que importamos de los módulos. Por ejemplo, cambiando el nombre de ''avg'' a ''average'':
 +
 +<code javascript>
 +import {sum, avg as average} from './math';
 +
 +console.log(sum(1, 2, 3));
 +
 +console.log(avgerage(1, 2, 3));
 +</code>
 ==== Importar todos los exports ==== ==== Importar todos los exports ====
 +
 +Cuando un módulo de ECMAScript 2015 tiene varios "exports nombrados" podemos importarlos todos de golpe usando el comodín ''*'' (asterisco) en el ''import'':
 +
 +<code javascript>
 +import * as mf from './math_functions';
 +let v1 = mf.add(1,2,3,4);
 +let v2 = mf.substract(10,1,2,3);
 +</code>
 +
 +**Cuando se usa el asterisco debe indicarse forzosamente un espacio de nombres**, mediante el uso de ''as''. En este ejemplo **todos los exports nombrados del módulo ''math_functions.js'' se incorporarán en el espacio de nombres ''mf''**. De ahí que usemos ''mf.add'' y ''mf.substract'' en lugar de ''add'' y ''substract'' directamente.
 +
 +==== Exports nombrados y por defecto ====
 +
 +Un módulo ECMAScript **puede tener un export por defecto y tantos exports nombrados como desee**. Cuando sea el caso debemos tener presente que:
 +
 +  * ''import m from './module''' importará el export por defecto del modulo ''e2''
 +  * ''import m, {e1 as y1,e2 as y2} from './module''' importará el export por defecto del modulo ''e2'' con el nombre local ''y2''
 +  * ''import m, * as x from './module''' importará el export por defecto del modulo ''module.js'' y le asignará el nombre local ''m'' y también importará todos los exports nombrados (uso de ''*'') dentro del espacio de nombres ''x''
 +
 +Como se puede ver, ¡la sentencia ''import'' es realmente flexible y potente!
  
 ==== DEMO: Uso de import con comodín ==== ==== DEMO: Uso de import con comodín ====
  
 +Tenemos el módulo ''math.js'' que exporta 2 elementos nombrados: ''sum'' y ''avg''.
 +
 +<code javascript>
 +export function sum(...a) {
 +    let accum = 0;
 + 
 +    for (let v of a) {
 +        accum += v;
 +    }
 + 
 +    return accum;
 +}
 + 
 +export function avg(...a) {
 +    if (a.length == 0) {
 +        return 0;
 +    }
 +    return sum(...a) / a.length;
 +}
 +</code>
 +
 +En el fichero ''main.js'' podremos hacer lo siguiente para importar los dos:
 +
 +<code javascript>
 +import * as m from './math';
 +
 +console.log(m.sum(1,2,3));
 +
 +console.log(m.avg(1,2,3));
 +</code>
 +
 +Creamos el //bundle//:
 +
 +<code bash>
 +npm run bundle
 +</code>
 +
 +Iniciamos un servidor web local (podemos usar [[https://www.npmjs.com/package/local-web-server|local-web-server]], de Node.js):
 +
 +<code bash>
 +ws -p 5000
 +</code>
 +
 +Y navegamos a ''%%http://localhost:5000%%''. Nos fijamos en la pestaña **Consola** de las herramientas para desarrolladores.
 ==== ¿Qué podemos exportar? ==== ==== ¿Qué podemos exportar? ====
  
 +**Un módulo ECMASCript 2015 puede exportar** (ya sea de forma nombrada o por defecto) **cualquier tipo de declaración**. Podemos exportar pues:
 +
 +  * Variables y/o constantes que pueden contener objetos, valores simples o funciones (incluyendo generadores)
 +  * Clases
 +  * Funciones
 +
 +==== Exports anónimos ====
 +
 +Los exports por defecto pueden ser anónimos. Eso significa que podemos exportar sin necesidad de colocar nombre alguno a lo que estamos exportando. Eso solo podemos hacerlo en los exports por defecto (ya que el módulo que importa siempre indica un nombre local para los exports por defecto). **Los exports nombrados siempre deben tener nombre**.
 +
 +Por lo tanto el siguiente código es correcto (supongamos que está en el fichero ''export''. Así en lugar de tener el siguiente código:
 +
 +<code javascript>
 +export function foo() {}
 +export class Bar {}
 +</code>
 +
 +Podemos tener el siguiente código:
 +
 +<code javascript>
 +function foo() {}    // No hay export
 +class Bar {}    // No hay export
 +// exportamos ambos exports nombrados de golpe
 +export {foo, Bar};
 +</code>
 +
 +En ambos casos el resultado es el mismo (dos exports nombrados (''foo'' y ''Bar'')). Usa la sintaxis que prefieras porque son totalmente equivalentes.
 +
 +La segunda sintaxis usa un enfoque parecido al //revealing module pattern//: se declara todo lo necesario dentro del módulo y al final se exporta todo lo público. En el caso del //revealing// se usa una sentencia ''return'' que devolvía un objeto conteniendo todos los elementos exportados del módulo. En este caso usamos la sentencia ''export''.
 +
 +Acostúmbrate a usar el sistema de módulos de ECMAScript. Recuerda que **hoy ya se soporta de serie en todos los navegadores modernos (e incluso en Node.js)** y en el caso de que debas soportar otros motores puedes usar un transpilador. Así que ¡no hay excusa para no utilizar módulos en tus aplicaciones! aumenta la legibilidad y mantenibilidad de nuestro código al permitir una mayor organización. El sistema de módulos de ES6 es lo suficientemente flexible y potente como para ser usado tanto en pequeños desarrollos como en grandes proyectos. No dudes en experimentar con él y en acostumbrarte a sus posibilidades... ¡pronto descubrirás que no puedes vivir sin él!
 ==== Utilizar módulos desde HTML ==== ==== Utilizar módulos desde HTML ====
 +
 +Hasta ahora has visto como cargar módulos **dinámicamente**, es decir a través del propio lenguaje. Pero los módulos se pueden cargar de forma estática a partir de una etiqueta ''<script>''. El uso más común de esto es utilizar dicha etiqueta para **cargar el módulo inicial desde una página web**. Dicho módulo inicial posteriormente cargará el resto de módulos usando ''import''.
 +
 +Si no existiese este soporte, el sistema de módulos se quedaría "cojo" para aplicaciones web, ya que no tendríamos ningún mecanismo para cargar dicho módulo inicial.
 +
 +=== Cargar un módulo usando <script> ===
 +
 +Para cargar un fichero como si se tratase de un módulo, basta con usar ''%%type="module"%%'' en la etiqueta ''<script>''. Al usar este valor de ''type'' todas las reglas de módulos se aplican a dicho fichero (ya sabes: modo estricto por defecto, ''this'' vale ''undefined'' y las declaraciones son locales al módulo). Por supuesto, eso implica que dicho código puede usar ''import'' y ''export'' para cargar dinámicamente otros módulos y exponer resultados respectivamente.
 +
 +No hay sintaxis definida para "importar" nada de lo que exporten los módulos cargados mediante ''<script>''. Es decir, los valores que exporte un módulo cargado de esta manera, "se pierden", ya que no pueden recogerse. Debes entender que la idea de cargar un módulo con ''<script>'' no es "importar" elementos de dicho módulo, sino **que dicho módulo tenga código ejecutable que interaccione con el DOM de la página**. Dado que estos módulos se están ejecutando en un navegador tienen acceso al contexto global de ese y por lo tanto a ''document''.
 +
 +<WRAP center round important 60%>
 +Importante: Los módulos cargados vía el tag ''<script>'' con el atributo ''%%type="module"%%'' se comportan como si tuvieran el [[https://www.jasoft.org/Blog/post/El-mejor-sitio-para-colocar-mis-scripts-191;Cabecera-cuerpo-carga-asincrona-o-carga-diferida.aspx|atributo ''defer'']] aplicado, es decir **no bloquean el //parser// de HTML mientras el script se carga**, y no se ejecutan hasta que el documento esté cargado por completo.
 +</WRAP>
 +
 +Por lo tanto dado el siguiente código:
 +
 +<code html>
 +<script type="module" src="1.js"></script>
 +<script src="2.js"></script>
 +<script defer src="3.js"></script>
 +</code>
 +
 +El orden de ejecución de los scripts es ''2.js'', ''1.js'' y ''3.js''.
 +
 +=== El atributo "nomodule" ===
 +
 +De igual modo que ahora disponemos de ''%%type="module"%%'' para indicar al navegador que cargue un determinado fichero ECMAScript como un módulo, también hay el atributo ''nomodule'', que se usa **para indicar que, si el navegador soporta módulos, entonces NO cargue dicho script**. De este modo podemos tener dos versiones de nuestro código:
 +
 +  * La versión con módulos, que cargaríamos via ''%%<script type="module">%%''
 +  * La versión sin módulos, que cargaríamos con ''<script nomodule>''. Los navegadores que soportan módulos, no cargarán esa versión.
  
 ==== DEMO: Cargar módulos desde HTML ==== ==== DEMO: Cargar módulos desde HTML ====
 +
 +Página ''index.html'' con un cuadro de texto:
 +
 +<code html>
 +<!DOCTYPE html>
 +<head>
 +    <title>Demo cargar módulos</title>
 +    <!-- Como queremos añadir un script JS como módulo, 
 +    usaremos el atributo "type" con el valor "module" para indicarlo
 +    -->
 +    <script type="module" src="./bootstrap.js"></script>
 +</head>
 +<body>
 +    <h1>Demo cargar modulos</h1>
 +    <input type="text" width="50" id="txtOne" value="0"/> <br />
 +    <input type="button" width="50" id="btnInc" />
 +</body>
 +</html>
 +</code>
 +
 +Como vemos, añadimos un módulo inicial, ''bootstrap.js'':
 +
 +<code javascript>
 +import {next} from './inc.js'
 +const txt = document.getElementById('txtOne');
 +const btn = document.getElementById('btnInc');
 +btn.addEventListener('click', () => {
 +    const value = parseInt(txt.value, 10);
 +    txt.value = next(value);
 +}, false);
 +</code>
 +
 +Como vemos, en ese módulo no se exporta nada. Esto pasa con los módulos que se cargan a través de la etiqueta ''<script>''.
 +
 +El módulo ''inc.js'':
 +
 +<code javascript>
 +import sum from './sum.js'
 +const next = i => sum(i,1);
 +export {sum, next};
 +</code>
 +
 +El módulo ''sum.js'':
 +
 +<code javascript>
 +export default (a,b) => a+b;
 +</code>
 +
 +
 +<WRAP center round info 60%>
 +Como hemos visto, ya no es necesario Babel o ningún otro transpilador en las últimas versiones de los navegadores porque soportan los módulos de forma nativa.
 +</WRAP>
  
 ==== DEMO: Soporte de módulos ES6 en NodeJS ==== ==== DEMO: Soporte de módulos ES6 en NodeJS ====
  
 +<WRAP center round important 60%>
 +Este código tiene varios años, es muy probable que ya no funcione igual o que las notas que acompañen esta sección estén desfasadas
 +</WRAP>
  
 +Fichero ''index.mjs'':
 +
 +<code javascript>
 +import {next} from './inc.mjs'
 +const value = 42;
 +console.log(next(value));
 +</code>
 +
 +<WRAP center round info 60%>
 +Node.js obliga que los ficheros de módulos tengan la extensión ''.mjs''.
 +</WRAP>
 +
 +Fichero ''.inc.mjs'':
 +
 +<code javascript>
 +import sum from './sum.mjs'
 +const next = i => sum(i,1);
 +export {sum, next};
 +</code>
 +
 +Fichero ''sum.mjs'':
 +
 +<code javascript>
 +export default (a,b) => a+b;
 +</code>
 +
 +Ejecutamos con Node.js indicando la característica experimental:
 +
 +<code bash>
 +node --experimental-modules index.mjs
 +</code>
  
 +===== Recursos =====
  
 +  * [[https://github.com/systemjs/systemjs|SystemJS]]
 +  * [[https://browserify.org/|browserify]]
 +  * [[https://webpack.github.io/|webpack]]
 +  * [[https://requirejs.org/|RequireJS]]
 +  * [[https://www.npmjs.com/package/local-web-server|NodeJS local web server]]
informatica/programacion/cursos/programacion_avanzada_javascript/modulos_javascript.1730199844.txt.gz · Última modificación: por tempwin