Web Components理论知识与实战

20 阅读7分钟

一、Web Components的概念

Web Components是网页组件,一次创建多处使用

二、Web Components 的组成

  1. Custom elements(自定义元素):一组js API,允许定义Custom elements及其行为,按需使用
  2. Shadow DOM(影子DOM):一组js API,将封装的影子DOM添加到主文档中,并且和主文档分开呈现,保持影子DOM中元素的功能私有,不与主文档中其他部分发生冲突
  3. HTML templates(HTML模板):<templete><slot>

三、兼容性

image.png

image.png

image.png

四、Custom Elements

Web Components有个很重要的特性,开发者能够将页面功能封装成Custom Elements

4.1 Custom Elements的分类:

根据是否继承HTML元素,将Custom Elements分为两类

Autonomous custom elements

独立的元素,不继承其他内建的HTML元素

<my-card>card</my-card>
      const card = document.createElement('my-card')
      card.innerHTML = '我的卡片'
      document.body.appendChild(card)
Customized built-in elements

继承自基本的HTML元素,创建时必须依赖现有的HTML元素,通过is属性指定Custom Element的名称

<p is="my-card">块级标签</p>
document.createElement('p', { is: 'my-card' })

4.2 CustomElementRegistry 对象

CustomElementRegistry 对象用来注册和操作自定义标签,通过window.customElements获取CustomElementRegistry 对象

image.png

CustomElementRegistry.define

用来创建一个自定义标签,语法如下

customElements.define(name, constructor, options)

参数:

  1. name:自定义标签名。注意:它不能是单个单词,且其中必须要有短横线,例如my-card
  2. contructor:自定义元素构造器,控制元素的表现形式、行为和生命周期等
  3. options:一个包含extends属性的配置对象,可选,指定所创建的元素继承自哪个内置元素,可以继承任何内置元素

返回值:undefined

      class MyCard extends HTMLParagraphElement {
        constructor() {
          super()
        }
      }
      customElements.define('my-card', MyCard, { extends: 'p' })
CustomElementRegistry.get

返回自定义标签的构造函数

const constructor = customElements.get('my-card')
CustomElementRegistry.getName
CustomElementRegistry.upgrade

更新root子树中所有包含影子DOM的自定义元素,语法:customElements.upgrade(root)

不调用upgrade方法:

      const el = document.createElement('my-card')
      class MyCard extends HTMLElement {}
      customElements.define('my-card', MyCard)
      console.log(el instanceof MyCard) // false

调用upgrade方法:

      const el = document.createElement('my-card')
      class MyCard extends HTMLElement {}
      customElements.define('my-card', MyCard)
      customElements.upgrade(el)
      console.log(el instanceof MyCard) // true
CustomElementRegistry.whenDefined

如果自定义元素还未定义,返回定义自定义元素时将会执行的promise; 如果自定义元素已定义,那么立即执行返回的promise

      class MyCard extends HTMLParagraphElement {
        constructor() {
          super()
        }
      }

      customElements.whenDefined('my-card').then(() => {
        console.log(`my-card 被注册`)
      })
      console.log('my-card 注册前')
      customElements.define('my-card', MyCard, { extends: 'p' })
      console.log('my-card 注册后')
      // my-card 注册前
      // my-card 注册后
      // my-card 被注册

当define执行后,再次执行以下代码,则会立即执行resolve方法

      customElements.whenDefined('my-card').then(res => {
        console.log(res)
        console.log(`my-card 被注册`)
      })

4.3 创建Custom Elements

创建和使用Autonomous custom elements
      class MyCard extends HTMLElement {
        constructor() {
          super()
          let shadow = this.attachShadow({ mode: 'open' })

          let containerEle = document.createElement('div')
          containerEle.style.display = 'flex'
          containerEle.style.flexDirection = 'column'
          containerEle.style.margin = '100px'
          containerEle.style.border = '1px solid #aaa'

          const headerEle = document.createElement('div')
          headerEle.innerText = '名片'
          headerEle.style.padding = '10px'
          headerEle.style.borderBottom = '1px solid blue'

          const nameEle = document.createElement('div')
          nameEle.innerText = '姓名:都敏俊'
          nameEle.style.padding = '10px'

          const sexEle = document.createElement('div')
          sexEle.innerText = '性别:男神'
          sexEle.style.padding = '10px'

          containerEle.appendChild(headerEle)
          containerEle.appendChild(nameEle)
          containerEle.appendChild(sexEle)
          shadow.appendChild(containerEle)
        }
      }

      customElements.define('my-card', MyCard)

使用:

    <my-card />

效果:

image.png

