Web components 教程

欢迎来到Web Components博客

没有特性的 HTML 元素

一般常使用标准的 HTML 元素,如:<p>欢迎进入我的博客</p> 组织 DOM。 其实,可以使用自定义的 HTML 元素, 比如:<greeting>欢迎进入我的博客</greeting> 可以在浏览器正常显示:

这里 <greeting> 便是 非标准的 HTML 元素。可以添加自定义样式:

                
                    <style>
                    greeting {
                        display: block;
                        font-size: 36px;
                        color: red;
                    }
                    </style>
                
            

运行结果如下:

接着,使用脚本操作这个元素:

                
                    <script>
                        'use strict'
                        let elements = document.querySelectorAll('greeting')
                        greeting_elem = elements[0]
                        greeting_elem.innerText = '你好,世界'
                    </script>
                
                

运行结果如下:

此例子说明,浏览器对待自定义元素,就像对待标准元素一样,只是没有默认的样式和行为。换言之,浏览器必须将自定义元素保留在 DOM 之中,但不会有任何语义信息和附加属性。除此之外,自定义元素与标准元素是一样的。

其实,所有的自定义元素都是HTMLUnknownElement 对象的实例。

标准 custom elements

为了让自定义的 HTML 元素支持一些 HTML 的内置属性,custom element 名称必须包括至少一个短横线 -,比如 my-elementsuper-button 都是有效的元素名,但 myelement 并不是。 这是为了确保 custom element 和内置 HTML 元素之间不会发生命名冲突。

此时自定义元素就不是HTMLUnknownElement 的实例了,而是HTMLElement 的实例。

                
                    <script>
                        'use strict'
                        let helloWorld = document.createElement('hello-world')
                        helloWorld instanceof HTMLUnknownElement // 输出 false
                        helloWorld instanceof HTMLElement // 输出 true
                    </script>
                
            

注意这里的 let helloWorld = document.createElement('hello-world') 等价于在 HTML 中的 <hello-world></hello-world>

换言之, document.createElement 函数用于创建一个由标签名称 tagName 指定的 HTML 元素。

创建 autonomous custom elements

可以通过 JavaScript 的 class 定义 HTML 的标准自定义元素。 比如,可以定义一个带有统一样式的标识代码的 HTML 元素。

            
                <script>
                'use strict'

                class InlineCode extends HTMLElement {
                  constructor() {
                    super()
                    this.classList.add('w3-card', 'w3-padding-small', 'w3-round-xxlarge', 'w3-light-gray')
                  }
                }
                
                customElements.define("inline-code", InlineCode)
                </script>
            
        

这样,便可以在 HTML 中以如,<inline-code>Alt</inline-code> 的方式使用自定义元素。 这种方式的自定义元素被称为自主定制元素(Autonomous custom element)。

注意:自主自定义元素的构造函数必须扩展 HTMLElement

创建 customized built-in elements

Customized built-in elements 继承自内置的或者已有的 HTML 元素。在创建时,你必须指定所需扩展的元素, 使用时,需要先写出基本的元素标签,并通过 is 属性指定 custom element 的名称。 例如 <p is="word-count">

更加详细的是如下例子:

            
                'use strict'

                class BlockCode extends HTMLPreElement {
                    constructor() {
                        super()
                        this.classList.add('w3-card', 'w3-code', 'w3-pale-green',
                        'w3-padding-small', 'w3-round-xlarge', 'w3-text-purple')
                    }
                }

                customElements.define("block-code", BlockCode, {extends: 'pre'})
            
        

在 HTML 中便可像下面的方式定义代码块:

            
                <pre is='block-code'>
                    <code>
                        ...
                    </code>
                </pre>
            
        

创建 shadow DOM

