适用于网站
为什么要导入?
想一想如何在网络上加载不同类型的资源。对于 JS,我们采用 <script src>
。对于 CSS,您的目标可能是 <link rel="stylesheet">
。对于图片,则为 <img>
。视频包含<video>
。音频,<audio>
... 直奔主题!大部分 Web 内容都采用简单的声明式方式自行加载。但对于 HTML 则不然。有以下几种选项供您选择:
<iframe>
- 久经考验,但重量级。iframe 的内容完全位于与网页不同的环境中。虽然这在大部分情况下是一项很棒的功能,但也会带来额外的挑战(将框架缩小到其内容所占的比例非常困难,通过脚本处理/脱离脚本、设置样式几乎是不可能的)。- AJAX - 我喜欢
xhr.responseType="document"
,但你是说我需要 JS 加载 HTML?好像不对哦。 - 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.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>
代码块附加到主页面。再次执行脚本。
- 导入中定义的函数以
- 导入操作不会阻止对主页面的解析。不过,系统会按顺序处理其中的脚本。这意味着您在保持正确的脚本顺序的同时,获得类似延迟的行为。详见下文。
交付 Web 组件
HTML Imports 的设计非常适合在网络上加载可重复使用的内容。具体来说,这是分发 Web 组件的理想方式。使用 Shadow DOM [1、2、3] 涵盖了从基本的 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 包含在许多不同的导入树中,但浏览器仅获取和处理一次文档。查看网络面板可以证明:
性能考虑因素
HTML 导入功能非常棒,但与任何新的网络技术一样,您应该明智地使用它们。Web 开发最佳实践仍然适用。以下是需要注意的一些事项。
串联导入
减少网络请求始终非常重要。如果您有许多顶级导入链接,请考虑将它们合并为一项资源并导入该文件!
Vulcanize 是 Polymer 团队开发的一款 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 解析器时。
支持在应用中的调试模式和非调试模式之间切换,只需更改导入目标本身即可。您的应用无需知道导入目标是捆绑/编译资源还是导入树。