Importaciones de HTML

Incluir para la Web

¿Por qué son importantes las importaciones?

Piensa en cómo cargas diferentes tipos de recursos en la Web. Para JS, tenemos <script src>. En el caso del CSS, es probable que la opción que prefieras sea <link rel="stylesheet">. En el caso de las imágenes, es <img>. El video tiene <video>. Audio, <audio>... vaya al grano. La mayor parte del contenido de la Web tiene una forma simple y declarativa de cargarse automáticamente. En el caso de HTML, no es así. Tienes las siguientes opciones:

  1. <iframe>: Probado y verdadero, pero pesado. El contenido de un iframe se aloja completamente en un contexto independiente del de tu página. Si bien esta es principalmente una gran función, crea desafíos adicionales (reducir el tamaño del fotograma a su contenido es difícil, extremadamente frustrante para el acceso y la salida de la secuencia de comandos, y casi imposible de diseñar).
  2. AJAX - Me encanta xhr.responseType="document", pero ¿me dicen que necesito JS para cargar HTML? No es correcto.
  3. CrazyHacksTM: incorporado en cadenas, oculto como comentarios (p.ej., <script type="text/html">). ¡Qué asco!

¿Ves la ironía? El contenido más básico de la Web, HTML, requiere mayor esfuerzo para trabajar. Afortunadamente, los componentes web están aquí para volver a ponernos en marcha.

Primeros pasos

Las importaciones de HTML, que forman parte de la transmisión 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 contenga un archivo .html. En otras palabras, esto hace que las importaciones sean una herramienta fantástica para cargar elementos HTML/CSS/JS relacionados.

Conceptos básicos

Para incluir una importación en tu página, declara un <link rel="import">:

<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 estar habilitada para CORS:

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

Detección de funciones y asistencia

Para detectar compatibilidad, verifica si .import existe 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 los navegadores aún está en las primeras etapas. Chrome 31 fue el primer navegador en implementar una implementación, pero otros proveedores de navegadores están esperando ver cómo funcionan los módulos de ES. Sin embargo, para otros navegadores, el polyfill webcomponents.js funciona muy bien hasta que todos sean compatibles.

Recursos de paquetes

Las importaciones proporcionan una convención para agrupar HTML/CSS/JS (incluso otras importaciones HTML) en un solo resultado. Es una función intrínseca, pero poderosa. Si creas un tema o una biblioteca, o solo quieres segmentar la app en bloques lógicos, es muy atractivo brindarles a los usuarios una sola URL. Podrías, incluso, entregar una app completa a través de una importación. Piénsalo un momento.

Un ejemplo del mundo real es Bootstrap. Bootstrap se compone de archivos individuales (boot.css, Boot.js, fuentes), requiere JQuery para sus complementos y proporciona ejemplos de lenguaje 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 que el típico JoeDeveloperTM toma la ruta más 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 simplemente cargan un vínculo de importación HTML. No necesitan molestarse con el diagrama de dispersión de archivos. En cambio, la totalidad de Bootstrap se administra y se incluye en un archivo de importación, boot.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>

Deja que pase eso. Es emocionante.

Eventos de carga o error

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

Las importaciones intentan cargarse de inmediato. Una manera sencilla de evitar dolores de cabeza es usar los atributos onload y 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)">

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 "implementar el contenido de ese archivo aquí". Significa "analizador, ve a buscar este documento para poder usarlo". Para usar realmente el contenido, debes tomar medidas y escribir el guion.

Un momento aha! crítico 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 aspectos esenciales de una importación con las APIs de DOM estándares.

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.
  • La <link> no tiene rel="import".
  • No se agregó el <link> al DOM.
  • Se quitó el <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 las importaciones

Las importaciones no están en el documento principal. Están conectados con ellos. Sin embargo, la importación puede actuar en la página principal aunque el documento principal predomina. Una importación puede acceder a su propio DOM o al DOM de la página que la importa:

Ejemplo: import.html que agrega una 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 de la importación hace referencia al documento importado (document.currentScript.ownerDocument) y anexa parte de ese documento a la página de importación (mainDoc.head.appendChild(...)). Es muy claro, si me lo 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 la importación document. 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. Nuevamente, se ejecuta una secuencia de comandos.
  • Las importaciones no bloquean el análisis de la página principal. Sin embargo, las secuencias de comandos dentro de ellos se procesan en orden. Esto significa que obtienes un comportamiento similar al diferido y, al mismo tiempo, mantienes un orden adecuado de secuencias de comandos. Más adelante, se brinda más información sobre este tema.

Cómo entregar componentes web

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

Incluye plantillas

El elemento de plantilla HTML es un ajuste natural para las importaciones HTML. <template> es excelente para el andamiaje de las secciones de lenguaje de marcado de modo que la app de importación las use como lo desee. Unir contenido en un <template> también te da el beneficio adicional de hacer que el contenido esté inerte hasta que lo uses. Es decir, las secuencias de comandos no se ejecutarán hasta que se agregue 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

Elementos personalizados es otra tecnología de componentes web que funciona absurdamente bien con las importaciones HTML. Las importaciones pueden ejecutar secuencias de comandos. ¿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>. En el primero, se muestra un elemento personalizado básico que se registra en 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.

La mejor parte del registro de elementos personalizados en una importación de HTML es que el importador simplemente declara tu elemento en su página. Sin 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, este flujo de trabajo por sí solo hace que las importaciones HTML sean una manera ideal de compartir componentes web.

Administra dependencias y subimportaciones

