Lo ideal es que los proyectos puedan importar los componentes que hemos desarrollado y que estos queden recogidos en un fichero independiente que pueda ser mantenido de forma eficiente. Aunque hay varias soluciones fuera de los estándares, también tenemos algunas opciones soportadas por Javascript e implementadas por los navegadores de forma nativa.
Pero antes de llegar a estas soluciones, vamos a ver lo que pudo ser y no fue en los componentes web: el HTML import:
HTML import
Originalmente, la especificación de los Web Components estableció un mecanismo de importación por medio de la etiqueta link
. Para ello añadió el atributo rel="import"
y utilizaba el atributo href
para apuntar a un fichero HTML donde teníamos las etiquetas template
con la plantilla y script
con el código del componente.
Lo cierto es que resultaba extremadamente sencillo de utilizar. Normalmente estas etiquetas se incluyen en el elemento head
y quedan más o menos de esta forma:
<head> <meta charset="UTF-8";> <title>HTML Import</title> <link ref="import" href="./components/my-component.html"> </head>
Poco duró esta aproximación, en estos momentos se considera una funcionalidad deprecated, es decir, no recomendada y descontinuada. Todo surgió cuando Mozilla puso en entredicho la idoneidad de esta solución y anunció que no la implementaría en Firefox. Se produjo una reacción en cadena, que ha terminado con el consenso de todos los fabricantes: lo considerarán una mala idea. Incluso Google, que había implementado de forma nativa esta funcionalidad, la retiró en Chrome 73 (abril de 2019). Aunque todavía funciona el polyfill desarrollado para dar soporte a esta instrucción, esta es una vía muerta y no debemos insistir en ella.
Es realmente una pena. Una importación basada en HTML permitía a los maquetadores que tienen poco conocimiento de Javascript importar custom elements con sencillez. Las razones de este abandono son complejas, relacionadas con el rendimiento en la carga de los elementos de las páginas, y seguramente Mozilla tenia muchas e importantes razones para negarse a esta funcionalidad, pero -en mi modesta opinión- hemos salido perdiendo todos.
Javascript import
La solución que nos ofrecen desde el consorcio que promueve los Web Components (webcomponents.org) es utilizar la instrucción import
de Javascript (ES6). De esta forma se traslada la importación, desde el ámbito declarativo del HTML, al ámbito del lenguaje de programación.
La instrucción import
permite importar un módulo escrito en Javascript. Hasta ES6 la única forma que teníamos de cargar un programa JS en el navegador era por medio de la etiqueta script
. Con la implementación de ES6, que ya tienen todos los navegadores modernos, podemos utilizar import
para cargar otro programa.
La única restricción que tenemos es que import
sólo se puede incluir en programas que carguemos como módulos, es decir, que el elemento script
tenga la propiedad type="module"
, por ejemplo:
<script type="module" src="./modules/my-module.js">
También puede ser utilizado de esta forma:
<script type="module"> import MyComponent from './modules/my-module.js'; customElements.define('my-component', MyComponent); </script>
La gran virtud es que los elementos definidos como globales dentro del módulo no afectan al contexto global del navegador, es decir, una variable o una clase definida como global en el módulo no queda definida en el objeto window
del navegador, queda retenida en el ámbito del módulo.
Si queremos que un determinado elemento definido en el módulo pueda ser utilizado fuera de él, es decir, que se pueda obtener una referencia en el módulo que lo importa, entonces debe utilizarse la expresión export
. Esta instrucción indica que elementos del módulo son accesibles desde el programa que lo importa.
export default class MyComponent extends HTMLElement { };
Hay algunas otras características de los módulos que seguramente sea interesante que conozcas, como los valores por defecto, cómo aplicar alias a los nombres exportados, etc. Te recomendamos que le des un vistazo a la documentación de MDN para obtener algo más de información.
¿Quién debe hacer la llamada a customElement.define()?
Cuando analizamos customElements a fondo, vimos que, una vez registrado un componente con un nombre concreto, no podemos volverlo a registrar con otra clase. El nombre queda registrado en el contexto general de custom tag y no puede ser reescrito. También vimos que una clase no puede ser utilizada más de una vez para crear varios custom tag, quedando reservada sólo para ese nombre de componente. Por lo tanto, sobre todo en proyectos de cierta envergadura, es importante establecer con claridad quien debe registrar el componente y con que nombre, para evitar conflictos.
Si el componente lo registra el módulo que lo contiene, es decir, el componente se registra a si mismo, este debe encargarse de evitar una carga más de una vez, por ejemplo, porqué dos programas tienen esta dependencia y realizan la importación del mismo fichero. El programador debe dejar claro en la documentación el nombre que se va a registrar y debemos asegurarnos qué no colisiona con otros nombres de nuestros proyectos, por ejemplo, por medio de algún prefijo.
class MyComponent extends HTMLElement { } if (!customElements.get ('generic-component')) { customElements.define ('generic-component', MyComponent); }
En el caso que exportemos la clase y sea el programa que hace la importación quien registra el componente con el nombre que él decida, seguramente debamos asegurarnos que hacemos un nivel adicional de herencia para evitar utilizar la misma clase en más de un componente.
import MyComponent from './modules/my-module.js'; customElements.define('my-component', class extends MyComponent {});
Sinceramente, no tengo claro cuál de los dos sistemas es más adecuado para proyectos grandes. Tengo cierta tendencia a pensar que es mejor exportar la clase y que registre el componente el programa que lo importa decidiendo cómo debe llamarse, pero en la práctica estamos usando más el modelo de cargar el módulo y que él registre el componente con el nombre que su desarrollar ha elegido.
También se puede utilizar script
Aunque la instrucción import
nos parezca muy atractiva, lo cierto es que nada nos impide cargar nuestros componentes con una simple instrucción script
del HTML, especialmente si el componente se registra así mismo.
Lo único que debemos tener en cuenta es se debería comportar como un módulo, es decir, no definir variables u otros elementos en el contexto global. Para controlar esto hemos utilizado en muchas ocasiones el Immediately-Invoked Function Expression (IIFE), es decir, expresiones de invocación directa de funciones, por ejemplo:
(function () { // my component code })();
En ES6 ya no es necesario utilizar una función y podemos utilizar simplemente un par de { }
para envolver nuestro código y utilizar const
y let
para definir las constantes y variables dentro de ese alcance. Quedarán recogidas dentro de la closure definida por el par de corchetes.
{ class MyComponent extends HTMLElement { } if (!customElements.get ('generic-component')) { customElements.define ('generic-component', MyComponent); } }
¿Dónde ponemos nuestro template?
Tal y como adelantamos cuando analizamos las diferentes formas de utilizar plantillas o templates para los componentes web, la limitación que tenemos con la instrucción import
es que sólo importa Javascript, es decir, no podemos utilizarla para importar directamente el elemento template
que contenga la plantilla de nuestro componente.
Hay bastante ejemplos que crean un elemento template
desde Javascript, pero como ya explicamos, es mejor utilizar las alternativas que ofrecen los template string o crear los elementos que necesitemos directamente con document.createElement()
. No tiene demasiada utilidad es crear el elemento template
y luego clonarlo, aunque es perfectamente posible.
En cualquier caso, todo el código de nuestro componente queda dentro del código, en un fichero .js
o .mjs
(que es cómo se suele identificar a los Módulos JavaScript) y no tenemos que “mezclar” HTML y Javascript en el fichero de nuestro componente o teniendo que cargar un fichero HTML y otro JS por diferentes medios.
Un momento, yo he visto utilizar import con ficheros CSS y HTML
Hay herramientas como WebPack que definen loaders específicos para cada tipo de fichero y permite que se puedan cargar con import
ficheros .html
, .css
e incluso .vue
o .jsx
. Todo esto es parte de la mágia que hacen este tipo de herramientas, pero están muy alejadas del estándar y del soporte nativo por los navegadores.
En el comité TC39, el que estandariza el Javascript, se está discutiendo sobre la posibilidad de utilizar la instrucción import
para cargar JSON, CSS o HTML. Es todavía una discusión preliminar, por lo que no podemos esperar a tener este tipo de soluciones como parte del estándar e implementadas en los navegadores hasta dentro de bastante tiempo.
Por supuesto podemos utilizar herramientas de compilación que resuelvan la importación, lo hacen de forma muy eficiente. Prácticamente todos los frameworks modernos los utilizan, pero con los web components no es estrictamente necesario la utilización de estos complementos, aunque se pueden utilizar sin problemas.
Loader personalizado
También podemos plantearnos hacer un loader personalizado. No es complicado implementar su propio cargador de componentes con XMLHttpRequest
o fetch
y, por ejemplo, un objeto DOMParse
. Básicamente lo que podemos hacer es escribir en un fichero una etiqueta template
y una etiqueta script
, cargamos este fichero, lo interpretamos como un fragmento HTML e insertamos su contenido en la página.
Realmente son unas pocas líneas de código. Aquí te tienes un ejemplo bastante completo:
export default function loadComponent (file) { return new Promise ((resolve, reject) => { fetch (file) .then (result => { return result.text (); }) .then (txt => { const parser = new DOMParser (); const fragment = parser.parseFromString (txt, 'text/html'); const originalTemplate = fragment.getElementsByTagName ('TEMPLATE')[ 0 ]; let template; if (originalTemplate) { template = document.createElement ('template'); template.innerHTML = originalTemplate.innerHTML; template.id = originalTemplate.id; document.body.appendChild (template); } const originalScript = fragment.getElementsByTagName ('SCRIPT')[ 0 ]; let script; if (originalScript) { script = document.createElement ('script'); script.innerHTML = originalScript.innerHTML; document.body.appendChild (script); } resolve ({template, script}); }) .catch (reject); }); }
Ahora podemos cargar un componente simplemente de esta forma:
<script type="module"> import loadComponent from './loadComponent.js'; loadComponent('./components/wc-countries.html'); </script>
Este es un cargador de ejemplo con una funcionalidad básica. Se pueden añadir otras funcionalidades, como poder disponer de más de una etiqueta template
, es bastante sencillo y puedes adaptar este programa a tus necesidades.
Conclusiones
Aunque fue una decepción el establecimiento como deprecated del HTML import, lo cierto es que el Javascript import permite resolver la carga de los componentes razonablemente, sobre todo cuando tenemos todo el contenido en un solo fichero .js
o .mjs
. Este cambio ha provocado cierta confusión en la comunidad, pero una vez aclarado, sabemos qué podemos utilizar.
Aunque no es del todo necesario, podemos construir nuestro propio cargador de componentes es bastante sencillo y aprovechar esta función para crear nuestra propia estructura de fichero, con los elementos y características que interesen en nuestra organización.
Siempre podemos utilizar alguna de las herramientas de compilación que permiten realizar import
de prácticamente cualquier tipo de fichero. Además de gestionar su carga, nos ayudarán a empaquetar todos los elementos de forma eficiente para la ejecución de nuestro proyecto.
Novedades
HTTP2 para programadores. Enviar mensajes del servidor al cliente con Server Sent Event (sin WebSockets)
En esta charla, organizada por MadridJS, Pablo Almunia nos muestra cómo la mayoría de nosotros cuando oímos hablar por primera vez de HTTP2 nos ilusionamos con las posibilidades que presumiblemente se abrían para el desarrollo de soluciones web avanzadas y cómo muchos nos sentimos defraudados con lo que realmente se podía implementar.
En esta charla podemos ver cómo funciona el HTTP2, que debemos tener en cuenta en el servidor para hace uso de este protocolo y, sobre todo, cómo podemos enviar información desde el servidor al cliente de forma efectiva y fácil. Veremos con detenimiento cómo por medio de los Server-Sent Events (SSE) podemos recibir en el cliente datos enviados desde el servidor sin utilizar websocket, simplificando enormemente la construcción de aplicaciones con comunicación bidireccional.
Observables en Javascript con Proxies
En esta charla, organizada por MadridJS, Pablo Almunia nos habla de la observación reactiva de objetos en Javascript por medio de Proxy. Se describe paso a paso cómo funcionan los Proxies y en que casos pueden ser nuestro mejor aliado. Veremos que no hay que tenerles miedo, son bastante sencillos de utilizar, y nos ofrecen una gran abanico de posibilidades.
Aplicaciones JAMStack, SEO friendly y escalables con NextJS
En esta charla de Madrid JS, Rafael Ventura nos describe las funcionalidades clave de NextJS, nos muestra en vivo cómo desarrollar una completa aplicación JAMStack con Server Side Rendering (SSR) y Static Site Generation (SSG) y termina mostrando como publicar esta aplicación en Vercel.
Stencil JS: mejora el Time To Market de tu producto, por Rubén Aguilera
En esta charla Rubén Aguilera nos cuenta los problemas que tienen muchas empresas a la hora de sacar productos accesibles, vistosos y usables en el Time To Market que requiere Negocio y cómo podemos minimizar este tiempo gracias al DevUI con StencilJS para adecuar una aplicación de Angular a las exigencias del mercado en tiempo record.
Una aproximación bastante buena la de usar fetch para cargar el template de una componente, ya que tristemente ya no es posible usar la etiqueta link para ese propósito.
Sin embargo un tema por resolver con esta aproximación es cuando la template contiene hiperenlaces y en general referencias a otros archivos como imágenes, hojas de estilos, scripts, etc. Se puede dar el caso que la componente pretenda cargar una imagen y obtenga un 404 no encontrado porque la ruta a dicha imagen no es accesible desde la página que incorporó la componente, así por ejemplo:
/index.html
/Components/componente.html (../images/imagen.jpg esta imagen estará rota cuando se use desde index.html)
/Images/imagen.jpg