Importaciones de HTML

Incluir para la Web

¿Por qué realizar importaciones?

Piensa en cómo cargas diferentes tipos de recursos en la Web. Para JS, tenemos <script src>. Para CSS, lo más probable es que uses <link rel="stylesheet">. Para las imágenes, es <img>. El video tiene <video>. Audio, <audio>... ¡ve al grano! La mayoría del contenido de la Web tiene una forma simple y declarativa de cargarse. No es así para el HTML. Estas son tus opciones:

  1. <iframe>: Es una opción comprobada, pero pesada. El contenido de un iframe se encuentra completamente en un contexto independiente del de tu página. Si bien es una función excelente, crea desafíos adicionales (ajustar el tamaño del marco a su contenido es difícil, es muy frustrante escribir secuencias de comandos para ingresar o salir de él y es casi imposible aplicarle diseño).
  2. AJAX: Me encanta xhr.responseType="document", pero ¿dices que necesito JS para cargar HTML? No parece correcto.
  3. CrazyHacks™: Se incorpora en cadenas y se oculta como comentarios (p. ej., <script type="text/html">). ¡Qué asco!

¿Ves la ironía? El contenido más básico de la Web, el HTML, requiere la mayor cantidad de esfuerzo para trabajar con él. Afortunadamente, los componentes web están aquí para ayudarnos a retomar el rumbo.

Cómo comenzar

Las importaciones de HTML, que forman parte del elenco de componentes web, son una forma de incluir documentos HTML en otros documentos HTML. Tampoco estás limitado al lenguaje de marcado. Una importación también puede incluir CSS, JavaScript o cualquier otro elemento que pueda contener un archivo .html. En otras palabras, esto hace que las importaciones sean una herramienta fantástica para cargar archivos HTML/CSS/JS relacionados.

Conceptos básicos

Declara un <link rel="import"> para incluir una importación en tu página:

<head>
    <link rel="import" href="/path/to/imports/stuff.html">
</head>

La URL de una importación se denomina ubicación de importación. Para cargar contenido de otro dominio, la ubicación de importación debe tener habilitado el CORS:

<!-- Resources on other origins must be CORS-enabled. -->
<link rel="import" href="http://example.com/elements.html">

Detección y compatibilidad de funciones

Para detectar la compatibilidad, verifica si existe .import en el elemento <link>:

function supportsImports() {
    return 'import' in document.createElement('link');
}

if (supportsImports()) {
    // Good to go!
} else {
    // Use other libraries/require systems to load files.
}

La compatibilidad con navegadores aún está en pañales. Chrome 31 fue el primer navegador en implementarlos, pero otros proveedores de navegadores están esperando ver cómo funcionan los módulos ES. Sin embargo, para otros navegadores, el polyfill de webcomponents.js funciona muy bien hasta que todo es compatible.

Cómo agrupar recursos

Las importaciones proporcionan una convención para agrupar HTML/CSS/JS (incluso otras importaciones de HTML) en un solo producto final. Es una función intrínseca, pero potente. Si estás creando un tema, una biblioteca o simplemente quieres segmentar tu app en secciones lógicas, es conveniente proporcionarles a los usuarios una sola URL. Incluso puedes entregar una app completa a través de una importación. Piensa en eso por un momento.

Un ejemplo del mundo real es Bootstrap. Bootstrap se compone de archivos individuales (bootstrap.css, bootstrap.js, fuentes), requiere JQuery para sus complementos y proporciona ejemplos de marcado. A los desarrolladores les gusta la flexibilidad a la carta. Les permite aceptar las partes del framework que quieren usar. Dicho esto, apostaría a que el típico JoeDeveloper™ elige la ruta fácil y descarga todo Bootstrap.

Las importaciones tienen mucho sentido para algo como Bootstrap. Te presento el futuro de la carga de Bootstrap:

<head>
    <link rel="import" href="bootstrap.html">
</head>

Los usuarios solo deben cargar un vínculo de importación de HTML. No es necesario que se preocupen por la dispersión de archivos. En cambio, la totalidad del arranque se administra y se encierra en una importación, bootstrap.html:

<link rel="stylesheet" href="bootstrap.css">
<link rel="stylesheet" href="fonts.css">
<script src="jquery.js"></script>
<script src="bootstrap.js"></script>
<script src="bootstrap-tooltip.js"></script>
<script src="bootstrap-dropdown.js"></script>
...

<!-- scaffolding markup -->
<template>
    ...
</template>

Espera un momento. Es muy emocionante.

Eventos de carga o error

El elemento <link> activa un evento load cuando se carga una importación correctamente y onerror cuando falla el intento (p. ej., si el recurso muestra un error 404).

Las importaciones intentan cargarse de inmediato. Una forma fácil de evitar dolores de cabeza es usar los atributos onload/onerror:

<script>
    function handleLoad(e) {
    console.log('Loaded import: ' + e.target.href);
    }
    function handleError(e) {
    console.log('Error loading import: ' + e.target.href);
    }
</script>

<link rel="import" href="file.html"
        onload="handleLoad(event)" onerror="handleError(event)">

O bien, si creas la importación de forma dinámica, haz lo siguiente:

var link = document.createElement('link');
link.rel = 'import';
// link.setAttribute('async', ''); // make it async!
link.href = 'file.html';
link.onload = function(e) {...};
link.onerror = function(e) {...};
document.head.appendChild(link);

Cómo usar el contenido

Incluir una importación en una página no significa "colocar el contenido de ese archivo aquí". Significa "analizador, ve y recupera este documento para que pueda usarlo". Para usar el contenido, debes tomar medidas y escribir una secuencia de comandos.

Un momento crítico de aha! es darse cuenta de que una importación es solo un documento. De hecho, el contenido de una importación se denomina documento de importación. Puedes manipular los elementos de una importación con las APIs de DOM estándar.

link.import

Para acceder al contenido de una importación, usa la propiedad .import del elemento de vínculo:

var content = document.querySelector('link[rel="import"]').import;

link.import es null en las siguientes condiciones:

  • El navegador no admite importaciones HTML.
  • El <link> no tiene rel="import".
  • No se agregó <link> al DOM.
  • Se quitó <link> del DOM.
  • El recurso no está habilitado para CORS.

Ejemplo completo

Supongamos que warnings.html contiene lo siguiente:

<div class="warning">
    <style>
    h3 {
        color: red !important;
    }
    </style>
    <h3>Warning!
    <p>This page is under construction
</div>

<div class="outdated">
    <h3>Heads up!
    <p>This content may be out of date
</div>

Los importadores pueden tomar una parte específica de este documento y clonarla en su página:

<head>
    <link rel="import" href="warnings.html">
</head>
<body>
    ...
    <script>
    var link = document.querySelector('link[rel="import"]');
    var content = link.import;

    // Grab DOM from warning.html's document.
    var el = content.querySelector('.warning');

    document.body.appendChild(el.cloneNode(true));
    </script>
</body>

Secuencias de comandos en importaciones

Las importaciones no están en el documento principal. Son satélites de él. Sin embargo, la importación puede actuar en la página principal, aunque el documento principal sea el principal. Una importación puede acceder a su propio DOM o al DOM de la página que la importa:

Ejemplo: import.html que agrega uno de sus hojas de estilo a la página principal

<link rel="stylesheet" href="http://www.example.com/styles.css">
<link rel="stylesheet" href="http://www.example.com/styles2.css">

<style>
/* Note: <style> in an import apply to the main
    document by default. That is, style tags don't need to be
    explicitly added to the main document. */
#somecontainer {
color: blue;
}
</style>
...

<script>
// importDoc references this import's document
var importDoc = document.currentScript.ownerDocument;

// mainDoc references the main document (the page that's importing us)
var mainDoc = document;

// Grab the first stylesheet from this import, clone it,
// and append it to the importing document.
    var styles = importDoc.querySelector('link[rel="stylesheet"]');
    mainDoc.head.appendChild(styles.cloneNode(true));
</script>

Observa lo que sucede aquí. La secuencia de comandos dentro de la importación hace referencia al documento importado (document.currentScript.ownerDocument) y agrega parte de ese documento a la página de importación (mainDoc.head.appendChild(...)). Es bastante complicado, si me preguntas.

