Web Components

Web Components

Web Components是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的web应用中使用它们。Mdn Web Components

  • HTML template
  • Shadow DOM
  • Custom Elements
  • HTML imports(已废弃)

Html template

  • html标签中的模板元素
  • template标签中的任何元素都不会被加载运行,比如img、script、style都不会生效
  • 当元素中的内容被添加到dom中才会开始加载执行
  • 判断是否支持template 'content' in document.createElement('template')
<template id="abc">
    <script>
        // 只有当这里的script标签被append到dom上才会执行
    	console.log(123)
    </script>
</template>

Shadow Dom

MDN Shadow Dom

Shadow DOM 是DOM上的一个子树,它与dom一样是一颗完整的树。重点在于shadow dom 创建了一块私人空间,html dom 与 shadow dom的作用域是完全隔离的。dom上的css无法影响到shadow dom中的元素,反之,亦然。

Shadow DOM 必须附加在一个元素上,可以是HTML文件中的一个元素,也可以是脚本中创建的元素;可以是原生的元素,如<div>、<p>也可以是自定义元素

浏览器中的videoselectinput

shadowRoot

shadowRoot 是 shadow dom 上的根节点,类似于dom上的document,但它没有那么多属性,下面是2个独有的属性:

  • mode 只读

  • host 只读 : shadow dom最外层的dom元素

shadowRoot.mode 表示shadow元素的开放模式。在初始化方法中attachShadow传递该值

attachShadow({mode: 'open'}) // 表示开放状态
  1. open 表示该shadow元素在dom中是开放的,其根节点可以再次被访问,并且可以再次操作内部元素。
  2. closed 表示该shadow元素在dom中是闭合的,无法访问其中的元素了。

初始化一个shadowRoot:

<div id="shadow"></div>
const shadowRoot = document.querySelector('#shadow').attachShadow({mode: 'open'})
shadowRoot.innerHTML = '<p>I am p tag</p>'

// 被初始化为open状态后,还可以再次获得
document.querySelector('#shadow').shadowRoot

需要注意,当初始化的时候设置mode=closed时候:

// 只有当前变量 shadowRoot 可以访问该元素
let shadowRoot = document.querySelector('#shadow').attachShadow({mode: 'closed'})
// 当丢失对该对象的引用后,那再也无法获取到了
shadowRoot = null
// 无法获得了
document.querySelector('#shadow').shadowRoot // null
slot

shadow dom 也有slot 元素,用法和vue一模一样,只不过没有相关的API可以使用。

Custom Elements

html原生标签自定义工具。Custom Elements 的核心,实际上就是利用 JavaScript 中的对象继承,去继承 HTML 原生的 HTMLElement 类(或是具体的某个原生 Element 类,比如 HTMLButtonElement),然后自己编写相关的生命周期函数,处理成员属性以及用户交互的事件。与 vuereact 组件有点类似。

使用自定义元素需要使用 customElements 属性

// 定义一个组件
window.customElements.define('componentName', 'class 实例')

使用define 函数来定义自定义组件,这样就可以直接在html中使用该标签,就和div、span性质一样了。

// 使用class的写法直接继承 HTMLElement
window.customElements.define('my-component', class extends HTMLElement {
	// 构造函数,可以在这里做一些初始化的操作,就像vue中的creatd
    constructor () {
        super()
        // 在这里做一些变量初始化,shadow dom 初始等操作
    }
})

使用 customElements.whenDefined 用于检测组件是否被定义,返回结果是一个Promise , 如果是pending 表示还未被注册,resolved 则表示已被注册了,那就需要换个其他牛逼的名字了

既然是自定义组件,那肯定要有生命周期了。它并没有react和vue的生命周期钩子那么多,只有4个:

  1. connectedCallback
  2. disconnectedCallback
  3. attributeChangedCallback
  4. adoptedCallback
connectedCallback

每次将自定义元素附加到文档连接元素时调用。每次移动节点时都会发生这种情况,并且可能在元素的内容被完全解析之前发生。

有点类似于 mountedcomponentDidMoun , 这时候组件已经在dom中被渲染完毕了。但当组件位置在dom节点中发生变化后,都会调用这个方法。可以在这里做一些初始化的操作。

disconnectedCallback

组件离开dom,在dom上找不到了。类似于 destroyed。一些销毁操作可以在这里处理。

#####attributeChangedCallback

监听属性的变化。类似 watch 只不过只能监听父级传递过来的属性,不能监听内部属性的。

监听属性变化还还需另一个方法,来指定哪些属性需要监听:

static get observedAttributes() { return ['value', 'name'] } // 表示监听 value 与 name 的变化
adoptedCallback

组件被移动的时候会触发这个钩子,文档上是说将组件从一个document中移动到另一个document中的时候。。。 使用 adoptNode 方法移动元素的时候才会触发。

两个document的情况应该是指有 iframe 的时候。

相关轮子

三者结合实操

下面是一个小demo,写的很糙,功能也很简单。

(function () {
    const templateEl = document.createElement('template')
    templateEl.innerHTML = `<div size="default" id="container">
        <span id="minus" class="left">
            <slot name="minus">-</slot>
        </span>
        <span id="plus" class="right">
            <slot name="plus">+</slot>
        </span>
        <input type="text" id="value">
    </div>
    <style>

        * {
            box-sizing: border-box;
        }

        div {
            display: inline-block;
            position: relative;
        }

        input {
            width: 100%;
            height: 100%;
            outline: none;
            color: #606266;
            border: 1px solid #dcdfe6;
            text-align: center;
            padding: 0;
            border-radius: 4px;
        }


        span {
            display: inline-block;
            position: absolute;
            text-align: center;
            color: #606266;
            cursor: pointer;
            background: #f5f7fa;
            user-select: none;
        }

        span[disabled='true'] {
            color: #c0c4cc;
            cursor: not-allowed;
        }

        span:hover {
            color: #409eff;
        }

        span:hover~input{
            border: 1px solid #409eff;
        }

        .left {
            left: 1px;
            top: 1px;
            border-radius: 4px 0 0 4px;
            border-right: 1px solid #dcdfe6;
        }

        .right {
            right: 1px;
            top: 1px;
            border-radius: 0 4px 4px 0;
            border-left: 1px solid #dcdfe6;
        }

        div[size='default'] {
            width: 180px;
            height: 40px;
        }

        div[size='default'] span {
            width: 41px;
            line-height: 38px;
        }

        div[size='default'] input {
            font-size: 14px;
            padding: 0 50px;
        }

        div[size='medium'] {
            width: 200px;
            height: 34px;
        }

        div[size='medium'] span {
            width: 36px;
            line-height: 32px;
        }

        
        div[size='medium'] input {
            font-size: 14px;
            padding: 0 43px;
        }

        div[size='small'] {
            width: 130px;
            height: 32px;
        }

        div[size='small'] span {
            width: 32px;
            line-height: 30px;
        }

        div[size='small'] input {
            font-size: 13px;
            padding: 0 39px;
        }
        
        div[size='mini'] {
            width: 130px;
            height: 26px;
        }

        div[size='mini'] span {
            width: 26px;
            line-height: 24px;
        }

        div[size='mini'] input {
            font-size: 12px;
            padding: 0 35px;
        }
    </style>
    `

    window.customElements.define('my-counter', class extends HTMLElement {

        constructor () {
            super()
            this.SIZE = {
                default: 'default',
                medium: 'medium',
                small: 'small',
                mini: 'mini'
            }

            this.watch = {
                size: (oldVal, newVal) => {
                    // 设置size
                    this._container.setAttribute('size', newVal || 'default')
                },
                count: (oldVal, newVal) => {
                    if (newVal >= this.data.max) {
                        // 禁用 加号 按钮
                        
                    }

                    if (newVal <= this.data.min) {
                        // 禁用 减号 按钮
                    }
                }
            }

            this.data = new Proxy({}, {
                // get (...params) {
                //     return Reflect.get(...params)
                // },
                set: (target, key, value, reciver) => {
                    if (this.watch[key]) {
                        // oldValue: target[key], new Value: value
                        this.watch[key](target[key], value)
                    }
                    return Reflect.set(target, key, value, reciver)
                }
            })
            
            // 获取模板
            const template = templateEl.content.cloneNode(true)
            // 外层容器
            this._container = template.querySelector('#container')
            // 计算器数值
            this.data.count = +this.getAttribute('value') || 0
            this.data.max = +this.getAttribute('max') || Number.MAX_SAFE_INTEGER
            this.data.min = +this.getAttribute('min') || Number.MIN_SAFE_INTEGER
            this.data.size = this.SIZE[this.getAttribute('size')]
            
            // input
            this._input = template.querySelector('#value')
            this._input.value = this.data.count
            // 减号
            const minus = template.querySelector('#minus')
            // 加号
            const plus = template.querySelector('#plus')
            // input 事件防止输入不规范数据
            this._input.addEventListener('input', _ => {
                this.changeValue(this._input.value)
            })

            minus.onclick = () => {
                this.minus()
            }

            plus.onclick = () => {
                this.plus()
            }

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

        // 渲染完毕的回调
        connectedCallback () {
        }

        // dom上移除的回调
        disconnectedCallback () {
        }

        adoptedCallback () {
            console.log('adoptedCallback')
        }

        // 监听属性变化
        attributeChangedCallback(name, oldValue, newValue) {
            switch (name) {
                case 'size':
                    this.data.size = newValue
                    break
                case 'value':
                    if ((this.data.count + '') !== newValue) {
                        this._input.value = this.data.count = newValue
                    }
                    break
            }
        }

        // 指定监听哪些属性
        static get observedAttributes() { return ['value', 'size'] }

        // 点击减号的回调事件
        minus () {
            this.changeValue(--this.data.count)
        }

        // 点击加号的回调事件
        plus () {
            this.changeValue(++this.data.count)
        }

        changeValue (value) {
            this._input.value = value = this.matchNumber(value)
            this.setAttribute('value', value)
            this.dispatchEvent(new CustomEvent('change', {
                detail: {value: value}
            }))
        }

        // 控制最后输入的是一个数字或者'.'
        matchNumber (value) {
            return +(value + '').match(/[\d\.]*/g)[0]
        }
    })
})()
<my-counter value="2" size="medium"></my-counter>
<my-counter value="3" size="small">
	<span slot="plus"></span>
</my-counter>
<my-counter value="4" size="mini"></my-counter>

因为js异步加载的问题,页面会出现先展示组件后注册的问题。自定义组件在注册后会css有一个:defined 伪类,在未注册的时候是没有的,可以根据这个属性来控制:

my-counter:not(:defined) {
  display: none;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值