Importações HTML

Incluir para a Web

Por que importar?

Pense em como você carrega diferentes tipos de recursos na Web. Para JS, temos <script src>. Para CSS, provavelmente você vai usar <link rel="stylesheet">. Para imagens, é <img>. O vídeo tem <video>. Áudio, <audio>… vá direto ao ponto! A maioria do conteúdo da Web tem uma maneira simples e declarativa de carregar. Não é o caso do HTML. Confira as opções:

  1. <iframe>: confiável, mas pesado. O conteúdo de um iframe fica totalmente em um contexto separado da sua página. Embora esse seja um ótimo recurso, ele cria outros desafios. Por exemplo, é difícil ajustar o tamanho do frame ao conteúdo, é muito frustrante criar um script para ele e quase impossível estilizá-lo.
  2. AJAX: eu adoro xhr.responseType="document", mas você está dizendo que preciso de JS para carregar HTML? Isso não parece certo.
  3. CrazyHacks™: incorporados em strings, ocultos como comentários (por exemplo, <script type="text/html">).

Percebeu a ironia? O conteúdo mais básico da Web, o HTML, exige o maior esforço para trabalhar. Felizmente, os componentes da Web estão aqui para nos ajudar a voltar ao caminho certo.

Primeiros passos

Importações de HTML, parte do programa Componentes da Web, é uma maneira de incluir documentos HTML em outros documentos HTML. Você também não está limitado à marcação. Uma importação também pode incluir CSS, JavaScript ou qualquer outra coisa que um arquivo .html possa conter. Em outras palavras, isso torna as importações uma ferramenta fantástica para carregar HTML/CSS/JS relacionados.

Noções básicas

Inclua uma importação na sua página declarando um <link rel="import">:

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

O URL de uma importação é chamado de local de importação. Para carregar conteúdo de outro domínio, o local de importação precisa ter o CORS ativado:

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

Detecção e suporte a recursos

Para detectar a compatibilidade, verifique se .import existe no elemento <link>:

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

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

O suporte a navegadores ainda está nos estágios iniciais. O Chrome 31 foi o primeiro navegador a ter uma implementação, mas outros fornecedores de navegadores estão esperando para ver como os módulos ES funcionam. No entanto, para outros navegadores, o polyfill do webcomponents.js funciona muito bem até que haja suporte amplo.

Recursos agrupados

As importações fornecem uma convenção para agrupar HTML/CSS/JS (até mesmo outras importações de HTML) em um único resultado. É um recurso intrínseco, mas poderoso. Se você estiver criando um tema, uma biblioteca ou apenas quiser segmentar seu app em partes lógicas, fornecer aos usuários um único URL é uma boa opção. Você pode até mesmo entregar um app inteiro por meio de uma importação. Pense nisso por um segundo.

Um exemplo real é o Bootstrap. O Bootstrap é composto por arquivos individuais (bootstrap.css, bootstrap.js, fontes), requer JQuery para os plug-ins e fornece exemplos de marcação. Os desenvolvedores gostam da flexibilidade do à la carte. Isso permite que eles comprem as partes do framework que eles querem usar. Dito isso, eu apostaria que o JoeDeveloper™ típico segue o caminho mais fácil e faz o download de todo o Bootstrap.

As importações fazem muito sentido para algo como o Bootstrap. Apresento a você o futuro do carregamento do Bootstrap:

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

Os usuários simplesmente carregam um link de importação de HTML. Eles não precisam se preocupar com a dispersão de arquivos. Em vez disso, todo o Bootstrap é gerenciado e agrupado em uma importação, 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>

Deixe isso de lado. É uma coisa incrível.

Eventos de carregamento/erro

O elemento <link> dispara um evento load quando uma importação é carregada com sucesso e onerror quando a tentativa falha (por exemplo, se o recurso 404).

As importações tentam ser carregadas imediatamente. Uma maneira fácil de evitar dores de cabeça é usar os 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)">

Ou, se você estiver criando a importação dinamicamente:

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);

Como usar o conteúdo