Reglas de JavaScript en una importación:

  • La secuencia de comandos de la importación se ejecuta en el contexto de la ventana que contiene el document de importación. Por lo tanto, window.document hace referencia al documento de la página principal. Esto tiene dos corolarios útiles:
    • Las funciones definidas en una importación terminan en window.
    • no tienes que hacer nada difícil, como agregar los bloques <script> de la importación a la página principal. Una vez más, se ejecuta la secuencia de comandos.
  • Las importaciones no bloquean el análisis de la página principal. Sin embargo, las secuencias de comandos que se encuentran dentro de ellos se procesan en orden. Esto significa que obtienes un comportamiento similar al aplazamiento mientras mantienes el orden correcto de la secuencia de comandos. Sigue leyendo para obtener más información.

Publicación de componentes web

El diseño de las importaciones de HTML se presta muy bien para cargar contenido reutilizable en la Web. En particular, es una forma ideal de distribuir componentes web. Desde <template> HTML básicos hasta elementos personalizados completos con Shadow DOM [1, 2, 3]. Cuando se usan estas tecnologías en conjunto, las importaciones se convierten en un #include para componentes web.

Se incluyen las plantillas.

El elemento HTML Template es una opción natural para HTML Imports. <template> es excelente para crear andamios de secciones de marcado para que la app de importación las use como desee. Unir contenido en un <template> también te brinda el beneficio adicional de que el contenido sea inerte hasta que se use. Es decir, las secuencias de comandos no se ejecutan hasta que se agrega la plantilla al DOM). ¡Increíble!

import.html

<template>
    <h1>Hello World!</h1>
    <!-- Img is not requested until the <template> goes live. -->
    <img src="world.png">
    <script>alert("Executed when the template is activated.");</script>
</template>
index.html

<head>
    <link rel="import" href="import.html">
</head>
<body>
    <div id="container"></div>
    <script>
    var link = document.querySelector('link[rel="import"]');

    // Clone the <template> in the import.
    var template = link.import.querySelector('template');
    var clone = document.importNode(template.content, true);

    document.querySelector('#container').appendChild(clone);
    </script>
</body>

Registra elementos personalizados

Custom Elements es otra tecnología de componentes web que funciona absurdamente bien con las importaciones HTML. Las importaciones pueden ejecutar secuencias de comandos, así que ¿por qué no definir y registrar tus elementos personalizados para que los usuarios no tengan que hacerlo? Llámalo "registro automático".

elements.html

<script>
    // Define and register <say-hi>.
    var proto = Object.create(HTMLElement.prototype);

    proto.createdCallback = function() {
    this.innerHTML = 'Hello, <b>' +
                        (this.getAttribute('name') || '?') + '</b>';
    };

    document.registerElement('say-hi', {prototype: proto});
</script>

<template id="t">
    <style>
    ::content > * {
        color: red;
    }
    </style>
    <span>I'm a shadow-element using Shadow DOM!</span>
    <content></content>
</template>

<script>
    (function() {
    var importDoc = document.currentScript.ownerDocument; // importee

    // Define and register <shadow-element>
    // that uses Shadow DOM and a template.
    var proto2 = Object.create(HTMLElement.prototype);

    proto2.createdCallback = function() {
        // get template in import
        var template = importDoc.querySelector('#t');

        // import template into
        var clone = document.importNode(template.content, true);

        var root = this.createShadowRoot();
        root.appendChild(clone);
    };

    document.registerElement('shadow-element', {prototype: proto2});
    })();
</script>

Esta importación define (y registra) dos elementos: <say-hi> y <shadow-element>. El primero muestra un elemento personalizado básico que se registra dentro de la importación. En el segundo ejemplo, se muestra cómo implementar un elemento personalizado que crea Shadow DOM a partir de un <template> y, luego, se registra a sí mismo.

La mejor parte de registrar elementos personalizados dentro de una importación de HTML es que el importador simplemente declara tu elemento en su página. No se necesitan cables.

index.html

<head>
    <link rel="import" href="elements.html">
