HTML-импорт

Включить для Интернета

Почему импорт?

Подумайте о том, как вы загружаете в Интернет различные типы ресурсов. Для JS у нас есть <script src> . Для CSS вам, вероятно, подойдет <link rel="stylesheet"> . Для изображений это <img> . Видео имеет <video> . Аудио, <audio> … К делу! Большая часть веб-контента имеет простой и декларативный способ загрузки. Не так для HTML. Вот ваши варианты:

  1. <iframe> — проверенный и надежный, но тяжелый. Содержимое iframe полностью находится в отдельном контексте от вашей страницы. Хотя это в основном отличная функция, она создает дополнительные проблемы (сжимать размер фрейма до его содержимого сложно, крайне неприятно вставлять/извлекать скрипты, практически невозможно стилизовать).
  2. AJAX . Мне нравится xhr.responseType="document" , но вы говорите, что мне нужен JS для загрузки HTML? Это кажется неправильным.
  3. CrazyHacks™ — встроен в строки, скрыты в виде комментариев (например, <script type="text/html"> ). Фу!

Видите иронию? Самый простой веб-контент, HTML, требует величайших усилий для работы . К счастью, веб-компоненты готовы вернуть нас в нужное русло.

Начиная

Импорт HTML , являющийся частью набора веб-компонентов , представляет собой способ включения HTML-документов в другие HTML-документы. Вы также не ограничены разметкой. Импорт также может включать CSS, JavaScript или что-либо еще, что может содержать файл .html . Другими словами, это делает импорт отличным инструментом для загрузки связанных HTML/CSS/JS .

Основы

Включите импорт на свою страницу, объявив <link rel="import"> :

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

URL-адрес импорта называется местом импорта . Чтобы загрузить контент из другого домена, в месте импорта должна быть включена поддержка CORS:

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

Обнаружение и поддержка функций

Чтобы обнаружить поддержку, проверьте, существует ли .import в элементе <link> :

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

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

Поддержка браузеров все еще находится на начальной стадии. Chrome 31 был первым браузером, в котором реализована такая возможность, но другие поставщики браузеров ждут, чтобы увидеть, как покажут себя модули ES. Однако для других браузеров полифил webcomComponents.js работает отлично, пока он не получит широкую поддержку.

Объединение ресурсов

Импорт обеспечивает соглашение для объединения HTML/CSS/JS (даже других импортов HTML) в единый результат. Это внутренняя функция, но мощная. Если вы создаете тему, библиотеку или просто хотите сегментировать свое приложение на логические фрагменты, предоставление пользователям единого URL-адреса является привлекательным решением. Черт возьми, вы даже можете доставить целое приложение с помощью импорта. Подумайте об этом на секунду.

Реальный пример — Bootstrap . Bootstrap состоит из отдельных файлов (bootstrap.css, bootstrap.js, шрифты), требует JQuery для своих плагинов и предоставляет примеры разметки. Разработчикам нравится гибкость à la carte. Это позволяет им покупать те части фреймворка, которые они хотят использовать. Тем не менее, я готов поспорить, что ваш типичный JoeDeveloper™ пойдет простым путем и загрузит весь Bootstrap.

Импорт имеет массу смысла для чего-то вроде Bootstrap. Представляю вам будущее загрузки Bootstrap:

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

Пользователи просто загружают ссылку импорта HTML. Им не нужно возиться с разбросанными файлами. Вместо этого весь Bootstrap управляется и заключен в импортируемый файл 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>

Пусть это постоит. Это захватывающая вещь.

События загрузки/ошибки

Элемент <link> запускает событие load , когда импорт загружается успешно, и onerror , когда попытка не удалась (например, если ресурс 404s).

Импорт пытается загрузиться немедленно. Простой способ избежать головной боли — использовать атрибуты 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)">

Или, если вы создаете импорт динамически:

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

Использование контента

Включение импорта на страницу не означает «поместить сюда содержимое этого файла». Это означает «парсер, принеси этот документ, чтобы я мог его использовать». Чтобы действительно использовать контент, вам нужно принять меры и написать сценарий.

