rwson

rwson

一个前端开发

Shadow DOM研究

Polymer中,提出了Web Component的概念,旨在让开发者可以封装出很多可复用的组件。现在,webkit添加了对该API支持,也就意味着我们不用借助框架,也可以自己封装出可复用的组件(通过自定义元素的形式),而不需要依赖其他框架来实现。

假设我们这边需要封装一个进度条组件,实现代码大概是这样的:

//  javascript

class CustomProgressBar extends HTMLElement {

	constructor(args) {
		super(args);

        //  createShadowRoot用来创建一个shadowDOM实例
		const shadowRoot = this.createShadowRoot();

        //  设置组件内的布局结构和样式
        shadowRoot.innerHTML = `
            <style type="text/css">
                :host {
                    display: inline-block;
                    width: 200px;
                    height: 30px;
                    box-sizing: border-box;
                    padding: 1px;
                }
                :host * {
                    -webkit-touch-callout: none;
                    -webkit-user-select: none;
                    -khtml-user-select: none;
                    -moz-user-select: none;
                    -ms-user-select: none;
                    user-select: none;
                }
                .progress {
                    display: inline-block;
                    width: 200px;
                    height: 30px;
                    position: relative;
                    border: 1px solid #000;
                }
                .progress > .bar {
                    background: red;
                    height: 100%;
                    width: 0;
                    transition: all 0.2s;
                }
                
                .progress .label {
                    position: absolute;
                    top: 0;
                    left: 0;
                    width: 100%;
                    text-align: center;
                    font-size: 14px;
                    line-height: 30px;
                    color: #000;
                }
            </style>
            <div class="progress" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
                <div class="bar"></div>
                <div class="label">0%</div>
            </div>
        `;

        //  将相关元素存储到成员变量中
        this._progressElement = shadowRoot.querySelector(".progress");
        this._bar = shadowRoot.querySelector(".bar");
        this._label = shadowRoot.querySelector(".label");
	}

    /**
     * 取得当前进度
     * @return {string}
     */
    get progress() {
        return Number(this._progressElement.getAttribute("aria-valuenow"));
    }

    /**
     * 设置进度
     * @param value
     */
    set progress(value) {
        //  最大值值最小值
        const max = this._progressElement.getAttribute("aria-valuemax"),
              min = this._progressElement.getAttribute("aria-valuemin");

        //  类型判断
        if(Number.isNaN(Number(value))) {
            throw new Error(`value must be an number type, you specified ${value} which is ${{}.toString.call(value).slice(8, -1).toLowerCase()}!`);
        }

        //  范围检测
        if(value > max || value < min) {
            throw new Error(`value must between ${min} to ${max} , you specified ${value}!`);
        }

        //  设置相关属性
        this._progressElement.setAttribute("aria-valuenow", value);
        this._bar.style.width = `${value}%`;
        this._label.textContent = `${value}%`;
    }

    /**
     * 提供可以绑定onclick的接口
     * @param callback
     */
    set onclick(callback) {
        if(typeof callback === "function") {
            this._progressElement.addEventListener("click", e => {
                callback.call(this, e);
            }, false);
        }
    }

}

//  调用 customElements.define定义自定义元素,第一个参数自定义元素名,第二个参数是HTMLElement的一个子类
customElements.define("custom-progress-bar", CustomProgressBar);

window.onload = () => {

    let customProgressBar = document.querySelector("custom-progress-bar"),
        progress;

    /**
     * 给进度条组件绑定onclick事件,每次点击进度加10
     * @param e
     */
    customProgressBar.onclick = (e) => {
        progress = Number(this.progress);
        if(progress >= 100) {
            progress = 0;
        } else {
            progress += 10;
        }
        this.progress = progress;
    };

};

//  HTML
//  现在我们可以通过new CustomProgressBar()或者custom-progress-bar来使用自定义元素了

<custom-progress-bar></custom-progress-bar>

至此我们的一个进度条组件就算封装完成了,需要注意的是,customElements.define方法对第一个参数有一些要求:

  • 必须以小写字母 a-z 开头
  • 不能包含大写字母 A-Z
  • 必须包含"-"

最后渲染出来是如下的布局结构:

一起看看实际的效果: