近年来,web 开发者们通过插件或者模块的形式在网上分享自己的代码,便于其他开发者们复用这些优秀的代码。同样的故事不断发生,人们不断的复用 JavaScript 文件,然后是 CSS 文件,当然还有 HTML 片段。但是你又必须祈祷这些引入的代码不会摧毁你的网站或者web app(有个根本问题,导致 HTML 和 JavaScript 构建出来的部件难以使用:部件中的 DOM 树并没有封装起来。 封装的缺乏意味着文档中的样式表会无意中影响部件中的某些部分; JavaScript 可能在无意中修改部件中的某些部分;你书写的 ID 也可能会把部件内部的 ID 覆盖)。
Web Components 是这类问题最好的良药,通过一种标准化的非侵入的方式封装一个组件,每个组件能组织好它自身的 HTML 结构、CSS 样式、JavaScript 代码,并且不会干扰页面上的其他代码。
Web Component 由四部分组成:
- HTML Imports
- HTML Templates
- Custom Elements
- Shadow DOM
我先解释一下:Template可以让你用声明的方式定义你的自定义元素的内容。Shadow DOM可以让一个元素的style、ID、class只作用到其本身。自定义元素可以让你自定义HTML标签。通过把这些跟HTML导入结合起来,你自定义的web 组件会变得模块化,具有复用性。任何人添加一个Link标签就可以使用它。
一、HTML Imports
在 HTML 主文件中添加要导入的文件
<link rel="import" href="xxx.html">
在HTML导入文件中不需要doctype、html、head、body这些标签
浏览器解析HTML文档的方式是线性的,这就是说HTML顶部的script会比底部先执行。并且,浏览器通常会等到JavaScript代码执行完毕后,才会接着解析后面的代码。
为了不让script 妨碍HTML的渲染,你可以在标签中添加async或defer属性(或者你也可以将script 标签放到页面的底部)。defer 属性会延迟脚本的执行,直到全部页面解析完毕。async 属性让浏览器异步地执行脚本,从而不会妨碍HTML的渲染。那么,HTML 导入是怎样工作的呢?
HTML导入文件中的脚本就跟含有defer属性一样。例如在下面的示例中,index.html会先执行script1.js和script2.js ,然后再执行script3.js。
index.html
<link rel="import" href="component.html"> // 1.
<title>Import Example</title>
<script src="script3.js"></script> // 4.
component.html
<script src="js/script1.js"></script> // 2.
<script src="js/script2.js"></script> // 3.
二、HTML Templates
原来的模板形式:
- script 元素
<script type="text/template">
<div>
this is your template content.
</div>
</script>
- textarea 元素
<textarea style="display:none;">
<div>
this is your template content.
</div>
</textarea>
现在的模板形式:
- template 元素
<template>
<div>
this is your template content.
</div>
</template>
主要有四个特性:
- 惰性:在使用前不会被渲染;
- 无副作用:在使用前,模板内部的各种脚本不会运行、图像不会加载等;
- 内容不可见:模板的内容不存在于文档中,使用选择器无法获取;
- 可被放置于任意位置:即使是 HTML 解析器不允许出现的位置,例如作为
<select>
的子元素。
三、Custom Elements
注册新元素
使用 document.registerElement()
可以创建一个自定义元素:
var XFoo = document.registerElement('x-foo');
document.body.appendChild(new XFoo());
document.registerElement() 的第一个参数是元素的标签名。这个标签名必须包括一个连字符(-)。
第二个参数是一个(可选的)对象,用于描述该元素的 prototype。
自定义元素默认继承自 HTMLElement,因此上一个示例等同于:
var XFoo = document.registerElement('x-foo', {
prototype: Object.create(HTMLElement.prototype)
});
生命周期及回调
- createdCallback
- attachedCallback
- detachedCallback
- attributeChangedCallback
扩展原生元素
假设平淡无奇的原生 <button>
元素不能满足你的需求,你想将其增强为一个“超级按钮”,可以通过创建一个继承 HTMLButtonElement.prototype
的新元素,来扩展 <button>
元素:
var MegaButton = document.registerElement('mega-button', {
prototype: Object.create(HTMLButtonElement.prototype)
});
元素提升
你有没有想过为什么 HTML 解析器对非标准标签不报错?比如,我们在页面中声明一个 <randomtag>
,一切都很和谐。根据 HTML 规范的表述:
非规范定义的元素必须使用 HTMLUnknownElement 接口。 —— HTML 规范
是非标准的,它会继承 HTMLUnknownElement。
对自定义元素来说,情况就不一样了。拥有合法元素名的自定义元素将继承 HTMLElement。
// “tabs”不是一个合法的自定义元素名
document.createElement('tabs').__proto__ === HTMLUnknownElement.prototype
// “x-tabs”是一个合法的自定义元素名
document.createElement('x-tabs').__proto__ == HTMLElement.prototype
Unresolved 元素
由于自定义元素是通过脚本执行 document.registerElement() 注册的,因此 它们可能在元素定义被注册到浏览器之前就已经声明或创建过了。例如:你可以先在页面中声明 ,以后再调用 document.registerElement(‘x-tabs’)。
在被提升到其定义之前,这些元素被称为 unresolved 元素。它们是拥有合法自定义元素名的 HTML 元素,只是还没有注册成为自定义元素。
四、Shadow DOM
有了 Shadow DOM,元素就可以和一个新类型的节点关联。这个新类型的节点称为 shadow root。与一个 shadow root 关联的元素称作一个 shadow host。shadow host 的内容不会渲染;shadow root 的内容会渲染。
关键点:shadow host 的内容投射(projected)到 <content>
元素出现的地方
示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div class="widget">Shadow Dom</div>
<template>
<h1>Hello, I am <content></content></h1>
</template>
<script>
var host = document.querySelector('.widget');
var root = host.createShadowRoot();
var template = document.querySelector('template');
root.appendChild(document.importNode(template.content, true));
</script>
</body>
</html>
示例效果图:
扩展:高级投射
在上面的例子中, 元素挑选了 shadow host 的所有内容。通过使用 select 特性,你可以控制 content 元素投射的内容。你也可以使用多个 content 元素。
比如说,如果你有一个包含如下内容的文档:
<div id="nameTag">
<div class="first">Bob</div>
<div>B. Love</div>
<div class="email">bob@</div>
</div>
shadow root 使用 CSS 选择器来选择特定内容:
<div style="background: purple; padding: 1em;">
<div style="color: red;">
<content select=".first"></content>
</div>
<div style="color: yellow;">
<content select="div"></content>
</div>
<div style="color: blue;">
<content select=".email"></content>
</div>
</div>
注意: select 只能选择 host 节点的直接子元素。也就是说,你不能选择后代元素(例如 select=”table tr”)。
Shadow DOM 样式封装
1、宿主元素(:host)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<style>
p {
color: red;
font-size: 18px;
}
</style>
<p class="normal">我是一个普通文本</p>
<p class="shadow"></p>
<script>
var host = document.querySelector('.shadow');
var root = host.createShadowRoot();
root.innerHTML = `
<style>
:host(p.shadow) {
color: blue;
font-size: 24px;
}
</style>
我是一个影子文本`;
</script>
</body>
</html>
注意上例中 shadow DOM 内的选择器是 :host(p.shadow),而不是跟外部平级的 :host(p)。 因为:host(p) 的优先级低于外部的 p 选择器,所以不会生效。需要使用 :host(p.shadow) 提升优先级,才能将 .shadow 中的样式覆盖。
在一个 shadow root 内支持多种宿主类型
:host 还有一种使用场景,那就是你创建了一个主题库,想在相同的 Shadow DOM 内为不同类型的宿主元素提供样式化。
:host(x-foo:host) {
/* 当宿主是 <x-foo> 元素时生效。 */
}
:host(x-bar:host) {
/* 当宿主是 <x-bar> 元素时生效。 */
}
:host(div) { {
/* 当宿主或宿主的祖先元素是 <div> 元素时生效。 */
}
2、宿主祖先元素(:host-context)
有时需要通过外部条件来改变宿主元素的样式,例如,可能需要根据元素设置的CSS主题类,然后再其基础做对我们组件进行修改。
用法和 :host() 类似,使用:host-context()伪类,括号里面可以指定根选择器。比如下面示例是针对带theme-lightde类下所有h2元素有效。
:host-context(.theme-light) h2 {
background-color: #eef;
}
试写第一个 WebComponent
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<link rel="import" href="components/favorite-colour.html">
</head>
<body>
<favorite-colour></favorite-colour>
</body>
</html>
favorite-colour.html
<template>
<style>
.coloured {
color: red;
}
</style>
<p>My favorite colour is: <strong class="coloured">Red</strong></p>
</template>
<script>
(function() {
// Creates an object based in the HTML Element prototype
var element = Object.create(HTMLElement.prototype);
// Gets content from <template>
var template = document.currentScript.ownerDocument.querySelector('template').content;
// Fires when an instance of the element is created
element.createdCallback = function() {
// Creates the shadow root
var shadowRoot = this.createShadowRoot();
// Adds a template clone into shadow root
var clone = document.importNode(template, true);
shadowRoot.appendChild(clone);
};
document.registerElement('favorite-colour', {
prototype: element
});
}());
</script>