Критическое aha! момент осознает, что импорт — это всего лишь документ. Фактически, содержимое импорта называется документом импорта . Вы можете управлять всем процессом импорта, используя стандартные API-интерфейсы DOM !

ссылка.импорт

Чтобы получить доступ к содержимому импорта, используйте свойство .import элемента ссылки:

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

link.import имеет null при следующих условиях:

  • Браузер не поддерживает импорт HTML.
  • В <link> нет rel="import" .
  • <link> не была добавлена ​​в DOM.
  • <link> была удалена из DOM.
  • Ресурс не поддерживает CORS.

Полный пример

Допустим, warnings.html содержит:

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

Импортеры могут получить определенную часть этого документа и клонировать ее на свою страницу:

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

Скрипты при импорте

Импорт не указан в основном документе. Они являются его спутниками. Тем не менее, ваш импорт по-прежнему может действовать на главной странице, даже если основной документ доминирует. Импорт может получить доступ к своему собственному DOM и/или DOM страницы, которая его импортирует:

Пример — import.html, который добавляет одну из своих таблиц стилей на главную страницу.

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

Обратите внимание, что здесь происходит. Сценарий внутри импорта ссылается на импортированный документ ( document.currentScript.ownerDocument ) и добавляет часть этого документа на страницу импорта ( mainDoc.head.appendChild(...) ). Довольно коряво, если вы спросите меня.

Правила JavaScript при импорте:

  • Скрипт при импорте выполняется в контексте окна, содержащего импортирующий document . Таким образом, window.document относится к документу главной страницы. Это имеет два полезных следствия:
    • функции, определенные при импорте, попадают в window .
    • вам не нужно делать ничего сложного, например добавлять блоки <script> импорта на главную страницу. И снова скрипт выполняется.
  • Импорт не блокирует парсинг главной страницы. Однако скрипты внутри них обрабатываются по порядку. Это означает, что вы получаете поведение, подобное отложенному, сохраняя при этом правильный порядок сценариев. Подробнее об этом ниже.

Доставка веб-компонентов

Дизайн импорта HTML прекрасно подходит для загрузки повторно используемого контента в Интернете. В частности, это идеальный способ распространения веб-компонентов. Все, от базового HTML <template> до полноценных пользовательских элементов с Shadow DOM [ 1 , 2 , 3 ]. Когда эти технологии используются совместно, импорт становится #include для веб-компонентов.

Включая шаблоны

Элемент HTML Template идеально подходит для импорта HTML. <template> отлично подходит для создания разделов разметки, которые импортирующее приложение может использовать по своему усмотрению. Обертывание контента в <template> также дает вам дополнительное преимущество, делая контент инертным до тех пор, пока он не будет использован. То есть сценарии не запускаются до тех пор, пока шаблон не будет добавлен в DOM). Бум!

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>

Регистрация пользовательских элементов

Пользовательские элементы — это еще одна технология веб-компонентов, которая до абсурда хорошо сочетается с импортом HTML. Импорт может выполнять скрипт, так почему бы не определить и не зарегистрировать свои пользовательские элементы, чтобы пользователям не приходилось это делать? Назовите это… «авторегистрация».

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>

Этот импорт определяет (и регистрирует) два элемента: <say-hi> и <shadow-element> . Первый показывает базовый пользовательский элемент, который регистрируется внутри импорта. Во втором примере показано, как реализовать пользовательский элемент, который создает Shadow DOM из <template> , а затем регистрирует себя.

Самое приятное в регистрации пользовательских элементов внутри импорта HTML заключается в том, что импортер просто объявляет ваш элемент на своей странице. Никакой проводки не требуется.

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>

По моему мнению, сам по себе этот рабочий процесс делает импорт HTML идеальным способом обмена веб-компонентами.

Управление зависимостями и субимпортом

Суб-импорт

Может быть полезно, чтобы один импорт включал другой. Например, если вы хотите повторно использовать или расширить другой компонент, используйте импорт для загрузки других элементов.

Ниже реальный пример от Polymer . Это новый компонент вкладок ( <paper-tabs> ), который повторно использует компонент макета и селектора. Зависимости управляются с помощью импорта HTML.