Incluir uma importação em uma página não significa "colocar o conteúdo desse arquivo aqui". Significa "parser, vá buscar este documento para que eu possa usá-lo". Para usar o conteúdo, você precisa agir e escrever o script.

Um momento crítico de aha! é perceber que uma importação é apenas um documento. Na verdade, o conteúdo de uma importação é chamado de documento de importação. Você pode manipular o conteúdo de uma importação usando APIs DOM padrão.

link.import

Para acessar o conteúdo de uma importação, use a propriedade .import do elemento de link:

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

link.import é null nas seguintes condições:

  • O navegador não oferece suporte a importações de HTML.
  • O <link> não tem rel="import".
  • O <link> não foi adicionado ao DOM.
  • O <link> foi removido do DOM.
  • O recurso não está ativado para CORS.

Exemplo completo

Digamos que warnings.html contém:

<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>

Os importadores podem pegar uma parte específica deste documento e cloná-la na 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>

Scripts em importações

As importações não estão no documento principal. Eles são satélites. No entanto, a importação ainda pode agir na página principal, mesmo que o documento principal seja o principal. Uma importação pode acessar o próprio DOM e/ou o DOM da página que a está importando:

Exemplo: import.html, que adiciona uma das folhas de estilo à 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>

Observe o que está acontecendo aqui. O script dentro da importação faz referência ao documento importado (document.currentScript.ownerDocument) e anexa parte dele à página de importação (mainDoc.head.appendChild(...)). É bem complicado, na minha opinião.

Regras do JavaScript em uma importação:

  • O script na importação é executado no contexto da janela que contém o document de importação. Portanto, window.document se refere ao documento da página principal. Isso tem duas consequências úteis:
    • As funções definidas em uma importação vão para window.
    • Você não precisa fazer nada difícil, como anexar os blocos <script> da importação à página principal. Novamente, o script é executado.
  • As importações não bloqueiam a análise da página principal. No entanto, os scripts dentro deles são processados em ordem. Isso significa que você terá um comportamento semelhante ao de adiamento, mantendo a ordem adequada do script. Confira mais informações abaixo.

Como enviar componentes da Web

O design de importações de HTML é ótimo para carregar conteúdo reutilizável na Web. Em particular, é uma maneira ideal de distribuir componentes da Web. Tudo, desde HTML <template>s básicos até elementos personalizados completos com Shadow DOM [1, 2, 3]. Quando essas tecnologias são usadas em conjunto, as importações se tornam um #include para componentes da Web.

Como incluir modelos

O elemento modelo HTML é uma opção natural para importações de HTML. O <template> é ótimo para criar seções de marcação para o app de importação usar como quiser. O uso de um <template> também tem a vantagem de tornar o conteúdo inativo até ser usado. Ou seja, os scripts não são executados até que o modelo seja adicionado ao DOM. Ótimo!

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>

Como registrar elementos personalizados

Os elementos personalizados são outra tecnologia de Web Components que funciona muito bem com as importações de HTML. As importações podem executar scripts. Por que não definir e registrar seus elementos personalizados para que os usuários não precisem fazer isso? Chame de "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>

Essa importação define (e registra) dois elementos, <say-hi> e <shadow-element>. O primeiro mostra um elemento personalizado básico que se registra dentro da importação. O segundo exemplo mostra como implementar um elemento personalizado que cria um shadow DOM a partir de uma <template> e depois se registra.

A melhor parte de registrar elementos personalizados em uma importação de HTML é que o importador simplesmente declara seu elemento na página. Não é necessário fiação.

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>

Na minha opinião, esse fluxo de trabalho sozinho torna as importações de HTML uma maneira ideal de compartilhar componentes da Web.

Como gerenciar dependências e subimportações

Subimportações

Pode ser útil incluir uma importação em outra. Por exemplo, se você quiser reutilizar ou estender outro componente, use uma importação para carregar os outros elementos.

Confira abaixo um exemplo real da Polymer. É um novo componente de guia (<paper-tabs>) que reutiliza um layout e um componente de seletor. As dependências são gerenciadas usando as importações de 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>

