HTML 导入

适用于网站

为什么要导入?

想一想如何在网络上加载不同类型的资源。对于 JS,我们采用 <script src>。对于 CSS,您的目标可能是 <link rel="stylesheet">。对于图片,则为 <img>。视频包含<video>。音频,<audio>... 直奔主题!大部分 Web 内容都采用简单的声明式方式自行加载。但对于 HTML 则不然。有以下几种选项供您选择:

  1. <iframe> - 久经考验,但重量级。iframe 的内容完全位于与网页不同的环境中。虽然这在大部分情况下是一项很棒的功能,但也会带来额外的挑战(将框架缩小到其内容所占的比例非常困难,通过脚本处理/脱离脚本、设置样式几乎是不可能的)。
  2. AJAX - 我喜欢 xhr.responseType="document",但你是说我需要 JS 加载 HTML?好像不对哦。
  3. CrazyHacksTM - 嵌入字符串,隐藏为评论(例如 <script type="text/html">)。太糟糕了!

明白了讽刺意味吗?网络最基本的内容(即 HTML)需要花费大量精力才能处理。幸运的是,Web 组件可帮助我们解决问题。

使用入门

HTML 导入网络组件类型转换的一部分,是一种将 HTML 文档包含在其他 HTML 文档中的方法。使用标记也不限于此。导入文件还可以包含 CSS、JavaScript 或 .html 文件可包含的其他任何内容。也就是说,这使得导入成为加载相关 HTML/CSS/JS 的神奇工具

基础知识

通过声明 <link rel="import"> 在页面上添加导入:

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

导入的网址称为导入位置。如需从其他网域加载内容,导入位置需要启用 CORS:

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

功能检测和支持

如需检测支持情况,请检查 <link> 元素中是否存在 .import

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 模块如何发挥作用。 不过,对于其他浏览器,在广泛支持之前,webcomponents.js polyfill 可以正常使用。

捆绑资源

导入提供了将 HTML/CSS/JS(甚至其他 HTML 导入内容)捆绑到一个交付项中的惯例。这是一项固有功能,但却非常强大。如果您要创建一个主题、库,或者只是想将应用细分为多个逻辑区块,那么为用户提供单一网址是非常有吸引力的。甚至,您甚至可以通过导入的方式交付整个应用。请思考以下问题。

真实示例是引导。引导加载程序由各个文件(bootstrap.css、bootstrap.js、字体)组成,其插件需要使用 JQuery,并且提供了标记示例。开发者喜欢按需定制的灵活性。只有这样,客户才能够购买自己想要使用的框架部分。不过,我敢打赌你们的典型 JoeDeveloperTM 会走轻松,并下载所有引导加载程序。

导入对于引导加载程序等工具很有用。我会向大家介绍加载引导加载程序的未来:

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

用户只需加载 HTML 导入链接即可。他们无需为分散的文件分发而烦恼。相反,整个引导加载程序会进行管理并封装在导入 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(例如,如果资源 404 错误)。

系统会尝试立即加载导入。避免令人头痛的一种简单方法是使用 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! 时刻是意识到导入只是一个文档。实际上,导入的内容称为“导入文档”。您可以使用标准 DOM API 操控导入内容

link.import

要访问导入的内容,请使用 link 元素的 .import 属性:

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

在以下情况下,link.importnull

  • 浏览器不支持 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> 代码块附加到主页面。再次执行脚本。
  • 导入操作不会阻止对主页面的解析。不过,系统会按顺序处理其中的脚本。这意味着您在保持正确的脚本顺序的同时,获得类似延迟的行为。详见下文。

交付 Web 组件

HTML Imports 的设计非常适合在网络上加载可重复使用的内容。具体来说,这是分发 Web 组件的理想方式。使用 Shadow DOM [123] 涵盖了从基本的 HTML <template> 到成熟的自定义元素等各种内容。当这些技术搭配使用时,导入操作会成为 Web 组件的 #include

添加模板

HTML 模板元素非常适合使用 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>

注册自定义元素

自定义元素是另一种 Web 组件技术,该技术与 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> 这两个元素。第一个显示了一个基本的自定义元素,该元素可在导入内自行注册。第二个示例展示了如何实现从 <template> 创建 Shadow DOM 并自行注册的自定义元素。

