碎片化学习前端之HTML(webComponent)

前言

webComponent 是 HTML5 推出的新特性,为组件化推广奠定基础。

webComponent 基本使用

原生组件,性能较好,但存在兼容性问题。其核心技术有:Custom elements, Shadow DOM, HTML Templates。

Custom elements

JavaScript API,用于定义 custom elements 及其行为。

<m-button type="primary">webComponent</m-button> // 调用

<script type="text/javascript">
class MButton extends HTMLElement {
     constructor() {
     	super();
     }
}
window.customElements.define('m-button', MButton) // 关联 HTML 和 JS,自定义组件
</script>

HTML Templates

<template>, <slot> 元素标记元素结构,并重用元素。

<m-button type="primary">webComponent</m-button>

<template id="m-btn">
    <button class="m-button">
        <slot>Default</slot>
    </button>
</template>

<script type="text/javascript">
class MButton extends HTMLElement {
    constructor() {
        super();

        let btnTmpl = document.getElementById('m-btn') // 定义模板并获取模板
     }
}
</script>

Shadow DOM

JavaScript API,用于封装 Shadow DOM 附加到元素上,并关联其功能,以此保证元素的功能私有,不会与其他元素冲突。

<m-button type="primary">webComponent</m-button>

<template id="m-btn">
    <button class="m-button">
        <slot>Default</slot>
    </button>
</template>

<script type="text/javascript">
class MButton extends HTMLElement {
     constructor() {
        super();

        let btnTmpl = document.getElementById('m-btn') // 定义模板并获取模板

        let shadow = this.attachShadow({ mode: 'open' }) // 配置 devtools 是否可查看 DOM 结构,open / close
        let cBtnTmpl = btnTmpl.content.cloneNode(true) // copy 模板便于重用

     	shadow.appendChild(cBtnTmpl) // 模板挂载 Shadow DOM
     }
}
</script>

webComponent 高阶使用

webComponent 高阶使用主要包括属性设置,样式设置,事件绑定等

webComponent 属性设置

webComponent 在调用组件时进行属性传值,在声明组件时通过 DOM API 获取属性值进行操作。

<m-button type="primary">webComponent</m-button>

<script type="text/javascript">
class MButton extends HTMLElement {
     constructor() {
     	super();
     	
     	let btnTmpl = document.getElementById('m-btn') // 定义模板并获取模板
     	
     	let shadow = this.attachShadow({ mode: 'open' })
     	let cBtnTmpl = btnTmpl.content.cloneNode(true) // copy 模板便于重用
     	
     	let type = this.getAttribute('type') // 读取属性值,进行操作
     	
     	shadow.appendChild(cBtnTmpl) // 模板挂载 Shadow DOM
     }
}
</script>

