美文网首页
web components 从0到1

web components 从0到1

作者: copyLeft | 来源:发表于2021-11-04 03:12 被阅读0次

    简单例子

    // 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' , 使用时: <c-title> </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 name='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

    相关文章

      网友评论

          本文标题:web components 从0到1

          本文链接:https://www.haomeiwen.com/subject/nvehzltx.html