web-components是一个比较新的开发技术,但组件化思想已经实际应用了很长时间,代表性的组件话框架有vue, react等。web-components可以直接使用原生HTMLJavascript来封装通用的 web 组件,其拥有简单直接、无任何依赖等特点,是 web 前端开发的一个强有力的技术。

文章前半部分讲web-components相关接口,最后有一个进度条组件实例,点击这里可直接跳转。

Web-Components API

Web-Components API有三个核心部分,分别是:Custom elements, Shadow DOM, HTML templates

Custom elements

custom elements顾名思义,允许我们自定义通用的 HTML 标签,可以是一个包含特定功能如:日历、时间显示、进度条等。

创建自定义元素 使用自定义元素,必须先使用customElements.define(name, constructor, options)方法注册元素后才能使用,参数分别为元素名称,元素构造函数,选项(可选)。自定义元素名称不能是单个字母

生命周期 自定义元素生命周期有 4 个回调函数:

  • connectedCallback:当 custom element 首次被插入文档 DOM 时调用。
  • disconnectedCallback:当 custom element 从文档 DOM 中删除时调用。
  • adoptedCallback:当 custom element 被移动时调用。
  • attributeChangedCallback:当 custom element 增加、删除、修改自身属性时调用。

Shadow DOM

通常情况下,我们封装 web 组件的时候,需要将组件内部的样式封装进去,同时也要避免被外部样式干扰。Shadow DOM是隐藏在一个 DOM 节点里面的节点树,主要特点就是于外部 DOM 树隔离,使组件维护成本大大降低。Shadow DOM并不是新鲜玩意儿,而是浏览器内部 API 实现,我们常用的一些标签如带控制按钮的video标签,其内部就是一个Shadow DOM树。

创建 Shadow DOM Shadow DOM有几个核心概念:

  • Shadow host: 一个常规 DOM 节点,Shadow DOM 会被添加到这个节点上。
  • Shadow tree: Shadow DOM 内部的 DOM 树。
  • shadow root: Shadow tree的根节点。
  • Shadow root: Shadow tree 的根节点。
  • Shadow boundary: Shadow DOM 结束的地方,也是常规 DOM 开始的地方。

Shadow DOM

创建Shadow DOM需要用到attachShadow({ mode: 'open' })方法,参数中mode的取值有两个:open, closed,用来配置是否可以从外部获取shadow tree。该函数返回一个Shadow Root

要获取一个shadow tree,可以使用Element.shadowRoot方法,如果shadow dom的 mode 选项为closed,那么改属性值为null

示例:

const shadowRoot = el.attachShadow({ mode: 'closed' })
shadowRoot.appendChild(childEl)

HTML templates

HTML模板包含两个标签:<template><slot>,如果有熟悉vue的同学对这两个标签应该不会陌生,其用法也非常相似。

创建 template 在编写 web 组件的时候,使用 JavaScript 来编写 HTML 会有诸多不便,<template>标签给我们提供了一个非常便捷的方式来编写HTML部分的代码,一个简单的示例:

<template id="my-paragraph">
  <p>My paragraph</p>
</template>

可以在 JavaScript 中来引用模板

let template = document.getElementById('my-paragraph')
let templateContent = template.content
document.body.appendChild(templateContent.cloneNode(true)) // cloneNode方法克隆整个节点,为了避免模板在多个地方被引用引发问题

添加 slot slot翻译为“插槽”,有时候,我们的组件需要提供更灵活的方式来展现,比如进度条组件可在使用的时候,自定义文字显示。这个时候slot便大有用处。我们通过一个简单的示意图来理解插槽的作用。

进度条组件

说了这么多,来实际操练一下,编写一个简单的进度条组件。

基本功能

  • 图形化显示进度
  • 可定制进度条显示文字

效果演示

实现

组件模板

<template id="my-progress-template">
  <style>
    .bg {
      width: 300px;
      height: 30px;
      border-radius: 15px;
      box-shadow: 0 0 4px 0 #2db7f5 inset;
      overflow: hidden;
    }
    .bar {
      height: 30px;
      width: 0;
      line-height: 30px;
      border-radius: 15px;
      box-shadow: 1px 0 2px 0 #2db7f5;
      background-color: #2db7f5;
      text-align: center;
      font-size: 14px;
      box-sizing: border-box;
      /* transition: width .3s ease-in-out; */
    }

    .bar .text {
      color: #fff;
    }
  </style>

  <div class="bg">
    <div class="bar">
      <slot class="text" name="text"></slot>
    </div>
  </div>
</template>

组件构造器

/**
 * 进度条组件构造函数
 * 继承HTMLElement
 */
class MyProgress extends HTMLElement {
  // 注意⚠️,attributeChangedCallback回调需要添加此方法,返回要监听变动的属性
  static get observedAttributes() {
    return ['value']
  }

  constructor(...args) {
    super(...args)
    this.init()
  }

  init() {
    let template = document.getElementById('my-progress-template')
    const shadow = this.attachShadow({ mode: 'open' })
    shadow.appendChild(template.content.cloneNode(true))
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'value') {
      const root = this.shadowRoot
      const bar = root.querySelector('.bar')
      let number = +newValue
      if (number > 1 || number < 0) {
        bar.style.width = 0
        throw new Error('progress value must between 0 and 1')
      }
      bar.style.width = number * 100 + '%'
    }
  }
}

完整代码查看连接:https://github.com/YES-Lee/blob/master/demo/simple-progress-bar.html

参考连接