Skip to content

Custom Elements

自定义元素 判断当前浏览器是否支持自定义元素 if( 'customElements' in window )

customElements 是一个浏览器window下的全局变量工具

第二个参数接收一个class类,这个class类要继承元素构造器 class extends HTMLElement { }

继承其他web Components

假如我们要二次封装别人的 Web Components,我们可以定义自定义元素名后,继承原 Web Components

customElements.get( ) 获取已经定义的原 Web Components

如👇

js
customElements.define('my-p', class extends customElements.get('origin-ui-p') {
  constructor() {
    super()
    // 此时写入的this操作将是基于原组件做的额外操作
    this.onclick = () => alert('基于原组件的额外点击事件')
  }
})

为什么强制要求命名带-

浏览器解析html,从上往下执行,我们常常要求在dom元素前先引入css资源,在dom元素后引入js资源,是为了让页面在无视js先渲染出内容

那么当出现了js定义的自定义元素标签时,js还能写在dom元素后面吗? 是可以的

html遇到带 - 的标签,并且不是原生标签将会跳过渲染并认为是未定义的标签,而不是报错 而不带 - 的标签将一律认为是原生标签,如果是不认识的标签将会报错

伪类选择器 选中未定义的自定义标签

上面说到浏览器会跳过渲染未定义的标签 这里有个伪类选择器可以选中已定义的元素标签 :defined { } ,进行样式编写

那么假如我们要选中未定义的元素标签(自定义元素) :not(:defined) {}

这样我们就可以给这个 Web Components 做一个加载效果

html
<html>
  <head>
    <meta charset="utf-8">
    <title>demo</title>
    <style>
      body { display: grid; place-items: center; }
      :not(:defined) {
        width: 120px;
        height: 20px;
        background: gray linear-gradient(60deg, transparent, transparent 20%, white 40%, transparent 60%) 0/300%;
        border-radius: 4px;
        animation: loading 2s infinite;
      }
      @keyframes loading {
        to { background-position: 300% 0 }
      }
    </style>    
  </head> 
  <body>
    <my-p></my-p>
    <script>
      setTimeout(()=>{
        customElements.define('my-p', class extends HTMLElement {
          constructor() {
            super()
            this.innerHTML = '<p>Web Components</p>'
          }
        })
      },3000)
    </script>
  </body>
</html>

效果如👇

customElements.whenDefined

👆 已经用到了 customElements上的defind方法创建定义自定义标签,它还有其他的几个方法可以使用 因为不算常用,暂时先不仔细研究,官方文档也不算详细,先有个印象

customElements.whenDefined( )

自定义标签生命周期

js
customElements.define('my-p', class extends HTMLElement {
  // 相当于vue3的setup
  constructor() {
    super()
    this.innerHTML = '<p>Web Components</p>'
  }
  // 相当于vue的mounted
  connectedCallback(){
    console.log('connectedCallback')
  }
  // 相当于vue3的unmounted
  disconnectedCallback(){
    console.log('disconnectedCallback')
  }
  adoptedCallback(){
    console.log('adoptedCallback')
  }
})

connectedCallback

当 WebComponents 第一次被挂在到 dom 上是触发的钩子,并且只会触发一次。类似 Vue 中的 mounted React 中的 useEffect(() => {}, []),componentDidMount。

disconnectedCallback

当自定义元素与文档 DOM 断开连接时被调用。

adoptedCallback

adopted: 收养、采用 当自定义元素被移动到新文档时被调用

如在 dom操作运行我们移动iframe的元素到主文档document Document.adoptNode( ) -MDN 效果类似剪切 此时剪切的dom元素是自定义元素标签的话,将会触发自定义标签的adoptedCallback生命周期

attributeChangedCallback

attributeChangedCallback 也是自定义自定义属性的生命周期,但是我们单独来看 字面意思,属性变化时触发 类似vue的watch 注意它不是元素的prop 注意它的触发时机比connectedCallback挂载要早,可以用oldVal有没有值判断是否未挂载

本生命周期起作用的前提是先定义需要监听的属性名static get observedAttributes(),因为标签上的预置属性是非常多的 这样的写法就会非常像vue的watch生效需要先在data声明

👇 实现 input 属性的 placeholder

html
<html>
<head>
  <title>demo</title>
</head>
<body>
  <my-input placeholder="请输入"></my-input>
  <script>
    customElements.define('my-input', class extends HTMLElement {
      // 类似vue的watch监听前需要在data中声明
      static get observedAttributes() { return ['placeholder'] }
      // 这个set是为了实现直接操作dom.placeholder = xx 也生效
      // 不加这个方法,则js只能通过dom.setAttribute()实现修改
      set placeholder(val) {
        this.querySelector('input').setAttribute('placeholder',val)
      }

      constructor() {
        super()
        this.innerHTML = '<input>'
      }
      // 查看attributeChangedCallback 和 connectedCallback 顺序
      connectedCallback() {
        console.log('connectedCallback')
      }
      // 可以用 oldVal 有没有值判断是否未挂载
      attributeChangedCallback(key, oldVal, newVal) {
        console.log('触发attributeChangedCallback', key, oldVal, newVal)
        if(key === 'placeholder') {
          this.querySelector('input').setAttribute('placeholder',newVal)
        }
      }
    })
  </script>
</body>
</html>

继承其他原生元素dom

👆 的placeholder示例中,我们需要手动选中内部的真实 input 标签来设置placeholder 我们现在希望我们的自定义标签就是input标签,它自带着placeholder功能

html
<html>
<head>
  <title>demo</title>
</head>
<body>
  <input is="my-input" placeholder="请输入"></input>
  <script>
    customElements.define('my-input', class extends HTMLInputElement {
      constructor() {
        super()
        this.disabled = true
      }
    }, { extends: 'input' })
  </script>
</body>
</html>

👆的自定义标签定义的时候,不再继承HTMLElement 而是 HTMLInputElement 其他具体的标签都是类似的做法,他们的本质都是继承自 HTMLElement

并且需要写上第3个参数, { extends: 'input' }

注意这种写法,safari 不支持 Safari不支持build-in自定义元素的兼容处理 引入 polyfillcustom-elements-builtin js库来兼容safari,但是还有些细微操作,详见👆的文章

参考材料