paper-tabs.html (упрощенный):

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

Разработчики приложений могут импортировать этот новый элемент, используя:

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

Когда в будущем появится новый, более потрясающий <iron-selector2> , вы сможете заменить <iron-selector> и сразу же начать его использовать. Вы не сломаете своих пользователей благодаря импорту и веб-компонентам.

Управление зависимостями

Мы все знаем, что загрузка JQuery более одного раза на страницу приводит к ошибкам. Не станет ли это огромной проблемой для веб-компонентов, когда несколько компонентов используют одну и ту же библиотеку? Нет, если мы используем импорт HTML! Их можно использовать для управления зависимостями.

Обертывая библиотеки в HTML-импорт, вы автоматически устраняете дублирование ресурсов. Документ анализируется только один раз. Скрипты выполняются только один раз. В качестве примера предположим, что вы определяете импорт jquery.html, который загружает копию JQuery.

jquery.html

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

Этот импорт можно повторно использовать в последующих импортах, например:

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>

Даже главная страница может включать jquery.html, если ей нужна библиотека:

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

Несмотря на то, что jquery.html включен во множество различных деревьев импорта, этот документ извлекается и обрабатывается браузером только один раз. Изучение сетевой панели подтверждает это:

jquery.html запрашивается один раз
jquery.html запрашивается один раз

Вопросы производительности

Импорт HTML — это просто потрясающе, но, как и в случае с любой новой веб-технологией, вы должны использовать его с умом. Лучшие практики веб-разработки по-прежнему актуальны. Ниже приведены некоторые вещи, которые следует иметь в виду.

Объединение импорта

Сокращение сетевых запросов всегда важно. Если у вас много ссылок для импорта верхнего уровня, рассмотрите возможность объединения их в один ресурс и импорта этого файла!

Vulcanize — это инструмент сборки npm от команды Polymer , который рекурсивно объединяет набор импортированных HTML-кодов в один файл. Думайте об этом как об этапе объединения веб-компонентов.

Импорт использует кеширование браузера

Многие люди забывают, что сетевой стек браузера был точно настроен на протяжении многих лет. Импорт (и субимпорт) также использует эту логику. Импорт http://cdn.com/bootstrap.html может содержать вложенные ресурсы, но они будут кэшироваться.

Контент полезен только тогда, когда вы его добавляете

Считайте контент инертным, пока не воспользуетесь его услугами. Возьмем обычную, динамически созданную таблицу стилей:

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

Браузер не будет запрашивать Styles.css, пока link не будет добавлена ​​в DOM:

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

Другой пример — динамически создаваемая разметка:

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

h2 относительно бессмысленен, пока вы не добавите его в DOM.

Та же концепция справедлива и для импортного документа. Если вы не добавите его содержимое в DOM, это будет бесполезно. Фактически, единственное, что «исполняется» непосредственно в документе импорта, — это <script> . См. сценарии в импорте .

Оптимизация для асинхронной загрузки

Импортирует рендеринг блоков

Импортирует блок рендеринга главной страницы . Это похоже на то, что делает <link rel="stylesheet"> . Причина, по которой браузер блокирует рендеринг в таблицах стилей, в первую очередь заключается в минимизации FOUC. Импортированные файлы ведут себя аналогичным образом, поскольку могут содержать таблицы стилей.

Чтобы быть полностью асинхронным и не блокировать парсер или рендеринг, используйте атрибут async :

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

Причина, по которой async не используется по умолчанию для импорта HTML, заключается в том, что она требует от разработчиков дополнительной работы. Синхронный по умолчанию означает, что HTML-импорты, внутри которых есть определения пользовательских элементов, гарантированно загружаются и обновляются по порядку. В полностью асинхронном мире разработчикам придется самим управлять этим танцем и временем обновлений.

Вы также можете создать асинхронный импорт динамически:

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

Импорт не блокирует синтаксический анализ

