Web Component

TIP

Web Component 是一个浏览器支持的原生组件,也可能是未来组件化(高内聚、可重用、可组合)开发的趋势

一、简介

1 参考链接

2 特点

  • 优点:原生组件,不需要框架,性能好代码少
  • 缺点:兼容性差,没有自动更新机制

二、核心三大件

  • Custom elements(自定义元素):一组JavaScript API,允许您定义custom elements及其行为,然后可以在您的用户界面中按照需要使用它们
  • Shadow DOM(影子DOM):一组JavaScript API,用于将封装的“影子”DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突
  • HTML templates(HTML模板): <template><slot> 元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用

三、生命周期

  • connectedCallback:当custom element首次被插入文档DOM时,被调用
  • disconnectedCallback:当 custom element从文档DOM中删除时,被调用
  • adoptedCallback:当 custom element被移动到新的文档时,被调用 (移动到iframe中)
  • attributeChangedCallback:当 custom element增加、删除、修改自身属性时,被调用

四、使用方法

  • 使用 CustomElementRegistry.define() 方法注册您的新自定义元素 ,并向其传递要定义的元素名称、指定元素功能的类、以及可选的其所继承自的元素
  • 使用Element.attachShadow() 方法将一个shadow DOM附加到自定义元素上。使用通常的DOM方法向shadow DOM中添加子元素、事件监听器等等
  • 使用 <template><slot> 定义一个HTML模板。再次使用常规DOM方法克隆模板并将其附加到您的shadow DOM中

1 Custom elements

  • 具体教程open in new window
  • custom elements含义为自定义标签
  • 主要通过CustomElementRegistry接口中的CustomElementRegistry.define(name, class, extends) 方法来注册一个自定义元素,具体参数如下:
    • name 自定义元素名称,名称不能是单个单词,必须用中横线隔开,避免与native标签冲突
    • class 自定义元素的类
    • extends 可选参数,指定继承的已创建的元素,被用于创建自定义元素
// 语法
customElements.define(name, constructor, options);

// 案例
window.customElements.define('lxh-button', LxhButton)

2 Shadow DOM

graph

// 案例
class LxhButton extends HTMLElement{
    constructor(){
        super();
        // 1 创建影子
        let shadow = this.attachShadow({mode:'open'});

		// 2 获取或者创造页面元素
        let btn = document.getElementById('btn');
        let cloneTemplate = btn.content.cloneNode(true); // 拷贝模板
		shadow.appendChild(cloneTemplate)

		// 3 编写样式
        const style = document.createElement('style');
        style.textContent = `
			.btn-wrapper {
				position: relative;
			}
			.btn {
				// ...
			}
		`
        shadow.appendChild(style);
    }
}

3 HTML templates

// 案例
<lxh-button type="primary">自定义原生按钮</lxh-button>

<template id="btn">
    <button class="btn-wrapper">
        <slot>按钮</slot>
    </button>
</template>

五、自定义按钮-Button

<!DOCTYPE html>
<html lang="en">

<head>
	<meta charset="UTF-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Document</title>
</head>

<body>
	<style>
		/* 用户自定义最外层的样式 */
		:root {
			--background-color: black;
			--text-color: yellow
		}
	</style>

	<!-- 使用组件 -->
	<lxh-button type="primary">自定义原生按钮</lxh-button>

	<!-- 自定义组件模板 -->
	<template id="btn">
		<button class="btn-wrapper">
			<slot>按钮</slot>
		</button>
	</template>

	<script>
		class LxhButton extends HTMLElement {
			constructor() {
				super();
				// 1 创建影子
				let shadow = this.attachShadow({ mode: 'open' });

				// 2 获取模板按钮元素
				let btn = document.getElementById('btn');
				// 由于dom操作具备移动性,所以需要拷贝一份
				let cloneTemplate = btn.content.cloneNode(true);
				shadow.appendChild(cloneTemplate)

				// 3 编写样式
				const style = document.createElement('style');
				// 获取组件中传入的属性type
				let type = this.getAttribute('type') || 'default';

				// 定义不同类型的样式
				const btnList = {
					'primary': {
						background: '#409eff',
						color: '#fff'
					},
					'default': {
						background: '#909399',
						color: '#fff'
					}
				}
				// 使用样式,如果最外层没有传入,就使用默认的
				style.textContent = `
                    .btn-wrapper {
                        outline:none;
                        border:none;
                        border-radius:4px;
                        padding:5px 20px;
                        display:inline-flex;
                        background:var(--background-color, ${btnList[type].background});
                        color:var(--text-color, ${btnList[type].color});
                        cursor:pointer
                    }
                `
				shadow.appendChild(style);
			}
		}

		// 注册自定义组件
		window.customElements.define('lxh-button', LxhButton);
	</script>
</body>
</html>

六、折叠面板-Collapse

  • 难点:子组件与父组件通信,使用自定义事件派发机制

1 目录结构

├─collapse.html
├─collapse.js
├─collapse-item.js
├─index.js

2 具体代码

collapse.html

<!DOCTYPE html>
<html lang="en">

<head>
	<meta charset="UTF-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Document</title>
</head>