Os desenvolvedores de apps podem importar esse novo elemento usando:

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

Quando um <iron-selector2> novo e mais incrível for lançado no futuro, você poderá trocar o <iron-selector> e começar a usar imediatamente. Você não vai interromper os usuários graças às importações e aos componentes da Web.

Gerenciamento de dependências

Todos sabemos que carregar o JQuery mais de uma vez por página causa erros. Isso não vai ser um problema enorme para os componentes da Web quando vários componentes usarem a mesma biblioteca? Não se usarmos importações de HTML. Eles podem ser usados para gerenciar dependências.

Ao agrupar bibliotecas em uma importação de HTML, você elimina automaticamente recursos duplicados. O documento só é analisado uma vez. Os scripts são executados apenas uma vez. Por exemplo, digamos que você defina uma importação, jquery.html, que carrega uma cópia do JQuery.

jquery.html

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

Essa importação pode ser reutilizada em importações subsequentes desta forma:

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>

Até mesmo a página principal pode incluir jquery.html se precisar da 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>

Embora o jquery.html seja incluído em muitas árvores de importação diferentes, o documento é buscado e processado apenas uma vez pelo navegador. O exame do painel de rede prova isso:

jquery.html é solicitado uma vez
jquery.html é solicitado uma vez

Considerações sobre desempenho

As importações de HTML são totalmente incríveis, mas, como qualquer nova tecnologia da Web, elas devem ser usadas com sabedoria. As práticas recomendadas de desenvolvimento da Web ainda são válidas. Confira a seguir algumas dicas.

Concatenar importações

Reduzir as solicitações de rede é sempre importante. Se você tiver muitos links de importação de nível superior, combine-os em um único recurso e importe esse arquivo.

O Vulcanize é uma ferramenta de build do npm da equipe do Polymer que aplana recursivamente um conjunto de importações de HTML em um único arquivo. Pense nisso como uma etapa de build de concatenação para componentes da Web.

As importações usam o armazenamento em cache do navegador

Muitas pessoas esquecem que a pilha de rede do navegador foi ajustada ao longo dos anos. As importações (e subimportações) também usam essa lógica. A importação http://cdn.com/bootstrap.html pode ter subrecursos, mas eles serão armazenados em cache.

O conteúdo só é útil quando você o adiciona

Pense no conteúdo como inerte até que você chame os serviços dele. Considere uma folha de estilo normal criada dinamicamente:

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

O navegador não vai solicitar styles.css até que link seja adicionado ao DOM:

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

Outro exemplo é a marcação criada dinamicamente:

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

O h2 é relativamente sem sentido até ser adicionado ao DOM.

O mesmo conceito se aplica ao documento de importação. A menos que você anexe o conteúdo ao DOM, ele não vai funcionar. Na verdade, a única coisa que "executa" diretamente no documento de importação é <script>. Consulte Como usar scripts em importações.

Como otimizar para carregamento assíncrono

Importa a renderização do bloco

As importações bloqueiam a renderização da página principal. Isso é semelhante ao que <link rel="stylesheet"> faz. O motivo pelo qual o navegador bloqueia a renderização em folhas de estilo é para minimizar o FOUC. As importações se comportam de maneira semelhante porque podem conter folhas de estilo.

Para ser completamente assíncrono e não bloquear o analisador ou a renderização, use o atributo async:

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

O motivo pelo qual async não é o padrão para importações de HTML é que ele exige mais trabalho dos desenvolvedores. Síncrono por padrão significa que as importações de HTML que têm definições de elementos personalizados são carregadas e atualizadas na ordem certa. Em um mundo totalmente assíncrono, os desenvolvedores teriam que gerenciar essa dança e atualizar os horários por conta própria.

Também é possível criar uma importação assíncrona de forma dinâmica:

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

As importações não bloqueiam a análise

