Web Component 是Html5 推出的一个新特征,即web 组件。提到web组件有一定前端开发经验的人对这个词汇并不陌生,在这个严重依赖前端框架来开发项目的时代,我们推崇使用组件化的思路来编写我们的页面,通过这些低耦合的组件我们可以像搭积木一样组织出我们的页面。

从我们的日常所见到的类似table select 这些耳熟能详的基础标签,更有后来更复杂的video raido等,都是web组件化的一种体现。可惜的是兼容性问题以及api的丰富度不够。目前主流还是使用框架来模拟组件的实现。随着浏览器兼容的越来越好,如果能完全使用web component来组织我们的项目,我们可以少引入很多第三方的框架来编排我们的页面

本文我们将来探究 Web Component

自定义 Web Component

关键的api,它是挂载在window上的api,并非document。

window.customElements.define()
// 参数类型
// name 是组件名称
// constructor 自定义组件的构造类
// options 更多的属性
(method) CustomElementRegistry.define(name: string, constructor: CustomElementConstructor, options?: ElementDefinitionOptions): void

  1. 自定义内置元素
    自定义内置元素的本质是扩展已有的内置元素的逻辑。他的好处是可以直接继承默认内置元素的例如语义、默认交互。用户只需要关注修改自定义交互即可。
    我们来自定义一个button 实现一个点击默认的操作。
class MyButton extends HTMLButtonElement {
    constructor () {
      // 必须加super 否则this 无法指向button
      super()
      this.addEventListener('click', function () {
        alert('this is my button')
      })
    }
  }
  customElements.define('my-button', MyButton, { extends: "button" })

申明的时候,需要通过extends传入一个已经存在的内置元素,调用的时候通过内置元素的is来扩展,

<button is="my-button">Click me</button>

  1. 定义一个完全自治的元素
    完全自治的元素,不依赖现有元素。是一个完全从view 到数据 到交互都自我管理的自定义元素。如上述他的缺点就在于开发者需要处理所有的语义和交互,写法比较繁琐。如果处理不好,会导致违背html语义标签的一些标准。
  class TextIcon extends HTMLElement {
    constructor() {
      super()
      this._text = null;
    }
    static observedAttributes = ['text'];
    attributeChangedCallback(name, oldValue, newValue) {
      this._text = newValue;
      this.updateRender();
    }
    connectedCallback() {
      this.updateRender();
    }
    get text () {
      return this._text;
    }
    set text(v) {
      console.log(v, 'ss')
      this.setAttribute('text', v)
    }
    updateRender () {
      this.innerText=this._text
    }
  }
  customElements.define('text-icon', TextIcon)

调用方式1

 <text-icon text="test1"></text-icon>

调用方式2

  const textIcon = document.createElement('text-icon')
  textIcon.setAttribute('text', 'sddd')
  document.body.appendChild(textIcon)

  const textIcon1 = new TextIcon()
  textIcon1.text = 12
  document.body.appendChild(textIcon1)
  1. 自定义元素升级
    由于可以先创建一个元素,然后再定义该元素。然后再升级它
    涉及API
  window.customElements.upgrade(elementNode)

注意看代码中的注释

// 通过createElemnt创建自定义元素节点,注意此时text-icon 还定义
const testUpgradeDom = document.createElement('text-icon')
// 开始定义text-icon 元素
class TextIcon extends HTMLElement {
    constructor() {
      super()
      this._text = null;
    }
    static observedAttributes = ['text'];
    attributeChangedCallback(name, oldValue, newValue) {
      this._text = newValue;
      this.updateRender();
    }
    connectedCallback() {
      this.updateRender();
    }
    get text () {
      return this._text;
    }
    set text(v) {
      console.log(v, 'ss')
      this.setAttribute('text', v)
    }
    updateRender () {
      this.innerText=this._text
    }
  }
  customElements.define('text-icon', TextIcon)

  // 此时我们可以看到testUpgradeDom 这个节点非自定义元素
  console.log(testUpgradeDom instanceof TextIcon);  //false
  // 调用升级api
  customElements.upgrade(testUpgradeDom)
  // 此时testUpgradeDom 就升级成了自定义元素创建的节点
  console.assert(testUpgradeDom instanceof TextIcon); // true

  1. 获取自定义元素构造器
customElements.get('text-icon')

  1. 监测自定义元素的定义
 customElements.whenDefined('text-icon').then(function() {
    // 自定义操作
 })

返回的是一个promise 当监测到text-icon 被定义后,我们可以去做一些操作,比如上面提到的升级操作等。

  1. attachInternals
    我们都知道form内置标签能自动关联内置的表单元素的值。该功能让我们能够扩展更多的自定义表单元素。它核心需要处理的是通过internals暴露的属性和方法来实现和form之间的交互和值的关系。通过该api来获取form元素的一些内置属性,来让自定义元素能够和from元素一样来处理例如通过name 来获取值等表单的特点。
class MyCheckbox extends HTMLElement{
	// 这个标示来控制是否是form关联的元素
    static formAssociated = true
    static observedAttributes = ['checked'];
    constructor() {
      super()
      this._internals = this.attachInternals()
      this.addEventListener('click', this._onClick.bind(this));
    }
    get form () {
      return this._internals.form;
    }
    get name () {
      return this._internals.name;
    }
    get type() {
      return this._internals.type
    }
    get checked () {
      return this.getAttribute('checked')
    }
    set checked(tag) {
      console.log(tag,'xx')
      this.toggleAttribute('checked', Boolean(tag))
    }
    attributeChangedCallback() {
      this._internals.setFormValue(this.checked? 'on' : 'off')
      // this._internals.ariaChecked = this.checked;
    }
    _onClick (event) {
      debugger
      this.checked = !this.checked
    }
  }
  customElements.define('my-checkbox', MyCheckbox)

调用

  <form method="post" action="">
    <label><my-checkbox  name="agreed"></my-checkbox> I read the agreement.</label>
    <input type="submit">
  </form>
  1. 一些生命周期
// 初始化
constructor()
// 组件第一次关联到文档
connectedCallback()
// 组件断开和文档的连接
disconnectedCallback()
// 组件关联到新的文档
adoptedCallback()
// 组件属性变化回调
attributeChangedCallback()
// 组件和表单关联的变化回调
formAssociatedCallback()
// 自定义关联表单组件的表单发生了reset操作的回调
formResetCallback()
// 自定义form元素 被设置为 disabled时的回调
formDisabledCallback()
// form restore时候触发
formStateRestoreCallback()

总结

几个注意点

  1. constructor里的super是必须的,否则就失去了this的关联属性
  2. 组件的命名要遵循规则
  3. 定义的类构造器中途不要出现return
  4. 构造器中不要出现document.write 或者window.open
  5. 在实际使用中,我们还可以将自定义元素 挂载在shadowdom上来做到类似沙箱一样的隔离能力

参考链接

web component

更多推荐

Web Component 详解