web components 的应该重要的特性就是封装:可以将 HTML 元素,样式和行为隐藏,并与其他代码项隔离。实现该特性的手段就是 shadow DOM。

            
                // 为新元素创建一个类
                class WordCount extends HTMLParagraphElement {
                    constructor() {
                        // 在构造器中先调用一下 super
                        super()
                        // 创建一个 shadow root
                        const shadow = this.attachShadow({ mode: 'open' })
                        //创建文本节点并向其添加计数器
                        this.text = document.createElement('span')
                        this.updateCount()
                        //将其添加到shadow root上
                        shadow.appendChild(this.text)
                        //当元素内容发生变化时更新计数
                        setInterval(this.updateCount, 200)
                    }

                    countWords(node) { // 统计英文单词数目
                        const text = node.innerText || node.textContent
                        return text.split(/\s+/g).length
                    }

                    updateCount() {
                        // 单词计数器指向元素的父级
                        const wcParent = this.parentNode
                        const count = `Words: ${this.countWords(wcParent)}`
                        this.text.textContent = count
                    }
                }

                // 定义新元素
                customElements.define('word-count', WordCount, { extends: 'p' })
            
        

只需要在你需要统计的 HTML 元素中嵌入 <p is="word-count"></p> 即可统计该元素内的空白符格式。

使用 <template>

下面以创建一个用户卡片为例介绍如何使用 <template> 保存一份可复用的 HTML 元素。首先,在 HTML 中创建一个模板:

            
                <template class="userCardTemplate">
                    <!-- 引入外部 CSS -->
                    <link rel='stylesheet' href="styles/user-card.css">
                    <!-- 定义图片 -->
                    <img class="image" alt="user card">
                    <div class="container">
                        <!-- 定义用户基本信息 -->
                        <p class="name"></p>
                        <p class="email"></p>
                        <button class="button">Follow John</button>
                    </div>
                </template>
            
        

接着,使用 JavaScript 创建 HTML 自定义元素:

            
                class UserCard extends HTMLElement {
                    constructor() {
                      super()
                      let shadow = this.attachShadow({ mode: 'open' })
                      let templateElem = document.querySelector('.userCardTemplate')
                      let content = templateElem.content.cloneNode(true)
                      content.querySelector('img').setAttribute('src', this.getAttribute('image'))
                      content.querySelector('.container>.name').innerText = this.getAttribute('name')
                      content.querySelector('.container>.email').innerText = this.getAttribute('email')
                      shadow.appendChild(content)
                    }
                  }
                  
                customElements.define("user-card", UserCard)
            
        

然后,便可以在 HTML 自如的使用该元素:

            
                <user-card image="your-card.png" name="User Name" email="yourmail@some-email.com">
                </user-card>
        
            
        

可以看到使用 <template> 极大的简化了代码。

使用 <slot>

虽然 <template> 已经为我们带来了便利,但是还是不够友好,下面我们使用 <slot> 作为一个占位符,重新定义用户卡片模板:

            
                <template class="userCardTemplate">
                    <link rel='stylesheet' href="styles/user-card.css">
                    <img class="image" alt="user card">
                    <div class="container">
                        <p><slot name='name'>User Name</slot>

<p><slot name='email'>yourmail@some-email.com</slot>

<button class="button">Follow John</button> </div> </template>

这时,JavaScript 便可以这样:

            
                class UserCard extends HTMLElement {
                    constructor() {
                      super()
                      let shadow = this.attachShadow({ mode: 'open' })
                      let templateElem = document.querySelector('.userCardTemplate')
                      let content = templateElem.content.cloneNode(true)
                      content.querySelector('img').setAttribute('src', this.getAttribute('image'))
                      shadow.appendChild(content)
                    }
                  }
                  
                  customElements.define("user-card", UserCard)
            
        

而在 HTML 中将会更加灵活使用此元素:

            
                <user-card image="your-card.png">
                    <ul slot='name'>
                        <li>Tom</li>
                        <li>Peter</li>
                    </ul>
                    <p slot='email'>xi@qq.com</p>
                </user-card>