Subimportaciones

Puede ser útil que una importación incluya otra. Por ejemplo, si deseas reutilizar o extender otro componente, usa una importación para cargar los demás 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 mediante importaciones HTML.

Papel-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 usando lo siguiente:

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

Cuando aparezca una <iron-selector2> más genial y genial en el futuro, podrás cambiar la <iron-selector> y comenzar a usarla de inmediato. Las importaciones y los componentes web no perjudican a tus usuarios.

Administración de dependencias

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

Si unes bibliotecas en una importación de HTML, se deducen automáticamente los recursos. El documento solo se analiza una vez. Las secuencias de comandos solo se ejecutan una vez. Como 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. Al examinar el panel de la red, se puede comprobar lo siguiente:

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

Consideraciones sobre el rendimiento

Las importaciones HTML son totalmente increíbles, pero, al igual que con cualquier tecnología web nueva, debes utilizarlas 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.

Concatenación de importaciones

Reducir las solicitudes de red siempre es importante. 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 acopla de forma recurrente un conjunto de importaciones 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 red del navegador se ha ajustado precisamente a lo largo de los años. Las importaciones (y subimportaciones) también aprovechan esta lógica. La importación de http://cdn.com/bootstrap.html puede tener subrecursos, pero se almacenarán en caché.

El contenido es útil solo cuando lo agregas

Piensa en el contenido como inerte hasta que invoques sus servicios. Toma una hoja de estilo normal creada de forma dinámica:

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

El navegador no solicitará style.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!';

La h2 no tendrá sentido hasta que la agregues al DOM.

Se aplica el mismo concepto al documento de importación. A menos que agregues su contenido al DOM, se realizará una no-op. De hecho, lo único que se "ejecuta" directamente en el documento de importación es <script>. Consulta Secuencias de comandos en las importaciones.

Cómo optimizar la carga asíncrona

Procesamiento de bloques de importación

Importa la renderización en bloque de la página principal. Esto es similar a lo que hace <link rel="stylesheet">. En primer lugar, el navegador bloquea la renderización en las hojas de estilo para minimizar el FOUC. Las importaciones se comportan de manera similar porque pueden contener hojas de estilo.

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

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

async no es la opción predeterminada para las importaciones HTML es porque requiere que los desarrolladores trabajen más. La función síncrona de forma predeterminada implica que las importaciones HTML que contengan definiciones de elementos personalizados se carguen y actualicen en orden. En un mundo completamente asíncrono, los desarrolladores tendrían que administrar los tiempos de los bailes y las actualizaciones 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 de las importaciones se procesan en orden, pero no bloquean la página de importación. Esto significa que obtienes un comportamiento similar al diferido y, al mismo tiempo, mantienes un orden adecuado de secuencias de comandos. Uno de los beneficios de colocar las importaciones en el <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 contener una secuencia de comandos que se debe ejecutar antes que la en 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 de tu app y el caso de uso, 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 (opción preferida): No tienes una secuencia de comandos en <head> o intercalada en <body>

Mi recomendación para colocar <script> es evitar seguir inmediatamente tus importaciones. Mueve los guiones lo más tarde posible en el juego, pero ya aplicas la práctica recomendada, ¿NO ES TÚ? ;)

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>

Todo está en la parte inferior.

Situación 1.5: La importación se suma

Otra opción es que la importación agregue su propio contenido. Si el autor de la importación establece un contrato para que el desarrollador de la app lo siga, 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 demora mucho tiempo en cargarse, el primer elemento <script> que le siga 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>

También puedes agregar 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>

Aspectos que debe tener en cuenta

  • El tipo de 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 observa 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 "#incluir el contenido aquí". Significa “analizador, ve a buscar este documento para poder usarlo más tarde”. Si bien las secuencias de comandos se ejecutan en el momento de la importación, es necesario agregar de manera explícita las hojas de estilo, el lenguaje de marcado y otros recursos a la página principal. Ten en cuenta que no es necesario agregar <style> de forma explícita. Esta es una gran diferencia entre las importaciones HTML y <iframe>, que dice "carga y procesa este contenido aquí".

Conclusión

Las importaciones de HTML permiten agrupar HTML/CSS/JS como un recurso único. Si bien es útil por sí sola, esta idea se vuelve extremadamente poderosa 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 ello entregado 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

  • Distribuye los recursos HTML/CSS/JS relacionados como un solo paquete. En teoría, podrías importar una aplicación web completa a otra.
  • Organización del código: Segmenta los conceptos de forma lógica en diferentes archivos para fomentar la modularidad y la reutilización**.
  • Publica una o más definiciones de elementos personalizados. Se puede usar una importación para register y, luego, incluirlos en una app. Esto implementa patrones de software adecuados, lo que mantiene la interfaz o definición del elemento separada de su uso.
  • Administra dependencias: Se eliminan los duplicados de los recursos automáticamente.
  • Secuencias de comandos de fragmentación: Antes de las importaciones, una biblioteca JS de tamaño grande analizaba por completo su archivo para comenzar a ejecutarse, lo cual era lento. Con las importaciones, la biblioteca puede comenzar a funcionar tan pronto como se analice el fragmento A. ¡Menos latencia!
// TODO: DevSite - Code sample removed as it used inline event handlers
  • Paraleliza el análisis HTML: Es la primera vez que el navegador puede ejecutar dos (o más) analizadores de HTML en paralelo.

  • Permite el cambio entre los modos de depuración y no de depuración en una app, con solo 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.