</head>
<body>
    <say-hi name="Eric"></say-hi>
    <shadow-element>
    <div>( I'm in the light dom )</div>
    </shadow-element>
</body>

En mi opinión, solo este flujo de trabajo hace que las importaciones de HTML sean una forma ideal de compartir componentes web.

Administra dependencias y subimportaciones

Importaciones secundarias

Puede ser útil que una importación incluya otra. Por ejemplo, si deseas volver a usar o extender otro componente, usa una importación para cargar los otros elementos.

A continuación, se muestra un ejemplo real de Polymer. Es un nuevo componente de pestaña (<paper-tabs>) que reutiliza un componente de diseño y selector. Las dependencias se administran con importaciones HTML.

paper-tabs.html (simplificado):

<link rel="import" href="iron-selector.html">
<link rel="import" href="classes/iron-flex-layout.html">

<dom-module id="paper-tabs">
    <template>
    <style>...</style>
    <iron-selector class="layout horizonta center">
        <content select="*"></content>
    </iron-selector>
    </template>
    <script>...</script>
</dom-module>

Los desarrolladores de apps pueden importar este nuevo elemento con las siguientes opciones:

<link rel="import" href="paper-tabs.html">
<paper-tabs></paper-tabs>

Cuando aparezca una <iron-selector2> nueva y más genial en el futuro, podrás cambiar <iron-selector> y comenzar a usarla de inmediato. No causarás problemas a los usuarios gracias a las importaciones y los componentes web.

Administración de dependencias

Todos sabemos que cargar jQuery más de una vez por página provoca errores. ¿No será un problema enorme para los componentes web cuando varios componentes usen la misma biblioteca? Esto no es posible si usamos importaciones HTML. Se pueden usar para administrar dependencias.

Cuando unes bibliotecas en una importación de HTML, quitas automáticamente los recursos duplicados. El documento solo se analiza una vez. Las secuencias de comandos solo se ejecutan una vez. A modo de ejemplo, supongamos que defines una importación, jquery.html, que carga una copia de JQuery.

jquery.html

<script src="http://cdn.com/jquery.js"></script>

Esta importación se puede volver a usar en importaciones posteriores de la siguiente manera:

import2.html

<link rel="import" href="jquery.html">
<div>Hello, I'm import 2</div>
ajax-element.html

<link rel="import" href="jquery.html">
<link rel="import" href="import2.html">

<script>
    var proto = Object.create(HTMLElement.prototype);

    proto.makeRequest = function(url, done) {
    return $.ajax(url).done(function() {
        done();
    });
    };

    document.registerElement('ajax-element', {prototype: proto});
</script>

Incluso la página principal puede incluir jquery.html si necesita la biblioteca:

<head>
    <link rel="import" href="jquery.html">
    <link rel="import" href="ajax-element.html">
</head>
<body>

...

<script>
    $(document).ready(function() {
    var el = document.createElement('ajax-element');
    el.makeRequest('http://example.com');
    });
</script>
</body>

A pesar de que jquery.html se incluye en muchos árboles de importación diferentes, el navegador solo recupera y procesa su documento una vez. El examen del panel Network demuestra lo siguiente:

Se solicita jquery.html una vez.
jquery.html se solicita una vez

Consideraciones de rendimiento

Las importaciones de HTML son muy útiles, pero, al igual que con cualquier tecnología web nueva, debes usarlas con prudencia. Las prácticas recomendadas para el desarrollo web siguen siendo válidas. A continuación, se incluyen algunos aspectos que debes tener en cuenta.

Concatena las importaciones

Siempre es importante reducir las solicitudes de red. Si tienes muchos vínculos de importación de nivel superior, considera combinarlos en un solo recurso y, luego, importar ese archivo.

Vulcanize es una herramienta de compilación de npm del equipo de Polymer que aplana de forma recursiva un conjunto de importaciones de HTML en un solo archivo. Considéralo un paso de compilación de concatenación para componentes web.

Las importaciones aprovechan el almacenamiento en caché del navegador

Muchas personas olvidan que la pila de redes del navegador se ha ajustado con precisión a lo largo de los años. Las importaciones (y las subimportaciones) también aprovechan esta lógica. Es posible que la importación de http://cdn.com/bootstrap.html tenga subrecursos, pero se almacenarán en caché.

El contenido solo es útil cuando lo agregas

Piensa en el contenido como inerte hasta que llames a sus servicios. Toma un folio de estilo normal creado de forma dinámica:

var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'styles.css';

El navegador no solicitará styles.css hasta que se agregue link al DOM:

document.head.appendChild(link); // browser requests styles.css

Otro ejemplo es el lenguaje de marcado creado de forma dinámica:

var h2 = document.createElement('h2');
h2.textContent = 'Booyah!';

El h2 no tiene mucho sentido hasta que lo agregas al DOM.

Lo mismo sucede con el documento de importación. A menos que adjuntes su contenido al DOM, no se realizará ninguna acción. De hecho, lo único que se "ejecuta" directamente en el documento de importación es <script>. Consulta cómo usar secuencias de comandos en las importaciones.

Optimización para la carga asíncrona

Importa la renderización de bloques

Las importaciones bloquean la renderización de la página principal. Esto es similar a lo que hace <link rel="stylesheet">. El motivo por el que el navegador bloquea la renderización en los diseños de página en primer lugar es para minimizar la FOUC. Las importaciones se comportan de manera similar porque pueden contener hojas de estilo.

Para ser completamente asíncrono y no bloquear el analizador ni la renderización, usa el atributo async:

<link rel="import" href="/path/to/import_that_takes_5secs.html" async>

async no es el valor predeterminado para las importaciones HTML porque requiere que los desarrolladores hagan más trabajo. La sincronización predeterminada significa que se garantiza que las importaciones de HTML que tienen definiciones de elementos personalizados se carguen y actualicen en orden. En un mundo completamente asincrónico, los desarrolladores tendrían que administrar esos tiempos de baile y actualización por su cuenta.

También puedes crear una importación asíncrona de forma dinámica:

var l = document.createElement('link');
l.rel = 'import';
l.href = 'elements.html';
l.setAttribute('async', '');
l.onload = function(e) { ... };

Las importaciones no bloquean el análisis

Las importaciones no bloquean el análisis de la página principal. Las secuencias de comandos dentro de las importaciones se procesan en orden, pero no bloquean la página de importación. Esto significa que obtienes un comportamiento similar al aplazamiento mientras mantienes el orden correcto de la secuencia de comandos. Un beneficio de colocar tus importaciones en <head> es que permite que el analizador comience a trabajar en el contenido lo antes posible. Dicho esto, es fundamental recordar que <script> en el documento principal sigue bloqueando la página. El primer <script> después de una importación bloqueará la renderización de la página. Esto se debe a que una importación puede tener una secuencia de comandos que debe ejecutarse antes que la secuencia de comandos de la página principal.

<head>
    <link rel="import" href="/path/to/import_that_takes_5secs.html">
    <script>console.log('I block page rendering');</script>
</head>

Según la estructura y el caso de uso de tu app, existen varias formas de optimizar el comportamiento asíncrono. Las siguientes técnicas mitigan el bloqueo de la renderización de la página principal.

Situación 1 (preferida): no tienes una secuencia de comandos en <head> ni intercalada en <body>

Mi recomendación para colocar <script> es evitar inmediatamente después de tus importaciones. Mueve las secuencias de comandos lo más tarde posible en el juego… pero ya estás siguiendo esa práctica recomendada, ¿NO? ;)