<body>
	<!-- 使用组件 -->
	<lxh-collapse>
		<lxh-collapse-item title="Node" name="1">
			<div>nodejs welcome</div>
		</lxh-collapse-item>

		<lxh-collapse-item title="react" name="2">
			<div>react welcome</div>
		</lxh-collapse-item>
		
		<lxh-collapse-item title="vue" name="3">
			<div>vue welcome</div>
		</lxh-collapse-item>
	</-collapse>

	<!-- 组件模板:没有实际意义, 不会渲染到页面上 -->
	<template id="collapse_tmpl">
		<div class="lxh-collapse">
			<slot></slot>
		</div>
	</template>

	<template id="collapse_item_tmpl">
		<div class="lxh-collapse-item">
			<div class="title"></div>
			<div class="content">
				<slot></slot>
			</div>
		</div>
	</template>

	<!-- 引入JS -->
	<script src="./index.js" type="module"></script>
</body>

</html>

index.js

import Collapse from './collapse.js';
import CollapseItem from './collapse-item.js';

// 注册组件
window.customElements.define('lxh-collapse', Collapse);
window.customElements.define('lxh-collapse-item', CollapseItem);


// 设置组件默认显示的状态
let defaultActive = ['1', '2']; // name:1 name:2 默认展开 3 应该隐藏
document.querySelector('lxh-collapse').setAttribute('active', JSON.stringify(defaultActive));

// 每个item需要获取到defaultActive 和自己的name属性比较,如果在里面就显示,不在里面就隐藏
document.querySelector('lxh-collapse').addEventListener('changeName', (e) => {
	let { isShow, name } = e.detail;
	if (isShow) {
		let index = defaultActive.indexOf(name);
		defaultActive.splice(index, 1);
	} else {
		defaultActive.push(name);
	}
	document.querySelector('lxh-collapse').setAttribute('active', JSON.stringify(defaultActive));
});

collapse.js

class Collapse extends HTMLElement {
	constructor() {
		super();
		const shadow = this.attachShadow({ mode: 'open' });

		const tmpl = document.getElementById('collapse_tmpl');
		let cloneTemplate = tmpl.content.cloneNode(true);
		shadow.appendChild(cloneTemplate);

		let style = document.createElement('style');
		// :host 代表的是影子的根元素
		style.textContent = `
            :host{
                display:flex;
                border:3px solid #ebebeb;
                border-radius:5px;
                width:100%;
            }
            .lxh-collapse{
                width:100%;
            }
        `
		shadow.appendChild(style);
		
		// 获取slot,监控slot变化
		let slot = shadow.querySelector('slot');
		slot.addEventListener('slotchange', (e) => {
			this.slotList = e.target.assignedElements();
			this.render();
		})
	}

	// 监控属性的变化
	static get observedAttributes() {
		return ['active']
	}
	// 属性更新
	attributeChangedCallback(key, oldVal, newVal) {
		if (key == 'active') {
			this.activeList = JSON.parse(newVal);
			this.render();
		}
	}
	// 重新渲染方法
	render() {
		if (this.slotList && this.activeList) {
			[...this.slotList].forEach(child => {
				child.setAttribute('active', JSON.stringify(this.activeList))
			});
		}
	}
	// connectedCallback(){
	//     console.log('插入到dom时执行的回调')
	// }
	// disconnectedCallback(){
	//     console.log('移除到dom时执行的回调')
	// }
	// adoptedCallback(){
	//     console.log('将组件移动到iframe 会执行')
	// }

}
export default Collapse

collapse-item.js

class CollapseItem extends HTMLElement {
	constructor() {
		super();
		let shadow = this.attachShadow({ mode: 'open' });

		let tmpl = document.getElementById('collapse_item_tmpl');
		let cloneTemplate = tmpl.content.cloneNode(true);
		shadow.appendChild(cloneTemplate);

		let style = document.createElement('style');
		style.textContent = `
            :host{
                width:100%;
            }
            .title{
                background:#f1f1f1;
                line-height:35px;
                height:35px;
            }
            .content{
                font-size:14px;
            }
        `
		shadow.appendChild(style)
		
		// 一些初始化变量
		this.isShow = true; // 标识自己是否需要显示
		this.titleEle = shadow.querySelector('.title');

		this.titleEle.addEventListener('click', () => {
			// 如果将结果传递给父亲  组件通信?
			document.querySelector('lxh-collapse').dispatchEvent(new CustomEvent('changeName', {
				detail: {
					name: this.getAttribute('name'),
					isShow: this.isShow
				}
			}))
		})
	}

	// 监控属性的变化
	static get observedAttributes() {
		return ['active', 'title', 'name']
	}
	// 属性更新
	attributeChangedCallback(key, oldVal, newVal) {
		switch (key) {
			case 'active':
				this.activeList = JSON.parse(newVal); // 子组件接受父组件的数据
				break;
			case 'title':
				this.titleEle.innerHTML = newVal; // 接受到title属性 作为dom的title
				break;
			case 'name':
				this.name = newVal
				break;
		}
		let name = this.name;
		if (this.activeList && name) {
			this.isShow = this.activeList.includes(name);
			this.shadowRoot.querySelector('.content').style.display = this.isShow ? 'block' : 'none'
		}
	}
}
export default CollapseItem