web components 从0到1

·  阅读 138
web components 从0到1

简单例子

// html
<c-button>
  click me
</c-button>


// js
// 定义元素类
class CButton extends HTMLElement{
  constructor(){
    super()
    this._root = this.attachShadow({ mode: 'closed' })
    this.tmp = document.createElement('template')
    this.tmp.innerHTML = `
       <style> 
         .c-button{ border: 0; background: #fff; color: orange }
       </style>
       <button class='c-button'>
         <slot />
       </button>
    `
    this._root.appendChild(this.tmmp.content.cloneNode(true))
  }
}
// 注册元素
window.customElements.define('c-button', CButton)
复制代码

定义

Web Components 是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的web应用中使用它们。

web components 就是一套提供自定义元素的的技术

组成

  • Custom elements(自定义元素) 提供自定义元素内容及行为的能力
  • Shadow DOM(影子DOM)提供元素封装性
  • HTML templates(HTML模板)提供使用html标签编写自定元素的内容结构

定义元素类

自定义元素通过API window.customElements.define(name, constructor, options) 注册到当前文档中,

该接口主要接收参数:

  • name 元素标签名, 例如: 'c-title' , 使用时:
  • constructor 元素构造函数,需要继承 HTMLElement 类
  • options 配置属性

所以我们需要通过类的方式定义自定义元素

class CustomElement extends HTMLElement {
   constructor(){
      ....
   }
}
// 注册
window.customElements.define('', CustomElement)
复制代码

html 模板

HTML templates 提供了两个新的元素标签

  • <template> 模板容器
  • <slot> 内容插槽

自定元素更接近于我们平常定义的UI组件,将具有一定功能的 html, css, js 封装在元素标签内。所以编写的流程也大致相同。

首先我们先定义内容的基础结构

const root = this.attachShadow({ mode: 'closed' })
const tmp = document.querySelector('template')
tmp.innerHTML = `
  // 内容样式 
 <style>
   .c-card{
      width: 400px;
      height: 300px;
      padding: 8px 10px;
      background: #fff;
      border: 1px solid #eee;
   }
  </style>


 // 内容结构
  <div class='c-card'>
     <slot />
  </div>
`
// 挂载内容
root.appendChild(tmp.content)
复制代码

除了使用模板字符串,还可以使用 html 模板或 document.createElement 构建内容结构

  • html 模板
// html
<template id='tmp'>
  <div class='c-card'>
    <slot />
  </div>
</template>


// js
class Card extends HTMLElement{
  construction(){
    ...
    const tmp = document.querySelector('#tmp')
    root.appendChild(tmp.content)
  }
}
// <template> 内容不会直接显示在html文档中
复制代码
  • createElement
class Card extends HTMLElement{
  construction(){
    ...
    const tmp = document.createElement('template')
    const slot = document.createElement('slot‘)
    
    card.classList.add('c-card')
    card.appendChild(slot)
    tmp.addpendChild(card)
    root.appendChild(tmp.content)
  }
}
复制代码

获取/自定义属性

  • 字段属性
// html
<c-card follow='10'> // 在模板中设置属性值
</c-card>


// js
{
  constructor(){
    ...
     const card = document.createElement('div')
    // 通过 getAttribute 查询元素上是否设置自定义属性值
     const follow =  this.getAttribute('title') || 0
     console.log('follow: ', follow, typeof follow) // 值类型未字符串
    card.innerHTML = follow
  }
}
// 使用js设置属性值
const card = document.querySelector('c-card')
card.setAttribute('follow', 20)
复制代码

这里有几个问题: 1. 通过 getAttribute 获取的值,类型都是字符串。2. 后续修改属性时,没有响应式的修改属性内容, 如果需要响应属性修改,需要配置标签的周期钩子。

