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 é <link rel="stylesheet">
. Para imagens, é <img>
. O vídeo tem <video>
. Áudio, <audio>
... Vamos direto ao assunto. A maior parte do conteúdo da Web tem uma maneira simples e declarativa de carregar por conta própria. Não é o caso do HTML. Confira as opções:
<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.- AJAX: eu adoro
xhr.responseType="document"
, mas você está dizendo que preciso de JS para carregar HTML? Isso não parece certo. - CrazyHacks™: incorporados em strings, ocultos como comentários (por exemplo,
<script type="text/html">
). Que nojo!
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ê não precisa se limitar a marcações. 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 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.
}
O suporte a navegadores ainda está nos primórdios. 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 vão funcionar. No entanto, para outros navegadores, o polyfill do webcomponents.js funciona muito bem até que haja suporte amplo.
Como agrupar recursos
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 aplicativo em blocos lógicos, é interessante fornecer aos usuários um único URL. 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), exige 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 "plop 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. É possível 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 temrel="import"
. - O
<link>
não foi adicionado ao DOM. - O elemento
<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 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 funções definidas em uma importação vão para
- 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 é adequado 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 de Modelo HTML é uma opção natural para importações 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 componentes da Web que funciona muito bem com importações 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 o Shadow DOM usando uma <template>
e 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 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 da Polymer. Ele é um novo componente de guia (<paper-tabs>
) que reutiliza um componente de layout e 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 usá-lo 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 unir as bibliotecas em uma importação HTML, você automaticamente elimina a duplicação de recursos. 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 própria 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:
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 aproveitam 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
não faz 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 será executado. 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 de blocos
As importações bloqueiam a renderização 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 em primeiro lugar é 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 HTML é porque exige que os desenvolvedores
façam mais trabalho. 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 dentro das 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 seu 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 a 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 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 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 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
.Recursos de outras origens precisam ser 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 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 ú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 os incluam no 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 registrar 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 redirecionamento: antes das importações, uma biblioteca JS de grande porte teria 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 for analisado. Menos latência.
// TODO: DevSite - Code sample removed as it used inline event handlers
Carrega em paralelo a análise de HTML: a primeira vez que o navegador conseguiu executar dois (ou mais) analisadores 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.