Por ejemplo:

<head>
    <link rel="import" href="/path/to/import.html">
    <link rel="import" href="/path/to/import2.html">
    <!-- avoid including script -->
</head>
<body>
    <!-- avoid including script -->

    <div id="container"></div>

    <!-- avoid including script -->
    ...

    <script>
    // Other scripts n' stuff.

    // Bring in the import content.
    var link = document.querySelector('link[rel="import"]');
    var post = link.import.querySelector('#blog-post');

    var container = document.querySelector('#container');
    container.appendChild(post.cloneNode(true));
    </script>
</body>

Está todo en la parte inferior.

Situación 1.5: la importación se agrega sola

Otra opción es que la importación agregue su propio contenido. Si el autor de la importación establece un contrato para que lo siga el desarrollador de la app, la importación puede agregarse a un área de la página principal:

import.html:

<div id="blog-post">...</div>
<script>
    var me = document.currentScript.ownerDocument;
    var post = me.querySelector('#blog-post');

    var container = document.querySelector('#container');
    container.appendChild(post.cloneNode(true));
</script>
index.html

<head>
    <link rel="import" href="/path/to/import.html">
</head>
<body>
    <!-- no need for script. the import takes care of things -->
</body>

Situación 2: Tienes una secuencia de comandos en <head> o intercalada en <body>