  • getter / setter
{
  constructor(){
    this._value = 0
  }
  // 除了通过 getAttribute 获取属性外,
  // 也可以通过 getter, setter 定义属性
  get value(){
     return this._value
  }
  set value(v){
     this._value = v
      console.log('value type: ',typeof v)
  }
}
// 修改属性值
const card = document.querySelector('c-card')
card.value = 20
// value type: number
复制代码

使用 getter , setter 后,可以通过直接赋值的方式设置属性值。 并且获取的值为原对象值类型,而非字符串

属性响应

上面修改非属性值时,我们无法响应属性的修改。要实现对属性的响应,需要依赖 attributeChangedCallback observedAttributes 两个属性

{
  // 属性响应函数, 属性修改后将触发该函数, 类似 vue 的watch函数
  attributeChangedCallback(name, oldValue, newValue){
    console.log(`属性名称: ${name}`)
    console.log(`旧属性值: ${oldValue}`)
    console.log(`新属性值: ${newValue}`)
  }
  // 并不是所有的属性都触发 attributeChangedCallback,只有在 observedAttributes 内注册的属性,才能触发回调
  // 注册触发响应的属性名称
  static observedAttributes = ['value']
  // observedAttributes 也可以为 getter, setter
}
复制代码

生命周期钩子

自定义元素内可以配置相应的周期钩子,以执行不同任务.

  • connectedCallback 插入时
  • disconnectedCallback 删除时
  • adoptedCallback 移动时
  • attributeChangedCallback 属性修改时

自定义事件

{
  constructor(){
    const card = document.createElement('div')
    card.addEventListener('click', () => {
      // 创建自定义事件
      const event = new CustomEvent('cardClick', {detail: 'from card'})
      // 抛出事件
      this.dispatchEvent(event)
    })
  }
}
// 挂载事件监听
cont card = document.querySelector('c-card')
card.addEventListener('cardClick', ({detail}) => console.log(detail))
复制代码

通信

在了解了自定义属性和自定事件后, 基本就满足了元素内与元素外通信的条件了。

模式类似vue的父子通信, 通过自定义属性获取外部值, 通过自定义事件向外抛出数据

{
  constructor(){
    this._value = 0
    this._root = this.attachShadow({ mode: 'closed' })
    this._rootElement = document.createElement('template')
    this._rootElement.innerHTML = `
<div>
 <button> add </button>
</div>
`
this._rootElement.content.querySelector('button')
.addEventListener('click', () => {
const event = new CustomEvent('add', {detail: this._value + 1})
this.dispatchEvent(event)
})
  }
  render(){
    const ele = document.createElement('div')
    ele.innerHTML = this._value
    this._root.appendChild(ele)
 }
 get value(){ return this._value }
 set value(n){ this._value = n; this.render() }
}
const ele = document.querySelect('element')
// 设置初始值
ele.value = 10
// 添加响应回调
ele.addEventListener('add', ({detail}) => {
  ele.value = detail
})
复制代码

插槽

使用<slot /> 可以为标签添加嵌套功能, 与 vue slot 类似。

{
  constructor(){
    const tmp = `
      <div>
        <div>
          默认插槽: <slot />
        </div>
        <div>
          具名插槽: <slot name='sub' />
        </div>
      </div>
    `
    ...
  }
}
// html
<element>
  <div slot='sub'> 附属信息 </div>
  <div> 主内容 </div>  
</element>
复制代码

隐藏 / 开放

shadow DOM 提供了隐藏元素实现的能力。外部将不能影响或获取到内部元素, 通过设置mode 类型开启 or 关闭。

{
  constructor(){
    // mode 的值可以为 'open' | 'closed'
    this._root = this.attachShadow({ mode: 'closed' }) // 禁止外部访问内部节点
  } 
}
复制代码

总结

web compoents 可以看作一种官方的组件化方案, 在不依赖其他MVVM框架或编译器的情况下,实现通用性组件。

在开发中,现有的API都比较简略,实际应用依然需要更上层的封装或工程化依赖做辅助。

参考

MDN Web Components

阮一峰 Web Components 入门实例教程

Web Components Tutorial or Beginners

分类:
前端
标签:
分类:
前端
标签: