包含网站媒体资源
为什么要导入?
想想您如何在 Web 上加载不同类型的资源。对于 JS,我们使用 <script src>
。对于 CSS,您可能首选 <link rel="stylesheet">
。对于映像,该值为 <img>
。视频有<video>
。音频,<audio>
… 直奔主题!大多数网络内容都有一种简单的声明式方式来加载自身。但 HTML 并非如此。您可以选择以下选项:
<iframe>
- 久经考验,但重量负担。iframe 的内容完全位于与您的网页不同的上下文中。虽然这在很大程度上是一项很棒的功能,但也会带来额外的挑战(收缩将框架的大小封装到其内容中很难,编写/退出脚本非常令人沮丧,几乎无法设置样式)。- AJAX - 我喜欢
xhr.responseType="document"
,但您说我需要 JS 才能加载 HTML?这似乎不对。 - CrazyHacks™ - 嵌入在字符串中,隐藏为注释(例如
<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 Modules 的浏览器,但其他浏览器供应商仍在观望 ES Modules 的表现。 不过,在其他浏览器中,webcomponents.js polyfill 非常有用,直到这些功能获得广泛支持。
捆绑资源
导入提供了将 HTML/CSS/JS(甚至其他 HTML 导入)捆绑到单个交付项中的惯例。这是一种内在功能,但功能强大。如果您要创建主题、库,或者只是想将应用细分为逻辑分块,那么向用户提供单个网址会很有吸引力。你甚至可以通过导入的方式提供整个应用。请想一想。
引导加载程序是一个真实的例子。Bootstrap 由单独的文件(bootstrap.css、bootstrap.js、字体)组成,需要 JQuery 来使用其插件,并提供标记示例。开发者喜欢按需灵活选择。这样,他们就可以接受自己想要使用的框架部分。尽管如此,我敢打赌,普通的 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
事件;当导入尝试失败时(例如资源返回 404 错误),则会触发 onerror
事件。
导入内容会尝试立即加载。使用 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 导入的设计非常适合在 Web 上加载可重复使用的内容。尤其是,它是分发 Web 组件的理想方式。从基本的 HTML <template>
到包含 Shadow DOM 的完整 自定义元素 [1、2、3],应有尽有。将这两种技术搭配使用时,导入项会成为 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 Imports 搭配使用效果非常出色。导入内容可以执行脚本,那么为何不定义并注册自定义元素,让用户无需执行此操作?我们称之为“自动注册”。
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 导入成为共享 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 导入功能就不用担心了!可用于管理依赖项。
通过将库封装在 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 技术一样,您应谨慎使用。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 之前,浏览器不会请求 styles.css:
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>
中。如果您无法避免在 <head>
中放置 <script>
,则动态添加导入内容可防止屏蔽网页:
<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>
注意事项
导入内容的 mimetype 为
text/html
。来自其他来源的资源需要启用 CORS。
系统将检索并解析一次来自同一网址的导入内容。这意味着,导入内容中的脚本仅在首次被看到时执行。
导入内容中的脚本会按顺序处理,但不会阻止主文档解析。
导入链接并不意味着“#include the content here”。这意味着“解析器,去提取这个文档,以便我稍后使用”。虽然脚本会在导入时执行,但样式表、标记和其他资源需要明确添加到主页中。请注意,无需明确添加
<style>
。这是 HTML 导入与<iframe>
之间的主要区别,后者指出“在此处加载并呈现此内容”。
总结
HTML 导入允许将 HTML/CSS/JS 打包为单个资源。虽然这本身很有用,但在 Web 组件领域,这种想法会变得非常强大。开发者可以创建可重复使用的组件,供他人使用并引入到自己的应用中,所有这些都通过 <link rel="import">
提供。
HTML 导入是一个简单的概念,但可为该平台实现许多有趣的用例。
使用场景
- 将相关的 HTML/CSS/JS 作为单个软件包分发。从理论上讲,您可以将一个完整的 Web 应用导入到另一个 Web 应用中。
- 代码整理 - 按照逻辑将概念细分为不同的文件,鼓励模块化和可重用性**。
- 提交一个或多个自定义元素定义。导入可用于注册并将其包含在应用中。这种做法遵循良好的软件模式,将元素的接口/定义与其使用方式分离开来。
- 管理依赖项 - 系统会自动删除重复的资源。
- 分块脚本 - 在导入之前,大型 JS 库的文件需要完全解析才能开始运行,速度很慢。使用导入功能后,库可以在解析分块 A 后立即开始工作。延迟时间更短!
// TODO: DevSite - Code sample removed as it used inline event handlers
并行解析 HTML - 这是浏览器首次能够并行运行两个(或更多)HTML 解析器。
只需更改导入目标本身,即可在应用中切换调试模式和非调试模式。您的应用无需知道导入目标是捆绑/编译资源还是导入树。