在 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 Imports 成为共享 Web 组件的理想方式。

管理依赖项和子导入

子导入

在一个导入中纳入另一个导入可能很有用。例如,如果您想重复使用或扩展其他组件,可以使用导入功能来加载其他元素。

以下是 Polymer 的一个真实示例。它是一个重复使用布局和选择器组件的新标签页组件 (<paper-tabs>)。依赖关系通过 HTML Imports 进行管理。

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> 并立即开始使用。导入和 Web 组件不会影响您的用户。

依赖项管理

我们都知道,每个网页多次加载 JQuery 会引发错误。有多个组件使用同一个库时,这对 Web 组件来说不是一个严重问题吗?如果我们使用 HTML Imports!它们可用于管理依赖项。

通过将库封装在 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 导入功能非常棒,但与任何新的网络技术一样,您应该明智地使用它们。Web 开发最佳实践仍然适用。以下是需要注意的一些事项。

串联导入

减少网络请求始终非常重要。如果您有许多顶级导入链接,请考虑将它们合并为一项资源并导入该文件!

VulcanizePolymer 团队开发的一款 npm 构建工具,该工具能够以递归方式将一组 HTML 导入内容扁平化为单个文件。可以将其视为 Web 组件的串联构建步骤。

导入操作会利用浏览器缓存

很多人都忘记了,多年来,浏览器的网络堆栈都经过了精心调整。导入(和子导入)也会利用此逻辑。http://cdn.com/bootstrap.html 导入操作可能包含子资源,但系统会缓存这些资源。

内容只有添加后才有用

在调用其服务之前,请将内容视为惰性内容。选取正常的、动态创建的样式表:

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

在将 link 添加到 DOM 之前,浏览器不会请求 style.css:

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

另一个示例是动态创建的标记:

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

在您将 h2 添加到 DOM 之前,它就相对没有意义。

相同的概念也适用于导入文档。除非您将其内容附加到 DOM,否则这是一项空操作。事实上,唯一会在导入文档中“执行”的内容是 <script>。请参阅导入脚本

针对异步加载进行优化

Imports 块渲染

导入主页面的块呈现。这与 <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> 末尾附近添加 import:

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

  • 系统会检索并解析一次来自同一网址的导入作业。也就是说,系统只在第一次看到导入时执行导入中的脚本。

  • 系统会按顺序处理导入中的脚本,但不会阻止主文档解析。

  • 导入链接并不意味着“#在此处包含内容”。它的意思是“解析器,提取此文档,以便我稍后使用”。虽然脚本会在导入时执行,但需要将样式表、标记和其他资源明确添加到主页面。请注意,不需要显式添加 <style>。这是 HTML Imports 与 <iframe> 之间的主要区别,后者指出“load and render this content here”。

总结

HTML 导入功能可将 HTML/CSS/JS 捆绑为单一资源。虽然这个概念本身就很有用,但在 Web 组件领域中会变得非常有用。开发者可以创建可重复使用的组件,供他人使用并引入到他们自己的应用中,所有操作均通过 <link rel="import"> 提供。

HTML 导入是一个简单的概念,但可为平台实现许多有趣的用例。

用例

  • 将相关的 HTML/CSS/JS 作为单个套装分发理论上,您可以将整个 Web 应用导入另一个应用。
  • 代码整理 - 以逻辑方式将概念分割为不同的文件,鼓励模块化和可重用性**。
  • 投放一个或多个自定义元素定义。导入操作可用于register并将其添加到应用中。这样做是良好的软件模式,能够将元素的接口/定义与其使用方式分开。
  • 管理依赖项 - 系统会自动删除重复资源。
  • 文本块 - 在导入之前,系统会对大型 JS 库的文件进行完全解析,以便开始运行,这非常缓慢。通过导入,库可以在分块 A 解析后立即开始工作。延迟时间更短!
// TODO: DevSite - Code sample removed as it used inline event handlers
  • 并行处理 HTML 解析 - 浏览器首次能够并行运行两个(或更多)HTML 解析器时。

  • 支持在应用中的调试模式和非调试模式之间切换,只需更改导入目标本身即可。您的应用无需知道导入目标是捆绑/编译资源还是导入树。