Web Components系列文章(一) – 基础概念

其实Web components的规范已经提出好多年了,但是依赖浏览器的底层实现,还有生态的发展,跟其他JS框架相比,一直没有火起来,尤其是国内,国外用得要多一些。最近研究了一下,以备不时之需。毕竟程序员要一直保持学习的状态…出来的技术或多或少了解一下。

写了一个系列文章,分为一下三篇,会陆续更新:

好了,现在开始正式的介绍。

概念

像React、Vue这样的前端框架都有组件化开发的概念,但是这些都依赖一个运行时代码。那么浏览器能否从底层上就支持组件化呢,于是制定web规范的组委会提出了一些规范,来让浏览器可以原生支持组件化的开发方式。这样,对浏览器厂商来说,需要实现这些规范,对程序员来说就是调用一些API。

主要包括以下三个规范:

通过一组JS API创建自定义html标签,也就是拓展了现有的内置html标签。并且提供了类似vue、react的生命周期钩子函数等,能够在适当的时机进行一些操作。

通过一组JS API 能创建一棵shadow dom tree,可以插入到当前html文档中。主要提供了CSS和DOM scoping。shadow dom tree内部的css以及DOM与文档其他部分是完全隔离的,因此内部也能用更通用的CSS选择器,而不必担心命名冲突,避免全局污染。

另外shadow root中的元素通过document.querySelector无法被获取到,只能通过先拿到shadow dom tree的根节点shadowRoot,用shadowRoot.querySelector这种方式访问。

可以看到,Shadow DOM主要作用是提供了封装性。

备注:document范围内的内容都被称为light DOM,shadow root中的内容被称为shadow DOM。

通过<template><slot>直接在HTML 文件定义可复用的结构,在文档中不会被渲染但是能通过DOM API获取:

<template id="myTpl">
  <p class="name">Name</p>
  <slot></slot>
</template>
复制代码

这三个规范并非一定要一起使用,只不过一起使用能更好地封装一个自定义组件。

兼容性和适用场景

  • Custom Elements

  • Shadow DOM

  • HTML Templates

对于不兼容的浏览器可以使用polyfills: webcomponents
/
polyfills
,能够支持到IE11。

由于web components依赖浏览器底层的实现,而不限定于某个特定的前端框架,例如Vue、React、Angular等,因此比较适合需要跨团队共享的UI组件,这类组件用web components来开发,就可以不用限定consumer适用的框架类型了。

另外,像Vue、React等的生态发展比较完善,提供了诸如路由、状态存储等周边,开发效率上也比用原生的web components高,因此web components基本也是需要与前端框架配合使用而不会取代这些流行的前端框架。

具体用法

这块讲得相对有点细,如果不想一开始纠结细节的,直接拉到最下面看一个完整的Button组件例子。另外我还写了一个Dialog,Dropdown组件,上传到github上了,欢迎down下来看,git传送门:my-web-component

Custom elements

注册自定义元素

customElements.define(tagName, constructor, options)
复制代码
  • tagName:组件名称,采用kebab-case规则,且必须带-
  • constructor:在其中对组件进行具体定义。采用es6 class的方式定义,如果不需要对现有的html标签进行扩展则继承 HTMLElement,以确保其拥有内置HTML标签的所有默认方法和属性。
class MyButton extends HTMLElement {
    constructor() {
        super()
        // do something
    }
}

customElements.define('my-button', MyButton)
复制代码
  • options:可选,目前只有一个选项extends,当创建的元素需要继承某个已有的内置元素时。此时需要extends特定的标签类,例如HTMLButtonElement
class MyButton extends HTMLButtonElement {
    consotructor() {
        super()
        // do something
    }
}

customElements.define('my-button', MyButton, {extends: 'button'})
复制代码

使用继承了内置元素的自定义组件:

在html中使用:

<button is="my-button">click</button>
复制代码

在js中使用:

document.createElement('button', {is: 'my-button'})
复制代码

生命周期钩子

  • connectedCallback

当custom element首次被插入文档DOM时被调用。

  • disconnectedCallback

当custom element从文档DOM中删除时被调用。

  • adoptedCallback

当custom element被移动到新的文档时被调用。(例如Iframe和主窗口之间)

  • attributeChangedCallback

当custom element上的attributes进行了添加、修改、删除时被调用。(当用户初次使用标签并传入属性时,attributeChangedCallback就会被调用一次。)

使用方法:

// 1. 该静态方法中返回需要观察的attributes名
static get observedAttributes() {return ['attr1', 'attr2']; }

// 2. 当观察的attributes发生变动时触发该钩子
attributeChangedCallback(attrName, oldValue, newValue) {
  // do something
}
复制代码

注意: attributes只接收string类型的。当需要传入对象类型时(例如Object、Array),要么JSON.stringify,要么通过通过JS获取到元素并且设值为property,推荐采用第二种方式,尤其是传递function时:

const myButton = document.querySelector('my-button');
myButton.label = 'Click Me';
复制代码

此时attributeChangedCallback无法监听元素property的变化,需要通过setter同步attribute:

set label(value) {
    this.setAttribute('label', value);
}
复制代码

如果是引用类型的数据,则完全没有必要再同步到attribute上了。可以直接在setter中更新渲染等操作而不是通过attributeChangedCallback

Shadow DOM

挂载shadow Dom

组件内部创建Shadow DOM:

const shadowRoot = element.attachShadow({mode: 'open'})
复制代码

该方法在自定义元素内部挂载了一棵Shadow DOM Tree,并且返回对ShadowRoot的引用。

例子:

// 获取页面的
const $root = document.getElementById('root');
const shadow = $root.attachShadow({mode: 'open'});
const $p = document.createElement('p');
$p.innerText = '创建一个 shadow 节点';
shadow.appendChild($p);
复制代码
  • element

可以是一切自定义标签或者允许的现有html标签。PS: 并非所有标签下都能挂载Shadow DOM,这里给出了一个可以在下面挂载Shadow DOM的元素列表:MDN Element.attachShadow()

shadowRoot可以像其他DOM节点一样调用appendChild方法添加子节点。

  • mode

有两个值,主要就是用来控制外部是否能过获取到shadowRoot从而对shadow dom tree内部进行节点访问(例如添加或者修改等):

  1. open: 表示shadowDOM subtree之外的JS代码能访问到其内部的元素,例如通过element.shadowRoot能获取到shadowRoot的引用。
  2. closed:反之。当调用element.shadowRoot时返回null
  • shadowRoot

是shadow DOM tree的根节点,整棵树相较与原本的节点树会进行单独渲染。
shadowRoot继承了Node,因此拥有普通htmlNode节点的属性和方法。此外还有一些自己的一些属性和方法:MDN ShadowRoot

HTML template

提供了组件模板,slot等功能,可以给slot指定名字变成具名slot,熟悉Vue的同学应该比较清楚,就跟Vue里slot的用法一毛一样。

通过JS API来获取模板内容并插入到shadow dom tree中:

const templateContent = document.querySelector('#tpl-id').content

// 由于模板需复用,在将模板内容添加到节点里时,需cloneNode
const shadowRoot = this.attachShadow({mode: 'open'})
shdowRoot.appendChild(templateContent.cloneNode(true))
复制代码

完整的Button组件例子

下面是一个简单的自定义button元素例子:

const myButtonTemplate = document.createElement('template');
 
myButtonTemplate.innerHTML = `
  <style>
    .container {
      padding: 8px;
    }
 
    button {
      display: block;
      overflow: hidden;
      position: relative;
      padding: 0 16px;
      font-size: 16px;
      font-weight: bold;
      text-overflow: ellipsis;
      white-space: nowrap;
      cursor: pointer;
      outline: none;
      width: 100%;
      height: 40px;
      box-sizing: border-box;
      border: 1px solid #a1a1a1;
      background: #ffffff;
      box-shadow: 0 2px 4px 0 rgba(0,0,0, 0.05), 0 2px 8px 0 rgba(161,161,161, 0.4);
      color: #363636;
      cursor: pointer;
    }
  </style>
 
  <div class="container">
    <button>Label</button>
  </div>
`;

class MyButton extends HTMLElement {
    constructor() {
        super();
        this._shadowRoot = this.attachShadow({ mode: 'open' });
        this._shadowRoot.appendChild(myButtonTemplate.content.cloneNode(true));

        this.$button = this._shadowRoot.querySelector('button')
        // custom event
        this.$button.addEventListener('click', () => {
            this.dispatchEvent(new CustomEvent('clicked', {
                detail: {
                    clickMessage: 'Hello from within the Custom Element'
                }
            }))
        })
    }
    // Reflecting an attribute to a property
    get label() {
        return this.getAttribute('label')
    }

    // Reflecting a property to an attribute
    set label(value) {
        this.setAttribute('label', value)
    }

    static get observedAttributes() {
        return ['label']
    }

    attributeChangedCallback(name, oldValue, newValue) {
        this.render()
    }

    render() {
        this.$button.innerHTML = this.label
    }
}
 
window.customElements.define('my-button', MyButton);
复制代码

在html中的使用:

<body>
    <my-button label="Click Me!"></my-button>
    <script src="./components/MyButton.js"></script>
    <script>
        document.querySelector('my-button').addEventListener('onClick', (event) => {
            console.log('event', event)
        })
    </script>
</body>
复制代码

git传送门:my-web-component

这里上传了更完整的代码,还包括Dialog和Dropdown组件,其中Dropdown组件给出了如何处理引用类型的数据。

参考

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享