Импорт не блокирует парсинг главной страницы . Скрипты внутри импорта обрабатываются по порядку, но не блокируют страницу импорта. Это означает, что вы получаете поведение, подобное отложенному, сохраняя при этом правильный порядок сценариев. Одним из преимуществ размещения импорта в <head> является то, что он позволяет парсеру начать работу над содержимым как можно скорее. При этом важно помнить, <script> в основном документе продолжает блокировать страницу. Первый <script> после импорта блокирует рендеринг страницы. Это связано с тем, что импорт может содержать сценарий, который необходимо выполнить перед сценарием на главной странице.

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

В зависимости от структуры вашего приложения и варианта использования существует несколько способов оптимизации асинхронного поведения. Приведенные ниже методы позволяют избежать блокировки рендеринга главной страницы.

Сценарий № 1 (предпочтительный): у вас нет скрипта в <head> или встроенного в <body>

Я рекомендую размещать <script> не сразу после импорта. Перемещайте сценарии как можно позже в игре… но вы уже применяете эту лучшую практику, не так ли!? ;)

Вот пример:

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

Все находится внизу.

Сценарий 1.5: импорт добавляется сам

Другой вариант — позволить импорту добавлять собственный контент. Если автор импорта заключает контракт, которому должен следовать разработчик приложения, импорт может добавиться в область главной страницы:

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>

Сценарий №2: у вас есть скрипт в <head> или встроен в <body>

Если у вас есть импорт, загрузка которого занимает много времени, первый <script> , следующий за ним на странице, заблокирует рендеринг страницы. Google Analytics, например, рекомендует помещать код отслеживания в <head> . Если вы не можете избежать размещения <script> в <head> , динамическое добавление импорта предотвратит блокировку страницы:

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

Альтернативно добавьте импорт ближе к концу <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>

Что следует помнить

  • MIME-тип импорта — text/html .

  • Ресурсы из других источников должны иметь поддержку CORS.

  • Импорт с одного и того же URL-адреса извлекается и анализируется один раз. Это означает, что сценарий импорта выполняется только при первом просмотре импорта.

  • Скрипты при импорте обрабатываются по порядку, но не блокируют основной анализ документа.

  • Ссылка на импорт не означает «#включите сюда контент». Это означает «парсер, принеси этот документ, чтобы я мог использовать его позже». Хотя сценарии выполняются во время импорта, таблицы стилей, разметка и другие ресурсы необходимо явно добавлять на главную страницу. Обратите внимание, <style> не нужно добавлять явно. Это основное различие между HTML Imports и <iframe> , в котором говорится: «загружайте и визуализируйте этот контент здесь».

Заключение

Импорт HTML позволяет объединить HTML/CSS/JS в один ресурс. Хотя эта идея полезна сама по себе, она становится чрезвычайно мощной в мире веб-компонентов. Разработчики могут создавать повторно используемые компоненты, которые другие могут использовать и добавлять в свои приложения, и все это доставляется через <link rel="import"> .

Импорт HTML — это простая концепция, но она позволяет реализовать ряд интересных вариантов использования платформы.

Варианты использования

  • Распространяйте связанные HTML/CSS/JS как один пакет . Теоретически вы можете импортировать одно веб-приложение в другое.
  • Организация кода — логически сегментируйте концепции в разные файлы, обеспечивая модульность и возможность повторного использования**.
  • Предоставьте одно или несколько определений пользовательских элементов . Импорт можно использовать для регистрации и включения их в приложение. Это практикует хорошие шаблоны программного обеспечения, сохраняя интерфейс/определение элемента отдельно от того, как он используется.
  • Управляйте зависимостями — ресурсы автоматически устраняются.
  • Сценарии фрагментов — перед импортом файл большой JS-библиотеки полностью анализировался перед запуском, что было медленным. При импорте библиотека может начать работать, как только будет проанализирован фрагмент A. Меньше задержки!
// TODO: DevSite - Code sample removed as it used inline event handlers
  • Распараллеливает анализ HTML — впервые браузер смог запустить два (или более) анализатора HTML параллельно.

  • Позволяет переключаться между режимами отладки и неотладки в приложении, просто изменяя саму цель импорта. Вашему приложению не обязательно знать, является ли цель импорта связанным/скомпилированным ресурсом или деревом импорта.