Importações HTML

Incluir para a Web

Por que fazer importações?

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

  1. <iframe>: válido, mas pesado. O conteúdo de um iframe fica totalmente em um contexto separado da sua página. Embora esse seja um recurso excelente, ele cria desafios adicionais (enfraquecer o tamanho do frame ao seu conteúdo é difícil, extremamente frustrante para dentro/fora do script, quase impossível de estilizar).
  2. AJAX: eu adoro xhr.responseType="document", mas você quer dizer que preciso de JS para carregar HTML? Isso não parece certo.
  3. CrazyHacksTM: incorporado em strings, oculto como comentários (por exemplo, <script type="text/html">). Eca!

Viu a ironia? O conteúdo mais básico da Web, o HTML, requer o maior esforço de trabalho. Felizmente, os Web Components estão aqui para nos colocar de volta nos trilhos.

Como começar

As importações HTML, parte do cast Web Components, são uma maneira de incluir documentos HTML em outros documentos HTML. Você também não se limita à marcação. Uma importação também pode incluir CSS, JavaScript ou qualquer outro elemento que um arquivo .html possa conter. Em outras palavras, isso faz das 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 estar ativado para CORS:

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

Detecção e suporte de recursos

Para detectar o suporte, 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.
}

A compatibilidade com navegadores ainda está em desenvolvimento. O Chrome 31 foi o primeiro navegador a ter uma implementação, mas outros fornecedores de navegadores estão esperando para saber como os módulos ES vão funcionar. No entanto, para outros navegadores, o polyfill da webcomponents.js funciona muito bem até que haja ampla compatibilidade.

Agrupar recursos

As importações fornecem convenção para agrupar HTML/CSS/JS (até mesmo outras importações HTML) em um único envio. É um recurso intrínseco, mas muito poderoso. Se você está criando um tema, uma biblioteca ou apenas deseja segmentar seu aplicativo em blocos lógicos, dar aos usuários um único URL é atraente. Você pode até entregar um app inteiro usando uma importação. Pense sobre isso por um segundo.

Um exemplo real é o bootstrap. O bootstrap é composto por arquivos individuais (bootstrap.css, bootstrap.js, fontes), requer JQuery para plug-ins e fornece exemplos de marcação. Os desenvolvedores gostam de flexibilidade à la carte. Ela permite que eles concordem com as partes da estrutura que que querem usar. Dito isso, apostaria que seu JoeDeveloperTM 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ês 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 dos arquivos. Em vez disso, a totalidade do Bootstrap é gerenciada e agrupada em uma importação, 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>

Aguarde. É emocionante.

Eventos de carregamento/erro

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

As importações tentam carregar 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);

Usar o conteúdo

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

Um momento importante do aha! é perceber que a importação é apenas um documento. Na verdade, o conteúdo de uma importação é chamado de documento de importação. Você consegue manipular o código 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 do link:

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

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

  • O navegador não é compatível com importações 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 contenha:

<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 copiar uma parte específica deste documento e cloná-la em sua 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, sua importação ainda poderá funcionar na página principal, mesmo que o documento principal seja soberano. Uma importação pode acessar seu próprio DOM e/ou o DOM da página que a está importando:

Exemplo: import.html que adiciona uma de suas 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 desse documento à página de importação (mainDoc.head.appendChild(...)). Muito difícil, se você perguntar.

Regras de 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. Ela tem dois correlativos úteis:
    • as funções definidas em uma importação terminam em 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ê tem um comportamento de adiamento, mantendo a ordem adequada do script. Mais informações sobre isso abaixo.

Fornecimento de componentes da Web

O design do HTML Imports adapta-se bem ao carregamento de conteúdo reutilizável na web. Em particular, é uma forma ideal de distribuir componentes da Web. Tudo, de HTML <template>s básicos a elementos personalizados completos com o Shadow DOM [1, 2, 3]. Quando essas tecnologias são usadas em conjunto, as importações se tornam um #include para os componentes da Web.

Como incluir modelos

O elemento HTML Template é adequado para importações HTML. <template> é ótimo para a estruturação de seções de marcação para o app de importação usar como quiser. Unir o conteúdo em um <template> também oferece o benefício extra de torná-lo inerte até que seja 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>

Registrar elementos personalizados

Elementos personalizados é outra tecnologia de componente da Web que funciona perfeitamente com importações HTML. As importações podem executar um script, então por que não definir e registrar seus elementos personalizados para que os usuários não precisem fazer isso? Dê a ele o nome de..."auto-registro".

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 é registrado dentro da importação. O segundo exemplo mostra como implementar um elemento personalizado que cria o Shadow DOM com base em um <template> e se registra.

A melhor parte de registrar elementos personalizados em uma importação HTML é que o importador simplesmente declara seu elemento na página dele. Não precisa de 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 por si só torna as importações HTML uma maneira ideal de compartilhar componentes da Web.

Como gerenciar dependências e subimportações

Subimportações

Pode ser útil para uma importação incluir 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 do Polymer. É um componente de nova guia (<paper-tabs>) que reutiliza um componente de layout e seletor. As dependências são gerenciadas usando importações 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>

Os desenvolvedores de apps podem importar esse novo elemento usando:

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

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

