定义:面向标准
封装
可重用
的定制元素
;
1 WebComponent 自定义元素
shadowDom 将封装的定制元素挂载到元素节点上,可以理解为内部具有一个隔离环境的沙箱,其中的 shadow Tree集合
与外部的其他DOM互不干扰,具有天然的样式隔离(对比vue中的scoped;
Component节点
: 自定义元素的根节点,元素内部可以通过 this
获取节点实例,可以通过 :host
设置样式,或者外部通过选择器直接修改;
slot Tree集合
: slot节点集合;
具有样式隔离的只有 shadow Tree 部分;
1.1 声明元素 - 注册可使用
🤯问题场景:站在浏览器的角度,在遇到一些自定义元素后,我们是如何知道哪些元素是自定义的?应该如何去渲染的呢?
为此Window
对象提供了一个只读属性customElements
,可用于注册新的自定义元素,或者获取之前定义过的自定义元素的信息。
// 定义一个新的自定义元素
customElements.define(name, constructor, options);
// 返回指定自定义元素的构造函数,如果未定义自定义元素,则返回undefined
customElements.get(name);
// 更新节点子树中所有包含阴影的自定义元素
customElements.upgrade(root);
// 当使用给定名称定义自定义元素时将会执行的回调
customElements.whenDefined(name);
1.1.1 注册可使用
我们可以根据继承的元素不同
来创建两种类型的自定义元素:定制元素、扩展内置元素;
// 定制元素
class CustomLifecycle extends HTMLElement {}
// 扩展内置元素
class WordCount extends HTMLParagraphElement {}
// 判断元素是否注册
if (!customElements.get('custom-lifecycle')) {
// 注册元素
customElements.define('custom-lifecycle', CustomLifecycle);
}
1.1.2 监听元素注册回调事件(返回promise
在首次加载、元素异步注册时,为了避免页面异常,需要确保元素注册完成后再进行UI的渲染;
customElements.whenDefined('custom-lifecycle').then(callback);
🤯问题1:如果元素已经注册,那么 resolve 会立即执行(但仍旧是异步事件;
🤯问题2:如果传入的 componentName 不是一个 有效的自定义元素名,那么将会触发 reject 回调;
1.2 生命周期
自定义元素的在挂载期间,会经过一系列的初始化过程:
生命周期的执行顺序对比react 或者vue中的执行顺序是类似的:constructor -> attributeChangedCallback -> connectedCallback
class CustomLifecycle extends HTMLElement {
// 自定义元素初始化;
// 对比 vue中的created;
constructor() {
// HTMLElement.prototype.constructor.call(this);
super();
console.log('constructor');
}
/**
* connectedCallback
* 当元素插入到 DOM 中时,将调用 connectedCallback;
* 在这里就可以通过 this.shadowRoot 拿到shadowDOM元素实例;
* 对比React的componentDidMount、vue的mount方法;
*/
connectedCallback() {
console.log('connectedCallback', this.shadowRoot);
this.mainEl = this.shadowRoot.querySelector('.main');
}
/**
* disconnectedCallback
* 只要从 DOM 中移除元素,就会调用 disconnectedCallback;
* 在这里可以清楚自定义元素内的定时器、事件监听等等;
*
* 但是,当用户直接关闭浏览器或浏览器标签时,这个方法将不会被调用;
* 可以用window.unload beforeunload或者widow.close 去触发在浏览器关闭是的回调;
*
* 对比React的componentWillUnmount、vue的destory方法;
*/
disconnectedCallback() {
console.log('disconnected!');
}
/**
* 设置哪些属性将会被监听变动,一般结合attributeChangedCallback使用;
* 对比React中的getDerivedStateFromProps、vue中的watch方法;
*/
static get observedAttributes() { return ['my-attr']; }
/**
* @param {*} name 属性名
* @param {*} oldVal 旧值
* @param {*} newVal 新值
*/
attributeChangedCallback(name, oldVal, newVal) {
console.log(`Attribute: ${name} changed!`);
if (name == 'open' && newValue == 'true') {
// 生命周期问题
if (this.mainEl) this.mainEl.xxx;
}
}
}
需要注意的是
attributeChangedCallback
执行在connectedCallback
之前,当元素observedAttributes
的监听属性存在默认值时,会触发;这就意味着,
attributeChangedCallback
中的实例方法可能不存在,需要逻辑判断兼容下; (tips:如果默认值会对UI有影响,可以在 constructor 中进行处理;
1.2.1 constructor
在这里完成自定义元素的内容定义(包含 shadowRoot 实例初始化、样式、DOM内容
等);
如果默认属性值会对UI有影响,可以在 constructor 中进行处理;
场景:按钮默认是disabled状态;
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
:host{}
</style>
<div>${this.defaultValue}</div>
`;
}
get defaultValue() {
return this.getAttribute('defaultValue');
}
1、根元素样式选择器
可以通过 :host 标识根元素,结合 元素属性值,可以进行各种条件下的 样式控制(如下图,:host控制的是 elmation-delay-list-item 节点样式;
<style>
:host{}
:host([disabled]) {}
:host([type="primary"]) {}
:host(:not([disabled])) {}
</style>
// 元素外部样式,仍旧可以覆盖 :host
elmation-delay-list-item {}
2、mode - shadowRoot的创建模式
可选值为 “open” 或者 “closed”;
当设置为 closed 时,意味着 shadowRoot 内部实现将完全封闭,无法被外部 JavaScript 修改(主要是属性值),例如 <video> 标签;
1.2.2 connectedCallback、disconnectedCallback
元素 连接
到DOM树时触发,这个时机包含 插入节点、移动节点
,所以可能被触发多次
;
在这期间可以通过 shadowRoot 实例设置事件监听、私有数据初始化
;
如果需要像 created 这样只需要在生命周期触发一次的方法,可以在
constructor
或者 通过this._isReady = true
; 添加实例属性标识来完成;
connectedCallback() {
this.btn = this.shadowRoot.getElementById('btn');
this.btn.addEventListener('click', () => {});
}
disconnectedCallback() {
this.btn.removeEventListener('click');
}
.2.3 attributeChangedCallback
配合 observedAttributes 使用,实现属性值变更后的逻辑处理(默认值也会触发
;
1.3 对外通信
自定义元素跟外部交流可以通过两种基本的途径:属性值、自定义事件、slot插槽;
1.3.1 属性值
1、通过 getter、setter 的方式获取、修改属性值(通过 getAttribute
和 setAttribute
)
class ElfinTransition extends HTMLElement {
get name() {
return this.getAttribute('name');
}
set name(value) {
if (value) {
this.setAttribute('name', value);
} else {
this.removeAttribute('value');
}
}
}
2、不同场景下获取到的属性值
<name> ==> this.getAttribute('name') => null
<name open> ==> this.getAttribute('name') => ''
<name open="true"> ==> this.getAttribute('name') => 'true'
1.3.2 自定义事件
在正常使用中,外部经常需要感知到内部的某些行为,这个应该如何实现呢?
方案是在元素内部自定义事件 new CustomEvent()
,通过 this.dispatchEvent
将事件广播出,外部用 addEventListener
监听(注意除了标准事件(如click、change...)不能直接使用类似 onEvent 的形式监听);
this.dispatchEvent(new CustomEvent('play', {
detail: {
slow: this._slow,
},
}));
获取传递出来的属性值 ==> event.detail.slow;
1.3.3 slot插槽
除了通过属性值传入普通的值,也可以通过slot传入 HTML内容
;(对比vue中的slot
1、具名插槽;
元素有一个特殊的 attribute:name
。这个 attribute 可以用来定义额外的插槽,一个不带 name
的 出口将会作为默认插槽(类比于default;
2、特殊的display值;
slot的默认display值为 contents
,意味这没有任何的布局效果(无法设置宽高、背景色等,但是可以设置字体相关,将该元素移除DOM树对布局也不会有影响;
3、特殊的HTML结构;
在视觉上,插槽元素是替换了 slot 的占位,但是两者在HTML结构中的位置并没有发生变化;
也就是插槽元素仍旧是存在于 shadowDOM 之外的,无法通过 this.shadowRoot.querySelector
匹配到(可以通过 this.querySelector
;
在 shadowDOM 中可以对 slot元素 上添加事件,这和在插槽上添加是一样的效果,不过一个是内部控制,一个是外部控制;
4、如果获取到 slot DOM 实例?
通过 slotchange
事件确定slot挂载的时机,建议通过 关联元素(item元素)、添加特殊标识(添加特殊属性值)
的办法,然后用 this.querySelector
获取到slot节点;
this.shadowRoot.addEventListener('slotchange', () => {
let nodes = this.querySelectorAll('elmation-delay-list-item');
});
1.4 元素样式
1、样式隔离
前面提到,可以通过 :host 设置根元素样式;
自定义元素内的样式(#shadow-root内),将不会影响到外部元素,外部的样式同样无法影响到内部;(样式隔离性
也就是说,在
:host
和slot插槽内节点
的样式,仍旧是可以被外部覆盖的;
2、样式覆盖
🤯场景:需要覆盖内部的元素样式呢?
除了通过 :host 和 slot,还可以使用 var变量;
内部JS
shadowRoot.innerHTML = `
<style>
button {
background: var(--background, #000);
}
</style>
<div>Sup</div>
`;
外部CSS
component {
--background: #fff;
}
或者
component.style.setProperty('--x', '100px');
缺点:无法随心所欲的覆盖 shadowDOM 元素中的样式,存在一定的局限性;