As importações não bloqueiam a análise da página principal. Os scripts nas importações são processados em ordem, mas não bloqueiam a página de importação. Isso significa que você terá um comportamento semelhante ao de adiamento, mantendo a ordem adequada do script. Um dos benefícios de colocar as importações no <head> é que ele permite que o analisador comece a trabalhar no conteúdo o mais rápido possível. No entanto, é importante lembrar que o <script> no documento principal ainda continua bloqueando a página. O primeiro <script> após uma importação vai bloquear a renderização da página. Isso acontece porque uma importação pode ter um script que precisa ser executado antes do script na página principal.

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

Dependendo da estrutura e do caso de uso do app, há várias maneiras de otimizar o comportamento assíncrono. As técnicas abaixo evitam o bloqueio da renderização da página principal.

Cenário 1 (preferencial): você não tem script em <head> ou inline em <body>

Minha recomendação para colocar <script> é evitar imediatamente após as importações. Mova os scripts o mais tarde possível no jogo, mas você já está seguindo essa prática recomendada, NÉ MESMO? ;)

Veja um exemplo:

<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>

Tudo está na parte de baixo.

Cenário 1.5: a importação é adicionada

Outra opção é fazer com que a importação adicione o próprio conteúdo. Se o autor da importação estabelecer um contrato para o desenvolvedor do app seguir, a importação poderá ser adicionada a uma área da 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>

Cenário 2: você tem um script em <head> ou inline em <body>

Se você tiver uma importação que demora muito para carregar, o primeiro <script> que a seguir na página vai impedir a renderização dela. O Google Analytics, por exemplo, recomenda colocar o código de acompanhamento no <head>. Se não for possível evitar colocar <script> no <head>, a adição dinâmica da importação vai impedir o bloqueio da 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, adicione a importação perto do final do <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>

Lembretes importantes

  • O tipo MIME de uma importação é text/html.

  • Os recursos de outras origens precisam ter o CORS ativado.

  • As importações do mesmo URL são recuperadas e analisadas uma vez. Isso significa que o script em uma importação só é executado na primeira vez que ela é detectada.

  • Os scripts em uma importação são processados em ordem, mas não bloqueiam a análise do documento principal.

  • Um link de importação não significa "#inclua o conteúdo aqui". Significa "parser, vá buscar este documento para que eu possa usá-lo mais tarde". Embora os scripts sejam executados no momento da importação, as folhas de estilo, a marcação e outros recursos precisam ser adicionados à página principal de forma explícita. <style> não precisa ser adicionado explicitamente. Essa é uma diferença importante entre as importações de HTML e <iframe>, que diz "carregue e renderize este conteúdo aqui".

Conclusão

As importações de HTML permitem agrupar HTML/CSS/JS como um único recurso. Embora seja útil por si só, essa ideia se torna extremamente poderosa no mundo dos componentes da Web. Os desenvolvedores podem criar componentes reutilizáveis para que outras pessoas os consumam e levem para o próprio app, tudo isso entregue por <link rel="import">.

As importações de HTML são um conceito simples, mas permitem vários casos de uso interessantes para a plataforma.

Casos de uso

  • Distribua HTML/CSS/JS relacionados como um único pacote. Teoricamente, é possível importar um app da Web inteiro para outro.
  • Organização do código: segmente os conceitos de forma lógica em diferentes arquivos, incentivando a modularidade e a reutilização**.
  • Envie uma ou mais definições de elemento personalizado. Uma importação pode ser usada para register e incluir elementos em um app. Isso pratica bons padrões de software, mantendo a interface/definição do elemento separada da forma como ele é usado.
  • Gerenciar dependências: os recursos são eliminados automaticamente.
  • Scripts de bloco: antes das importações, uma biblioteca JS de grande porte tinha o arquivo totalmente analisado para começar a ser executada, o que era lento. Com as importações, a biblioteca pode começar a funcionar assim que o bloco A for analisado. Menos latência.
// TODO: DevSite - Code sample removed as it used inline event handlers
  • Análise paralela de HTML: pela primeira vez, o navegador consegue executar dois (ou mais) analisadores de HTML em paralelo.

  • Permite alternar entre modos de depuração e não depuração em um app, apenas mudando o destino de importação. O app não precisa saber se o destino da importação é um recurso agrupado/compilado ou uma árvore de importação.