简介
Web Components
标准非常重要的一个特性是,它使开发者能够将HTML页面的功能封装为custom elements
(自定义标签)。
技术组成
1. Custome Elements
:自定义元素,通过 JavaScript API
来创建。
两种类型
Autonomous custom elements
是独立的元素,它不继承其他内建的HTML元素。你可以直接把它们写成HTML标签的形式,来在页面上使用。例如<popup-info>
,或者是document.createElement("popup-info")
这样。
Customized built-in elements
继承自基本的HTML元素。在创建时,你必须指定所需扩展的元素(正如上面例子所示),使用时,需要先写出基本的元素标签,并通过 is 属性指定custom element
的名称。例如<p is="word-count">
, 或者document.createElement("p", { is: "word-count" })
。
main.js
// Create a class for the element
class Square extends HTMLElement {
// Specify observed attributes so that
// attributeChangedCallback will work
static get observedAttributes() {
return ['c', 'l'];
}
constructor() {
// Always call super first in constructor
super();
const shadow = this.attachShadow({mode: 'open'});
console.log('shaow-->', shadow);
const div = document.createElement('div');
const style = document.createElement('style');
shadow.appendChild(style);
shadow.appendChild(div);
}
connectedCallback() {
console.log('Custom square element added to page.');
updateStyle(this);
}
disconnectedCallback() {
console.log('Custom square element removed from page.');
}
adoptedCallback() {
console.log('Custom square element moved to new page.');
}
attributeChangedCallback(name, oldValue, newValue) {
console.log('Custom square element attributes changed.');
updateStyle(this);
}
}
customElements.define('custom-square', Square);
function updateStyle(elem) {
console.log('elem-->', elem);
const shadow = elem.shadowRoot;
shadow.querySelector('style').textContent = `
div {
width: ${elem.getAttribute('l')}px;
height: ${elem.getAttribute('l')}px;
background-color: ${elem.getAttribute('c')};
}
`;
}
const add = document.querySelector('.add');
const update = document.querySelector('.update');
const remove = document.querySelector('.remove');
let square;
update.disabled = true;
remove.disabled = true;
function random(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
add.onclick = function() {
// Create a custom square element
square = document.createElement('custom-square');
square.setAttribute('l', '100');
square.setAttribute('c', 'red');
document.body.appendChild(square);
update.disabled = false;
remove.disabled = false;
add.disabled = true;
};
update.onclick = function() {
// Randomly update square's attributes
square.setAttribute('l', random(50, 200));
square.setAttribute('c', `rgb(${random(0, 255)}, ${random(0, 255)}, ${random(0, 255)})`);
};
remove.onclick = function() {
// Remove the square
document.body.removeChild(square);
update.disabled = true;
remove.disabled = true;
add.disabled = false;
};
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Life cycle callbacks test</title>
<style>
custom-square {
margin: 20px;
}
</style>
<script defer src="main.js"></script>
</head>
<body>
<h1>Life cycle callbacks test</h1>
<div>
<button class="add">Add custom-square to DOM</button>
<button class="update">Update attributes</button>
<button class="remove">Remove custom-square from DOM</button>
</div>
</body>
</html>
运行结果
兼容性:Firefox、Chrome和Opera默认就支持
custom elements
。Safari目前只支持autonomous custom elements
(自主自定义标签),而 Edge也正在积极实现中。
2. Shadow DOM
:隔离 CSS 和 JavaScript,跟操作常规Dom类似。
<video>
元素就是使用shadow dom
实现一系列的按钮和其他控制器。可以使用 Element.attachShadow({ mode: 'open' })
方法来将一个 shadow root 附加到任何一个元素上。open
表示可以通过页面内的 JavaScript 方法来获取 Shadow DOM
。{ mode: 'closed' }
表示 Shadow DOM 是封闭的,不允许外部访问。
使用方式.
相关术语
Shadow host:一个常规 DOM节点,Shadow DOM 会被附加到这个节点上。
Shadow tree:Shadow DOM内部的DOM树。
Shadow boundary:Shadow DOM结束的地方,也是常规 DOM开始的地方。
Shadow root: Shadow tree的根节点。
兼容性:Firefox(从版本 63 开始),Chrome,Opera 和 Safari 默认支持 Shadow DOM。
例子
main.js
// Create a class for the element
class PopUpInfo extends HTMLElement {
constructor() {
// Always call super first in constructor
super();
// Create a shadow root
const shadow = this.attachShadow({mode: 'open'});
// Create spans
const wrapper = document.createElement('span');
wrapper.setAttribute('class', 'wrapper');
const icon = document.createElement('span');
icon.setAttribute('class', 'icon');
icon.setAttribute('tabindex', 0);
const info = document.createElement('span');
info.setAttribute('class', 'info');
// Take attribute content and put it inside the info span
const text = this.getAttribute('data-text');
info.textContent = text;
// Insert icon
let imgUrl;
if(this.hasAttribute('img')) {
imgUrl = this.getAttribute('img');
} else {
imgUrl = 'img/default.png';
}
const img = document.createElement('img');
img.src = imgUrl;
icon.appendChild(img);
// Apply external styles to the shadow dom
const linkElem = document.createElement('link');
linkElem.setAttribute('rel', 'stylesheet');
linkElem.setAttribute('href', 'style.css');
// Attach the created elements to the shadow dom
shadow.appendChild(linkElem);
shadow.appendChild(wrapper);
wrapper.appendChild(icon);
wrapper.appendChild(info);
}
}
// Define the new element
customElements.define('popup-info', PopUpInfo);
style.css
.wrapper {
position: relative;
}
.info {
font-size: 0.8rem;
width: 200px;
display: inline-block;
border: 1px solid black;
padding: 10px;
background: white;
border-radius: 10px;
opacity: 0;
transition: 0.6s all;
position: absolute;
bottom: 20px;
left: 10px;
z-index: 3;
}
img {
width: 1.2rem;
}
.icon:hover + .info, .icon:focus + .info {
opacity: 1;
}
index.html
<h1>Pop-up info widget - web components</h1>
<form>
<div>
<label for="cvc">Enter your CVC <popup-info img="img/alt.png" data-text="Your card validation code (CVC) is an extra security feature — it is the last 3 or 4 numbers on the back of your card."></popup-info></label>
<input type="text" id="cvc">
</div>
</form>
运行结果
shadow dom VS iframe
shadow dom
- 渲染得更快,占用内存更少;
- 具备较好的灵活性;
- 隔离样式;
iframe
MDN-iframe
- 增加内存和其他计算资源,因为每个浏览上下文都拥有完整的文档环境;
- 隔离样式
- 需要动态设置高度,否则内容超出高度,会显示滚动条;
- 无法操作内嵌表单
- 移动端兼容性差
- 需要服务器辅助配置,否则会出现跨域
dom嵌入页面提高性能的兼容性写法
let content = `
<style>
body { /* for fallback iframe */
margin: 0;
}
p {
border: 1px solid #ccc;
padding: 1rem;
color: red;
font-family: sans-serif;
}
</style>
<p>Element with Shadow DOM</p>
`;
let el = document.querySelector('.my-element');
if (document.body.attachShadow) {
let shadow = el.attachShadow({ mode: 'open' }); // Allows JS access inside
shadow.innerHTML = content;
} else {
let newiframe = document.createElement('iframe');
'srcdoc' in newiframe ?
newiframe.srcdoc = content :
newiframe.src = 'data:text/html;charset=UTF-8,' + content;
let parent = el.parentNode;
parent.replaceChild(newiframe, el);
}
3. Templates and Slots
:用户在HTML中定义的模板,只有调用的时候才会被渲染。
可以通过使用 <template>
和 <slot>
元素创建一个可以用来灵活填充 Web组件的 shadow DOM 的模板。请查看例子
template例子
自定义my-paragraph
标签,实现内容是,创建一个 shadow dom,将模板的内容拷贝进 shadow 根结点上。
<template id="my-paragraph">
<style>
p {
color: white;
background-color: #666;
padding: 5px;
}
</style>
<p>My paragraph</p>
</template>
<my-paragraph></my-paragraph>
customElements.define('my-paragraph',
class extends HTMLElement {
constructor() {
super();
let template = document.getElementById('my-paragraph');
let templateContent = template.content;
const shadowRoot = this.attachShadow({mode: 'open'})
.appendChild(templateContent.cloneNode(true));
}
})
slot插槽例子
customElements.define('person-details',
class extends HTMLElement {
constructor() {
super();
const template = document.getElementById('person-template');
const templateContent = template.content;
const shadowRoot = this.attachShadow({mode: 'open'});
const style = document.createElement('style');
style.textContent = `
div { padding: 10px; border: 1px solid gray; width: 200px; margin: 10px; }
h2 { margin: 0 0 10px; }
ul { margin: 0; }
p { margin: 10px 0; }
::slotted(*) { color: gray; font-family: sans-serif; }
`;
shadowRoot.appendChild(style);
shadowRoot.appendChild(templateContent.cloneNode(true));
}
});
<template id="person-template">
<div>
<h2>Personal ID Card</h2>
<slot name="person-name">NAME MISSING</slot>
<ul>
<li><slot name="person-age">AGE MISSING</slot></li>
<li><slot name="person-occupation">OCCUPATION MISSING</slot></li>
</ul>
</div>
</template>
<person-details>
<p slot="person-name">Morgan Stanley</p>
<span slot="person-age">36</span>
<span slot="person-occupation">Accountant</span>
</person-details>
<person-details>
<p slot="person-name">Dr. Shazaam</p>
<span slot="person-age">Immortal</span>
<span slot="person-occupation">Superhero</span>
</person-details>
<person-details>
<p slot="person-name">Boris</p>
<span slot="age">27</span>
<span slot="i-am-awesome">Time traveller</span>
</person-details>