Gerenciamento de dependências

Todos nós sabemos que carregar JQuery mais de uma vez por página causa erros. Isso não será um problema enorme para o Web Components quando vários componentes usam a mesma biblioteca? Não se usarmos importações em HTML. Elas podem ser usadas para gerenciar dependências.

Ao unir bibliotecas em uma importação de HTML, você elimina automaticamente as duplicações. O documento só é analisado uma vez. Os scripts são executados apenas uma vez. Como 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, como:

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>

Apesar do jquery.html estar incluído em muitas árvores de importação diferentes, seu documento só é obtido e processado uma vez pelo navegador. O exame do painel de rede prova o seguinte:

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

Considerações sobre performance

As importações em HTML são incríveis, mas, como acontece com qualquer nova tecnologia da Web, você deve usá-las com sabedoria. As práticas recomendadas de desenvolvimento da Web ainda são verdadeiras. Lembre-se do seguinte.

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.

Vulcanize é uma ferramenta de build npm da equipe do Polymer que nivela de maneira recursiva um conjunto de importações HTML em um único arquivo. Pense nisso como uma etapa de criação de concatenação para Web Components.

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

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

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

Pense no conteúdo como inerte até chamar seus serviços. Pegue 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 estilos.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!';

A h2 é relativamente irrelevante até que você a adicione ao DOM.

O mesmo conceito se aplica ao documento de importação. A menos que você anexe o conteúdo ao DOM, ele será considerado um ambiente autônomo. Na verdade, o único elemento que "executa" diretamente no documento de importação é <script>. Consulte scripts em importações.

Otimização para carregamento assíncrono

Importa a renderização de blocos

Importa a renderização em blocos da página principal. Isso é semelhante ao que <link rel="stylesheet"> faz. A razão pela 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>

async não é o padrão para importações HTML porque exige que os desenvolvedores façam mais trabalho. Por padrão, "síncrono" significa que as importações HTML que têm definições de elementos personalizados dentro delas têm a garantia de serem carregadas e atualizadas, na ordem. Em um mundo completamente assíncrono, os desenvolvedores teriam que gerenciar essa dança e atualizar os tempos por conta própria.

Você também pode criar uma importação assíncrona e 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 dentro das importações são processados em ordem, mas não bloqueiam a página de importação. Isso significa que você tem um comportamento de adiamento, mantendo a ordem adequada do script. Uma vantagem de colocar as importações no <head> é que isso permite que o analisador comece a trabalhar no conteúdo o mais rápido possível. Dito isso, é essencial lembrar que <script> no documento principal ainda continua bloqueando a página. O primeiro <script> após a importação bloqueará a renderização da página. Isso ocorre 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 seu app, há várias maneiras de otimizar o comportamento assíncrono. As técnicas abaixo reduzem o bloqueio da renderização da página principal.

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

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

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

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 a ser seguido pelo desenvolvedor do app, a importação poderá se adicionar 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 script em <head> ou inline em <body>

Se você tiver uma importação que demora muito para carregar, a primeira <script> que a seguir na página vai impedir a renderização. 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>, adicionar a importação de forma dinâmica vai evitar 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 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>

Coisas a se lembrar

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

  • Recursos de outras origens precisam estar ativados para CORS.

  • 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 a importação é vista.

  • 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 "#incluir o conteúdo aqui". Significa "analisador, saia e busque este documento para que eu possa usá-lo mais tarde". Embora os scripts sejam executados no momento da importação, folhas de estilo, marcações e outros recursos precisam ser explicitamente adicionados à página principal. Observe que <style> não precisa ser adicionada explicitamente. Essa é uma grande diferença entre as importações HTML e <iframe>, que diz "carregue e renderize este conteúdo aqui".

Conclusão

As importações HTML permitem agrupar HTML/CSS/JS como um único recurso. Embora útil por si só, essa ideia se torna extremamente poderosa no mundo do Web Components. Os desenvolvedores podem criar componentes reutilizáveis para outras pessoas consumirem e trazerem para o próprio app, todos entregues pelo <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 como um único pacote. Teoricamente, é possível importar um aplicativo da Web inteiro para outro.
  • Organização de código: segmente conceitos de maneira lógica em diferentes arquivos, incentivando a modularidade e a reutilização**.
  • Envie uma ou mais definições de elementos personalizados. Uma importação pode ser usada para register e incluí-los em um app. Isso pratica bons padrões de software, mantendo a interface/definição do elemento separada da forma como ela é usada.
  • Gerenciar dependências: a duplicação dos recursos é eliminada automaticamente.
  • Scripts fragmentados: antes das importações, uma biblioteca JS de grande tamanho tinha o arquivo totalmente analisado para começar a ser executado, o que era lento. Com as importações, a biblioteca pode começar a funcionar assim que o bloco A é analisado. Menos latência.
// TODO: DevSite - Code sample removed as it used inline event handlers
  • Paraleliza a análise de HTML: é a primeira vez que o navegador executa dois (ou mais) analisadores de HTML em paralelo.

  • Ativa a alternância entre os modos de depuração e não depuração em um app, basta mudar o próprio destino de importação. O app não precisa saber se o destino da importação é um recurso empacotado/compilado ou uma árvore de importação.