Si tienes una importación que tarda mucho tiempo en cargarse, la primera <script> que le sigue en la página bloqueará la renderización de la página. Por ejemplo, Google Analytics recomienda colocar el código de seguimiento en <head>. Si no puedes evitar colocar <script> en <head>, agregar la importación de forma dinámica evitará que se bloquee la página:

<head>
    <script>
    function addImportLink(url) {
        var link = document.createElement('link');
        link.rel = 'import';
        link.href = url;
        link.onload = function(e) {
        var post = this.import.querySelector('#blog-post');

        var container = document.querySelector('#container');
        container.appendChild(post.cloneNode(true));
        };
        document.head.appendChild(link);
    }

    addImportLink('/path/to/import.html'); // Import is added early :)
    </script>
    <script>
    // other scripts
    </script>
</head>
<body>
    <div id="container"></div>
    ...
</body>

Como alternativa, agrega la importación cerca del final de <body>:

<head>
    <script>
    // other scripts
    </script>
</head>
<body>
    <div id="container"></div>
    ...

    <script>
    function addImportLink(url) { ... }

    addImportLink('/path/to/import.html'); // Import is added very late :(
    </script>
</body>

Información importante

  • El tipo mime de una importación es text/html.

  • Los recursos de otros orígenes deben estar habilitados para CORS.

  • Las importaciones de la misma URL se recuperan y analizan una vez. Esto significa que la secuencia de comandos de una importación solo se ejecuta la primera vez que se ve la importación.

  • Las secuencias de comandos de una importación se procesan en orden, pero no bloquean el análisis del documento principal.

  • Un vínculo de importación no significa "#include the content here". Significa "analizador, recupera este documento para poder usarlo más tarde". Mientras las secuencias de comandos se ejecutan en el momento de la importación, los diseños de página, el lenguaje de marcado y otros recursos deben agregarse a la página principal de forma explícita. Ten en cuenta que no es necesario agregar <style> de forma explícita. Esta es una diferencia importante entre las importaciones HTML y <iframe>, que indica "cargar y renderizar este contenido aquí".

Conclusión

Las importaciones de HTML permiten agrupar HTML/CSS/JS como un solo recurso. Si bien es útil por sí sola, esta idea se vuelve extremadamente potente en el mundo de los componentes web. Los desarrolladores pueden crear componentes reutilizables para que otros los consuman y los incorporen a su propia app, todo a través de <link rel="import">.

Las importaciones de HTML son un concepto simple, pero permiten varios casos de uso interesantes para la plataforma.

Casos de uso

  • Distributa el HTML/CSS/JS relacionado como un solo paquete. En teoría, podrías importar una app web completa a otra.
  • Organización del código: Segmenta los conceptos de forma lógica en diferentes archivos, lo que fomenta la modularidad y la reutilización**.
  • Publica una o más definiciones de elemento personalizado. Se puede usar una importación para register y, luego, incluirlos en una app. Esto permite practicar buenos patrones de software, ya que mantiene la interfaz o definición del elemento separada de cómo se usa.
  • Administrar dependencias: Los recursos se eliminan automáticamente.
  • Chunk scripts: Antes de las importaciones, una biblioteca de JS de gran tamaño tenía su archivo analizado por completo para comenzar a ejecutarse, lo que era lento. Con las importaciones, la biblioteca puede comenzar a funcionar en cuanto se analiza el fragmento A. Menos latencia
// TODO: DevSite - Code sample removed as it used inline event handlers
  • Se paraleliza el análisis de HTML: Es la primera vez que el navegador puede ejecutar dos (o más) analizadores de HTML en paralelo.

  • Habilita el cambio entre los modos de depuración y no depuración en una app, solo con cambiar el destino de importación. Tu app no necesita saber si el destino de importación es un recurso empaquetado o compilado, o un árbol de importación.