创建和使用Customized built-in elements
      class MyCard extends HTMLDivElement {
        constructor() {
          super()
          let shadow = this.attachShadow({ mode: 'open' })

          let containerEle = document.createElement('div')
          containerEle.style.display = 'flex'
          containerEle.style.flexDirection = 'column'
          containerEle.style.margin = '100px'
          containerEle.style.border = '1px solid #aaa'

          const headerEle = document.createElement('div')
          headerEle.innerText = '名片'
          headerEle.style.padding = '10px'
          headerEle.style.borderBottom = '1px solid blue'

          const nameEle = document.createElement('div')
          nameEle.innerText = '姓名:都敏俊'
          nameEle.style.padding = '10px'

          const sexEle = document.createElement('div')
          sexEle.innerText = '性别:男神'
          sexEle.style.padding = '10px'

          containerEle.appendChild(headerEle)
          containerEle.appendChild(nameEle)
          containerEle.appendChild(sexEle)
          shadow.appendChild(containerEle)
        }
      }

      customElements.define('my-card', MyCard, { extends: 'div' })

使用:

    <div is="my-card"></div>

效果:

image.png

总结
自定义标签类型继承自define方法是否需要第三个参数使用
Autonomous custom elements只能继承HTMLElement不需要直接使用自定义标签名作为标签名,如<my-card />
Customized built-in elements继承可用的基本HTML标签类,如HTMLDivElement必须要传入第三个参数,一般为{ extends: '标签名' }组件构造函数类继承类的基本标签名+is='自定义标签名',如<div is="my-card"></div>

五、Shadow DOM

在初涉前端时,就好奇像<input /><select></select><audio></audio><video></video>这些标签,用起来很简单,但是在页面上的布局和功能却很丰富,当时给自己的解释是:这些标签都是系统控制渲染的

现在再想想,这些解释有些道理,但是没有说到根本原因,前端老手的解释应该是:这些元素都是以组件的形式存在,所展现出来的布局和功能都是在组件内部定义好的,使用该组件时,内部的布局会渲染在页面上。

既然说input是组件,那为什么开发者工具中看到的input只是一个普通标签

image.png

如果想要看到各个组件内部的DOM结构,操作步骤:

image.png

image.png

5.1 Shadow DOM的概念

Shadow DOM是HTML的一个规范,允许开发者封装自己的HTML标签、css样式和特定的js代码,创建类似video这样的自定义标签

5.2 Shadow DOM的结构

以shadow-root为起始根节点,在这个根节点的下方,可以是任意元素,和普通的DOM元素一样

image.png

5.3 Shadow DOM术语

  1. Shadow host:一个常规DOM节点,shadow DOM会附加到这个节点上
  2. Shadow tree:shadow DOM内部的DOM树
  3. Shadow boundary:shadow DOM的分界线
  4. Shadow root:shadow tree的根节点

5.4 用法

使用Element.attachShadow()方法给指定的元素挂载一个shadow DOM,并且返回shadowRoot的引用

attachShadow的参数:

  1. 是一个对象,并且必须指定mode属性,值为open或者closed,open时会返回shadowRoot的引用,closed返回null
  2. delegatesFocus,默认为false,可以设置为true,自动聚焦
    <style>
      .content {
        color: blue;
        text-decoration: underline;
      }
    </style>
    
    
    
    <shadow-display>
      <div style="background-color: #f0f0f0">插槽</div>
    </shadow-display>
    <hr />
    <shadow-display>
      <div style="background-color: red">插槽</div>
    </shadow-display>
    <hr />
    <script>
      class ShadowDisplay extends HTMLElement {
        constructor() {
          super()
          const shadow = this.attachShadow({ mode: 'open', delegatesFocus: true })
          shadow.innerHTML = `
            <style>
              :host { display: block; all: initial; }
              .content { font-size: 20px; color: pink; }
            </style>
            <div class="content">
              <p>这是Shadow DOM中的内容,不受外部样式影响</p>
              <input type="text" placeholder="请输入内容" />
              <slot></slot>
            </div>
          `
        }
      }
      customElements.define('shadow-display', ShadowDisplay)
      const element = document.querySelector('shadow-display')
      const shadowRoot = element.shadowRoot
      const contentElement = shadowRoot.querySelector('.content')
      contentElement.style.color = 'green'
      console.log(element.shadowRoot)

image.png

5.5 shadow host的css选择器

5.5.1 :host伪类选择器就是自定义标签元素,只在shadowDOM中有效

    <shadow-display> </shadow-display>
    <script>
      class ShadowDisplay extends HTMLElement {
        constructor() {
          super()
          const shadow = this.attachShadow({ mode: 'open' })
          shadow.innerHTML = `
            <style>
              :host { display: block; width: 200px; height: 200px; border: 3px solid black; }
              :host .content { font-size: 22px; color: pink; }
            </style>
            <div class="content">
              <p>这是Shadow DOM中的内容,不受外部样式影响</p>
            </div>
          `
        }
      }
      customElements.define('shadow-display', ShadowDisplay)
    </script>

此例中的:host就是shadow-display元素 image.png

5.5.2 :host()伪类函数,选择包含特定选择器的自定义元素