webComponent 样式设置

  1. template 设置样式

    <m-button type="primary">webComponent</m-button>
    
    <template id="m-btn">
        <style type="text/css"> // 定义模板样式
            .m-button {
                border: none;
                outline: none;
                border-radius: 4px;
                padding: 5px 20px;
            }
        </style>
        <button class="m-button">
            <slot>Default</slot>
        </button>
    </template>
    
    class MButton extends HTMLElement {
        constructor() {
            super();
    
            let btnTmpl = document.getElementById('m-btn') // 定义模板并获取模板
    
            let shadow = this.attachShadow({ mode: 'open' })
            let cBtnTmpl = btnTmpl.content.cloneNode(true) // copy 模板便于重用
    
            let type = this.getAttribute('type') // 读取属性值,进行操作
    
            shadow.appendChild(cBtnTmpl) // 模板挂载 Shadow DOM
        }
    }
    
  2. Shadow DOM 设置样式

    <m-button type="primary">webComponent</m-button>
    
    <template id="m-btn">
        <style type="text/css"> // 定义模板样式
            .m-button {
                border: none;
                outline: none;
                border-radius: 4px;
            padding: 5px 20px;
            }
        </style>
        <button class="m-button">
            // 具名插槽使用:模板内<slot name="btn-text"></slot>,模板外调用时<span slot="btn-text"></span>
            <slot>Default</slot> 
        </button>
    </template>
    
    class MButton extends HTMLElement {
        constructor() {
            super();
    
            let btnTmpl = document.getElementById('m-btn') // 定义模板并获取模板
    
            let shadow = this.attachShadow({ mode: 'open' })
            let cBtnTmpl = btnTmpl.content.cloneNode(true) // copy 模板便于重用
    
            let type = this.getAttribute('type') // 读取属性值,进行操作
    
            const sheets = {
                'primary': {
                    background: '#409EFF',
                    color: '#FFF'
                },
                'success': {
                    background: '#67C23A',
                    color: '#FFF'
                },
                'warning': {
                    background: '#E6A23C',
                    color: '#FFF'
                },
                'danger': {
                    background: '#F56C6C',
                    color: '#FFF'
                },
                'default': {
                    background: '#909399',
                    color: '#FFF'
                },
            }
    
            const style = document.createElement('style')
            style.textContent = `
                .m-button {
                    background: ${sheets[type].background}; // 属性 配置css 样式
                    color: ${sheets[type].color};
                }
            `
    
            shadow.appendChild(style)
            shadow.appendChild(cBtnTmpl) // 模板挂载 Shadow DOM
        }
    }
    

    注意:webComponent 通过 Shadow DOM 隔离以后,外界无法修改样式,只能通过属性或者设置 CSS 变量的方式修改。

  3. css 变量设置样式

    <style type="text/css">
    :root {
        --text-color: #fff;
    }
    </style>
    
    <m-button type="primary">webComponent</m-button>
    
    <template id="m-btn">
        <style type="text/css"> // 定义模板样式
            .m-button {
                border: none;
                outline: none;
                border-radius: 4px;
                padding: 5px 20px;
            }
        </style>
        <button class="m-button">
        <slot>Default</slot>
        </button>
    </template>
    
    class MButton extends HTMLElement {
        constructor() {
            super();
    
            let btnTmpl = document.getElementById('m-btn') // 定义模板并获取模板
    
            let shadow = this.attachShadow({ mode: 'open' })
            let cBtnTmpl = btnTmpl.content.cloneNode(true) // copy 模板便于重用
    
            let type = this.getAttribute('type') // 读取属性值,进行操作
    
            const sheets = {
                'primary': {
                    background: '#409EFF',
                    color: '#FFF'
                },
                'success': {
                    background: '#67C23A',
                    color: '#FFF'
                },
                'warning': {
                    background: '#E6A23C',
                    color: '#FFF'
                },
                danger': {
                    background: '#F56C6C',
                    color: '#FFF'
                },
                'default': {
                    background: '#909399',
                    color: '#FFF'
                },
            }
    
            const style = document.createElement('style')
            style.textContent = `
                .m-button {
                    background: ${sheets[type].background}; // 属性 配置 css 样式
                    color: var(--text-color, ${sheets[type].color}); // css 变量配置 css 样式
                }
            `
    
            shadow.appendChild(style) // 样式挂载至 Shadow DOM
            shadow.appendChild(cBtnTmpl) // 模板挂载至 Shadow DOM
        }
    }
    

注意:通过 templates 设置样式的优先级是高于 css 变量和 Shadow DOM 的。

webComponent 事件绑定

<m-button type="primary">webComponent</m-button>

<template id="m-btn">
    <button class="m-button">
        <slot>Default</slot>
    </button>
</template>

<script type="text/javascript">
class MButton extends HTMLElement {
    constructor() {
     	super();
     	
     	let btnTmpl = document.getElementById('m-btn') // 定义模板并获取模板
     	
     	let shadow = this.attachShadow({ mode: 'open' }) // 配置 devtools 是否可查看 DOM 结构,open / close
     	let cBtnTmpl = btnTmpl.content.cloneNode(true) // copy 模板便于重用
     	
     	cBtnTmpl.querySelector('.m-button').addEventListener('click', this.onClick)
     	
     	shadow.appendChild(cBtnTmpl) // 模板挂载 Shadow DOM
    }
     onClick() {
        alert('click')
    }
}
</script>

webComponent 生命周期

<m-button type="primary">webComponent</m-button>

<template id="m-btn">
    <button class="m-button">
        <slot>Default</slot>
    </button>
</template>

<script type="text/javascript">
class MButton extends HTMLElement {
    constructor() {
     	super();
     	
     	let btnTmpl = document.getElementById('m-btn') // 定义模板并获取模板
     	
     	let shadow = this.attachShadow({ mode: 'open' }) // 配置 devtools 是否可查看 DOM 结构,open / close
     	let cBtnTmpl = btnTmpl.content.cloneNode(true) // copy 模板便于重用
     	
     	cBtnTmpl.querySelector('.m-button').addEventListener('click', this.onClick)
     	
     	shadow.appendChild(cBtnTmpl) // 模板挂载 Shadow DOM
    }
	
    static get observedAttributes() {
        return [ 'type' ] // 监控 type 属性是否改变
    }
    
    connectedCallback() {
        // 组件首次挂载时调用
    }

    attributeChangedCallback(key, oldValue, newValue) {
        // 组件更新时调用,key 为属性名,oldValue, newValue 为属性值
    }

    disconnectedCallback() {
        // 组件移除时调用
    }
}
</script>

Demo 案例

利用 webComponent 简单实现折叠组件,包括父子组件间通信。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>折叠组件 m-collapse</title>
</head>
<body>
    <m-collapse>
        <m-collapse-item title="Node" name="1">
            <div>nodejs</div>
        </m-collapse-item>
        <m-collapse-item title="React" name="2">
            <div>React</div>
        </m-collapse-item>
        <m-collapse-item title="Vue" name="3">
            <div>Vue</div>
        </m-collapse-item>
    </m-collapse>

    <template id="collapse">
        <style type="text/css">
            /* 为 m-collapse(Shadow DOM 根元素) 设置样式 */
            :host {
                border: 1px solid #ebebeb;
                border-radius: 5px;
            }
            .m-collapse {
                width: 100%;
            }
        </style>

        <div class="m-collapse">
            <slot></slot>
        </div>
    </template>

    <template id="collapse-item">
        <style type="text/css">
            .m-collapse-item {
                width: 100%;
            }
            .m-collapse-item .title {
                background: #f1f1f1;
                line-height: 35px;
                height: 35px;
            }
            .m-collapse-item .content {
                font-size: 14px;
            }
        </style>

        <div class="m-collapse-item">
            <div class="title"></div>
            <div class="content">
                <slot></slot>
            </div>
        </div>
    </template>

    <script type="text/javascript">
        class MCollapse extends HTMLElement {
            constructor() {
                super();
                let shadow = this.attachShadow({ mode: 'open' })

                let tmpl = document.getElementById('collapse')

                let cTmpl = tmpl.content.cloneNode(true)

                let style = document.createElement('style')

                style.textContent = `
                    .m-collapse {
                            color: red;
                    }
                `

                shadow.appendChild(style)
                shadow.appendChild(cTmpl)

                // 监控 slot
                let slot = shadow.querySelector('slot')
                slot.addEventListener('slotchange', event => {
                    this.slots = event.target.assignedElements(); // 获取所有 slot
                    this.render()
                })
            }

            static get observedAttributes() {
                return [ 'active' ]
            }

            connectedCallback() {
                console.log('组件挂载')
            }

            attributeChangedCallback(key, oldValue, newValue) {
                console.log('组件更新调用')
                if(key === 'active') {
                    this.actives = newValue
                    this.render()
                }
            }

            disconnectedCallback() {
                console.log('组件卸载')
            }

            render() {
                // 父组件给子组件通信
                if(this.actives && this.slots) {
                    ([...this.slots]).forEach(slot => {
                        slot.setAttribute('active', JSON.stringify(this.actives))
                    })
                    }
            }
        }

        window.customElements.define('m-collapse', MCollapse)

        class MCollapseItem extends HTMLElement {
            constructor() {
                super();
                this.isShow = true // 默认显示 collapse-item

                let shadow = this.attachShadow({ mode: 'open' })

                let tmpl = document.getElementById('collapse-item')

                let cTmpl = tmpl.content.cloneNode(true)

                let style = document.createElement('style')

                style.textContent = `
                    .m-collapse-item {
                        border-bottom: 1px solid #000;
                    }
                `

                shadow.appendChild(style)
                shadow.appendChild(cTmpl)

                this.titleEle = shadow.querySelector('.title')

                // 绑定事件
                this.titleEle.addEventListener('click', () => {
                    // 子组件给父组件通信(自定义事件)
                    document.querySelector('m-collapse').dispatchEvent(new CustomEvent(
                        'collapseEvent',
                        {
                            detail: {
                                name: this.getAttribute('name'),
                                isShow: this.isShow
                            }
                        }
                    ))
                })
            }

            static get observedAttributes() {
                return [ 'active', 'title', 'name' ]
            }

            attributeChangedCallback(key, oldValue, newValue) {
                console.log('item 组件更新调用', key)
                switch(key) {
                    case 'active':
                        this.actives = JSON.parse(newValue)
                        break
                    case 'title':
                        this.titleEle.innerHTML = newValue
                        break
                    case 'name':
                        this.name = newValue
                        break
                    default:
                        break;
                }
                if(this.actives && this.name) {
                    this.isShow = this.actives.includes(this.name)
                    this.shadowRoot.querySelector('.content').style.display = this.isShow ? 'block' : 'none'
                }
            }
        }

        window.customElements.define('m-collapse-item', MCollapseItem)



        let actives = [ '1', '2' ] // 默认 1,2 展开
        document.querySelector('m-collapse').setAttribute('active', JSON.stringify(actives))

        document.querySelector('m-collapse').addEventListener('collapseEvent', (event) => {
            if(event.detail.isShow) {
                let index = actives.indexOf(event.detail.name)
                actives.splice(index, 1)
            } else {
                actives.push(event.detail.name)
            }
            document.querySelector('m-collapse').setAttribute('active', JSON.stringify(actives))
        })
    </script>
</body>
</html>

后记

webComponent 提供了一种组件定义的方式,但其根本仍是借助 DOM API 进行操作,对于原生开发能力有较高的要求。

posted @ 2023-05-10 11:57  深巷酒  阅读(294)  评论(0编辑  收藏  举报