WebComponent
优点:原生组件,不需要框架,性能好代码少
缺点:有兼容性问题
组件化好处:高内聚,可重用,可组合
核心技术
custom elements
: 一组JavaScript API
,允许定义custom elements
及其行为,然后可以在用户界面中按照需要使用它们Shadow DOM
: 一组JavaScript API
,用于将封装的DOM树附加到元素(与主文档DOM分开呈现)
并控制其关联的功能。通过这种方式,可以保持元素的功能私有,可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突HTML templates
:<template>
和slot
元素使用户编写不在呈现页面中显示的标记模版。然后它们可以作为定义元素机构的基础被多次重用
实现自定义Button组件
HTML template模版
template中的内容是定义的
button
组件样式,slot
可以获取自定义组件中的内容,插入到模版对应的位置
<comp-button type='primary'>我是一个按钮</comp-button>
<template id="btn">
<button class="comp-btn">
<slot>我是默认值</slot>
</button>
</template>
组件实现
class CompButton extends HTMLElement {
constructor() {
super();
//创建一个影子,这里的this是comp-button
const shadow = this.attachShadow({
mode: 'open'
});
//找到模版
const temp = document.getElementById('btn');
//克隆模版,这样可以保证模版的复用性
const cloneTemp = temp.content.cloneNode(true);
//设置一些属性
const style = document.createElement('style');
const type = this.getAttribute('type') || 'default'
const styleList = {
'primary': {
background: '#409eff',
color: '#fff'
},
'default': {
background: '#909399',
color: '#fff'
}
}
style.textContent = `
.comp-btn{
outline:none;
border:none;
border-radius:4px;
padding:5px 10px;
display:inline-flex;
background:var(--background-color,${styleList[type].background});
color:var(--text-color,${styleList[type].color});
cursor:pointer
}
`
//样式添加到影子
shadow.appendChild(style);
//模版挂载到影子上
shadow.appendChild(cloneTemp);
}
}
定义自定义组件
//定义一个自定义组件
window.customElements.define('comp-button', CompButton);
WebComponent生命周期
connectedCallback
: 当custom element首次被插入文档DOM时,被调用disconnectedCallback
: 当custom element从文档DOM中删除是,被调用adoptedCallback
: 当custom element被移动到新的文档时,被调用(移动到iframe中)attributeChangedCallback
: 当custom element增减,删除,修改自身属性时,被调用
实现类似Collapse 折叠面板
├── index.html
├── index.js
├── ly-collapse-item.js
└── ly-collapse.js
效果展示
index.html
<!--
* @Author: dfh
* @Date: 2021-02-02 07:58:07
* @LastEditors: dfh
* @LastEditTime: 2021-02-02 09:02:54
* @Modified By: dfh
* @FilePath: /day15-webcomponent/2.index.html
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ly-collapse>
<ly-collapse-item title="一致性 Consistency" name="1">
<div>与现实生活一致:与现实生活的流程、逻辑保持一致,遵循用户习惯的语言和概念;</div>
<div>在界面中一致:所有的元素和结构需保持一致,比如:设计样式、图标和文本、元素的位置等。</div>
</ly-collapse-item>
<ly-collapse-item title="反馈 Feedback" name="2">
<div>控制反馈:通过界面样式和交互动效让用户可以清晰的感知自己的操作;</div>
<div>页面反馈:操作后,通过页面元素的变化清晰地展现当前状态。</div>
</ly-collapse-item>
<ly-collapse-item title="效率 Efficiency" name="3">
<div>简化流程:设计简洁直观的操作流程;</div>
<div>清晰明确:语言表达清晰且表意明确,让用户快速理解进而作出决策;</div>
<div>帮助用户识别:界面简单直白,让用户快速识别而非回忆,减少用户记忆负担。</div>
</ly-collapse-item>
<ly-collapse-item title="可控 Controllability" name="4">
<div>用户决策:根据场景可给予用户操作建议或安全提示,但不能代替用户进行决策;</div>
<div>结果可控:用户可以自由的进行操作,包括撤销、回退和终止当前操作等。</div>
</ly-collapse-item>
</ly-collapse>
<!-- template没有实际意义,不会渲染到页面上 -->
<template id='collapse-tmpl'>
<div class="collapse">
<slot></slot>
</div>
</template>
<template id='collapse-item-tmpl'>
<div class='title'></div>
<div class='content'>
<slot></slot>
</div>
</template>
<!-- type='module'一定要写 -->
<script src="./index.js" type='module'></script>
</body>
</html>
index.js
/*
* @Author: dfh
* @Date: 2021-02-02 07:59:52
* @LastEditors: dfh
* @LastEditTime: 2021-02-02 10:19:58
* @Modified By: dfh
* @FilePath: /day15-webcomponent/index.js
*/
import LyCollapse from './ly-collapse.js';
import LyCollapseItem from './ly-collapse-item.js';
window.customElements.define('ly-collapse', LyCollapse);
window.customElements.define('ly-collapse-item', LyCollapseItem);
//设置默认状态:1展示,其他隐藏
const defaultActive = ['1'];
//获取collapse元素
const collapseEl = document.querySelector('ly-collapse');
//给孩子传递数据
collapseEl.setAttribute('active', JSON.stringify(defaultActive));
//监听自定义事件
collapseEl.addEventListener('changeName', e => {
const {
isShow,
name
} = e.detail;
if (isShow) {
const index = defaultActive.indexOf(name);
//移除
defaultActive.splice(index, 1);
} else {
//添加
defaultActive.push(name);
}
collapseEl.setAttribute('active', JSON.stringify(defaultActive));
});
ly-collapse.js
/*
* @Author: dfh
* @Date: 2021-02-02 07:59:33
* @LastEditors: dfh
* @LastEditTime: 2021-02-02 10:01:01
* @Modified By: dfh
* @FilePath: /day15-webcomponent/ly-collapse.js
*/
export default class LyCollapse extends HTMLElement {
constructor() {
super();
//创建影子节点
const shadow = this.attachShadow({
mode: 'open'
});
//找到模版
const tmpl = document.getElementById('collapse-tmpl');
//克隆模版,true代表克隆包括子孙节点
const tmplClone = tmpl.content.cloneNode(true);
//样式
let style = document.createElement('style');
//:host代表的是影子根元素
style.textContent = `
:host{
display:flex;
border:3px solid #ebebeb;
border-radius:5px;
width:100%;
}
.collapse{
padding:10px;
width:100%;
}
`
//将模版根影子关联起来
shadow.appendChild(tmplClone);
//添加样式
shadow.appendChild(style);
//监控slot变化
const 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') { //接受active变化处理
this.activeList = newVal;
this.render();
}
}
render() {
//activeList有值并且孩子已经加载完毕
if (this.activeList && this.slotList) {
this.slotList.forEach(child => {
child.setAttribute('active', this.activeList);
});
}
}
}
ly-collapse-item.js
/*
* @Author: dfh
* @Date: 2021-02-02 07:59:46
* @LastEditors: dfh
* @LastEditTime: 2021-02-02 10:24:42
* @Modified By: dfh
* @FilePath: /day15-webcomponent/ly-collapse-item.js
*/
export default class LyCollapseItem extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({
mode: 'open'
});
const tmpl = document.getElementById('collapse-item-tmpl');
const tmplClone = tmpl.content.cloneNode(true);
this.isShow = false; //标识是否需要显示
const style = document.createElement('style');
style.textContent = `
:host{
width:100%;
border-bottom:1px solid #ebeef5;
display:flex;
flex-direction:column;
padding:10px 0;
color:black;
}
.title{
background:#f1f1f1;
line-height:35px;
font-size:18px;
}
.content{
font-size:14px;
}
`
shadow.appendChild(style);
shadow.appendChild(tmplClone);
this.titleEl = shadow.querySelector('.title');
//给title设置点击事件
this.titleEl.addEventListener('click', () => {
//dispatchEvent派发一个自定义事件
document.querySelector('ly-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 'title': //接受title属性
this.titleEl.innerHTML = newVal;
break;
case 'active':
this.activeList = JSON.parse(newVal); //父组件传递的值
break;
case 'name':
this.name = newVal;
break;
default:
break;
}
if (this.name && this.activeList) {
//active数字中是否包含name
this.isShow = this.activeList.includes(this.name);
//this.shadowRoot获取影子树
this.shadowRoot.querySelector('.content').style.display = this.isShow ? 'block' : 'none';
}
}
}