:host可以替换为:host(.custom-shadow),要求自定义元素必须设置custom-shadow类名

image.png

5.5.3 :host-context()伪类函数,选择有特定选择器父元素的自定义元素

:host-context()找到自定义元素,并且该元素的父级包含特定的选择器

    <div id="container">
      <shadow-display id="custom-shadow"> </shadow-display>
    </div>
    <shadow-display id="custom-shadow"> </shadow-display>
    <script>
      class ShadowDisplay extends HTMLElement {
        constructor() {
          super()
          const shadow = this.attachShadow({ mode: 'open' })
          shadow.innerHTML = `
            <style>
              :host-context(#container) { display: block; width: 200px; height: 200px; border: 3px solid black; }
              :host .content { font-size: 22px; color: pink;  }
            </style>
            <div class="content">
              <p>这是Shadow DOM中的内容,不受外部样式影响</p>
            </div>
          `
        }
      }
      customElements.define('shadow-display', ShadowDisplay)
    </script>

image.png

六、HTML templates

6.1 templates的概念

templates是html页面中使用的一组元素标签,即<template></template>,在解析的过程中会被处理,不会显示在页面上,可以被js获取到

6.2 简单使用

    <p>我是p标签</p>

    <template>我是template标签</template>

image.png

使用js可以获取到template,并渲染到页面上

      const tem = document.querySelector('template')

      document.body.appendChild(tem.content)

七、slots

    <template id="user-intro-template">
      <div class="header">自我介绍</div>
      <div class="details">我的名字是 <slot name="userName">都敏俊希</slot></div>
    </template>

    <user-intro-template></user-intro-template>
    <user-intro-template>
      <span slot="userName">张三</span>
    </user-intro-template>
    <user-intro-template>
      <span slot="userName">李四</span>
    </user-intro-template>

    <script>
      class UserIntroTemplate extends HTMLElement {
        constructor() {
          super()
          const template = document.getElementById('user-intro-template')
          const templateContent = template.content
          this.attachShadow({ mode: 'open' }).appendChild(templateContent.cloneNode(true))
        }
      }
      customElements.define('user-intro-template', UserIntroTemplate)
    </script>

image.png

通过这个例子,可以得知:slots是给模板元素传值,增强可扩展性

7.1 slot的name属性

带有name的slot标签就是“具名插槽”,在使用插槽的标签上使用slot="age"指定具体的插槽

    <template id="user-intro-template">
      <div class="header">自我介绍</div>
      <div class="details">我的名字是 <slot name="userName">都敏俊希</slot></div>
      <div class="details">我的年龄是 <slot name="age">20</slot></div>
    </template>
    
    <user-intro-template>
      <span slot="userName">张三</span>
      <span slot="age">19</span>
    </user-intro-template>

和vue插槽的相比,这些是一样的:

  1. 匿名插槽和具名插槽
  2. 没有传值的情况下,会使用预设内容

上面的例子是在shadowDOM中使用slots的,如果在正常的文档流中使用,那么不具有插槽的效果。上面的例子中,slot标签是定义在template中的,其实使用div也是可以的,但template的好处是它不渲染在页面上

八、demo:使用Web Component写一个身份证组件

    <template id="IdCardWrapper">
      <style>
        :host {
          display: inline-block;
          width: 400px;
          height: 240px;
          border: 1px solid black;
          border-radius: 10px;
        }
        .row {
          margin-top: 20px;
          margin-left: 20px;
          display: flex;
          align-items: center;
        }
        .row .label {
          font-size: 12px;
          color: blue;
          margin-right: 10px;
        }
      </style>

      <div class="row">
        <div class="label">姓名</div>
        <div class="value"><slot name="userName">都敏俊希</slot></div>
      </div>
      <div class="row">
        <div class="label">性别</div>
        <div class="value"><slot name="gender"></slot></div>
      </div>
      <div class="row">
        <div class="label">出生</div>
        <div class="value"><slot name="birth">2000年1月1日</slot></div>
      </div>
      <div class="row">
        <div class="label">住址</div>
        <div class="value"><slot name="address">xx省xx市</slot></div>
      </div>
      <div class="row">
        <div class="label">公民身份号码</div>
        <div class="value"><slot name="idNumber">123456789123456789</slot></div>
      </div>
    </template>

    <id-card>
      <span slot="userName">千颂伊</span>
      <span slot="gender"></span>
      <span slot="birth">2005年1月1日</span>
      <span slot="address">安徽省合肥市</span>
      <span slot="idNumber">34260120050101001X</span>
    </id-card>
    <id-card></id-card>
    <script>
      class IdCard extends HTMLElement {
        constructor() {
          super()
          this.shadow = this.attachShadow({ mode: "open" })
          let tempEle = document.getElementById("IdCardWrapper")
          this.shadow.appendChild(document.importNode(tempEle.content, true))
        }
      }
      customElements.define("id-card", IdCard)
    </script>

image.png