Web Component
Web Component 是一套不同的技术, 允许创建可重用的定制元素(它们的功能封装在代码之外)并且在 web 应用中使用它们
- Custom element(自定义元素): 一组 JavaScript API, 允许自定义元素及其行为, 然后在用户界面中按照需要使用它们
- Shadow DOM(影子 DOM): 一组 JavaScript API, 用于将封装的 “影子”DOM 树附加到指定元素(与页面 DOM 分开呈现)并控制其关联的功能. 通过这种方式, 可以保持自定义元素的功能私有, 这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突
- HTML template(HTML 模板):
<template>
和 <slot>
元素可以编写不在呈现页面中显示的标记模板. 然后它们可以作为自定义元素结构的基础被多次重用
自定义元素
==封装== ==重用==
创建自定义元素, 扩展浏览器中可用的元素集
类型
- 自定义内置元素, 继承自标准的 HTML 元素, 例如 HTMLImageElement, HTMLParagraphElement
- 独立自定义元素, 继承自 HTML 元素基类 HTMLElement, 必须从头实现它们的行为
实现自定义元素
使用 ES6 中的类实现一个自定义元素, 该类可以扩展 HTMLElement 或者其它定制的接口
- 在构造函数中设置初始化状态和默认值, 注册事件监听器, 创建一个影子根(shadowRoot)
- 在构造函数中不能检查元素的属性或子元素, 不能添加新的属性或子元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| class WordCount extends HTMLParagraphElement { constructor() { super(); } }
class PopupInfo extends HTMLElement { constructor() { super(); } connectedCallback() { const shadow = this.attachShadow({ mode: 'open' });
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');
const text = this.getAttribute('data-text'); info.textContent = text;
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);
const style = document.createElement('style'); console.log(style.isConnected);
style.textContent = ` .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; } `;
shadow.appendChild(style); console.log(style.isConnected); shadow.appendChild(wrapper); wrapper.appendChild(icon); wrapper.appendChild(info); } }
|
自定义元素生命周期
- connectedCallback() 每当元素添加到文档中时调用, 对自定义元素的操作在此钩子中实现
- disconnectedCallback() 每当元素从文档中移除时调用
- adoptedCallback() 每当元素被移动到新文档中时调用
- attributeChangedCallback() 在属性更改、添加、移除或替换时调用, 接收三个参数,
- name, 发生变化的属性名称
- oldValue, 属性的旧值
- newValue, 属性的新值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| window.customElements.define( 'my-custom-element', class extends HTMLElement { constructor() { super(); } connectedCallback() { console.log('自定义元素添加到页面'); } disconnectedCallback() { console.log('自定义元素从页面中移除'); } adoptedCallback() { console.log('自定义元素移动到新页面'); } attributeChangedCallback(name, oldValue, newValue) {} } );
|
响应属性变化
1 2 3 4 5 6 7 8 9 10 11 12 13
| window.customElements.define( 'my-custom-element', class extends HTMLElement { static observedAttributes = ['size']; constructor() { super(); } attributeChangedCallback(name, oldValue, newValue) { console.log(`属性 ${name} 已由 ${oldValue} 变更为 ${newValue}`); } } );
|
1 2
| <my-custom-element size="100"></my-custom-element>
|
自定义元素注册
注册自定义元素使用 window.customElements.define()
方法, 接收三个参数
- name, 自定义元素的名称, 使用
kebab-case
格式
- constructor, 自定义元素的构造函数
- options, 可选, 一个对象,
- extends, 指定自定义元素要扩展的内置元素名称
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| window.customElements.define( 'word-count', class extends HTMLParagraphElement { constructor() { super(); } connectedCallback() { console.log('自定义元素添加到页面'); } }, { extends: 'p' } );
window.customElements.define('popup-info', PopupInfo);
|
使用自定义元素
使用内置元素时, 将自定义元素的名称作为 is 属性的值
1 2 3
| <popup-info> </popup-info>
|
影子 DOM
将一个 DOM 树附加到一个元素上, 并且使该树的的内部与页面中运行的 Javascript 和 CSS 相互隔离
- 影子宿主(Shadow host), 影子 DOM 附加到的常规 DOM 节点
- 影子树(Shadow tree), 影子 DOM 内部的 DOM 树
- 影子边界(Shadow boundary), 影子 DOM 终止, 常规 DOM 开始的地方
- 影子根(Shadow root), 影子树的根节点
创建影子 DOM
使用页面中指定的 DOM 元素作为影子宿主, 调用宿主的 attachShadow()
方法创建影子 DOM
- Element.shadowRoot 通过影子宿主的 shadowRoot 属性访问影子 DOM 的内部
- Element.attachShadow() 创建影子 DOM
- mode, 指定影子 DOM 树的封装模式
- open, 允许从外部访问影子 DOM 根节点, Element.shadowRoot 返回一个 ShadowRoot 对象
- closed, 拒绝从外部访问关闭的 影子 DOM 根节点, Element.shadowRoot 返回 null
- delegatesFocus, 焦点委托, 当设置为 true 时, 指定减轻自定义元素的聚焦性能问题行为
1 2 3 4 5 6 7 8 9
| <div id="host"></div>
<script> const host = document.querySelector('#host'); const shadow = host.attachShadow({ mode: 'open' }); const span = document.createElement('span'); span.textContent = "I'm in the shadow DOM"; shadow.appendChild(span); </script>
|
CSS 封装
编程式
创建单一样式表并将其与多个 DOM 树共享
通过构建一个 CSSStyleSheet
对象并将其附加到影子根
- replace() 和 replaceSync() 替换当前样式表的内容, 只能用在通过 CSSStyleSheet 构造函数创建的 styleSheet 对象上
- replace() 方法异步的设置其内容, 返回一个 Promise
- replaceSync() 方法同步的设置其内容
- insertRule(rule [, index]) 向当前样式表指定位置插入样式规则, index 默认为 0, 返回值为新插入的规则在样式表中的索引
- deleteRule(index) 从样式表中删除指定的样式规则, index 为样式规则的索引
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const sheet = new CSSStyleSheet();
sheet.replaceSync('span { color: red; border: 2px dotted black;}');
const host = document.querySelector('#host');
const shadow = host.attachShadow({ mode: 'open' });
shadow.adoptedStyleSheets = [sheet];
const span = document.createElement('span'); span.textContent = "I'm in the shadow DOM"; shadow.appendChild(span);
|
声明式
不需要在不同组件之间共享样式表
通过在 template
元素的声明中添加一个 <style>
元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <template id="my-custom-element"> <style> span { color: red; border: 2px solid blue; } </style> <span>shadow DOM</span> </template>
<div id="host"></div>
<script> const host = document.querySelector('#host'); const shadow = host.attachShadow({ mode: 'open' }); const template = document.getElementById('my-custom-element');
shadow.appendChild(template.content); </script>
|
template 和 slots
template
template
元素中的内容不会在 DOM 中呈现, 但仍可用 javascript 去引用它
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <template id="my-paragraph"> <style> p { color: red; font-size: 18px; } </style> <p>This is my paragraph</p> </template>
<script> window.customElements.define( 'my-paragraph', class extends HTMLElement { constructor() { super(); let template = document.getElementById('my-paragraph').content;
const shadowRoot = this.attachShadow({ mode: 'open' }); shadowRoot.appendChild(template.cloneNode(true)); } } ); </script>
|
slots
使用 slots 增加 template 元素的灵活性
- 标记中包含未定义相关的插槽内容或者浏览器不支持 slot 属性时显示默认内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| <template id="my-paragraph"> <style> p { color: red; font-size: 18px; } </style> <p> <slot name="my-text">default text</slot> </p> </template>
<my-paragraph> <span slot="my-text">from slot</span> </my-paragraph>
<script> window.customElements.define( 'my-paragraph', class extends HTMLElement { constructor() { super(); const template = document.getElementById('my-paragraph').content; const shadowRoot = this.attachShadow({ mode: 'open' }).appendChild( template.cloneNode(true) ); } } ); </script>
|